Terraform infrastructure for deploying highly secure, scalable, and cost-effective static sites on AWS using S3, CloudFront, Route53, ACM, and WAF.
- AWS WAF v2 with 5 layers of protection: Rate Limiting, OWASP Common Rules, Known Bad Inputs, IP Reputation, and Anonymous IP filtering
- Complete Security Headers: HSTS (preload), CSP, X-Frame-Options, X-Content-Type-Options, XSS-Protection, Referrer-Policy, Permissions-Policy
- Private S3 with AES-256 encryption (SSE-S3), versioning, and exclusive access via CloudFront (OAC with SigV4)
- TLS 1.2+ with configurable minimum protocol version
- Flexible authentication: supports both OIDC (recommended) and static access keys for GitHub Actions
- CloudFront with HTTP/3 (QUIC) for low latency
- IPv6 with A and AAAA records
- Automatic compression (Brotli/Gzip)
- SSL Certificate with SAN for root domain and www
- SPA routing via CloudFront Function
- CloudWatch Dashboard with request, error, bytes, and cache hit rate metrics
- Alarms for 5xx/4xx error rate spikes and WAF block surges
- Email notifications via SNS
- WAF Logging to CloudWatch (block/count events only)
- CloudFront Real-Time Metrics
- 4-step deployment with approval gates for safety
- Terraform Plan saved as artifact for review before applying
- Automatic DNS propagation check before certificate validation
- Automatic CloudFront cache invalidation after deploy
βββ src/ # Main infrastructure
β βββ main.tf # Provider and backend
β βββ variables.tf # Input variables (with validations)
β βββ locals.tf # Local values and tags
β βββ s3.tf # S3 bucket (encryption, versioning, upload)
β βββ cloudfront.tf # CloudFront (HTTP/3, headers, OAC, function)
β βββ acm.tf # ACM certificate (DNS validation, SAN www)
β βββ route53.tf # DNS (A, AAAA, www, ACM validation)
β βββ waf.tf # WAF v2 (rate limit, managed rules, logging)
β βββ monitoring.tf # CloudWatch (dashboard, alarms, SNS)
β βββ data.tf # Data sources
β βββ outputs.tf # Outputs
β βββ function/
β βββ function.js # CloudFront function (SPA rewrite)
β
βββ bootstrap/ # Terraform state backend
β βββ main.tf # S3 + DynamoDB for state locking
β βββ variables.tf
β βββ outputs.tf
β
βββ .github/
βββ workflows/
β βββ bootstrap.yml # Create backend (manual, one-time)
β βββ deploy.yml # Main deployment (4 steps)
β βββ pr-check.yml # PR validation
βββ dependabot.yml
- AWS Account with permissions to create IAM, S3, CloudFront, Route53, ACM, and WAF resources
- Registered domain at any registrar (Route 53, GoDaddy, Namecheap, etc.)
- GitHub repository with GitHub Actions enabled
You have two options for authenticating GitHub Actions with AWS:
- In your repository, go to Settings > Secrets and variables > Actions
- Create two secrets:
AWS_ACCESS_KEY_ID: Your AWS access keyAWS_SECRET_ACCESS_KEY: Your AWS secret access key
OIDC uses short-lived tokens instead of long-lived access keys:
- In the AWS console, go to IAM > Identity providers > Add provider
- Select OpenID Connect
- Provider URL:
https://token.actions.githubusercontent.com - Audience:
sts.amazonaws.com - Create an IAM Role with a trust policy for your repository:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::YOUR_ACCOUNT_ID::oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:YOUR_ORG/YOUR_REPO:*"
}
}
}
]
}- Attach policies for S3, CloudFront, Route53, ACM, WAF, CloudWatch, and SNS
- In your GitHub repository, add the secret
AWS_ROLE_ARNwith the role ARN
Note: The workflows automatically detect which credentials are available. If
AWS_ROLE_ARNis set, OIDC is used. Otherwise, it falls back to access keys.
- In GitHub, go to Actions > Bootstrap State Backend > Run workflow
- Enter the S3 bucket name for the state
- After execution, update
src/main.tfwith the S3 backend configuration
Create a src/terraform.tfvars file:
domain = "yourdomain.com"
bucket_name = "yourdomain-static-site"
files_path = "./function"
# Optional (all cost-bearing features default to false)
project_name = "MySite"
environment = "production"
enable_waf = true
enable_monitoring = true
enable_s3_versioning = true
notification_email = "your@email.com"
csp_policy = "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';"Add the DOMAIN variable in GitHub at Settings > Environments > dns-validated > Environment variables.
- Push to
mainβ triggers the workflow - Plan runs automatically and saves the plan as an artifact
- DNS Deploy β creates the Route53 zone and outputs nameservers in the logs
- Configure nameservers at your domain registrar
- Wait for DNS propagation (minutes to hours)
- Approve the
dns-validatedenvironment β the workflow verifies DNS propagation and creates the ACM certificate + full infrastructure - Approve the
productionenvironment β uploads files and invalidates the cache
Push β [Plan] β [DNS Deploy] β Nameservers in logs
β
Configure nameservers at registrar
β
[Approve dns-validated] β [Verify DNS] β [ACM + Infra]
β
[Approve production] β [Upload + Cache Invalidation]
| Variable | Type | Default | Description |
|---|---|---|---|
domain |
string | β | Site domain (e.g., example.com) |
bucket_name |
string | β | S3 bucket name |
files_path |
string | β | Path to static site files |
domain_enabled |
bool | false |
Enable domain-related resources (ACM, CloudFront, etc.) |
enable_waf |
bool | false |
Enable WAF (~$5/month + rules + requests) |
project_name |
string | StaticSite |
Project name for tags |
environment |
string | production |
Environment (development/staging/production) |
csp_policy |
string | restrictive | Customizable Content-Security-Policy |
waf_rate_limit |
number | 2000 |
Max requests per 5min per IP |
enable_monitoring |
bool | false |
CloudWatch dashboard and alarms (~$10-15/month) |
notification_email |
string | "" |
Email for alerts (empty = disabled) |
minimum_tls_version |
string | TLSv1.2_2021 |
Minimum TLS version |
force_destroy_zone |
bool | false |
Allow DNS zone destruction |
enable_s3_versioning |
bool | false |
S3 versioning (extra storage cost) |
All cost-bearing features are opt-in (disabled by default).
| Resource | Free Tier | Cost (if enabled) | Variable |
|---|---|---|---|
| S3 + CloudFront | 5GB + 1TB/month + 10M req | < $0.05/month | always active |
| Route53 | β | $0.50/month per zone | always active |
| ACM | Free | Free | always active |
| S3 Versioning | β | ~$0.02-2.00/month | enable_s3_versioning |
| WAF | β | ~$7-10/month | enable_waf |
| Monitoring | 10 free alarms | ~$10-15/month | enable_monitoring |
- Verify nameservers were correctly configured at the registrar
- Use
dig NS yourdomain.comto check propagation - Some registrars take up to 48 hours to propagate
- Confirm nameservers point to Route53
- Check validation records:
dig CNAME _acm-challenge.yourdomain.com - ACM has a 72-hour timeout for validation
- The S3 bucket is private by design β access only via CloudFront
- Verify the bucket policy references the correct CloudFront distribution
- Review WAF logs in CloudWatch (
aws-waf-logs-*) - Adjust
waf_rate_limitas needed
- Ensure either
AWS_ACCESS_KEY_ID+AWS_SECRET_ACCESS_KEYorAWS_ROLE_ARNsecrets are configured - For OIDC, verify the IAM OIDC provider and role trust policy are set up correctly
- The PR quality check workflow does not require AWS credentials
Contributions are welcome! Open issues and pull requests to improve this project.