S3 // CloudFront // Route53 // GitHub Actions CI/CD // Lambda // DynamoDB
This project documents how I built and deployed my personal resume site on AWS. The goal was to create a lightweight portfolio that is easy to maintain, globally available, automated via CI/CD, and backed by a simple serverless component for a real visitor counter.
The result is a static website architecture that is inexpensive to run, straightforward to update, fully automated on code push, and a practical example of combining core AWS services into a small production-style deployment.
main.
The frontend was built with HTML, CSS, and a small amount of JavaScript. At a minimum, the deployment requires an index.html entry point and any supporting assets such as images, icons, or additional pages.
Because the site is static, updates are simple: modify the files locally and upload the latest version to the hosting bucket.
I created an S3 bucket aligned with the site domain and uploaded the static assets to it. S3 acts as the origin for the website content and provides a simple deployment target for future updates.
For a basic static hosting setup, the bucket can expose public read access to the site objects. An example policy is shown below:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::mycoolwebsite.com/*"
}
]
}I placed CloudFront in front of the S3 origin to improve global performance, enable HTTPS, and provide a cleaner public entry point for the website.
In this setup, the S3 website endpoint is used as the origin rather than the REST endpoint.
mycoolwebsite.com.s3-website-ap-southeast-1.amazonaws.comindex.htmlRoute53 was used to manage DNS for the custom domain. The public record is an A alias record pointing to the CloudFront distribution, which keeps the URL clean and avoids exposing the distribution domain to visitors.
Type: A (Alias)
Name: mycoolwebsite.com
Alias Target: dxxxxxxxxxxxx.cloudfront.netTo make the site slightly more dynamic, I added a visitor counter backed by DynamoDB and Lambda. Unlike a browser-only solution, this approach keeps a shared count that is visible to every visitor.
VisitorCount with a primary key like id.Example Lambda Function (Node.js):
const AWS = require('aws-sdk');
const dynamo = new AWS.DynamoDB.DocumentClient();
exports.handler = async (event) => {
const params = {
TableName: 'VisitorCount',
Key: { id: 'main' },
UpdateExpression: 'ADD #c :inc',
ExpressionAttributeNames: { '#c': 'count' },
ExpressionAttributeValues: { ':inc': 1 },
ReturnValues: 'UPDATED_NEW'
};
const result = await dynamo.update(params).promise();
return {
statusCode: 200,
headers: { 'Access-Control-Allow-Origin': '*' },
body: JSON.stringify({ count: result.Attributes.count })
};
};Example Client-Side Request:
<script>
fetch('https://lqk9r6hlp1.execute-api.ap-southeast-1.amazonaws.com/default/VisitorCounter')
.then(res => res.json())
.then(data => {
document.getElementById('visitors').textContent = data.count;
});
</script>Instead of manually logging into AWS or executing AWS CLI commands locally to upload files, I configured an automated continuous deployment pipeline using GitHub Actions.
Whenever new code is pushed to the main branch, the workflow triggers automatically to perform two primary actions:
aws s3 sync to transfer only updated static assets to the S3 origin bucket while removing deleted files.create-invalidation request so CloudFront edge nodes instantly discard old cached assets and pull the fresh files from S3.Example GitHub Actions Deployment Workflow (.github/workflows/deploy.yml):
name: Deploy Resume Website to AWS
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Configure AWS Credentials
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-southeast-1
- name: Sync Static Assets to S3
run: |
aws s3 sync . s3://mycoolwebsite.com --delete --exclude ".git*" --exclude ".github*"
- name: Invalidate CloudFront Cache
run: |
aws cloudfront create-invalidation --distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} --paths "/*"This project was a practical exercise in building and operating a complete cloud-hosted application. It combines static asset hosting, CDN edge delivery, custom DNS management, automated CI/CD deployment pipelines, and serverless backend components in a way that is production-ready, low-cost, and easy to maintain.
Beyond serving as my personal website, it acts as a compact case study for deploying static frontend applications to AWS with automated DevOps practices.