I recently came across a Sub stack article titled “Build Your Cloud Portfolio: 10 Projects That Will Land You the Interview.” To put theory into practice, I decided to start with the first challenge: Level 1 – The Foundation (Beginner).
This foundational project focuses on core cloud services, static website hosting, and basic networking concepts—all essential building blocks for anyone starting out in cloud engineering. It was a natural place to begin, as it emphasizes understanding how cloud infrastructure works together rather than jumping straight into advanced tooling.
The Architecture: S3 (Frontend) → CloudFront/CDN → API Gateway → Lambda/Functions → DynamoDB.
1.Architecture Overview
We are building:
S3 (Frontend) → CloudFront/CDN → API Gateway → Lambda → DynamoDB
S3: Hosts the static frontend (HTML, CSS, JS).
CloudFront: CDN for caching and delivering frontend globally.
API Gateway: Exposes an HTTP POST route /visit.
Lambda: Runs serverless JS code to update visitor counts.
DynamoDB: Stores the visitor count.
2. Frontend Setup
My frontend/ folder contains:
index.htmlmain.jsstyle.cssassets/Goal: Increment a visitor counter in the footer using JS calling the Lambda API.
Mistake: Initially, API_URL wasn’t defined in main.js, causing
ReferenceError: API_URL is not defined.Fix:
Defined const API_URL = "<your-api-endpoint>/visit";at the top ofmain.js.
3. Lambda Setup
Created Lambda function
visitorCounterwith Node.js 24.x runtime.First Mistake: Using ES modules (
import) causedSyntaxError: Cannot use import statement outside a module.Fix: Ensure the Lambda handler is set to
visitorCounter.handlerand zipped properly.Second Mistake: Lambda role didn’t have DynamoDB permissions, causing:
AccessDeniedException: ...not authorized to perform: dynamodb:UpdateItem...
Fix: Added inline policy
VisitorCounterDynamoDBAccessto the Lambda execution role.Lambda now uses this updated code:
import { DynamoDBClient, UpdateItemCommand } from "@aws-sdk/client-dynamodb";
const client = new DynamoDBClient({ region: "us-east-1" });
export const handler = async () => {
const params = {
TableName: "VisitorCounter",
Key: { counter: { S: "visits" } },
UpdateExpression: "SET #v = if_not_exists(#v, :start) + :inc",
ExpressionAttributeNames: { "#v": "value" },
ExpressionAttributeValues: {
":start": { N: "0" },
":inc": { N: "1" }
},
ReturnValues: "UPDATED_NEW"
};
const command = new UpdateItemCommand(params);
const result = await client.send(command);
return {
statusCode: 200,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ count: result.Attributes.value.N })
};
};
4. API Gateway Setup
Created HTTP API
VisitorCounterAPIwith POST /visit route.Attached Lambda integration to this route.
Added permission for API Gateway to invoke Lambda:
aws lambda add-permission \
--function-name visitorCounter \
--principal apigateway.amazonaws.com \
--statement-id apigateway-invoke \
--action lambda:InvokeFunction \
--source-arn arn:aws:execute-api:us-east-1:579747663377:6yxams50x3/*/POST/visit
-
Enabled CORS for CloudFront + browser requests:
- Access-Control-Allow-Origin:
* - Allowed Methods:
POST
- Access-Control-Allow-Origin:
-
Mistake: Initially got
CORSerrors and500 Internal Server Error.- Fix: Enabled CORS and corrected Lambda code (ReturnValues + proper JSON body).
The Invoke URL:
https://6yxams50x3.execute-api.us-east-1.amazonaws.com/visit
- Frontend JS calls this URL to increment the counter.
5. CloudFront & S3
S3 Bucket:
cloud-resume-alokhostsindex.html,main.js,style.css,assets/.CloudFront Distribution:
d2ruu2h7u4sx55.cloudfront.netpoints to the S3 bucket.-
Mistake: Uploading JS with
--acl public-readfailed (AccessControlListNotSupported).-
Fix: Removed
--aclsince the bucket policy handles public access.
-
Fix: Removed
-
Mistake: Page showed
XML Access Denied.- Fix: Enabled proper Origin Access Control (OAC) for CloudFront to read S3 bucket.
Invalidated
main.jsto update browser cache:
aws cloudfront create-invalidation --distribution-id E1UKGUKGZU5H7B --paths /main.js
6. DynamoDB
Table:
VisitorCounterPartition key:
counter(String)Stored visitor count: attribute
value(Number).Verified that API calls increment the value:
visits: 7
7. Frontend JS
- Updated
main.js:
document.getElementById("year").textContent = new Date().getFullYear();
const API_URL = "https://6yxams50x3.execute-api.us-east-1.amazonaws.com/visit";
async function incrementVisitorCounter() {
try {
const response = await fetch(API_URL, { method: "POST" });
if (response.ok) {
const data = await response.json();
document.getElementById("visitor-count").textContent = data.count;
console.log("Visitor count updated!");
} else console.error("Failed to update visitor count", response.statusText);
} catch (err) {
console.error("Error calling API", err);
}
}
incrementVisitorCounter();
- Added a span in footer to display count:
<p>© <span id="year"></span> Alok | Visitors: <span id="visitor-count"></span></p>
-
Mistake: Earlier,
exportstatements causedUnexpected token 'export'in browser.- Fix: Used plain JS for frontend; Lambda uses ES modules internally.
8. Logs & Debugging
- Used CloudWatch Logs for Lambda errors:
/aws/lambda/visitorCounter.
Errors encountered:
Runtime.UserCodeSyntaxError→ fixed by correcting module syntax.AccessDeniedException→ fixed with proper IAM policy.CORS errors→ fixed in API Gateway settings.
9. GitHub & Terraform
Your frontend repo is on GitHub.
S3 + CloudFront can sync automatically using CI/CD pipelines, but we haven’t yet wired Terraform for this.
✅ Status Now
Frontend loads from CloudFront.
Visitor counter increments on page load.
Lambda updates DynamoDB correctly.
CORS and permissions fixed.
Logs available in CloudWatch.
Final puzzle
Terraform automation for the entire stack (S3, CloudFront, Lambda, API Gateway, DynamoDB).
Auto-sync from GitHub for frontend updates.
Top comments (0)