In this project, I deployed melvyn-tan.com as a secure static website hosted on AWS. The site uses a private S3 bucket with CloudFront as the CDN, Route 53 for DNS, and ACM certificates for HTTPS. I set up logging, CloudTrail, and budgets for monitoring, and automated deployments first with AWS CodePipeline/CodeBuild before migrating to GitHub Actions with OIDC for long-term use.

Architecture

  • Route 53 hosted zone manages melvyn-tan.com and www.melvyn-tan.com.
  • CloudFront distribution fronts a private S3 bucket (melvyn-tan.com) with Origin Access Control (OAC).
  • ACM certificate in us-east-1 provides HTTPS for CloudFront.
  • CloudFront Functions handle:
    1. Redirect from www → apex.
    2. Rewrite of /page//page/index.html for pretty URLs.
  • CloudFront logs stored in S3; CloudTrail enabled for auditing API actions.
  • AWS Budgets + billing alarms notify of unexpected charges.

CloudFront distribution overview with ACM certificate
CloudFront distribution configured with ACM certificate, alternate domain names, and logging enabled.


What I built

1) Domain, DNS and TLS

  • Registered melvyn-tan.com in Route 53 and created a hosted zone.
  • Requested an ACM certificate in us-east-1 for both apex and www domains.
  • Attached certificate to CloudFront and configured alternate CNAMEs.
  • Learned that CloudFront requires ACM certificates in us-east-1, and Route 53 DNS validation makes setup simple.

2) Static hosting on S3 behind CloudFront

  • Created an S3 bucket with Block Public Access enabled, default encryption (SSE-S3), and versioning.
  • Did not enable the S3 website endpoint; instead used CloudFront as the entry point.
  • Configured Origin Access Control so only CloudFront can fetch objects from S3.
  • Wrote bucket policy scoped to:
    1. Allow only CloudFront origin requests.
    2. Deny all other public access.
  • Learned how OAC fully replaces OAI, keeping the bucket private and secure.

3) Edge behavior and URLs

  • Wrote CloudFront Function to redirect all requests from www.melvyn-tan.com to melvyn-tan.com.
  • Wrote another CloudFront Function to rewrite requests like /about/ into /about/index.html.
  • Set index.html as the default root object.
  • Learned that CloudFront Functions are lightweight, fast, and cheaper than Lambda@Edge for simple request/response rewrites.

4) Logging, auditing and budgets

  • Enabled CloudFront logs to an S3 bucket and set a lifecycle policy to transition old logs to Glacier for cost savings.
  • Enabled CloudTrail to log all management events across the account.
  • Created AWS Budgets and set up alarms to receive notifications if charges exceed thresholds.
  • Learned that proactive cost monitoring and logging are important to discover and address unexpected charges.

AWS Budgets alarm configuration
AWS Budget alarm configured to notify when costs exceed threshold.


Automation Phase 1: AWS CodePipeline and CodeBuild

To learn AWS DevTools, I first created a pipeline with CodePipeline and CodeBuild.

  • Source: connected my GitHub repo via AWS-managed GitHub App.
  • Build: created a CodeBuild project with a buildspec.yml file that:
    1. Installed Ruby, Bundler, and Jekyll.
    2. Ran the Jekyll build to generate the site into the _site folder.
    3. Synced _site/ to S3 with aws s3 sync --delete.
    4. Invalidated CloudFront cache with aws cloudfront create-invalidation --distribution-id <ID> --paths "/*".
  • IAM: created a role for CodeBuild with policies granting:
    1. s3:PutObject, s3:DeleteObject, s3:ListBucket only for arn:aws:s3:::melvyn-tan.com/*.
    2. cloudfront:CreateInvalidation for all distributions.
  • Troubleshooting:
    1. Fixed Jekyll build issues accordingly.
    2. Corrected command syntax (line continuations broke invalidations).
    3. Added missing ListBucket permissions to resolve AccessDenied errors.
  • Cost: CodePipeline costs $1 per month per pipeline regardless of use, CodeBuild billed per minute. I deleted CodePipeline after confirming it worked.
  • Learned: CodePipeline/CodeBuild is useful to understand AWS DevOps tooling, but not cost-effective for small static sites.

Automation Phase 2: GitHub Actions with OIDC

For long-term automation I migrated to GitHub Actions with OIDC (keyless auth).

  • IAM OIDC provider: added https://token.actions.githubusercontent.com with audience sts.amazonaws.com.
  • IAM role: created GitHubActionsDeployRole with:
    1. Trust policy allowing only repo:melvyn9/melvyn9.github.io:ref:refs/heads/melvyn-about.
    2. Permissions policy allowing S3 write access to melvyn-tan.com and cloudfront:CreateInvalidation.
  • GitHub Actions workflow (.github/workflows/deploy.yml) that triggers on pushes to the melvyn-about branch.
    1. Check out repo.
    2. Configure AWS credentials with OIDC.
    3. Install Ruby and dependencies via Bundler.
    4. Run Jekyll build to generate _site.
    5. Deploy with aws s3 sync _site/ s3://melvyn-tan.com --delete.
    6. Invalidate CloudFront cache with aws cloudfront create-invalidation --paths "/*".
  • Fixed bundler cross-platform issue by updating Gemfile.lock to support Linux builds.
  • Learned: OIDC eliminates long-lived AWS keys, restricts access to repo + branch, and automatically rotates temporary credentials.

GitHub Actions workflow run
Successful GitHub Actions workflow showing Jekyll build, S3 sync, and CloudFront invalidation.


Operational notes

  • S3 sync uploads only changed files, deletes removed files, and does not overwrite unchanged ones.
  • CloudFront invalidation with /* clears cache across all files, ensuring users always see the latest version. This is fine for small sites, though I could optimize later.
  • Verified that no AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY remain in GitHub Secrets since OIDC provides keyless auth.

Outcome

  • Pushing to the melvyn-about branch builds and deploys the site live within minutes.
  • The architecture ensures the S3 bucket is fully private, served only through CloudFront, with HTTPS enabled.
  • Clean URLs, logging, budgets, and auditing are in place for production-grade reliability.
  • I validated two CI/CD approaches (AWS-native vs GitHub Actions) and selected the one that is secure, simple, and free for my scale.

What I learned

  • How to design a private static site with CloudFront OAC and least-privilege S3 policies.
  • How to use CloudFront Functions for redirects and URL rewriting.
  • How to build and deploy Jekyll sites using CI/CD pipelines.
  • Practical IAM: trust policies vs permission policies, and scoping by repo/branch.
  • Debugging CI pipeline build issues and adapting lockfiles for different platforms.
  • Cost awareness: CodePipeline charges even when idle; GitHub Actions is free for public repos.