Overview
This module provisions:- S3 bucket with encryption at rest
- Versioning for file recovery
- Lifecycle rules for cost optimization
- CORS configuration for frontend uploads
- IAM role for Kubernetes pods (IRSA)
- Public access blocking
Resources Created
S3 Bucket
- Resource:
aws_s3_bucket.app_storage - Naming:
{project_name}-{environment}-app-storage-{aws_account_id} - Use cases: User uploads, generated reports, data exports
Public Access Block
- Resource:
aws_s3_bucket_public_access_block.app_storage - Prevents accidental public exposure
- Blocks all public ACLs and policies
Versioning
- Resource:
aws_s3_bucket_versioning.app_storage - Maintains all versions of objects
- Enables recovery from accidental deletions
- Old versions automatically expire per lifecycle rules
Encryption
- Resource:
aws_s3_bucket_server_side_encryption_configuration.app_storage - Algorithm: AES-256 (S3-managed keys)
- Bucket key: Enabled to reduce KMS costs
- Transparent to applications
Lifecycle Configuration
- Resource:
aws_s3_bucket_lifecycle_configuration.app_storage
- After 30 days → Standard-IA (40% cheaper)
- After 90 days → Glacier (70% cheaper)
- After 365 days → Automatic deletion
- Old versions deleted after 30 days
- Aborts multipart uploads incomplete after 7 days
- Prevents charges for abandoned uploads
CORS Configuration
- Resource:
aws_s3_bucket_cors_configuration.app_storage - Allows frontend to upload files directly to S3
- Bypasses backend for large files (more efficient)
- Configurable allowed origins
IAM Role for Service Accounts
- Resource:
aws_iam_role.s3_access - Uses OIDC provider from EKS cluster
- Allows specific Kubernetes service account to access S3
- No static credentials needed in pods
Variables
Project name for bucket naming and tagging
Deployment environment:
dev, staging, or prodAWS account ID for unique bucket naming.Obtain with:
aws sts get-caller-identity --query Account --output textOrigins allowed to make cross-origin requests to S3.Examples:
- Dev:
["http://localhost:5173", "http://localhost:3000"] - Prod:
["https://app.govtech.example.com"]
ARN of the EKS OIDC provider. Use
oidc_provider_arn output from kubernetes-cluster module.OIDC provider URL without
https:// prefix. Use oidc_provider_url output from kubernetes-cluster module.Outputs
S3 bucket name (same as bucket name)
ARN of the S3 bucket for IAM policies
FQDN of the bucket:
{bucket-name}.s3.{region}.amazonaws.comARN of IAM role for Kubernetes service accounts to assume.Annotate ServiceAccount with this ARN to grant S3 access.
Name of the IAM role for S3 access
Usage Example
Kubernetes Integration
Service Account Configuration
Annotate Kubernetes ServiceAccount to grant S3 access:Application Code
Pods automatically receive temporary credentials: Node.js (AWS SDK v3):Direct Frontend Uploads
Generate presigned URLs for secure frontend uploads: Backend (Node.js):Lifecycle Management
Storage Classes
| Class | Use Case | Cost (relative) | Retrieval |
|---|---|---|---|
| Standard | Frequently accessed | 1x | Instant |
| Standard-IA | Infrequent access | 0.6x | Instant |
| Glacier | Archive | 0.3x | Minutes-hours |
Custom Lifecycle Rules
Modify lifecycle transitions:Prefix-Based Rules
Different rules for different file types:Security Best Practices
Block Public Access
All public access is blocked by default. Files are only accessible:- Via IAM authenticated requests
- Through presigned URLs with expiration
Encryption
All files encrypted at rest with AES-256. For compliance requirements, use KMS:Access Logging
Enable S3 access logs for audit trail:Bucket Policies
Enforce SSL/TLS for all requests:Cost Optimization
Intelligent-Tiering
For unpredictable access patterns:Lifecycle Analysis
Use S3 Storage Lens to analyze access patterns and optimize lifecycle rules:Object Size Optimization
- Objects < 128 KB: Standard is most cost-effective
- Objects < 1 MB: Avoid Glacier (minimum object size charges)
- Large objects: Use multipart upload (> 100 MB)
Monitoring
CloudWatch Metrics
Key metrics to monitor:BucketSizeBytes- Total storage usageNumberOfObjects- Object countAllRequests- Request rate4xxErrors/5xxErrors- Error rates
Alarms
Troubleshooting
CORS Errors
If frontend cannot upload:- Verify origin in
cors_allowed_originsmatches exactly (including protocol) - Check browser console for specific CORS error
- Ensure presigned URL includes correct headers
Access Denied
From pods:- Verify ServiceAccount has correct role annotation
- Check IRSA trust policy includes correct OIDC provider
- Test credentials:
- Verify presigned URL hasn’t expired
- Check IAM role has
s3:PutObjectpermission - Ensure
Content-Typeheader matches presigned URL
Lifecycle Not Working
Lifecycle rules run daily:- Changes may take 24-48 hours to take effect
- Check rule status: Enabled vs Disabled
- Verify filter prefix matches object keys