Deploy to AWS S3 ¶
Amazon S3 provides highly durable object storage that’s ideal for hosting static websites. Combined with CloudFront CDN, it offers enterprise-grade performance and security.
Prerequisites ¶ #
- An AWS account
- AWS CLI installed and configured
- Your markata-go site ready to build
Cost ¶ #
| Service | Free Tier | After Free Tier |
|---|---|---|
| S3 Storage | 5 GB (12 months) | ~$0.023/GB/month |
| S3 Requests | 20,000 GET (12 months) | ~$0.0004/1000 requests |
| CloudFront | 1 TB transfer (12 months) | ~$0.085/GB |
| Route 53 | N/A | $0.50/hosted zone/month |
A typical blog costs $1-5/month after the free tier expires.
Architecture Options ¶ #
| Setup | Complexity | HTTPS | Custom Domain | CDN |
|---|---|---|---|---|
| S3 only | Low | No | Limited | No |
| S3 + CloudFront | Medium | Yes | Yes | Yes |
| S3 + CloudFront + Route 53 | High | Yes | Yes (apex) | Yes |
This guide covers the recommended S3 + CloudFront setup.
Step 1: Create S3 Bucket ¶ #
Using AWS Console ¶ #
- Go to S3 Console
- Click Create bucket
- Enter bucket name (e.g.,
my-site-bucket) - Choose a region close to your audience
- Uncheck Block all public access (we’ll use CloudFront for access)
- Click Create bucket
Using AWS CLI ¶ #
# Create bucket
aws s3 mb s3://my-site-bucket --region us-east-1
# Enable static website hosting
aws s3 website s3://my-site-bucket --index-document index.html --error-document 404.html
Step 2: Configure Bucket Policy ¶ #
Create a bucket policy to allow CloudFront access:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontAccess",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-site-bucket/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::ACCOUNT_ID:distribution/DISTRIBUTION_ID"
}
}
}
]
}
You’ll update this policy after creating the CloudFront distribution.
Step 3: Create CloudFront Distribution ¶ #
Using AWS Console ¶ #
- Go to CloudFront Console
- Click Create distribution
- Configure origin:
- Origin domain: Select your S3 bucket
- Origin access: Origin access control settings (recommended)
- Click Create control setting > Create
- Configure default cache behavior:
- Viewer protocol policy: Redirect HTTP to HTTPS
- Allowed HTTP methods: GET, HEAD
- Cache policy: CachingOptimized
- Configure settings:
- Price class: Use all edge locations (or choose based on audience)
- Default root object:
index.html
- Click Create distribution
Update S3 Bucket Policy ¶ #
After creating the distribution, update the bucket policy with the correct ARN:
# Get distribution ARN from CloudFront console
# Update bucket policy with the distribution ARN
Step 4: Build and Deploy ¶ #
Build Your Site ¶ #
# Set the CloudFront URL (or custom domain)
export MARKATA_GO_URL=https://d1234abcd.cloudfront.net
# Build
markata-go build --clean
Deploy to S3 ¶ #
# Sync all files
aws s3 sync public/ s3://my-site-bucket --delete
# With cache headers for static assets
aws s3 sync public/static/ s3://my-site-bucket/static/ \
--cache-control "public, max-age=31536000, immutable"
# HTML files with no-cache
aws s3 sync public/ s3://my-site-bucket \
--exclude "static/*" \
--cache-control "public, max-age=0, must-revalidate"
Invalidate CloudFront Cache ¶ #
After deploying, invalidate the cache to see changes immediately:
aws cloudfront create-invalidation \
--distribution-id YOUR_DISTRIBUTION_ID \
--paths "/*"
Step 5: Custom Domain Setup ¶ #
Option A: Using Route 53 (Recommended) ¶ #
-
Create Hosted Zone
aws route53 create-hosted-zone --name example.com --caller-reference $(date +%s) -
Update Domain Nameservers
Get the nameservers from Route 53 and update them at your registrar.
-
Request SSL Certificate
In AWS Certificate Manager (ACM) - must be in us-east-1 for CloudFront:
aws acm request-certificate \ --domain-name example.com \ --subject-alternative-names "*.example.com" \ --validation-method DNS \ --region us-east-1 -
Add Certificate to CloudFront
Update your distribution:
- Alternate domain names:
example.com,www.example.com - Custom SSL certificate: Select your ACM certificate
- Alternate domain names:
-
Create DNS Records
In Route 53, create A records as aliases to your CloudFront distribution.
Option B: External DNS ¶ #
- Request ACM certificate (as above)
- Validate via DNS by adding CNAME records at your registrar
- Add alternate domain names to CloudFront
- Create CNAME record pointing to your CloudFront domain:
www.example.com -> d1234abcd.cloudfront.net
Note: Apex domains (example.com without www) require Route 53 or a DNS provider that supports ALIAS/ANAME records.
Automation with GitHub Actions ¶ #
Create .github/workflows/deploy.yml:
name: Deploy to AWS
on:
push:
branches: [main]
env:
AWS_REGION: us-east-1
S3_BUCKET: my-site-bucket
CLOUDFRONT_DISTRIBUTION_ID: E1234567890ABC
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::ACCOUNT_ID:role/GitHubActionsRole
aws-region: ${{ env.AWS_REGION }}
- name: Build
run: |
go install github.com/WaylonWalker/markata-go/cmd/markata-go@latest
markata-go build --clean
env:
MARKATA_GO_URL: https://example.com
- name: Deploy to S3
run: |
# Static assets with long cache
aws s3 sync public/static/ s3://${{ env.S3_BUCKET }}/static/ \
--cache-control "public, max-age=31536000, immutable"
# Everything else
aws s3 sync public/ s3://${{ env.S3_BUCKET }} \
--exclude "static/*" \
--cache-control "public, max-age=0, must-revalidate" \
--delete
- name: Invalidate CloudFront
run: |
aws cloudfront create-invalidation \
--distribution-id ${{ env.CLOUDFRONT_DISTRIBUTION_ID }} \
--paths "/*"
IAM Role for GitHub Actions ¶ #
Create an IAM role with OIDC trust for GitHub:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::ACCOUNT_ID:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:YOUR_ORG/YOUR_REPO:*"
}
}
}
]
}
Attach a policy allowing S3 and CloudFront access:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:DeleteObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::my-site-bucket",
"arn:aws:s3:::my-site-bucket/*"
]
},
{
"Effect": "Allow",
"Action": "cloudfront:CreateInvalidation",
"Resource": "arn:aws:cloudfront::ACCOUNT_ID:distribution/DISTRIBUTION_ID"
}
]
}
Troubleshooting ¶ #
Access Denied Errors ¶ #
- Check bucket policy allows CloudFront access
- Verify Origin Access Control is configured
- Ensure the distribution ARN in bucket policy is correct
404 on Subpages ¶ #
Configure CloudFront error pages:
- Go to your distribution > Error pages
- Create custom error response:
- HTTP error code: 403
- Response page path:
/index.html - HTTP response code: 200
Or use a CloudFront Function for clean URLs (see below).
CloudFront Function for Clean URLs ¶ #
Create a function to handle clean URLs:
function handler(event) {
var request = event.request;
var uri = request.uri;
// Check whether the URI is missing a file extension
if (!uri.includes('.')) {
// If URI doesn't end with /, add it
if (!uri.endsWith('/')) {
uri += '/';
}
// Append index.html
request.uri = uri + 'index.html';
}
return request;
}
Attach to your distribution’s viewer request.
Changes Not Appearing ¶ #
- Wait for CloudFront cache to expire, or
- Create a cache invalidation:
aws cloudfront create-invalidation \ --distribution-id YOUR_DISTRIBUTION_ID \ --paths "/*"
SSL Certificate Issues ¶ #
- ACM certificate must be in us-east-1 region
- Certificate must be validated (check ACM console)
- Alternate domain names must match certificate
Cost Optimization ¶ #
Reduce CloudFront Costs ¶ #
- Use Price Class 100 (North America and Europe only) for regional sites
- Enable Compress objects automatically
- Set appropriate cache TTLs
Reduce S3 Costs ¶ #
- Enable S3 Intelligent-Tiering for rarely accessed sites
- Use S3 Standard for frequently accessed content
- Clean up old versions if versioning is enabled
Monitor Costs ¶ #
Set up AWS Budgets to alert you:
aws budgets create-budget \
--account-id YOUR_ACCOUNT_ID \
--budget file://budget.json \
--notifications-with-subscribers file://notifications.json
Next Steps ¶ #
- AWS S3 Docs - Official S3 documentation
- CloudFront Docs - CDN configuration
- Configuration Guide - Customize your markata-go site