⬅ Back to Home

AWS

S3 // CloudFront // Route53 // GitHub Actions CI/CD // Lambda // DynamoDB

Project Overview:

Overview

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.

Architecture Overview

AWS Static Website Architecture Diagram

Step 1: Prepare the Static Site

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.

Step 2: Configure the S3 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/*"
    }
  ]
}

Step 3: Set Up CloudFront

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.

Step 4: Connect the Domain with Route53

Route53 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.net

Step 5: Add a Shared Visitor Counter

To 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.

  1. Create a DynamoDB table such as VisitorCount with a primary key like id.
  2. Create a Lambda function that increments the stored count and returns the updated value.
  3. Expose the function through API Gateway so the static site can request the current count from JavaScript.

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>

Step 6: Automate Deployments with GitHub Actions (CI/CD)

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:

  1. S3 Sync: Executes aws s3 sync to transfer only updated static assets to the S3 origin bucket while removing deleted files.
  2. CloudFront Cache Invalidation: Issues a 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 "/*"

Outcome and Key Takeaways

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.

⬅ Return to Home