Architecture · Terraform

How it's built.

visitors tracked by Lambda + DynamoDB
Path A — Static Site
Path B — Serverless API
👤 Browser
Visitor
DNS lookup
🌐 DNS
Route 53
routes to
🔒 CDN · HTTPS
CloudFront
ACM cert
static request
🪣 Storage
S3 Bucket
HTML · CSS · JS
served via OAC
API call
🔌 REST API
API Gateway
Serverless
Lambda
WIP
🗃️ NoSQL DB
DynamoDB
WIP
visitor counter
contact form
CI/CD
🐙 git push
→ triggers →
⚙️ GitHub Actions
→ terraform apply →
AWS

Service Breakdown

Every tool, explained.

🪣 In Progress

Amazon S3

Static Website Hosting

All the HTML, CSS, and JS files for this site live in an S3 bucket. S3 is object storage cheap, durable, and scales automatically. The bucket is configured for static website hosting with public-read access blocked at bucket level (CloudFront handles delivery instead).

Bucket policies Static hosting Object storage
🌍 In Progress

Amazon CloudFront

CDN + HTTPS

CloudFront sits in front of S3 and serves the site from edge locations globally so visitors get fast load times regardless of where they are. It also handles HTTPS via an ACM (AWS Certificate Manager) certificate, meaning the site runs securely on a custom domain.

CDN / Edge locations HTTPS / TLS Origin access control
🌐 Planned

Amazon Route 53

DNS Management

Route 53 will handle the DNS for the custom domain pointing it at the CloudFront distribution. This demonstrates understanding of DNS records (A records, CNAME, aliases), hosted zones, and how domain resolution works in AWS.

Hosted zones DNS records Alias records
Planned

AWS Lambda

Visitor Counter API

A serverless Python function that runs whenever someone visits the site it reads the current visitor count from DynamoDB, increments it, and returns the value. No server to manage, no cost when idle. This is the core of the Cloud Resume Challenge's backend requirement.

Serverless Python Event-driven IAM roles
🗃️ Planned

Amazon DynamoDB

Visitor Count Storage

A single DynamoDB table stores the visitor counter. DynamoDB is a NoSQL key-value database fully managed, serverless, and scales to any load. For this use case it's overkill, but that's the point: learning when and how to use it.

NoSQL Key-value store Read/write capacity
⚙️ Planned

GitHub Actions

CI/CD Pipeline

Every time I push code to the GitHub repo, a GitHub Actions workflow automatically syncs the updated files to S3 and invalidates the CloudFront cache. No manual uploads the pipeline handles deployment. This demonstrates real DevOps practice.

Terraform CI/CD YAML workflows AWS credentials S3 sync
🔐 Planned

AWS IAM

Access & Permissions

IAM roles and policies control what each service can access. Lambda gets a role that only allows it to read/write the specific DynamoDB table. GitHub Actions uses an IAM user with only the permissions needed to deploy. Least privilege principle throughout.

IAM roles Policies Least privilege
🔑 Planned

AWS Certificate Manager

SSL/TLS Certificate

ACM provisions and manages the HTTPS certificate for the custom domain. It handles renewal automatically no manual certificate management. Attached to the CloudFront distribution so all traffic is encrypted in transit.

TLS certificates HTTPS Auto-renewal
🔗 Planned

Amazon API Gateway

REST API for Lambda

API Gateway creates a public HTTPS endpoint that the frontend JavaScript can call to trigger the Lambda visitor counter. It handles routing, request validation, and CORS connecting the frontend to the serverless backend cleanly.

REST API CORS Lambda integration

Bonus Feature

Contact form, stored in AWS.

The contact form on this site does not just send an email and disappear. Every message submitted gets stored in DynamoDB via a Lambda function triggered by API Gateway. That means I have a record of every message in the cloud, queryable, durable, and fully serverless. It also gives this project a real two-way data flow to demonstrate, not just static hosting.

Contact Form Request Flow
📝 Form User submits
HTTPS POST
🔗 API Gateway REST endpoint
triggers
Lambda validates + saves
writes to
🗃️ DynamoDB stored forever

What gets stored

Name, email, message body, timestamp, unique message ID

Cost to run

Effectively zero. Lambda and DynamoDB free tier covers thousands of messages per month

Why it matters

Demonstrates a real serverless write pattern, not just static hosting

Why This Project

Skills it demonstrates.

Terraform IaC — entire stack defined as code. Reproducible, version-controlled infrastructure from a single config file.
S3 + CloudFront deployment Static hosting with a CDN, custom domain, HTTPS. The foundation of AWS web delivery.
Serverless architecture Lambda + API Gateway + DynamoDB is the classic serverless pattern for event-driven backends.
IAM least-privilege Every service only has the permissions it actually needs. Core security principle in AWS.
CI/CD pipeline Automated deployment via GitHub Actions means no manual processes, and demonstrates real DevOps thinking.
DNS & networking Route 53 + CloudFront + ACM covers how DNS resolution, CDN routing, and TLS certificates work together in AWS.
Frontend + backend integration JavaScript calling an API Gateway endpoint shows full-stack awareness, not just infrastructure.

Security

How it's secured.

Security isn't an afterthought here — it's built into every layer. This is what a secure-by-default serverless deployment looks like in practice.

🪣

S3 — Block Public Access

No direct bucket access

Block Public Access is enabled at the bucket level. Nobody can reach the files directly — all traffic must go through CloudFront. This is one of the most common S3 misconfigurations and it's explicitly locked down here. CloudFront uses an Origin Access Control (OAC) identity to fetch files, so S3 never needs to be public.

🌐

CloudFront — HTTPS Only

