AWS S3 Tutorial: Store, Serve, and Secure Static Files
A practical Amazon S3 tutorial covering bucket creation, object uploads, static site hosting, bucket policies, presigned URLs, versioning, and SDK automation.
Amazon S3 is one of the oldest and most foundational AWS services, but it still trips up developers who treat it like a file system. It is not a POSIX filesystem; it is an object store with a flat structure, HTTP APIs, and eventual consistency for some metadata operations. In this tutorial, you will create a real bucket, upload objects, make a static site public, generate presigned URLs, and automate access.
What is Amazon S3?
Amazon S3 (Simple Storage Service) stores data as objects inside buckets. Each object consists of data, metadata, and a unique key. Buckets are regional resources with globally unique names. S3 delivers 99.999999999% durability and scales without provisioning. The [AWS S3 documentation](https://docs.aws.amazon.com/AmazonS3/latest/userguide/Welcome.html) is the most current reference for limits, naming rules, and API behavior.
Prerequisites
An AWS account with IAM permissions to create and list S3 buckets.
AWS CLI v2 installed if you want to follow terminal commands. See the [AWS CLI documentation](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-welcome.html).
A static file such as `index.html` to upload.
Step 1: Create a bucket
Use the AWS Management Console or the CLI. Bucket names must be globally unique, 3–63 characters long, and contain only lowercase letters, numbers, hyphens, and dots. Pick a region close to your users.
aws s3api create-bucket --bucket your-project-assets --region us-east-1Note: `us-east-1` does not require a `LocationConstraint`. For other regions, add the region-specific configuration:
aws s3api create-bucket --bucket your-project-assets --region eu-west-1 --create-bucket-configuration LocationConstraint=eu-west-1Step 2: Upload an object
Objects are referenced by bucket and key. The key can contain slashes to simulate folders, but S3 has no folders. Uploading with the CLI:
aws s3 cp index.html s3://your-project-assets/index.htmlAdd `--acl bucket-owner-full-control` only if you are uploading to a bucket owned by another account. Otherwise, keep ACLs disabled and manage access with bucket policies.
Step 3: Control access with bucket policies
By default, new S3 buckets block public access. Do not disable those settings unless you need a public static website. For public content, enable static website hosting and attach a policy that allows `s3:GetObject` on the bucket ARN. Use the AWS policy generator or edit the bucket policy in the console. The policy needs:
`Principal` set to `*`
`Action` set to `s3:GetObject`
`Resource` set to `arn:aws:s3:::your-project-assets/*`
In the AWS CLI, you can apply a policy file:
aws s3api put-bucket-policy --bucket your-project-assets --policy file://bucket-policy.jsonStep 4: Host a static website
The S3 website endpoint supports HTML, CSS, JavaScript, and simple redirects. Enable it in the bucket properties or with:
aws s3 website s3://your-project-assets/ --index-document index.html --error-document error.htmlThe website endpoint is HTTP-only. For HTTPS, place CloudFront or another CDN in front of the bucket. See [CloudFront documentation](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Introduction.html) for distribution setup. The structure looks like:
`Users -> CloudFront -> S3 bucket`
Use Origin Access Control in CloudFront so the S3 bucket remains private.
Step 5: Generate presigned URLs for private files
If a bucket is private, you can grant temporary access with a presigned URL. AWS SDKs and CLI generate time-limited URLs without changing bucket permissions.
aws s3 presign s3://your-project-assets/reports/quarterly.pdf --expires-in 3600This URL works for the specified duration only. Presigned URLs are ideal for downloads, uploads, and sharing sensitive files.
Step 6: Use lifecycle rules and versioning
Enable versioning to protect against accidental deletions, then add lifecycle rules to delete or archive old versions. Example: keep noncurrent versions for 30 days, then expire them. In the console, go to Management > Lifecycle rules. In the CLI:
aws s3api put-bucket-lifecycle-configuration --bucket your-project-assets --lifecycle-configuration file://lifecycle.jsonThis controls storage costs without manual cleanup.
Step 7: Automate with the AWS SDK
For application code, use the AWS SDK for JavaScript or your language of choice. See [AWS SDK for JavaScript v3](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/) for more examples. A simple upload:
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
const client = new S3Client({ region: 'us-east-1' });
await client.send(new PutObjectCommand({
Bucket: 'your-project-assets',
Key: 'images/logo.png',
Body: fileBody,
ContentType: 'image/png',
}));Do not hardcode credentials. Use IAM roles for EC2, Lambda, or ECS, or short-lived credentials from your identity provider.
Common mistakes to avoid
Treating S3 as a file system with rename and partial updates. S3 objects are immutable; replace objects to update them.
Granting public read to entire buckets when only one prefix should be public.
Ignoring block public access settings and leaving public buckets exposed.
Not using CloudFront for public websites over HTTPS.
Final thoughts
S3 is a building block, not the whole house. Combine it with CloudFront, IAM, and lifecycle rules to create scalable storage for static assets, logs, backups, or data lakes. Start with a small bucket, test permissions, then integrate with your stack.