Cloud Resume Challenge on AWS
Deploy your resume as a serverless AWS app with a live visitor counter — S3, CloudFront, Lambda, DynamoDB, API Gateway, GitHub Actions.
Domains & Technologies
Blueprint Walkthrough
Architecture Overview
This project deploys a static resume as a fully serverless AWS application with a live visitor counter. A browser request hits CloudFront, which serves cached HTML/CSS/JS from an S3 bucket over HTTPS. Client-side JavaScript then calls an API Gateway endpoint, which invokes a Lambda function that atomically increments a counter stored in DynamoDB.
Every layer here maps directly to a production pattern: S3 for static assets, CloudFront for CDN + TLS, API Gateway as the HTTP front door, Lambda for stateless compute, and DynamoDB for low-latency key-value storage. GitHub Actions ties it together with automated test-and-deploy pipelines for both the frontend and backend.
[ Browser ] | v[ CloudFront ] (HTTPS, edge caching) | v[ S3 Bucket ] (HTML / CSS / JS) | v (JS fetch call)[ API Gateway ] (GET /count) | v[ Lambda Function ] (Python, boto3) | v[ DynamoDB Table ] (visitor count)RememberEvery service used here fits inside the AWS Free Tier at this traffic volume — S3, CloudFront, Lambda, DynamoDB, and API Gateway all have generous free allowances.
What each service actually does (read this before you start)
If you are new to AWS, here is what each of the five services is, in plain terms, before you touch any commands:
- S3 (Simple Storage Service) is object storage — think of it like a folder in the cloud that can hold any file. It has a special mode called "static website hosting" that lets it serve HTML/CSS/JS files directly to a browser, like a basic web server, but with no server for you to manage or patch.
- CloudFront is a Content Delivery Network (CDN). It copies your files to data centers around the world so people load your site from a server near them instead of one far away. It also gives you free HTTPS — S3 alone cannot do this.
- DynamoDB is a NoSQL database — a place to store simple pieces of data (like a visitor count) without needing to design tables and relationships the way you would in MySQL or PostgreSQL. You just store a key and a value.
- Lambda is a way to run a small piece of code (a "function") without owning or managing a server. AWS runs your Python code only when something triggers it, and you are billed only for the milliseconds it actually runs.
- API Gateway is the "front door" that lets a browser call your Lambda function over the internet. A browser cannot call Lambda directly — API Gateway turns your function into a normal HTTPS URL the browser can
fetch().
Together: the browser loads your resume from CloudFront/S3, then JavaScript on the page quietly asks API Gateway "how many people have visited?", API Gateway wakes up Lambda, Lambda adds 1 to the number stored in DynamoDB and hands the new number back, and your page displays it.
Problem Solved
A resume as a PDF on your laptop has no way to prove you can operate cloud infrastructure — it just sits there. Recruiters see a document, not evidence of skill. This project turns your resume into a live, HTTPS-served application with a working backend, giving you a real URL and a real system to explain in interviews.
It also solves the deeper problem of environment setup for cloud beginners: instead of a toy example, you touch IAM roles, CDN caching, atomic database writes, CORS, and CI/CD — the same primitives used to run production APIs at scale.
| Component | Role | Free Tier Limit |
|---|---|---|
S3 |
Stores static files | 5 GB, 20K GETs/mo |
CloudFront |
HTTPS + CDN | 1 TB transfer/mo |
Lambda |
Runs counter logic | 1M requests/mo |
Before You Start
You need four things set up. If you already have them, skip ahead to Milestone 1.
- An AWS account. Sign up free at
aws.amazon.com— you'll need a credit/debit card for identity verification, but this entire project stays inside the Free Tier if you follow the steps as written. - The AWS CLI installed. This is the command-line tool that lets you type
aws ...commands instead of clicking through the AWS web console. Install it fromaws.amazon.com/cli, then runaws configureand paste in an Access Key ID and Secret Access Key (create these under IAM → Users → Security credentials in the AWS Console). - A terminal. macOS/Linux: use the built-in Terminal. Windows: use WSL (Windows Subsystem for Linux) or Git Bash — the
bashcommands in this guide won't run in plain Command Prompt. - A GitHub account, for Milestone 7.
TipRun
aws sts get-caller-identityafter configuring the CLI. If it prints your AWS account ID back to you, your credentials are working and you're ready to go.
Step-by-Step Implementation Milestones
Milestone 1: Write and Package the Frontend
Create the project structure. frontend/ will hold the files your browser downloads; backend/ will hold the Lambda code — keeping them separate mirrors how real projects are organized:
mkdir cloud-resume && cd cloud-resumemkdir frontend backendfrontend/index.html. This is a plain HTML page — no framework, no build step. The <span id="visitor-count"> is the one part JavaScript will update after the page loads:
<html lang="en"><head> <meta charset="UTF-8"> <title>Cloud Resume</title> <link rel="stylesheet" href="style.css"></head><body> <div class="container"> <h1>Your Name</h1> <p>Cloud Engineer | AWS | Terraform</p> <footer>Visitors: <span id="visitor-count">Loading...</span></footer> </div> <script src="index.js"></script></body></html>frontend/style.css — basic styling so the page isn't unstyled text:
body { font-family: Arial, sans-serif; max-width: 700px; margin: 60px auto; color: #222;}.container { padding: 20px; }footer { margin-top: 40px; color: #888; font-size: 0.9em; }frontend/index.js — this is the only "logic" in the whole frontend. Read it line by line:
async function updateVisitorCount() { try { // This placeholder gets replaced with your real API Gateway // URL in Milestone 6, once that URL actually exists. const API_URL = 'YOUR_API_GATEWAY_URL_HERE'; // fetch() asks the browser to make an HTTP GET request. // "await" pauses this function until the response arrives. const res = await fetch(API_URL); // The Lambda function returns JSON like {"count": 42}. // .json() parses that response body into a JS object. const data = await res.json(); // Find the <span id="visitor-count"> in the HTML and // replace its text ("Loading...") with the real number. document.getElementById('visitor-count') .textContent = data.count; } catch (err) { // If the API isn't ready yet, or the network fails, // show something readable instead of a broken page. document.getElementById('visitor-count') .textContent = 'unavailable'; }} // Run the function immediately once the script loads.updateVisitorCount();TipKeep the frontend framework-free for this project. Vanilla HTML/CSS/JS keeps focus on the AWS plumbing rather than build tooling — you can always rebuild it in React later once the AWS side works.
Sanity-check the page locally before touching AWS — open frontend/index.html directly in your browser. The layout should look right and the counter will show "unavailable" (expected — there's no API yet). If the page itself is broken, fix that before moving to AWS.
Milestone 2: Create the S3 Bucket and Enable Static Hosting
S3 bucket names must be globally unique — across every AWS account in the world, not just yours. your-unique-resume-bucket-2026 will already be taken; replace it with something like yourname-resume-2026 everywhere it appears in this guide (all three commands below, plus every later milestone that references it).
# "mb" = make bucket. --region pins it to Mumbai (ap-south-1);# pick whatever region is closest to you or your target audience.aws s3 mb s3://your-unique-resume-bucket-2026 \ --region ap-south-1 # "sync" uploads every file in frontend/ to the bucket.# Run this again any time you change index.html, style.css, or index.js.aws s3 sync frontend/ \ s3://your-unique-resume-bucket-2026/ # Turns on "static website hosting" mode on the bucket.# --index-document is served for the root URL ("/").# --error-document is served for 404s — using index.html here# means any bad path still shows your resume instead of an error page.aws s3 website \ s3://your-unique-resume-bucket-2026/ \ --index-document index.html \ --error-document index.htmlBy default, S3 buckets are private — nobody outside your AWS account can read the files. A bucket policy is a JSON document that grants specific permissions to specific people. Here, "Principal": "*" means "anyone on the internet", and "Action": "s3:GetObject" means "can download files" (not upload, not delete):
cat > bucket-policy.json << 'EOF'{ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": "*", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::your-unique-resume-bucket-2026/*" }]}EOF # Attach that policy to the bucket. Without this step, visitors# get "Access Denied" even though the files exist.aws s3api put-bucket-policy \ --bucket your-unique-resume-bucket-2026 \ --policy file://bucket-policy.jsonRememberIf
put-bucket-policyfails with an error mentioning "Block Public Access", your account has a default safety setting that blocks public buckets. Turn it off for just this bucket in the S3 Console under the bucket's Permissions tab → "Block public access" → Edit → uncheck all four boxes → Save. This is safe here because you're only granting read access to static files, never write or delete.
Check that it worked — visit the S3 website endpoint shown by this command in your browser:
echo "http://your-unique-resume-bucket-2026.s3-website.ap-south-1.amazonaws.com"You should see your resume, over plain HTTP (no padlock yet — that comes in Milestone 3).
Milestone 3: Add CloudFront in Front of S3
A distribution is CloudFront's name for "a CDN configuration pointed at your content." Creating one tells AWS: copy whatever is at this S3 website endpoint out to edge locations worldwide, and give me a public HTTPS domain for it.
# --origin-domain-name points CloudFront at your S3 website endpoint# from Milestone 2 (not the bucket name — the s3-website URL).# --default-root-object means visiting the bare domain serves index.html.aws cloudfront create-distribution \ --origin-domain-name your-unique-resume-bucket-2026.s3-website.ap-south-1.amazonaws.com \ --default-root-object index.html \ --query 'Distribution.DomainName' \ --output textThis prints something like d1abc2def3ghij.cloudfront.net — save it, you'll need it repeatedly.
CloudFront takes 5–15 minutes to finish deploying to every edge location worldwide the first time. Check on it with:
aws cloudfront list-distributions \ --query 'DistributionList.Items[0].Status' \ --output textThis returns InProgress while it's still rolling out, then Deployed when it's ready. Only once it shows Deployed should you visit https://d1abc2def3ghij.cloudfront.net in your browser — you should see your resume, this time with a padlock icon (HTTPS).
Common MistakeForgetting that S3 website endpoints only serve plain HTTP. CloudFront is what adds the free TLS certificate — skipping it means no HTTPS padlock, and browsers increasingly warn users away from plain-HTTP sites.
TipIf the page loads but looks broken (missing CSS, broken images), you likely pointed CloudFront at the wrong origin — double check it's the
*.s3-website.*.amazonaws.comendpoint from Milestone 2, not the plain S3 bucket domain (*.s3.amazonaws.com), which is a different, non-website mode.
Milestone 4: Create the DynamoDB Table
Unlike a SQL table with fixed columns, a DynamoDB table only needs you to define its primary key upfront — every other attribute is added freely per item. Here the primary key is id (a string), and the whole table will hold exactly one item, since you only need one counter.
# --attribute-definitions declares "id" exists and is type S (String).# --key-schema says "id" is the HASH key — DynamoDB's term for# the primary key used to look items up.# --billing-mode PAY_PER_REQUEST means you pay only per read/write,# not for idle provisioned capacity — the right choice at this scale.aws dynamodb create-table \ --table-name cloud-resume-visitors \ --attribute-definitions AttributeName=id,AttributeType=S \ --key-schema AttributeName=id,KeyType=HASH \ --billing-mode PAY_PER_REQUEST \ --region ap-south-1 # Insert the starting item. "S" = String type, "N" = Number type# (DynamoDB is explicit about types in its JSON format).# This creates one row: { id: "visitors", count: 0 }.aws dynamodb put-item \ --table-name cloud-resume-visitors \ --item '{"id":{"S":"visitors"},"count":{"N":"0"}}'Confirm the item exists before moving on:
aws dynamodb get-item \ --table-name cloud-resume-visitors \ --key '{"id":{"S":"visitors"}}'You should see the item printed back with count: "0".
Milestone 5: Write and Deploy the Lambda Function
backend/lambda_function.py:
import jsonimport boto3 def lambda_handler(event, context): dynamodb = boto3.resource( 'dynamodb', region_name='ap-south-1' ) table = dynamodb.Table('cloud-resume-visitors') response = table.update_item( Key={'id': 'visitors'}, UpdateExpression='ADD #c :inc', ExpressionAttributeNames={'#c': 'count'}, ExpressionAttributeValues={':inc': 1}, ReturnValues='UPDATED_NEW' ) new_count = int(response['Attributes']['count']) return { 'statusCode': 200, 'headers': { 'Access-Control-Allow-Origin': '*', 'Content-Type': 'application/json' }, 'body': json.dumps({'count': new_count}) }SecurityUse
dynamodb:UpdateItemscoped to only this table in production instead ofAmazonDynamoDBFullAccess. Full access is used here only to simplify the learning setup.
Package and deploy:
cd backendzip function.zip lambda_function.py aws lambda create-function \ --function-name cloud-resume-counter \ --runtime python3.12 \ --role arn:aws:iam::ACCOUNT_ID:role/cloud-resume-lambda-role \ --handler lambda_function.lambda_handler \ --zip-file fileb://function.zip \ --region ap-south-1Milestone 6: Expose Lambda via API Gateway
API_ID=$(aws apigateway create-rest-api \ --name cloud-resume-api \ --query 'id' --output text) ROOT_ID=$(aws apigateway get-resources \ --rest-api-id $API_ID \ --query 'items[0].id' --output text) RESOURCE_ID=$(aws apigateway create-resource \ --rest-api-id $API_ID \ --parent-id $ROOT_ID \ --path-part count \ --query 'id' --output text) aws apigateway put-method \ --rest-api-id $API_ID \ --resource-id $RESOURCE_ID \ --http-method GET \ --authorization-type NONE aws apigateway create-deployment \ --rest-api-id $API_ID \ --stage-name prodUpdate index.js with the real API URL, then re-sync to S3 and invalidate the CloudFront cache:
aws s3 sync frontend/ s3://your-unique-resume-bucket-2026/aws cloudfront create-invalidation \ --distribution-id YOUR_DIST_ID \ --paths "/*"Milestone 7: Automate with GitHub Actions
.github/workflows/deploy.yml (backend repo):
name: Deploy Backendon: push: branches: [main]jobs: test-and-deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '3.12' - run: pip install pytest boto3 - run: pytest test_lambda.py -v - uses: aws-actions/configure-aws-credentials@v4 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: ap-south-1 - run: zip function.zip lambda_function.py - run: | aws lambda update-function-code \ --function-name cloud-resume-counter \ --zip-file fileb://function.zipSecurityNever commit AWS credentials to code. Store them only as GitHub Secrets — public repos are scanned continuously by bots looking for leaked keys.
Validation & Testing
# 1. Confirm the resume loads over HTTPScurl -I https://YOUR_DIST_ID.cloudfront.net## Expected: HTTP/2 200 # 2. Confirm the counter API respondscurl https://$API_ID.execute-api.ap-south-1.amazonaws.com/prod/count## Expected: {"count": N} # 3. Confirm DynamoDB is incrementingaws dynamodb get-item \ --table-name cloud-resume-visitors \ --key '{"id":{"S":"visitors"}}'## Expected: count increases on each API callCommon MistakeForgetting
Access-Control-Allow-Originin the Lambda response body. Without it, the browser silently blocks the fetch even though the API itself works — always test withcurlfirst to isolate frontend vs. backend issues.
Videos & Guides
AWS Static Website Hosting on S3 — Official Guide
Official AWS docs for configuring S3 bucket static website hosting, including bucket policy and index/error document setup.
Amazon CloudFront Developer Guide
Reference for creating and configuring CloudFront distributions, origins, and cache invalidation.
AWS Lambda with DynamoDB — Boto3 Reference
Boto3 SDK reference for DynamoDB operations used in the Lambda counter function, including update_item and atomic counters.