AWS S3 Buckets: 101
Create buckets, upload objects, and configure policies using floci's S3-compatible API. No AWS account needed.
What You’ll Build
By the end of this lab you’ll have a local S3 environment running entirely on your laptop:
- Two buckets with objects organized using key prefixes
- A working boto3 client that uploads, lists, and downloads objects against
localhost:4566 - A pre-signed URL that grants time-limited read access to a private object
- A bucket policy that restricts write access to a specific IAM principal
No AWS account. No IAM role. No per-request charges.
How It Works
floci listens on port 4566 and implements the full S3 REST API. Every AWS SDK call hits localhost:4566 instead of s3.amazonaws.com. floci accepts any non-empty credentials.
boto3 / AWS CLI / aws-sdk-js
│ PUT · GET · HEAD · DELETE
▼
floci :4566
│ /<bucket>/<key>
▼
local filesystem
No code changes beyond setting AWS_ENDPOINT_URL or passing endpoint_url to the client.
Prerequisites
Step 1: Start floci
Create a docker-compose.yml:
services:
floci:
image: floci/floci:latest
ports:
- "4566:4566"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
docker-compose up -d
# confirm it's healthy
curl http://localhost:4566/_floci/health
Step 2: Configure Credentials
floci accepts any non-empty credentials. Set these once for the session:
export AWS_ENDPOINT_URL=http://localhost:4566
export AWS_ACCESS_KEY_ID=test
export AWS_SECRET_ACCESS_KEY=test
export AWS_DEFAULT_REGION=us-east-1
Step 3: Create Buckets and Upload Objects
S3 organizes data into flat key-value stores. Slashes in keys act as logical prefixes, making it easy to group related objects without real subdirectories.
# Create two buckets
aws s3 mb s3://raw-data
aws s3 mb s3://processed-data
# Create some sample files
cat > january.csv <<'EOF'
date,region,sales
2024-01-01,us-east,1200.00
2024-01-02,us-west,980.50
2024-01-03,eu-west,1540.00
EOF
cat > february.csv <<'EOF'
date,region,sales
2024-02-01,us-east,1350.00
2024-02-02,us-west,1100.00
2024-02-03,eu-west,1620.00
EOF
# Upload with a prefix to organize by month
aws s3 cp january.csv s3://raw-data/sales/2024/01/data.csv
aws s3 cp february.csv s3://raw-data/sales/2024/02/data.csv
# Upload a second file directly
echo "processed on $(date)" > summary.txt
aws s3 cp summary.txt s3://processed-data/reports/summary.txt
Step 4: List, Download, and Delete Objects
# List all objects in a bucket
aws s3 ls s3://raw-data --recursive
# List only a specific prefix
aws s3 ls s3://raw-data/sales/2024/01/
# Download a single object
aws s3 cp s3://raw-data/sales/2024/01/data.csv january-back.csv
cat january-back.csv
# Sync a prefix to a local directory
aws s3 sync s3://raw-data/sales/2024/ ./local-sales/
# Delete a single object
aws s3 rm s3://processed-data/reports/summary.txt
# Delete all objects under a prefix
aws s3 rm s3://raw-data/sales/2024/02/ --recursive
Expected output from aws s3 ls s3://raw-data --recursive:
2024-01-15 10:00:00 155 sales/2024/01/data.csv
2024-01-15 10:00:01 156 sales/2024/02/data.csv
Step 5: Pre-signed URLs
Pre-signed URLs grant time-limited access to a private object without sharing your credentials. The URL contains a signature that expires after the time you specify.
Save the following as presign.py and run it:
import boto3
import urllib.request
boto_kwargs = dict(
endpoint_url='http://localhost:4566',
region_name='us-east-1',
aws_access_key_id='test',
aws_secret_access_key='test',
)
s3 = boto3.client('s3', **boto_kwargs)
# Make sure the object exists
s3.create_bucket(Bucket='raw-data')
s3.put_object(
Bucket='raw-data',
Key='sales/2024/01/data.csv',
Body=b'date,region,sales\n2024-01-01,us-east,1200.00\n',
)
# Generate a pre-signed URL valid for 60 seconds
url = s3.generate_presigned_url(
ClientMethod='get_object',
Params={'Bucket': 'raw-data', 'Key': 'sales/2024/01/data.csv'},
ExpiresIn=60,
)
print('Pre-signed URL:')
print(url)
# Fetch it with plain HTTP — no AWS credentials needed
with urllib.request.urlopen(url) as resp:
print('\nContent:')
print(resp.read().decode())
Expected output:
Pre-signed URL:
http://localhost:4566/raw-data/sales/2024/01/data.csv?X-Amz-Algorithm=AWS4-HMAC-SHA256&...
Content:
date,region,sales
2024-01-01,us-east,1200.00
Step 6: Bucket Policy
Bucket policies control which principals can perform which actions on a bucket. floci evaluates the same policy JSON format as real S3, so you can test access rules locally before deploying them.
# Apply a policy that allows any principal to read objects
# but only the 'writer' identity to write them
cat > policy.json <<'EOF'
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicRead",
"Effect": "Allow",
"Principal": "*",
"Action": ["s3:GetObject"],
"Resource": "arn:aws:s3:::processed-data/*"
},
{
"Sid": "RestrictedWrite",
"Effect": "Deny",
"NotPrincipal": {
"AWS": "arn:aws:iam::000000000000:user/writer"
},
"Action": ["s3:PutObject", "s3:DeleteObject"],
"Resource": "arn:aws:s3:::processed-data/*"
}
]
}
EOF
aws s3api put-bucket-policy \
--bucket processed-data \
--policy file://policy.json
# Verify the policy was stored
aws s3api get-bucket-policy --bucket processed-data
Bonus: Full Flow with boto3
import boto3
s3 = boto3.client(
's3',
endpoint_url='http://localhost:4566',
region_name='us-east-1',
aws_access_key_id='test',
aws_secret_access_key='test',
)
# Create a bucket
s3.create_bucket(Bucket='demo')
# Upload several objects
for month in ['01', '02', '03']:
s3.put_object(
Bucket='demo',
Key='reports/2024/' + month + '/summary.txt',
Body=('Month ' + month + ' report').encode(),
ContentType='text/plain',
)
# List with a prefix filter
resp = s3.list_objects_v2(Bucket='demo', Prefix='reports/2024/')
print('Objects:')
for obj in resp.get('Contents', []):
print(' ' + obj['Key'] + ' (' + str(obj['Size']) + ' bytes)')
# Download one
data = s3.get_object(Bucket='demo', Key='reports/2024/01/summary.txt')
print('\nContent of 01/summary.txt:')
print(data['Body'].read().decode())
Expected output:
Objects:
reports/2024/01/summary.txt (14 bytes)
reports/2024/02/summary.txt (14 bytes)
reports/2024/03/summary.txt (14 bytes)
Content of 01/summary.txt:
Month 01 report
What You Learned
- floci exposes the real S3 REST API on
localhost:4566. No SDK changes beyondendpoint_url - floci accepts any non-empty credentials.
test/testis the convention for local development - Key prefixes organize objects without real directories.
aws s3 ls --recursiveandlist_objects_v2(Prefix=...)both respect them - Pre-signed URLs are structurally identical to real S3 pre-signed URLs. Use them to validate expiry and signature logic before going to production
- Bucket policies use the same JSON format as real S3, so you can test access rules locally
Next Steps
- Store JSON objects and query them with Athena + Glue — see the Athena + S3: 101 lab
- Enable versioning on a bucket (
aws s3api put-bucket-versioning) and observe how overwrites stack - Set up an S3 event notification to trigger a Lambda function when a new object lands in a prefix