Back to KB
Difficulty
Intermediate
Read Time
6 min

Host a Static Website on AWS S3 in Minutes

By Codcompass TeamΒ·Β·6 min read

Architecting Serverless Static Assets: A Production-Grade S3 Deployment Guide

Current Situation Analysis

The modern web architecture has decoupled frontend presentation from backend logic, leading to a surge in static site generation. Despite this shift, many engineering teams continue to provision virtual machines or container clusters for static content, introducing unnecessary operational overhead, patching requirements, and scaling complexity. Amazon S3 provides a native, serverless storage layer capable of serving HTML, CSS, JavaScript, and media assets directly to clients without compute instances.

This capability is frequently misunderstood or underutilized. Developers often treat S3 merely as object storage rather than a hosting platform, or they deploy insecurely by misconfiguring public access controls. Furthermore, a critical architectural gap exists: the native S3 website endpoint supports only HTTP, lacking TLS termination. Teams that overlook this limitation expose users to man-in-the-middle risks and fail modern security audits.

Data from AWS indicates that S3 offers 99.999999999% durability for objects. For low-traffic applications, the AWS Free Tier covers sufficient storage, requests, and data transfer to host static sites at zero cost. However, as traffic scales, request-based pricing and bandwidth costs can escalate if caching strategies are not implemented. The optimal production pattern requires integrating S3 with a Content Delivery Network (CDN) to resolve security, performance, and cost concerns simultaneously.

WOW Moment: Key Findings

The decision to host static assets involves trade-offs between operational complexity, security compliance, and performance. The following comparison highlights why the S3-only approach is insufficient for production environments and why the S3-plus-CDN pattern is the industry standard.

Deployment PatternMonthly Cost (Est. 10GB/100k req)Latency ProfileHTTPS SupportOperational Overhead
EC2 Instance$15.00 – $50.00+RegionalManual ConfigHigh (Patching, Scaling, OS Mgmt)
S3 Website Endpoint$0.023 – $0.050RegionalNone (HTTP Only)Low
S3 + CloudFront$0.010 – $0.030Global EdgeNative TLSMinimal

Why this matters: The S3 website endpoint is cost-effective but fails production security requirements due to the lack of HTTPS. Introducing CloudFront not only enables TLS termination but also caches content at edge locations, reducing latency for global users and significantly lowering S3 request costs by serving cached responses. The marginal cost increase for the CDN is offset by reduced S3 GET requests and improved security posture.

Core Solution

Implementing a production-ready static site on AWS requires 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

  1. 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.
  2. 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.
  3. Versioning: Enabled by default to support rollback capabilities and accidental deletion protection.
  4. 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', {
      bucke

tName: 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

- [ ] **Provision Bucket**: Create S3 bucket with versioning enabled and name matching target domain.
- [ ] **Apply Policy**: Attach bucket policy allowing `s3:GetObject` for public read access.
- [ ] **Configure Hosting**: Set index and error documents in bucket properties.
- [ ] **Deploy CDN**: Create CloudFront distribution pointing to S3 origin; enable HTTPS and security headers.
- [ ] **Set Cache Headers**: Configure upload scripts to apply `Cache-Control` metadata based on file type.
- [ ] **Route DNS**: Create Route 53 Alias record pointing to CloudFront distribution.
- [ ] **Validate**: Test HTTP/HTTPS endpoints, verify 404 handling, and check cache hit ratios.

#### 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.

```bash
# 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

  1. Initialize Infrastructure: Run cdk deploy with the provided construct to provision the bucket and policy.
  2. Upload Assets: Execute the aws s3 sync commands from the configuration template to upload your build output.
  3. Verify Endpoint: Access the WebsiteEndpoint output from the CDK deployment to confirm the site loads over HTTP.
  4. Enable HTTPS: Create a CloudFront distribution with the S3 bucket as the origin. Update DNS to point to the CloudFront domain.
  5. Monitor: Check CloudWatch metrics for 4xxErrorRate and 5xxErrorRate to ensure healthy operation.