# S3 bucket for static assets
resource "aws_s3_bucket" "assets" {
bucket = "${var.project_name}-assets-${var.environment}"
}
resource "aws_s3_bucket_versioning" "assets" {
bucket = aws_s3_bucket.assets.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "assets" {
bucket = aws_s3_bucket.assets.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
resource "aws_s3_bucket_public_access_block" "assets" {
bucket = aws_s3_bucket.assets.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_s3_bucket_lifecycle_configuration" "assets" {
bucket = aws_s3_bucket.assets.id
rule {
id = "cleanup-old-versions"
status = "Enabled"
noncurrent_version_expiration {
noncurrent_days = 30
}
}
}
resource "aws_s3_bucket_cors_configuration" "assets" {
bucket = aws_s3_bucket.assets.id
cors_rule {
allowed_headers = ["*"]
allowed_methods = ["GET", "HEAD"]
allowed_origins = ["https://${var.domain_name}"]
max_age_seconds = 86400
}
}
# CloudFront distribution
resource "aws_cloudfront_distribution" "assets" {
enabled = true
is_ipv6_enabled = true
default_root_object = "index.html"
price_class = "PriceClass_100"
aliases = ["cdn.${var.domain_name}"]
origin {
domain_name = aws_s3_bucket.assets.bucket_regional_domain_name
origin_id = "s3-assets"
origin_access_control_id = aws_cloudfront_origin_access_control.assets.id
}
default_cache_behavior {
allowed_methods = ["GET", "HEAD", "OPTIONS"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "s3-assets"
compress = true
forwarded_values {
query_string = false
cookies {
forward = "none"
}
}
viewer_protocol_policy = "redirect-to-https"
min_ttl = 0
default_ttl = 86400
max_ttl = 31536000
function_association {
event_type = "viewer-response"
function_arn = aws_cloudfront_function.security_headers.arn
}
}
# Cache behavior for hashed assets (long cache)
ordered_cache_behavior {
path_pattern = "/assets/*"
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "s3-assets"
compress = true
forwarded_values {
query_string = false
cookies {
forward = "none"
}
}
viewer_protocol_policy = "redirect-to-https"
min_ttl = 31536000
default_ttl = 31536000
max_ttl = 31536000
}
viewer_certificate {
acm_certificate_arn = aws_acm_certificate.cdn.arn
ssl_support_method = "sni-only"
minimum_protocol_version = "TLSv1.2_2021"
}
restrictions {
geo_restriction {
restriction_type = "none"
}
}
}
resource "aws_cloudfront_origin_access_control" "assets" {
name = "${var.project_name}-oac"
origin_access_control_origin_type = "s3"
signing_behavior = "always"
signing_protocol = "sigv4"
}
# S3 bucket policy for CloudFront
resource "aws_s3_bucket_policy" "assets" {
bucket = aws_s3_bucket.assets.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
Service = "cloudfront.amazonaws.com"
}
Action = "s3:GetObject"
Resource = "${aws_s3_bucket.assets.arn}/*"
Condition = {
StringEquals = {
"AWS:SourceArn" = aws_cloudfront_distribution.assets.arn
}
}
}
]
})
}
# Security headers function
resource "aws_cloudfront_function" "security_headers" {
name = "${var.project_name}-security-headers"
runtime = "cloudfront-js-2.0"
code = <<-EOF
function handler(event) {
var response = event.response;
var headers = response.headers;
headers['x-frame-options'] = {value: 'DENY'};
headers['x-content-type-options'] = {value: 'nosniff'};
headers['x-xss-protection'] = {value: '1; mode=block'};
headers['strict-transport-security'] = {value: 'max-age=31536000; includeSubDomains'};
return response;
}
EOF
}
#!/bin/bash
set -euo pipefail
BUCKET="myapp-assets-production"
DISTRIBUTION_ID="E1234567890ABC"
BUILD_DIR="dist"
# Build assets
npm run build
# Sync hashed assets (long cache)
aws s3 sync "$BUILD_DIR/assets" "s3://$BUCKET/assets" --cache-control "public, max-age=31536000, immutable" --delete
# Sync HTML and other files (short cache)
aws s3 sync "$BUILD_DIR" "s3://$BUCKET" --cache-control "public, max-age=300" --exclude "assets/*" --delete
# Invalidate CloudFront cache
aws cloudfront create-invalidation --distribution-id "$DISTRIBUTION_ID" --paths "/index.html" "/manifest.json"
echo "Assets deployed and CDN cache invalidated"
AWS S3 stores static assets with high durability and availability. Bucket policies control access with JSON policy documents. CloudFront CDN distributes assets globally with edge caching. Origin Access Control (OAC) restricts S3 access to CloudFront only. cache-control headers and TTLs control caching behavior. Invalidation with /* clears CDN cache after deployments. S3 versioning enables rollback of assets. Lifecycle rules transition old versions to cheaper storage or delete them. CORS configuration allows cross-origin requests. Server-side encryption with SSE-S3 or SSE-KMS protects data at rest. CloudFront functions run lightweight logic at the edge for headers or redirects. Terraform provisions the complete stack declaratively.