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
andwww.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:
- Redirect from
www → apex
. - Rewrite of
/page/
→/page/index.html
for pretty URLs.
- Redirect from
- CloudFront logs stored in S3; CloudTrail enabled for auditing API actions.
- AWS Budgets + billing alarms notify of unexpected charges.
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 andwww
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:
- Allow only CloudFront origin requests.
- 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
tomelvyn-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 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:- Installed Ruby, Bundler, and Jekyll.
- Ran the Jekyll build to generate the site into the
_site
folder. - Synced
_site/
to S3 withaws s3 sync --delete
. - Invalidated CloudFront cache with
aws cloudfront create-invalidation --distribution-id <ID> --paths "/*"
.
- IAM: created a role for CodeBuild with policies granting:
s3:PutObject
,s3:DeleteObject
,s3:ListBucket
only forarn:aws:s3:::melvyn-tan.com/*
.cloudfront:CreateInvalidation
for all distributions.
- Troubleshooting:
- Fixed Jekyll build issues accordingly.
- Corrected command syntax (line continuations broke invalidations).
- 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 audiencests.amazonaws.com
. - IAM role: created
GitHubActionsDeployRole
with:- Trust policy allowing only
repo:melvyn9/melvyn9.github.io:ref:refs/heads/melvyn-about
. - Permissions policy allowing S3 write access to
melvyn-tan.com
andcloudfront:CreateInvalidation
.
- Trust policy allowing only
- GitHub Actions workflow (
.github/workflows/deploy.yml
) that triggers on pushes to themelvyn-about
branch.- Check out repo.
- Configure AWS credentials with OIDC.
- Install Ruby and dependencies via Bundler.
- Run Jekyll build to generate
_site
. - Deploy with
aws s3 sync _site/ s3://melvyn-tan.com --delete
. - 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.
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
orAWS_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.