You have a certificate and an IAM role. Now you need somewhere to put your site files. An S3 bucket is the storage layer. CloudFront will serve it to the world in a later post.
This works for any site that builds to static files: plain HTML, React, Vue, Astro, Svelte. If your framework needs a server at runtime (Next.js, Remix, Hydrogen), you’d use something like Fargate instead. We’ll cover that later in the series.
The Bucket
This is a new stack, separate from your certificate stack. The resource type is AWS::S3::Bucket. The bucket stays private and versioned, with a lifecycle rule to keep storage costs down.
AWSTemplateFormatVersion: '2010-09-09'
Description: S3 hosting, CloudFront, CI/CD pipeline, and Route 53 for a plain static site.
Resources:
S3WebsiteBucket:
Type: 'AWS::S3::Bucket'
DeletionPolicy: Delete
UpdateReplacePolicy: Delete
Properties:
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
VersioningConfiguration:
Status: Enabled
LifecycleConfiguration:
Rules:
- Id: AutoCleanupOldVersions
Status: Enabled
NoncurrentVersionExpiration:
NoncurrentDays: 7
A few things to note:
No BucketName property. When you omit it, CloudFormation generates a unique name from your stack name and region, so deploying the same template more than once never collides. It’s the same trap that bites people who set TableName on DynamoDB tables explicitly.
DeletionPolicy: Delete tells CloudFormation to delete the bucket when the stack goes away, and UpdateReplacePolicy: Delete does the same if an update ever forces a replacement. Deleting the stack should leave nothing behind. A versioned bucket won’t delete while it still holds objects, so post 16 adds a small Lambda that empties it first. Your git repo is the source of truth for the site, not the bucket.
PublicAccessBlockConfiguration locks down all four public access settings. Nobody reaches your files through S3 directly. CloudFront handles public access through an Origin Access Control in a future post.
VersioningConfiguration: Enabled keeps every version of every file. There is no cap. Every deploy pushes old versions to noncurrent status, and they stick around until something removes them. This gives you a rollback path if a deploy goes wrong.
NoncurrentVersionExpiration with NoncurrentDays: 7 is that “something.” Without it, noncurrent versions pile up forever and you pay for all of them. This rule automatically deletes noncurrent versions 7 days after they become noncurrent.
The current version of every file is never touched, no matter how old it is. Only replaced versions get the 7-day countdown.
If you need to roll back after 7 days, the old versions are gone and you’ll need to redeploy from your git repo. That’s fine for a static site since your repo is the source of truth, not the bucket.
BucketEncryption with AES256 sets SSE-S3 encryption explicitly. S3 encrypts all objects by default, so this block is technically optional. Including it makes the encryption visible in the template instead of relying on an implicit default. Anyone reading the YAML can see exactly what’s configured.