A public, live portfolio, tracking my UK cloud job hunt, every application, sponsorship status, certifications and interview outcome. Built to demonstrate AWS skills and to help other international students see what works.
- Static site: HTML/CSS/JS hosted in a private S3 bucket
- CDN + HTTPS: CloudFront with Origin Access Control (OAC), ACM certificate
- DNS: Route 53 with a custom domain
- API: API Gateway → Lambda → DynamoDB for visitor counter
- CI/CD: GitHub Actions deploying on push to
mainvia OIDC federation (no long-lived AWS keys)
Live at: https://sabbirahmed.uk
- Private S3 bucket holding HTML/CSS/JS assets
- CloudFront distribution with Origin Access Control (OAC) reading from S3
- HTTPS via ACM certificate (us-east-1)
- TLSv1.2_2021 security policy
- HTTP/2 enabled, global edge distribution
- Route 53 hosted zone with alias A-records for apex and www
- No public access to S3; all reads go through CloudFront
Visitor counter implemented as:
- DynamoDB table (
visitor-count) — single item, atomic increment viaADDUpdateExpression - Lambda function (
visitor-counter, Python 3.14) — boto3 against DynamoDB - API Gateway HTTP API —
GET /visit, CORS configured for browser clients - IAM execution role scoped to
dynamodb:GetItemanddynamodb:UpdateItemon the specific table ARN (least privilege)
Client-side fetch from index.html updates the count on page load.
Push to main deploys the site automatically:
- GitHub Actions workflow triggers on push
- Workflow assumes an AWS IAM role via OIDC federation — no long-lived AWS access keys stored in GitHub
- Role's trust policy is scoped to
repo:sabbirsavvy/cloud-portfolio:ref:refs/heads/main— only main branch on this repo can assume it - Permissions policy scoped to
s3:PutObject/DeleteObject/ListBucket/GetObjecton the one bucket +cloudfront:CreateInvalidationon the one distribution - Workflow syncs files to S3 and issues a CloudFront invalidation
- Site is live ~60-90 seconds after
git push
End-to-end: editing index.html locally → live at https://sabbirahmed.uk in under two minutes, no manual steps, no credentials in repo secrets.
Metrics chosen to answer six questions at a glance:
- Is anyone visiting? CloudFront request count.
- Are visitors getting errors? CloudFront total error rate.
- Is the API working? API Gateway 2xx/4xx/5xx response mix.
- Is the Lambda healthy? Invocations, errors, average duration.
- Is DynamoDB happy and within capacity? Consumed read/write capacity units.
- Is anything starting to cost real money? Estimated charges in USD.
The dashboard is the answer to "how would you operate this if it had real users?"
A working contact form on the site, end-to-end via AWS:
- HTML form → JavaScript
fetchPOSTs to API Gateway - API Gateway
POST /contact→ second Lambda (contact-form-handler, Python 3.14) - Lambda validates input (length limits, required fields, basic email format), then calls SES
SendEmail - Email arrives in my inbox, with
Reply-Toset to the form-filler's address so direct replies work - Lambda IAM role scoped to
ses:SendEmailon the specific verified identity ARN — not the whole account
SES is in sandbox mode (200 emails/day cap, recipients must be verified). For this use case the only recipient is me, so sandbox is sufficient — no production-access request needed. A real product with arbitrary recipients would need that step.