HTTP → HTTPS redirect enforced

CloudFront is configured to redirect all HTTP requests to HTTPS automatically. TLS 1.2 minimum. The ACM certificate is attached to the distribution so the custom domain gets HTTPS at no extra cost. No unencrypted traffic ever reaches the origin.

🔐

IAM — Least Privilege

No * permissions anywhere

Every role and policy is scoped to exactly what it needs. Lambda's execution role allows dynamodb:PutItem and dynamodb:GetItem on one specific table — nothing else. GitHub Actions CI/CD role can only push to S3 and invalidate CloudFront. No admin access, no wildcard actions.

API Gateway — CORS + Throttling

Rate limited, origin restricted

CORS is configured to only accept requests from the site's own domain — no other origin can call the API. Throttling is set at the stage level (requests per second + burst limit) to prevent abuse and unexpected Lambda invocations running up a bill. Both are things people often forget until something goes wrong.

🗃️

DynamoDB — Encryption at Rest

AWS managed keys, on by default

DynamoDB encrypts all data at rest by default using AWS-managed keys — no configuration required, but worth knowing it's there. Contact form submissions (name, email, message) are stored encrypted. No plaintext sensitive data sitting in a table.

🔑

No Hardcoded Secrets

Credentials never in source code

No AWS credentials, API keys, or secrets exist anywhere in the codebase. GitHub Actions authenticates via OIDC (OpenID Connect) — a short-lived token is issued per deployment, no long-lived access keys stored as secrets. Lambda uses its IAM execution role automatically. Nothing to rotate, nothing to leak.

Security posture summary

No public S3

Bucket is private. CloudFront OAC is the only entity that can read files.

No persistent credentials

OIDC for CI/CD, IAM roles for Lambda. Zero long-lived access keys in the codebase.

Encrypted in transit + at rest

HTTPS enforced by CloudFront. DynamoDB encrypted by default. TLS 1.2 minimum.

Cost Breakdown

What this actually costs.

One thing that separates people who understand the cloud from people who just use it: knowing what things cost. Here is a real monthly estimate for this entire stack, broken down by service, with free tier limits noted where they apply.

Assumptions: personal portfolio site, ~1,000 visitors/month, ~50 contact form submissions/month, files totalling ~5MB.

Service What it's doing Free tier Est. monthly
S3 Storing site files (~5MB) 5GB free forever $0.00
CloudFront CDN delivery, ~1GB transfer/month 1TB/month free (12mo) $0.00
Route 53 DNS for custom domain No free tier $0.50
ACM SSL certificate for HTTPS Free with CloudFront $0.00
Lambda Visitor counter + contact form (~1,050 invocations/mo) 1M requests free forever $0.00
DynamoDB Storing visitor count + contact messages 25GB + 25 WCU free forever $0.00
API Gateway REST API endpoint (~1,050 calls/month) 1M calls free (12mo) $0.00
GitHub Actions CI/CD pipeline, ~20 deploys/month 2,000 min/month free $0.00
Total estimated monthly cost ~$0.50

After free tier ends

CloudFront and API Gateway free tiers expire after 12 months. After that, estimated cost rises to roughly $0.60/month at this traffic level. Still less than a coffee.

If traffic scaled 100x

100,000 visitors/month would cost roughly $2-4/month. The serverless architecture means cost scales linearly with actual usage, no idle server costs.

Domain cost

The only real fixed cost is the domain name itself (~$12/year) plus the Route 53 hosted zone ($0.50/month). Everything else is effectively free at this scale.

Want to verify these numbers yourself?

AWS Pricing Calculator →

Infrastructure as Code

Provisioned with Terraform.

This project is being provisioned with Terraform. Every resource — the S3 bucket, CloudFront distribution, Lambda functions, DynamoDB tables, API Gateway, IAM roles — will be defined as code in a single Terraform config. One terraform apply to build the entire stack from scratch. That is Infrastructure as Code, and it is how real engineering teams work.

🏗️

Terraform

HashiCorp · Cloud-agnostic

Write declarative config files that describe your entire AWS infrastructure. Terraform figures out what needs to be created, changed, or deleted. The same config can deploy to AWS, GCP, or Azure with minimal changes.

# What this project would look like
resource "aws_s3_bucket" "site" {
  bucket = "mh-dev-portfolio"
  tags = { Env = "prod" }
}

resource "aws_cloudfront_distribution" "cdn" {
  origin { domain_name = aws_s3_bucket.site.bucket_domain_name }
  enabled = true
}
Declarative State management Multi-cloud Industry standard
☁️

CloudFormation

AWS Native · YAML/JSON

AWS's own IaC service. Write a YAML or JSON template that describes every resource. CloudFormation handles provisioning, rollbacks if something fails, and tracks everything as a stack you can update or delete in one shot.

# CloudFormation equivalent
Resources:
  SiteBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: mh-dev-portfolio
  CDN:
    Type: AWS::CloudFront::Distribution
AWS native Free to use Stack rollbacks Deep AWS integration

Why this matters to employers

Reproducibility

Spin up an identical environment in minutes. No more "works on my account" problems. Every environment, dev, staging, prod, is built from the same code.

Version control

Infrastructure changes go through pull requests like any other code. You can see who changed what, when, and roll back if something breaks.

It's on the job spec

Almost every cloud engineer role lists Terraform or CloudFormation. Understanding what IaC is and why it exists puts you ahead of candidates who have only ever used the console.

The goal: every resource in this stack defined as Terraform HCL. Run terraform apply once and the entire infrastructure comes up. Run terraform destroy and it's gone cleanly. No orphaned resources, no manual steps, no drift.