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'
Resources:
SiteBucket:
Type: 'AWS::S3::Bucket'
DeletionPolicy: Retain
UpdateReplacePolicy: Retain
Properties:
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
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. This avoids naming collisions if you deploy the same template more than once. It also prevents the “already exists” error. If you hardcode a name like BucketName: my-site and use DeletionPolicy: Retain, deleting and recreating the stack fails because the retained bucket still occupies that name. This is the same problem that bites people with DynamoDB tables when they set TableName explicitly. Auto-generated names sidestep it entirely.
DeletionPolicy: Retain tells CloudFormation to leave the bucket alone if the stack is deleted. Your site files survive even if someone accidentally tears down the stack. UpdateReplacePolicy: Retain does the same during updates. If a change forces CloudFormation to replace the bucket (create new, delete old), Retain keeps the original instead of deleting it.
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.
You don’t need to add encryption. S3 encrypts all objects with SSE-S3 by default.
Updating the Role
Your IAM role from post 2 needs S3 permissions. Add a new statement to the inline policy in your role stack.
- Effect: Allow
Action:
- 's3:CreateBucket'
- 's3:DeleteBucket'
- 's3:PutBucketPublicAccessBlock'
- 's3:GetBucketPublicAccessBlock'
- 's3:PutBucketVersioning'
- 's3:GetBucketVersioning'
- 's3:PutLifecycleConfiguration'
- 's3:GetLifecycleConfiguration'
- 's3:PutBucketTagging'
- 's3:GetBucketTagging'
Resource: '*'
- Create and Delete let CloudFormation manage the bucket’s lifecycle.
- PublicAccessBlock actions let it apply and read back the lockdown configuration.
- Versioning actions let it enable and check versioning status.
- LifecycleConfiguration actions let it set up the cleanup rule for noncurrent versions.
- Tagging actions let CloudFormation tag the bucket. CloudFormation tags every resource it manages, so it always needs these.