res a disciplined approach to infrastructure provisioning. We recommend using Infrastructure as Code (IaC) to ensure reproducibility and auditability. The following TypeScript example utilizes the AWS CDK to provision the bucket, enforce security policies, and configure hosting parameters.
Architecture Decisions
- Bucket Naming: The bucket name must match the target domain exactly (e.g.,
assets.codcompass.io) if you intend to use Route 53 alias records for root domain routing.
- Public Access Strategy: AWS is deprecating ACL-based permissions. We use Bucket Policies exclusively for access control. We block public ACLs but allow public read access via policy to maintain compatibility while adhering to best practices.
- Versioning: Enabled by default to support rollback capabilities and accidental deletion protection.
- Error Handling: A custom error document is configured to handle 404 scenarios gracefully, essential for Single Page Applications (SPAs) that rely on client-side routing.
Implementation Code
This CDK construct provisions the storage layer with security and hosting configurations baked in.
import * as cdk from 'aws-cdk-lib';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as iam from 'aws-cdk-lib/aws-iam';
import { Construct } from 'constructs';
export interface StaticSiteProps {
domainName: string;
indexDocument: string;
errorDocument: string;
}
export class StaticSiteBucket extends Construct {
public readonly bucket: s3.Bucket;
constructor(scope: Construct, id: string, props: StaticSiteProps) {
super(scope, id);
this.bucket = new s3.Bucket(this, 'ProductionStaticAssets', {
bucketName: props.domainName,
versioned: true,
// Block public ACLs to enforce policy-based access
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ACLS,
// Configure native website hosting features
websiteIndexDocument: props.indexDocument,
websiteErrorDocument: props.errorDocument,
// Enable encryption at rest
encryption: s3.BucketEncryption.S3_MANAGED,
});
// Attach resource policy for public read access
this.bucket.addToResourcePolicy(new iam.PolicyStatement({
sid: 'AllowPublicRead',
effect: cdk.aws_iam.Effect.ALLOW,
principals: [new iam.AnyPrincipal()],
actions: ['s3:GetObject'],
resources: [this.bucket.arnForObjects('*')],
}));
// Output the website endpoint for verification
new cdk.CfnOutput(this, 'WebsiteEndpoint', {
value: this.bucket.bucketWebsiteUrl,
description: 'S3 Website Endpoint URL',
});
}
}
Rationale
blockPublicAccess.BLOCK_ACLS: This setting prevents the use of legacy ACLs, forcing all access control through the bucket policy. This reduces the attack surface and aligns with AWS security recommendations.
websiteIndexDocument / websiteErrorDocument: These properties configure the bucket to behave like a web server, serving the specified files when a directory is requested or an error occurs.
S3_MANAGED Encryption: Ensures all objects are encrypted at rest without managing keys, providing a baseline security requirement at no extra cost.
- Policy Scope: The policy grants
s3:GetObject to *. This is safe for public assets. Write operations are restricted to IAM roles with explicit s3:PutObject permissions, preventing unauthorized uploads.
Pitfall Guide
Production deployments often fail due to subtle configuration errors. The following pitfalls address common mistakes observed in real-world implementations.
| Pitfall Name | Explanation | Fix |
|---|
| The HTTPS Illusion | Developers assume S3 supports HTTPS on the website endpoint. It does not. The endpoint is HTTP-only. | Deploy CloudFront in front of the bucket. Configure the distribution to use the S3 origin and enable HTTPS viewer protocol policy. |
| Route 53 Alias Mismatch | Route 53 alias records pointing to S3 require the bucket name to match the domain exactly. A mismatch causes DNS resolution failure. | Ensure the S3 bucket name is identical to the domain (e.g., www.example.com bucket for www.example.com domain). |
| Stale Asset Delivery | Failing to set Cache-Control headers results in browsers re-fetching assets on every visit, increasing latency and S3 costs. | Set Cache-Control: max-age=31536000, immutable on hashed assets during upload. Use shorter TTLs for index.html. |
| Case-Sensitive Routing | S3 object keys are case-sensitive. Requesting /About when the file is about.html returns a 404. | Enforce lowercase naming conventions in your build pipeline. Configure the error document to handle client-side routing fallbacks. |
| CORS Configuration Gaps | Static sites making API calls to different domains fail without proper Cross-Origin Resource Sharing headers. | Add a CORS configuration to the bucket allowing GET requests from your domain. Use AllowedOrigins and AllowedHeaders. |
| Security Headers Absence | S3 does not inject security headers like X-Frame-Options or Content-Security-Policy. | Use CloudFront Functions or Lambda@Edge to inject security headers into responses before they reach the client. |
| Cost Leakage via Requests | High traffic to uncacheable content generates excessive S3 GET requests, which are billed per request. | Ensure CloudFront caching is effective. Set appropriate TTLs. Monitor 4xxErrorRate and 5xxErrorRate metrics to detect misconfigurations. |
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Internal Dev/Test | S3 Website Endpoint Only | Rapid deployment; HTTPS not required for internal tools. | Lowest. Pay only for storage and requests. |
| Public Production | S3 + CloudFront | Mandatory HTTPS; global performance; reduced S3 costs via caching. | Moderate. CDN adds cost but saves on S3 requests. |
| SPA with Client Routing | S3 + CloudFront + Lambda@Edge | CloudFront can intercept 404s and serve index.html for client-side routers. | Higher. Lambda@Edge execution costs apply. |
| High-Security App | S3 + CloudFront + OAI + WAF | Origin Access Identity restricts direct S3 access; WAF blocks malicious traffic. | Higher. WAF rules and OAI add complexity/cost. |
Configuration Template
Use this AWS CLI command to sync your build artifacts with optimized cache control. This template excludes HTML files from long-term caching while maximizing cache duration for static assets.
# Sync distribution folder to S3 with cache control strategies
aws s3 sync ./dist s3://prod-assets-codcompass.io \
--delete \
--cache-control "max-age=0, must-revalidate" \
--exclude "*.html" \
--exclude "*.json" \
--exclude "sitemap.xml" \
--exclude "robots.txt"
# Apply long-term caching to hashed assets
aws s3 sync ./dist s3://prod-assets-codcompass.io \
--cache-control "max-age=31536000, public, immutable" \
--include "*.js" \
--include "*.css" \
--include "*.png" \
--include "*.jpg" \
--include "*.svg" \
--include "*.woff2"
Quick Start Guide
- Initialize Infrastructure: Run
cdk deploy with the provided construct to provision the bucket and policy.
- Upload Assets: Execute the
aws s3 sync commands from the configuration template to upload your build output.
- Verify Endpoint: Access the
WebsiteEndpoint output from the CDK deployment to confirm the site loads over HTTP.
- Enable HTTPS: Create a CloudFront distribution with the S3 bucket as the origin. Update DNS to point to the CloudFront domain.
- Monitor: Check CloudWatch metrics for
4xxErrorRate and 5xxErrorRate to ensure healthy operation.