- Overview
- Architecture
- AWS Resources
- CI/CD Pipeline
- Security
- Observability
- Cost
- Deployment
- Repository Structure
- Lessons Learned
A personal cloud engineering portfolio hosted on AWS using a production-style architecture β private S3 origin, global CDN delivery via CloudFront, custom domain with TLS, fully automated deployments on every git push, and operational monitoring with alerting.
| Goal | Implementation |
|---|---|
| β‘ Global performance | CloudFront CDN with edge caching |
| π HTTPS everywhere | ACM TLS certificate, HTTP β HTTPS redirect |
| π« No public S3 | Private bucket, Origin Access Control (OAC) |
| π Zero-touch deploys | git push β CodePipeline β CodeBuild β live |
| π Operational visibility | CloudWatch alarms, CloudFront access logs |
| Layer | Service | Purpose |
|---|---|---|
| ποΈ Origin | Amazon S3 | Private static file storage |
| π CDN | Amazon CloudFront | Global edge delivery + HTTPS termination |
| π DNS | Amazon Route 53 | Custom domain resolution |
| π TLS | AWS Certificate Manager | Free public TLS certificate |
| π CI/CD | CodePipeline + CodeBuild | Automated build and deploy |
| π Monitoring | CloudWatch + SNS | 5xx alerting and metrics |
| π Logging | CloudFront Access Logs | Traffic analysis and debugging |
flowchart LR
subgraph DEV["<b>π§βπ» Developer</b>"]
A["<b>git push</b><br>main branch"]
end
subgraph CICD["<b>π CI / CD</b>"]
B["<b>π₯ GitHub</b><br>Webhook Trigger"]
C["<b>βοΈ CodePipeline</b><br>Orchestration"]
D["<b>π¨ CodeBuild</b><br>buildspec.yml"]
end
subgraph INFRA["<b>βοΈ AWS Infrastructure</b>"]
E["<b>ποΈ S3</b><br>Private Origin"]
F["<b>π CloudFront</b><br>Edge CDN + TLS"]
G["<b>π Route 53</b><br>DNS Resolution"]
H["<b>π ACM</b><br>TLS Certificate"]
end
subgraph CLIENT["<b>π€ End User</b>"]
I["<b>π Browser</b><br>HTTPS Request"]
end
A -- "push event" --> B
B --> C
C --> D
D -- "aws s3 sync" --> E
D -- "cache invalidation" --> F
I -- "shaunvividszportfolio.com" --> G
G -- "A Record (Alias)" --> F
H -. "TLS termination" .-> F
F -- "OAC fetch" --> E
F -- "cached response" --> I
style DEV fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e3a5f
style CICD fill:#dcfce7,stroke:#16a34a,stroke-width:2px,color:#14532d
style INFRA fill:#fef9c3,stroke:#ca8a04,stroke-width:2px,color:#713f12
style CLIENT fill:#f3e8ff,stroke:#7c3aed,stroke-width:2px,color:#581c87
style A fill:#bfdbfe,stroke:#3b82f6,stroke-width:1px,color:#1e3a5f
style B fill:#bbf7d0,stroke:#22c55e,stroke-width:1px,color:#14532d
style C fill:#bbf7d0,stroke:#22c55e,stroke-width:1px,color:#14532d
style D fill:#bbf7d0,stroke:#22c55e,stroke-width:1px,color:#14532d
style E fill:#fef08a,stroke:#eab308,stroke-width:1px,color:#713f12
style F fill:#fef08a,stroke:#eab308,stroke-width:1px,color:#713f12
style G fill:#fef08a,stroke:#eab308,stroke-width:1px,color:#713f12
style H fill:#fef08a,stroke:#eab308,stroke-width:1px,color:#713f12
style I fill:#e9d5ff,stroke:#a855f7,stroke-width:1px,color:#581c87
| Bucket | Region | Purpose | Configuration |
|---|---|---|---|
shaun-portfolio-content |
eu-west-1 |
Static site files |
β
Block all public access β OAC-only bucket policy β Default encryption (SSE-S3) β Versioning enabled |
shaun-portfolio-cf-logs |
eu-west-1 |
CloudFront access logs |
β
Block all public access β ACL for CloudFront logging |
Bucket Policy (content bucket):
{
"Effect": "Allow",
"Principal": { "Service": "cloudfront.amazonaws.com" },
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::shaun-portfolio-content/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::<ACCOUNT>:distribution/<DIST_ID>"
}
}
}| Setting | Value |
|---|---|
| Origin | S3 via Origin Access Control (OAC) |
| Default root object | index.html |
| Custom domain | www.shaunvividszportfolio.com |
| TLS certificate | ACM public cert (us-east-1) |
| Viewer protocol | Redirect HTTP β HTTPS |
| Cache policy | Managed CachingOptimized |
| Access logging | Enabled β shaun-portfolio-cf-logs |
| Record | Type | Target |
|---|---|---|
www.shaunvividszportfolio.com |
A (Alias) | CloudFront distribution |
- Certificate type: Public
- Region:
us-east-1(required for CloudFront) - Domain:
*.shaunvividszportfolio.com - Validation: DNS (CNAME record in Route 53)
- Auto-renewal: β Enabled
flowchart LR
subgraph PIPE["<b>βοΈ CodePipeline</b>"]
direction LR
S["<b>π₯ Source</b><br>GitHub<br>main branch"]
B["<b>π¨ Build</b><br>CodeBuild<br>buildspec.yml"]
D["<b>π€ Deploy</b><br>S3 sync +<br>CloudFront invalidate"]
end
S --> B --> D
style PIPE fill:#f0fdf4,stroke:#16a34a,stroke-width:2px,color:#14532d
style S fill:#dbeafe,stroke:#3b82f6,stroke-width:1px,color:#1e3a5f
style B fill:#fef9c3,stroke:#ca8a04,stroke-width:1px,color:#713f12
style D fill:#dcfce7,stroke:#22c55e,stroke-width:1px,color:#14532d
version: 0.2
env:
parameter-store:
S3_BUCKET: "/portfolio/static-site/s3-bucket"
CLOUDFRONT_DISTRIBUTION_ID: "/portfolio/static-site/cf-distribution-id"
phases:
install:
commands:
- echo "Install phase - nothing to install for basic static site"
build:
commands:
- echo "Build phase - no framework build step for plain HTML"
post_build:
commands:
- echo "Deploying to S3..."
- aws s3 sync . s3://$S3_BUCKET --delete --exclude ".git/*" --exclude "buildspec.yml"
- echo "Creating CloudFront invalidation..."
- aws cloudfront create-invalidation --distribution-id $CLOUDFRONT_DISTRIBUTION_ID --paths "/*"
artifacts:
files:
- '**/*'π‘ Note: Sensitive values (
S3_BUCKET,CLOUDFRONT_DISTRIBUTION_ID) are pulled from AWS Systems Manager Parameter Store β no secrets hardcoded in the repo.
The CodeBuild service role follows least-privilege principles:
| Permission | Resource | Purpose |
|---|---|---|
s3:ListBucket |
Content bucket | List existing objects |
s3:GetObject |
Content bucket /* |
Read files during sync |
s3:PutObject |
Content bucket /* |
Upload new/changed files |
s3:DeleteObject |
Content bucket /* |
Remove deleted files (--delete) |
cloudfront:CreateInvalidation |
Distribution ARN | Purge CDN cache |
logs:* |
CodeBuild log group | Write build logs |
ssm:GetParameters |
Parameter Store paths | Read deploy config |
| Control | Implementation |
|---|---|
| π« No public S3 | Block all public access enabled; bucket policy only allows CloudFront OAC |
| π HTTPS enforced | CloudFront viewer protocol redirects HTTP β HTTPS; ACM TLS on custom domain |
| π Least-privilege IAM | CodeBuild role scoped to specific bucket + distribution only |
| ποΈ Encryption | S3 default encryption (SSE-S3); TLS in transit |
| π Audit trail | CloudFront access logs + CodeBuild logs in CloudWatch |
| π Origin isolation | OAC replaces legacy OAI β S3 is never directly accessible |
| Setting | Value |
|---|---|
| Region | us-east-1 (CloudFront metrics) |
| Metric | 5xxErrorRate |
| Period | 5 minutes |
| Threshold | > 1% for 1 consecutive period |
| Action | SNS β email notification |
- Destination:
shaun-portfolio-cf-logs(private S3 bucket) - Use cases:
- π Debug 4xx/5xx errors
- π Analyse traffic patterns
- π Future: feed into Athena/Glue for dashboards
- Log group:
/aws/codebuild/shaunvividsz-portfolio-build - Region:
eu-west-1 - Contents: Full output of
s3 syncandcloudfront create-invalidation
- AWS Budgets monthly limit with email alerts on actual + forecasted spend
- CloudWatch billing alarm on
EstimatedChargesinus-east-1
This architecture is designed for near-zero cost for a personal site:
| Service | Cost Driver | Expected |
|---|---|---|
| ποΈ S3 | Storage + GET requests | ~ $0.01/mo |
| π CloudFront | Data transfer + requests | ~ $0.00 (free tier) |
| π Route 53 | 1 hosted zone + queries | ~ $0.50/mo |
| π ACM | Public certificate | Free |
| π CodePipeline | 1 pipeline | Free (1 free) |
| π¨ CodeBuild | Build minutes | ~ $0.00 (100 min/mo free) |
| π CloudWatch | 1 alarm + logs | ~ $0.10/mo |
| Total | < $1/mo |
β No EC2 instances, no RDS databases, no continuously running compute.
# 1. Make your changes
vim index.html
# 2. Commit and push
git add .
git commit -m "Update portfolio"
git push origin main
# 3. CodePipeline automatically:
# π₯ Pulls code from GitHub
# π¨ Runs CodeBuild (buildspec.yml)
# π€ Syncs to S3 + invalidates CloudFront cache
#
# 4. β
Live at https://www.shaunvividszportfolio.com| Stage | Action | Duration |
|---|---|---|
| π₯ Source | Pull from GitHub main |
~10s |
| π¨ Build | CodeBuild runs buildspec.yml |
~30s |
| π€ Deploy | s3 sync + CloudFront invalidation |
~20s |
| β Live | Changes propagated to edge locations | ~60s |
AWS-PORTFOLIO-WEBSITE/
βββ π index.html # Main portfolio page (HTML + CSS + Lucide icons)
βββ π buildspec.yml # CodeBuild deploy specification
βββ π README.md # This documentation
βββ π projects/ # Individual project READMEs
βββ readme (2).md # Serverless Blog Platform
βββ README (3).md # Three-Tier Blog App
βββ README (4).md # Serverless Ordering System
βββ README (5).md # Static Portfolio Site (this project)
| Challenge | Resolution |
|---|---|
π ACM cert must be in us-east-1 for CloudFront |
Created cert in us-east-1 with DNS validation via Route 53 |
| π OAI vs OAC for S3 origin | Used modern OAC β simpler bucket policy, supports SSE-KMS |
| π Cache stale after deploy | Added cloudfront create-invalidation --paths "/*" in buildspec |
| π CodeBuild IAM too broad initially | Scoped down to specific bucket ARN + distribution ARN only |
| π No visibility into errors | Added CloudWatch 5xx alarm + SNS email notification |
| # | Project | Architecture | Key Services |
|---|---|---|---|
| 01 | Portfolio Site (this) | Static + Edge | S3, CloudFront, CodePipeline |
| 02 | Three-Tier Blog App | VPC + 3-Tier | VPC, ALB, EC2 ASG, RDS |
| 03 | Serverless Blog Platform | Serverless Full-Stack | Lambda, API GW, DynamoDB |
| 04 | Serverless Ordering System | Event-Driven Saga | Step Functions, SQS, DynamoDB |
Built by Shaun Β· Deployed on AWS Β· Automated with CodePipeline
