AWS Deployment Architecture
Complete guide to deploying Sunday on AWS with production-grade architecture.
Architecture Overview
AWS Services Required
| Service | Purpose | Estimated Monthly Cost |
|---|---|---|
| Route 53 | DNS management | $0.50/zone + queries |
| CloudFront | CDN, edge caching | $0.085/GB |
| WAF | Web application firewall | $5 + $1/million requests |
| ALB | Load balancer | $16.20 + $0.008/LCU-hour |
| ECS Fargate | Container orchestration | ~$73/month (2 vCPU, 4GB) |
| DocumentDB | MongoDB-compatible database | ~$200/month (db.t3.medium) |
| ElastiCache | Redis for caching/sessions | ~$50/month (cache.t3.micro) |
| S3 | File storage | $0.023/GB |
| SQS | Message queues | $0.40/million requests |
| SES | Transactional email | $0.10/1000 emails |
| OpenSearch | Full-text search | ~$80/month (t3.small) |
| Secrets Manager | Secrets storage | $0.40/secret/month |
| CloudWatch | Monitoring & logging | ~$10-50/month |
Estimated Total: $485-700/month for a small-medium production setup.
1. VPC Architecture
VPC Design
VPC: 10.0.0.0/16
├── Public Subnets (2 AZs)
│ ├── 10.0.1.0/24 (us-east-1a) - ALB, NAT Gateway
│ └── 10.0.2.0/24 (us-east-1b) - ALB, NAT Gateway
├── Private Subnets (2 AZs)
│ ├── 10.0.10.0/24 (us-east-1a) - ECS Tasks
│ └── 10.0.20.0/24 (us-east-1b) - ECS Tasks
└── Database Subnets (2 AZs)
├── 10.0.100.0/24 (us-east-1a) - DocumentDB, ElastiCache
└── 10.0.200.0/24 (us-east-1b) - DocumentDB, ElastiCacheTerraform Configuration
# vpc.tf
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0"
name = "sunday-vpc"
cidr = "10.0.0.0/16"
azs = ["us-east-1a", "us-east-1b"]
public_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
private_subnets = ["10.0.10.0/24", "10.0.20.0/24"]
database_subnets = ["10.0.100.0/24", "10.0.200.0/24"]
enable_nat_gateway = true
single_nat_gateway = false # High availability
enable_vpn_gateway = false
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Environment = "production"
Project = "sunday"
}
}2. Container Deployment (ECS Fargate)
Dockerfile
# Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
# Create non-root user
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT 3000
CMD ["node", "server.js"]ECS Task Definition
{
"family": "sunday-app",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "1024",
"memory": "2048",
"executionRoleArn": "arn:aws:iam::ACCOUNT:role/ecsTaskExecutionRole",
"taskRoleArn": "arn:aws:iam::ACCOUNT:role/sundayTaskRole",
"containerDefinitions": [
{
"name": "sunday",
"image": "ACCOUNT.dkr.ecr.us-east-1.amazonaws.com/sunday:latest",
"essential": true,
"portMappings": [
{
"containerPort": 3000,
"protocol": "tcp"
}
],
"environment": [
{"name": "NODE_ENV", "value": "production"}
],
"secrets": [
{
"name": "MONGODB_URI",
"valueFrom": "arn:aws:secretsmanager:us-east-1:ACCOUNT:secret:sunday/mongodb"
},
{
"name": "REDIS_URL",
"valueFrom": "arn:aws:secretsmanager:us-east-1:ACCOUNT:secret:sunday/redis"
},
{
"name": "JWT_SECRET",
"valueFrom": "arn:aws:secretsmanager:us-east-1:ACCOUNT:secret:sunday/jwt"
}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/sunday",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "ecs"
}
},
"healthCheck": {
"command": ["CMD-SHELL", "wget -q -O - http://localhost:3000/api/health || exit 1"],
"interval": 30,
"timeout": 5,
"retries": 3
}
}
]
}Terraform ECS Configuration
# ecs.tf
resource "aws_ecs_cluster" "main" {
name = "sunday-cluster"
setting {
name = "containerInsights"
value = "enabled"
}
}
resource "aws_ecs_service" "sunday" {
name = "sunday-service"
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.sunday.arn
desired_count = 2
launch_type = "FARGATE"
network_configuration {
subnets = module.vpc.private_subnets
security_groups = [aws_security_group.ecs.id]
assign_public_ip = false
}
load_balancer {
target_group_arn = aws_lb_target_group.sunday.arn
container_name = "sunday"
container_port = 3000
}
deployment_configuration {
maximum_percent = 200
minimum_healthy_percent = 100
}
# Enable autoscaling
lifecycle {
ignore_changes = [desired_count]
}
}
# Auto Scaling
resource "aws_appautoscaling_target" "ecs" {
max_capacity = 10
min_capacity = 2
resource_id = "service/${aws_ecs_cluster.main.name}/${aws_ecs_service.sunday.name}"
scalable_dimension = "ecs:service:DesiredCount"
service_namespace = "ecs"
}
resource "aws_appautoscaling_policy" "cpu" {
name = "cpu-autoscaling"
policy_type = "TargetTrackingScaling"
resource_id = aws_appautoscaling_target.ecs.resource_id
scalable_dimension = aws_appautoscaling_target.ecs.scalable_dimension
service_namespace = aws_appautoscaling_target.ecs.service_namespace
target_tracking_scaling_policy_configuration {
predefined_metric_specification {
predefined_metric_type = "ECSServiceAverageCPUUtilization"
}
target_value = 70.0
scale_in_cooldown = 300
scale_out_cooldown = 60
}
}3. Database (DocumentDB)
# documentdb.tf
resource "aws_docdb_cluster" "main" {
cluster_identifier = "sunday-docdb"
engine = "docdb"
master_username = "sunday_admin"
master_password = var.docdb_password
backup_retention_period = 7
preferred_backup_window = "07:00-09:00"
skip_final_snapshot = false
vpc_security_group_ids = [aws_security_group.docdb.id]
db_subnet_group_name = aws_docdb_subnet_group.main.name
enabled_cloudwatch_logs_exports = ["audit", "profiler"]
# Encryption at rest
storage_encrypted = true
kms_key_id = aws_kms_key.main.arn
tags = {
Environment = "production"
}
}
resource "aws_docdb_cluster_instance" "main" {
count = 2 # Primary + Read Replica
identifier = "sunday-docdb-${count.index}"
cluster_identifier = aws_docdb_cluster.main.id
instance_class = "db.t3.medium"
}
resource "aws_docdb_subnet_group" "main" {
name = "sunday-docdb-subnet"
subnet_ids = module.vpc.database_subnets
}Connection String Configuration
// For DocumentDB, add TLS certificate
const options: MongoClientOptions = {
tls: true,
tlsCAFile: '/path/to/rds-combined-ca-bundle.pem',
retryWrites: false, // DocumentDB limitation
readPreference: 'secondaryPreferred'
}4. Caching (ElastiCache Redis)
# elasticache.tf
resource "aws_elasticache_replication_group" "main" {
replication_group_id = "sunday-redis"
description = "Redis cluster for Sunday"
node_type = "cache.t3.micro"
num_cache_clusters = 2
automatic_failover_enabled = true
multi_az_enabled = true
engine = "redis"
engine_version = "7.0"
port = 6379
subnet_group_name = aws_elasticache_subnet_group.main.name
security_group_ids = [aws_security_group.redis.id]
# Encryption
at_rest_encryption_enabled = true
transit_encryption_enabled = true
auth_token = var.redis_auth_token
# Snapshots
snapshot_retention_limit = 7
snapshot_window = "05:00-06:00"
tags = {
Environment = "production"
}
}
resource "aws_elasticache_subnet_group" "main" {
name = "sunday-redis-subnet"
subnet_ids = module.vpc.database_subnets
}5. CDN and Load Balancer
# cloudfront.tf
resource "aws_cloudfront_distribution" "main" {
enabled = true
is_ipv6_enabled = true
aliases = ["app.sunday.com"]
origin {
domain_name = aws_lb.main.dns_name
origin_id = "alb"
custom_origin_config {
http_port = 80
https_port = 443
origin_protocol_policy = "https-only"
origin_ssl_protocols = ["TLSv1.2"]
}
}
# Static assets origin (S3)
origin {
domain_name = aws_s3_bucket.assets.bucket_regional_domain_name
origin_id = "s3-assets"
s3_origin_config {
origin_access_identity = aws_cloudfront_origin_access_identity.main.cloudfront_access_identity_path
}
}
default_cache_behavior {
allowed_methods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "alb"
forwarded_values {
query_string = true
cookies {
forward = "all"
}
headers = ["Authorization", "Host", "Accept", "Accept-Language"]
}
viewer_protocol_policy = "redirect-to-https"
min_ttl = 0
default_ttl = 0
max_ttl = 0
}
# Cache static assets
ordered_cache_behavior {
path_pattern = "/_next/static/*"
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "alb"
forwarded_values {
query_string = false
cookies {
forward = "none"
}
}
min_ttl = 31536000
default_ttl = 31536000
max_ttl = 31536000
compress = true
viewer_protocol_policy = "redirect-to-https"
}
# Cache uploaded files from S3
ordered_cache_behavior {
path_pattern = "/uploads/*"
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "s3-assets"
forwarded_values {
query_string = false
cookies {
forward = "none"
}
}
min_ttl = 86400
default_ttl = 604800
max_ttl = 31536000
compress = true
viewer_protocol_policy = "redirect-to-https"
}
restrictions {
geo_restriction {
restriction_type = "none"
}
}
viewer_certificate {
acm_certificate_arn = aws_acm_certificate.main.arn
ssl_support_method = "sni-only"
minimum_protocol_version = "TLSv1.2_2021"
}
web_acl_id = aws_wafv2_web_acl.main.arn
}6. Message Queue (SQS)
# sqs.tf
resource "aws_sqs_queue" "email" {
name = "sunday-email-queue"
delay_seconds = 0
max_message_size = 262144
message_retention_seconds = 86400
receive_wait_time_seconds = 20
visibility_timeout_seconds = 300
redrive_policy = jsonencode({
deadLetterTargetArn = aws_sqs_queue.email_dlq.arn
maxReceiveCount = 3
})
tags = {
Environment = "production"
}
}
resource "aws_sqs_queue" "email_dlq" {
name = "sunday-email-dlq"
message_retention_seconds = 1209600 # 14 days
}
resource "aws_sqs_queue" "automation" {
name = "sunday-automation-queue"
visibility_timeout_seconds = 60
redrive_policy = jsonencode({
deadLetterTargetArn = aws_sqs_queue.automation_dlq.arn
maxReceiveCount = 5
})
}
resource "aws_sqs_queue" "automation_dlq" {
name = "sunday-automation-dlq"
}7. Lambda Workers
# lambda.tf
resource "aws_lambda_function" "email_worker" {
filename = "lambda/email-worker.zip"
function_name = "sunday-email-worker"
role = aws_iam_role.lambda.arn
handler = "index.handler"
runtime = "nodejs20.x"
timeout = 60
memory_size = 256
vpc_config {
subnet_ids = module.vpc.private_subnets
security_group_ids = [aws_security_group.lambda.id]
}
environment {
variables = {
SES_REGION = "us-east-1"
}
}
}
resource "aws_lambda_event_source_mapping" "email" {
event_source_arn = aws_sqs_queue.email.arn
function_name = aws_lambda_function.email_worker.arn
batch_size = 10
}8. Security Configuration
WAF Rules
# waf.tf
resource "aws_wafv2_web_acl" "main" {
name = "sunday-waf"
description = "WAF for Sunday application"
scope = "CLOUDFRONT"
default_action {
allow {}
}
# Rate limiting
rule {
name = "RateLimitRule"
priority = 1
override_action {
none {}
}
statement {
rate_based_statement {
limit = 2000
aggregate_key_type = "IP"
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "RateLimitRule"
sampled_requests_enabled = true
}
}
# AWS Managed Rules
rule {
name = "AWSManagedRulesCommonRuleSet"
priority = 2
override_action {
none {}
}
statement {
managed_rule_group_statement {
name = "AWSManagedRulesCommonRuleSet"
vendor_name = "AWS"
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "CommonRules"
sampled_requests_enabled = true
}
}
# SQL Injection protection
rule {
name = "AWSManagedRulesSQLiRuleSet"
priority = 3
override_action {
none {}
}
statement {
managed_rule_group_statement {
name = "AWSManagedRulesSQLiRuleSet"
vendor_name = "AWS"
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "SQLiRules"
sampled_requests_enabled = true
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "SundayWAF"
sampled_requests_enabled = true
}
}Security Groups
# security_groups.tf
resource "aws_security_group" "alb" {
name = "sunday-alb-sg"
description = "ALB Security Group"
vpc_id = module.vpc.vpc_id
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_security_group" "ecs" {
name = "sunday-ecs-sg"
description = "ECS Tasks Security Group"
vpc_id = module.vpc.vpc_id
ingress {
from_port = 3000
to_port = 3000
protocol = "tcp"
security_groups = [aws_security_group.alb.id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_security_group" "docdb" {
name = "sunday-docdb-sg"
description = "DocumentDB Security Group"
vpc_id = module.vpc.vpc_id
ingress {
from_port = 27017
to_port = 27017
protocol = "tcp"
security_groups = [aws_security_group.ecs.id]
}
}
resource "aws_security_group" "redis" {
name = "sunday-redis-sg"
description = "Redis Security Group"
vpc_id = module.vpc.vpc_id
ingress {
from_port = 6379
to_port = 6379
protocol = "tcp"
security_groups = [aws_security_group.ecs.id]
}
}9. Monitoring and Alerting
# cloudwatch.tf
resource "aws_cloudwatch_dashboard" "main" {
dashboard_name = "sunday-dashboard"
dashboard_body = jsonencode({
widgets = [
{
type = "metric"
x = 0
y = 0
width = 12
height = 6
properties = {
metrics = [
["AWS/ECS", "CPUUtilization", "ClusterName", "sunday-cluster", "ServiceName", "sunday-service"]
]
title = "ECS CPU Utilization"
}
},
{
type = "metric"
x = 12
y = 0
width = 12
height = 6
properties = {
metrics = [
["AWS/ApplicationELB", "RequestCount", "LoadBalancer", aws_lb.main.arn_suffix]
]
title = "Request Count"
stat = "Sum"
period = 60
}
}
]
})
}
resource "aws_cloudwatch_metric_alarm" "high_cpu" {
alarm_name = "sunday-high-cpu"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 2
metric_name = "CPUUtilization"
namespace = "AWS/ECS"
period = 300
statistic = "Average"
threshold = 80
alarm_description = "ECS CPU usage exceeded 80%"
dimensions = {
ClusterName = aws_ecs_cluster.main.name
ServiceName = aws_ecs_service.sunday.name
}
alarm_actions = [aws_sns_topic.alerts.arn]
ok_actions = [aws_sns_topic.alerts.arn]
}
resource "aws_cloudwatch_metric_alarm" "5xx_errors" {
alarm_name = "sunday-5xx-errors"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 1
metric_name = "HTTPCode_Target_5XX_Count"
namespace = "AWS/ApplicationELB"
period = 60
statistic = "Sum"
threshold = 10
alarm_description = "High 5XX error rate"
dimensions = {
LoadBalancer = aws_lb.main.arn_suffix
}
alarm_actions = [aws_sns_topic.alerts.arn]
}Deployment Pipeline
GitHub Actions Workflow
# .github/workflows/deploy.yml
name: Deploy to AWS
on:
push:
branches: [main]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Build, tag, and push image
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
ECR_REPOSITORY: sunday
IMAGE_TAG: ${{ github.sha }}
run: |
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
docker tag $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG $ECR_REGISTRY/$ECR_REPOSITORY:latest
docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest
- name: Update ECS service
run: |
aws ecs update-service \
--cluster sunday-cluster \
--service sunday-service \
--force-new-deploymentCost Optimization Tips
- Use Spot Instances for non-critical workloads
- Reserved Capacity for predictable workloads (up to 72% savings)
- Right-size instances based on actual usage
- S3 Intelligent-Tiering for file storage
- CloudFront caching to reduce origin requests
- Lambda for sporadic workloads instead of always-on containers
Checklist
- Create VPC with proper subnets
- Set up DocumentDB cluster with replicas
- Configure ElastiCache Redis cluster
- Create ECR repository and push Docker image
- Create ECS cluster and service
- Configure ALB with health checks
- Set up CloudFront distribution
- Configure WAF rules
- Create SQS queues
- Deploy Lambda workers
- Store secrets in Secrets Manager
- Set up CloudWatch dashboards and alarms
- Configure Route 53 DNS
- Set up CI/CD pipeline