Advanced
AWS Deployment

AWS Deployment Architecture

Complete guide to deploying Sunday on AWS with production-grade architecture.

Architecture Overview


AWS Services Required

ServicePurposeEstimated Monthly Cost
Route 53DNS management$0.50/zone + queries
CloudFrontCDN, edge caching$0.085/GB
WAFWeb application firewall$5 + $1/million requests
ALBLoad balancer$16.20 + $0.008/LCU-hour
ECS FargateContainer orchestration~$73/month (2 vCPU, 4GB)
DocumentDBMongoDB-compatible database~$200/month (db.t3.medium)
ElastiCacheRedis for caching/sessions~$50/month (cache.t3.micro)
S3File storage$0.023/GB
SQSMessage queues$0.40/million requests
SESTransactional email$0.10/1000 emails
OpenSearchFull-text search~$80/month (t3.small)
Secrets ManagerSecrets storage$0.40/secret/month
CloudWatchMonitoring & 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, ElastiCache

Terraform 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-deployment

Cost Optimization Tips

  1. Use Spot Instances for non-critical workloads
  2. Reserved Capacity for predictable workloads (up to 72% savings)
  3. Right-size instances based on actual usage
  4. S3 Intelligent-Tiering for file storage
  5. CloudFront caching to reduce origin requests
  6. 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