Day 2 of 5
⏱ ~60 minutes
Deploy to AWS — Day 2

S3 — Host Static Sites and Store Files

Use S3 to host static websites, store application assets, and manage file uploads with presigned URLs and bucket policies.

S3: Simple Storage Service

S3 stores objects (files) in buckets. It's the foundation of most AWS architectures: static website hosting, file uploads, build artifacts, logs, backup storage, and AI dataset storage. The pricing is minimal and it scales infinitely.

Create a Bucket and Host a Static Site

Terminal — Create bucket
# Bucket names must be globally unique
aws s3 mb s3://my-app-static-site-2026

# Enable static website hosting
aws s3 website s3://my-app-static-site-2026 \
  --index-document index.html \
  --error-document error.html

# Upload files
aws s3 sync ./dist s3://my-app-static-site-2026

# Make files publicly readable
aws s3api put-bucket-policy \
  --bucket my-app-static-site-2026 \
  --policy file://bucket-policy.json
bucket-policy.json
{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "PublicReadGetObject",
    "Effect": "Allow",
    "Principal": "*",
    "Action": "s3:GetObject",
    "Resource": "arn:aws:s3:::my-app-static-site-2026/*"
  }]
}

Your site will be available at http://my-app-static-site-2026.s3-website-us-east-1.amazonaws.com. In Day 5 we'll put CloudFront + HTTPS in front of it.

File Uploads with Presigned URLs

For user file uploads, never expose your AWS credentials to the browser. Instead, generate a presigned URL on the server — a time-limited URL that allows a specific S3 operation without needing credentials.

Node.js — Generate presigned upload URL
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

const s3 = new S3Client({ region: 'us-east-1' });

export async function getUploadUrl(filename, contentType) {
  const key = `uploads/${Date.now()}-${filename}`;
  const command = new PutObjectCommand({
    Bucket: process.env.S3_BUCKET,
    Key: key,
    ContentType: contentType,
  });
  const url = await getSignedUrl(s3, command, { expiresIn: 300 }); // 5 min
  return { url, key };
}

// Express route
app.post('/api/upload-url', async (req, res) => {
  const { filename, contentType } = req.body;
  const { url, key } = await getUploadUrl(filename, contentType);
  res.json({ url, key });
});
💡
Frontend upload pattern: Call your API to get the presigned URL, then PUT the file directly to S3 from the browser. Your server never handles the file bytes — it just authorizes the upload.

S3 Bucket Policies vs IAM Policies

Bucket policies attach to the bucket and apply to anyone accessing it (including anonymous users if you use "Principal": "*"). IAM policies attach to users and roles. For public read, use a bucket policy. For your app's write access, use an IAM role.

Day 2 Exercise
Deploy a Static Site to S3
  1. Create an S3 bucket with a unique name
  2. Enable static website hosting on the bucket
  3. Create a simple index.html and upload it with aws s3 sync
  4. Apply the public read bucket policy
  5. Visit the S3 website URL and confirm it loads

Day 2 Summary

  • S3 bucket names are globally unique — prefix with your project name
  • Static website hosting serves HTML/CSS/JS directly from S3
  • Presigned URLs let browsers upload files without exposing credentials
  • Bucket policies control public access; IAM policies control service access
  • aws s3 sync is the fastest way to deploy a frontend build
Finished this lesson?