ExpertКейс24 min

Практика: деплой PHP/Go приложения в AWS

Полная архитектура с Terraform: VPC, ALB, ECS Fargate, RDS PostgreSQL, ElastiCache Redis, S3, CloudWatch, Route53, ACM

Архитектура проекта

Мы создадим полную production-ready инфраструктуру для типичного веб-приложения: PHP API (Symfony) + Go API Gateway + Nuxt Frontend. Всё в Docker-контейнерах на ECS Fargate.

ASCII-диаграмма архитектуры

                           ┌──────────────┐
                           │   Route 53   │
                           │  DNS Zone    │
                           │ example.com  │
                           └──────┬───────┘
                                  │
                           ┌──────▼───────┐
                           │  ACM Cert    │
                           │  *.example   │
                           │   .com       │
                           └──────┬───────┘
                                  │
                    ┌─────────────▼─────────────┐
                    │   Application Load         │
                    │   Balancer (ALB)            │
                    │   :443 (HTTPS)              │
                    └─────┬──────────┬───────────┘
                          │          │
        ┌─────────────────┼──────────┼─────────────────┐
        │                 VPC: 10.0.0.0/16              │
        │                                               │
        │  ┌──────────────────────────────────────────┐ │
        │  │         PUBLIC SUBNETS                    │ │
        │  │  10.0.1.0/24 (AZ-a)  10.0.2.0/24 (AZ-b) │ │
        │  │  ┌──────────┐        ┌──────────┐        │ │
        │  │  │ NAT GW   │        │ NAT GW   │        │ │
        │  │  └──────────┘        └──────────┘        │ │
        │  └──────────────────────────────────────────┘ │
        │                                               │
        │  ┌──────────────────────────────────────────┐ │
        │  │         PRIVATE SUBNETS (App)             │ │
        │  │  10.0.10.0/24 (AZ-a) 10.0.11.0/24 (AZ-b)│ │
        │  │                                           │ │
        │  │  ┌────────────────┐  ┌────────────────┐  │ │
        │  │  │  ECS Fargate   │  │  ECS Fargate   │  │ │
        │  │  │  ┌──────────┐  │  │  ┌──────────┐  │  │ │
        │  │  │  │ PHP API  │  │  │  │ PHP API  │  │  │ │
        │  │  │  │ (Symfony) │  │  │  │ (Symfony) │  │  │ │
        │  │  │  └──────────┘  │  │  └──────────┘  │  │ │
        │  │  │  ┌──────────┐  │  │  ┌──────────┐  │  │ │
        │  │  │  │ Go GW    │  │  │  │ Go GW    │  │  │ │
        │  │  │  └──────────┘  │  │  └──────────┘  │  │ │
        │  │  └────────────────┘  └────────────────┘  │ │
        │  └──────────────────────────────────────────┘ │
        │                                               │
        │  ┌──────────────────────────────────────────┐ │
        │  │         PRIVATE SUBNETS (Data)            │ │
        │  │  10.0.20.0/24 (AZ-a) 10.0.21.0/24 (AZ-b)│ │
        │  │                                           │ │
        │  │  ┌────────────────┐  ┌────────────────┐  │ │
        │  │  │ RDS PostgreSQL │  │  ElastiCache   │  │ │
        │  │  │  (Multi-AZ)    │  │  Redis Cluster │  │ │
        │  │  │  Primary + RR  │  │                │  │ │
        │  │  └────────────────┘  └────────────────┘  │ │
        │  └──────────────────────────────────────────┘ │
        │                                               │
        └───────────────────────────────────────────────┘
                          │
               ┌──────────┼──────────┐
               │          │          │
        ┌──────▼──┐ ┌─────▼────┐ ┌──▼──────────┐
        │   S3    │ │CloudWatch│ │ ECR         │
        │ Bucket  │ │ Logs +   │ │ Docker      │
        │ Assets  │ │ Alarms   │ │ Registry    │
        └─────────┘ └──────────┘ └─────────────┘

Структура Terraform-проекта

infrastructure/
├── modules/
│   ├── networking/          # VPC, subnets, NAT, security groups
│   ├── ecs/                 # ECS cluster, services, task definitions
│   ├── database/            # RDS PostgreSQL
│   ├── cache/               # ElastiCache Redis
│   ├── storage/             # S3 buckets
│   ├── dns/                 # Route53, ACM
│   └── monitoring/          # CloudWatch, SNS
├── environments/
│   ├── dev/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   ├── outputs.tf
│   │   ├── backend.tf
│   │   └── terraform.tfvars
│   └── prod/
│       ├── main.tf
│       ├── variables.tf
│       ├── outputs.tf
│       ├── backend.tf
│       └── terraform.tfvars
└── global/
    └── ecr/                 # ECR repositories (shared)
        └── main.tf

1. Provider и Backend

# environments/prod/main.tf
terraform {
  required_version = ">= 1.9"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.80"
    }
    random = {
      source  = "hashicorp/random"
      version = "~> 3.6"
    }
  }
}

provider "aws" {
  region = var.aws_region

  default_tags {
    tags = {
      Project     = var.project
      Environment = var.environment
      ManagedBy   = "terraform"
    }
  }
}

# Provider for ACM certificate (must be in us-east-1 for CloudFront)
provider "aws" {
  alias  = "us_east_1"
  region = "us-east-1"
}
# environments/prod/backend.tf
terraform {
  backend "s3" {
    bucket         = "myapp-terraform-state"
    key            = "prod/terraform.tfstate"
    region         = "eu-central-1"
    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}
# environments/prod/variables.tf
variable "project" {
  type    = string
  default = "myapp"
}

variable "environment" {
  type    = string
  default = "prod"
}

variable "aws_region" {
  type    = string
  default = "eu-central-1"
}

variable "domain_name" {
  type    = string
  default = "example.com"
}

variable "vpc_cidr" {
  type    = string
  default = "10.0.0.0/16"
}

variable "php_image" {
  description = "PHP API Docker image URI"
  type        = string
}

variable "go_image" {
  description = "Go Gateway Docker image URI"
  type        = string
}

variable "php_cpu" {
  type    = number
  default = 512
}

variable "php_memory" {
  type    = number
  default = 1024
}

variable "go_cpu" {
  type    = number
  default = 256
}

variable "go_memory" {
  type    = number
  default = 512
}

variable "php_desired_count" {
  type    = number
  default = 2
}

variable "go_desired_count" {
  type    = number
  default = 2
}

variable "db_instance_class" {
  type    = string
  default = "db.r6g.large"
}

variable "db_allocated_storage" {
  type    = number
  default = 100
}

variable "redis_node_type" {
  type    = string
  default = "cache.r6g.large"
}

variable "alert_email" {
  type    = string
  default = "[email protected]"
}

2. Networking (VPC)

# modules/networking/main.tf
locals {
  name_prefix = "${var.project}-${var.environment}"

  azs = [
    "${var.aws_region}a",
    "${var.aws_region}b",
  ]

  # Subnet CIDR allocation
  public_subnets  = [for i, az in local.azs : cidrsubnet(var.vpc_cidr, 8, i + 1)]
  app_subnets     = [for i, az in local.azs : cidrsubnet(var.vpc_cidr, 8, i + 10)]
  data_subnets    = [for i, az in local.azs : cidrsubnet(var.vpc_cidr, 8, i + 20)]
}

# ========== VPC ==========
resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = { Name = "${local.name_prefix}-vpc" }
}

# ========== Internet Gateway ==========
resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id
  tags   = { Name = "${local.name_prefix}-igw" }
}

# ========== Public Subnets ==========
resource "aws_subnet" "public" {
  count = length(local.azs)

  vpc_id                  = aws_vpc.main.id
  cidr_block              = local.public_subnets[count.index]
  availability_zone       = local.azs[count.index]
  map_public_ip_on_launch = true

  tags = {
    Name = "${local.name_prefix}-public-${local.azs[count.index]}"
    Tier = "public"
  }
}

# ========== App Subnets (private) ==========
resource "aws_subnet" "app" {
  count = length(local.azs)

  vpc_id            = aws_vpc.main.id
  cidr_block        = local.app_subnets[count.index]
  availability_zone = local.azs[count.index]

  tags = {
    Name = "${local.name_prefix}-app-${local.azs[count.index]}"
    Tier = "app"
  }
}

# ========== Data Subnets (private) ==========
resource "aws_subnet" "data" {
  count = length(local.azs)

  vpc_id            = aws_vpc.main.id
  cidr_block        = local.data_subnets[count.index]
  availability_zone = local.azs[count.index]

  tags = {
    Name = "${local.name_prefix}-data-${local.azs[count.index]}"
    Tier = "data"
  }
}

# ========== NAT Gateways ==========
resource "aws_eip" "nat" {
  count  = length(local.azs)
  domain = "vpc"
  tags   = { Name = "${local.name_prefix}-nat-eip-${count.index}" }
}

resource "aws_nat_gateway" "main" {
  count = length(local.azs)

  allocation_id = aws_eip.nat[count.index].id
  subnet_id     = aws_subnet.public[count.index].id

  tags = { Name = "${local.name_prefix}-nat-${local.azs[count.index]}" }

  depends_on = [aws_internet_gateway.main]
}

# ========== Route Tables ==========
# Public route table
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.main.id
  }

  tags = { Name = "${local.name_prefix}-public-rt" }
}

resource "aws_route_table_association" "public" {
  count          = length(local.azs)
  subnet_id      = aws_subnet.public[count.index].id
  route_table_id = aws_route_table.public.id
}

# Private route tables (one per AZ for HA NAT)
resource "aws_route_table" "private" {
  count  = length(local.azs)
  vpc_id = aws_vpc.main.id

  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.main[count.index].id
  }

  tags = { Name = "${local.name_prefix}-private-rt-${local.azs[count.index]}" }
}

resource "aws_route_table_association" "app" {
  count          = length(local.azs)
  subnet_id      = aws_subnet.app[count.index].id
  route_table_id = aws_route_table.private[count.index].id
}

resource "aws_route_table_association" "data" {
  count          = length(local.azs)
  subnet_id      = aws_subnet.data[count.index].id
  route_table_id = aws_route_table.private[count.index].id
}

# ========== Security Groups ==========

# ALB Security Group
resource "aws_security_group" "alb" {
  name        = "${local.name_prefix}-alb-sg"
  description = "ALB security group"
  vpc_id      = aws_vpc.main.id

  ingress {
    description = "HTTPS"
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    description = "HTTP (redirect to HTTPS)"
    from_port   = 80
    to_port     = 80
    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"]
  }

  tags = { Name = "${local.name_prefix}-alb-sg" }
}

# ECS Tasks Security Group
resource "aws_security_group" "ecs" {
  name        = "${local.name_prefix}-ecs-sg"
  description = "ECS tasks security group"
  vpc_id      = aws_vpc.main.id

  ingress {
    description     = "From ALB"
    from_port       = 0
    to_port         = 65535
    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"]
  }

  tags = { Name = "${local.name_prefix}-ecs-sg" }
}

# Database Security Group
resource "aws_security_group" "database" {
  name        = "${local.name_prefix}-db-sg"
  description = "Database security group"
  vpc_id      = aws_vpc.main.id

  ingress {
    description     = "PostgreSQL from ECS"
    from_port       = 5432
    to_port         = 5432
    protocol        = "tcp"
    security_groups = [aws_security_group.ecs.id]
  }

  tags = { Name = "${local.name_prefix}-db-sg" }
}

# Redis Security Group
resource "aws_security_group" "redis" {
  name        = "${local.name_prefix}-redis-sg"
  description = "Redis security group"
  vpc_id      = aws_vpc.main.id

  ingress {
    description     = "Redis from ECS"
    from_port       = 6379
    to_port         = 6379
    protocol        = "tcp"
    security_groups = [aws_security_group.ecs.id]
  }

  tags = { Name = "${local.name_prefix}-redis-sg" }
}
# modules/networking/outputs.tf
output "vpc_id" {
  value = aws_vpc.main.id
}

output "public_subnet_ids" {
  value = aws_subnet.public[*].id
}

output "app_subnet_ids" {
  value = aws_subnet.app[*].id
}

output "data_subnet_ids" {
  value = aws_subnet.data[*].id
}

output "alb_security_group_id" {
  value = aws_security_group.alb.id
}

output "ecs_security_group_id" {
  value = aws_security_group.ecs.id
}

output "database_security_group_id" {
  value = aws_security_group.database.id
}

output "redis_security_group_id" {
  value = aws_security_group.redis.id
}

3. Application Load Balancer

# modules/ecs/alb.tf

resource "aws_lb" "main" {
  name               = "${var.name_prefix}-alb"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [var.alb_security_group_id]
  subnets            = var.public_subnet_ids

  enable_deletion_protection = var.environment == "prod"

  access_logs {
    bucket  = var.logs_bucket_id
    prefix  = "alb-logs"
    enabled = true
  }

  tags = { Name = "${var.name_prefix}-alb" }
}

# HTTPS Listener
resource "aws_lb_listener" "https" {
  load_balancer_arn = aws_lb.main.arn
  port              = 443
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-TLS13-1-2-2021-06"
  certificate_arn   = var.certificate_arn

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.go_gateway.arn
  }
}

# HTTP → HTTPS redirect
resource "aws_lb_listener" "http_redirect" {
  load_balancer_arn = aws_lb.main.arn
  port              = 80
  protocol          = "HTTP"

  default_action {
    type = "redirect"
    redirect {
      port        = "443"
      protocol    = "HTTPS"
      status_code = "HTTP_301"
    }
  }
}

# Listener rule: /api/* → PHP API
resource "aws_lb_listener_rule" "php_api" {
  listener_arn = aws_lb_listener.https.arn
  priority     = 100

  action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.php_api.arn
  }

  condition {
    path_pattern {
      values = ["/api/*"]
    }
  }
}

# Target Group: Go Gateway
resource "aws_lb_target_group" "go_gateway" {
  name        = "${var.name_prefix}-go-tg"
  port        = 8080
  protocol    = "HTTP"
  vpc_id      = var.vpc_id
  target_type = "ip"

  health_check {
    enabled             = true
    healthy_threshold   = 3
    unhealthy_threshold = 3
    timeout             = 5
    interval            = 30
    path                = "/health"
    matcher             = "200"
  }

  deregistration_delay = 30

  tags = { Name = "${var.name_prefix}-go-tg" }
}

# Target Group: PHP API
resource "aws_lb_target_group" "php_api" {
  name        = "${var.name_prefix}-php-tg"
  port        = 80
  protocol    = "HTTP"
  vpc_id      = var.vpc_id
  target_type = "ip"

  health_check {
    enabled             = true
    healthy_threshold   = 3
    unhealthy_threshold = 3
    timeout             = 10
    interval            = 30
    path                = "/api/health"
    matcher             = "200"
  }

  deregistration_delay = 60

  tags = { Name = "${var.name_prefix}-php-tg" }
}

4. ECS Fargate (Compute)

# modules/ecs/cluster.tf

resource "aws_ecs_cluster" "main" {
  name = "${var.name_prefix}-cluster"

  setting {
    name  = "containerInsights"
    value = "enabled"
  }

  configuration {
    execute_command_configuration {
      logging = "OVERRIDE"

      log_configuration {
        cloud_watch_log_group_name = aws_cloudwatch_log_group.ecs.name
      }
    }
  }

  tags = { Name = "${var.name_prefix}-cluster" }
}

resource "aws_cloudwatch_log_group" "ecs" {
  name              = "/ecs/${var.name_prefix}"
  retention_in_days = var.environment == "prod" ? 90 : 14

  tags = { Name = "${var.name_prefix}-ecs-logs" }
}

# ========== IAM Roles ==========

# Task Execution Role (used by ECS agent)
data "aws_iam_policy_document" "ecs_task_execution_assume" {
  statement {
    actions = ["sts:AssumeRole"]
    principals {
      type        = "Service"
      identifiers = ["ecs-tasks.amazonaws.com"]
    }
  }
}

resource "aws_iam_role" "ecs_task_execution" {
  name               = "${var.name_prefix}-ecs-execution"
  assume_role_policy = data.aws_iam_policy_document.ecs_task_execution_assume.json
}

resource "aws_iam_role_policy_attachment" "ecs_task_execution" {
  role       = aws_iam_role.ecs_task_execution.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}

# Allow pulling secrets
resource "aws_iam_role_policy" "ecs_task_execution_secrets" {
  name = "${var.name_prefix}-secrets-access"
  role = aws_iam_role.ecs_task_execution.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "secretsmanager:GetSecretValue",
          "ssm:GetParameters",
        ]
        Resource = [
          var.db_secret_arn,
          "arn:aws:ssm:${var.aws_region}:*:parameter/${var.project}/${var.environment}/*",
        ]
      }
    ]
  })
}

# Task Role (used by the application)
resource "aws_iam_role" "ecs_task" {
  name               = "${var.name_prefix}-ecs-task"
  assume_role_policy = data.aws_iam_policy_document.ecs_task_execution_assume.json
}

resource "aws_iam_role_policy" "ecs_task_app" {
  name = "${var.name_prefix}-app-permissions"
  role = aws_iam_role.ecs_task.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "s3:GetObject",
          "s3:PutObject",
          "s3:DeleteObject",
        ]
        Resource = "${var.assets_bucket_arn}/*"
      },
      {
        Effect = "Allow"
        Action = [
          "s3:ListBucket",
        ]
        Resource = var.assets_bucket_arn
      }
    ]
  })
}

PHP API Task Definition и Service

# modules/ecs/php_service.tf

resource "aws_ecs_task_definition" "php_api" {
  family                   = "${var.name_prefix}-php-api"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  cpu                      = var.php_cpu
  memory                   = var.php_memory
  execution_role_arn       = aws_iam_role.ecs_task_execution.arn
  task_role_arn            = aws_iam_role.ecs_task.arn

  container_definitions = jsonencode([
    {
      name  = "php-api"
      image = var.php_image

      portMappings = [
        {
          containerPort = 80
          protocol      = "tcp"
        }
      ]

      environment = [
        { name = "APP_ENV", value = var.environment == "prod" ? "prod" : "dev" },
        { name = "APP_DEBUG", value = var.environment == "prod" ? "0" : "1" },
        { name = "REDIS_HOST", value = var.redis_endpoint },
        { name = "REDIS_PORT", value = "6379" },
        { name = "S3_BUCKET", value = var.assets_bucket_name },
        { name = "AWS_REGION", value = var.aws_region },
      ]

      secrets = [
        {
          name      = "DATABASE_URL"
          valueFrom = "${var.db_secret_arn}:connection_string::"
        },
        {
          name      = "APP_SECRET"
          valueFrom = "arn:aws:ssm:${var.aws_region}:${data.aws_caller_identity.current.account_id}:parameter/${var.project}/${var.environment}/app-secret"
        }
      ]

      logConfiguration = {
        logDriver = "awslogs"
        options = {
          "awslogs-group"         = aws_cloudwatch_log_group.ecs.name
          "awslogs-region"        = var.aws_region
          "awslogs-stream-prefix" = "php-api"
        }
      }

      healthCheck = {
        command     = ["CMD-SHELL", "curl -f http://localhost/api/health || exit 1"]
        interval    = 30
        timeout     = 5
        retries     = 3
        startPeriod = 60
      }
    }
  ])

  tags = { Name = "${var.name_prefix}-php-api" }
}

data "aws_caller_identity" "current" {}

resource "aws_ecs_service" "php_api" {
  name            = "${var.name_prefix}-php-api"
  cluster         = aws_ecs_cluster.main.id
  task_definition = aws_ecs_task_definition.php_api.arn
  desired_count   = var.php_desired_count
  launch_type     = "FARGATE"

  network_configuration {
    subnets          = var.app_subnet_ids
    security_groups  = [var.ecs_security_group_id]
    assign_public_ip = false
  }

  load_balancer {
    target_group_arn = aws_lb_target_group.php_api.arn
    container_name   = "php-api"
    container_port   = 80
  }

  deployment_configuration {
    maximum_percent         = 200
    minimum_healthy_percent = 100

    deployment_circuit_breaker {
      enable   = true
      rollback = true  # Auto-rollback on deployment failure
    }
  }

  lifecycle {
    ignore_changes = [desired_count]  # Managed by auto-scaling
  }

  tags = { Name = "${var.name_prefix}-php-api" }
}

# Auto-scaling for PHP API
resource "aws_appautoscaling_target" "php_api" {
  max_capacity       = var.php_max_count
  min_capacity       = var.php_desired_count
  resource_id        = "service/${aws_ecs_cluster.main.name}/${aws_ecs_service.php_api.name}"
  scalable_dimension = "ecs:service:DesiredCount"
  service_namespace  = "ecs"
}

resource "aws_appautoscaling_policy" "php_api_cpu" {
  name               = "${var.name_prefix}-php-cpu-scaling"
  policy_type        = "TargetTrackingScaling"
  resource_id        = aws_appautoscaling_target.php_api.resource_id
  scalable_dimension = aws_appautoscaling_target.php_api.scalable_dimension
  service_namespace  = aws_appautoscaling_target.php_api.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
  }
}

resource "aws_appautoscaling_policy" "php_api_memory" {
  name               = "${var.name_prefix}-php-memory-scaling"
  policy_type        = "TargetTrackingScaling"
  resource_id        = aws_appautoscaling_target.php_api.resource_id
  scalable_dimension = aws_appautoscaling_target.php_api.scalable_dimension
  service_namespace  = aws_appautoscaling_target.php_api.service_namespace

  target_tracking_scaling_policy_configuration {
    predefined_metric_specification {
      predefined_metric_type = "ECSServiceAverageMemoryUtilization"
    }
    target_value       = 80.0
    scale_in_cooldown  = 300
    scale_out_cooldown = 60
  }
}

Go Gateway Task Definition и Service

# modules/ecs/go_service.tf

resource "aws_ecs_task_definition" "go_gateway" {
  family                   = "${var.name_prefix}-go-gateway"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  cpu                      = var.go_cpu
  memory                   = var.go_memory
  execution_role_arn       = aws_iam_role.ecs_task_execution.arn
  task_role_arn            = aws_iam_role.ecs_task.arn

  container_definitions = jsonencode([
    {
      name  = "go-gateway"
      image = var.go_image

      portMappings = [
        {
          containerPort = 8080
          protocol      = "tcp"
        }
      ]

      environment = [
        { name = "ENV", value = var.environment },
        { name = "PHP_API_URL", value = "http://${var.name_prefix}-php-api.${var.name_prefix}-cluster:80" },
        { name = "REDIS_ADDR", value = "${var.redis_endpoint}:6379" },
        { name = "PORT", value = "8080" },
        { name = "LOG_LEVEL", value = var.environment == "prod" ? "info" : "debug" },
      ]

      logConfiguration = {
        logDriver = "awslogs"
        options = {
          "awslogs-group"         = aws_cloudwatch_log_group.ecs.name
          "awslogs-region"        = var.aws_region
          "awslogs-stream-prefix" = "go-gateway"
        }
      }

      healthCheck = {
        command     = ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1"]
        interval    = 15
        timeout     = 5
        retries     = 3
        startPeriod = 10
      }
    }
  ])

  tags = { Name = "${var.name_prefix}-go-gateway" }
}

resource "aws_ecs_service" "go_gateway" {
  name            = "${var.name_prefix}-go-gateway"
  cluster         = aws_ecs_cluster.main.id
  task_definition = aws_ecs_task_definition.go_gateway.arn
  desired_count   = var.go_desired_count
  launch_type     = "FARGATE"

  network_configuration {
    subnets          = var.app_subnet_ids
    security_groups  = [var.ecs_security_group_id]
    assign_public_ip = false
  }

  load_balancer {
    target_group_arn = aws_lb_target_group.go_gateway.arn
    container_name   = "go-gateway"
    container_port   = 8080
  }

  deployment_configuration {
    maximum_percent         = 200
    minimum_healthy_percent = 100

    deployment_circuit_breaker {
      enable   = true
      rollback = true
    }
  }

  lifecycle {
    ignore_changes = [desired_count]
  }

  tags = { Name = "${var.name_prefix}-go-gateway" }
}

5. Docker Images и ECR

# global/ecr/main.tf
resource "aws_ecr_repository" "php_api" {
  name                 = "myapp/php-api"
  image_tag_mutability = "IMMUTABLE"  # Tags can't be overwritten

  image_scanning_configuration {
    scan_on_push = true
  }

  encryption_configuration {
    encryption_type = "AES256"
  }

  tags = {
    Project   = "myapp"
    ManagedBy = "terraform"
  }
}

resource "aws_ecr_repository" "go_gateway" {
  name                 = "myapp/go-gateway"
  image_tag_mutability = "IMMUTABLE"

  image_scanning_configuration {
    scan_on_push = true
  }

  tags = {
    Project   = "myapp"
    ManagedBy = "terraform"
  }
}

# Lifecycle policy: keep last 30 images
resource "aws_ecr_lifecycle_policy" "php_api" {
  repository = aws_ecr_repository.php_api.name

  policy = jsonencode({
    rules = [{
      rulePriority = 1
      description  = "Keep last 30 images"
      selection = {
        tagStatus   = "any"
        countType   = "imageCountMoreThan"
        countNumber = 30
      }
      action = {
        type = "expire"
      }
    }]
  })
}

Dockerfile: PHP API (Multi-stage)

# infrastructure/docker/php/Dockerfile
FROM php:8.4-fpm-alpine AS base

# Install system dependencies
RUN apk add --no-cache \
    postgresql-dev \
    icu-dev \
    libzip-dev \
    && docker-php-ext-install \
    pdo_pgsql \
    intl \
    opcache \
    zip

# Install Composer
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer

# ========== Dependencies stage ==========
FROM base AS deps

WORKDIR /app
COPY api/composer.json api/composer.lock ./
RUN composer install --no-dev --no-scripts --no-autoloader --prefer-dist

# ========== Build stage ==========
FROM base AS build

WORKDIR /app
COPY --from=deps /app/vendor ./vendor
COPY api/ .
RUN composer dump-autoload --optimize --classmap-authoritative

# ========== Production stage ==========
FROM base AS production

# Security: non-root user
RUN addgroup -g 1000 app && adduser -u 1000 -G app -D app

WORKDIR /app
COPY --from=build --chown=app:app /app .

# PHP configuration for production
COPY infrastructure/docker/php/php.ini /usr/local/etc/php/conf.d/app.ini

USER app

EXPOSE 80
HEALTHCHECK --interval=30s --timeout=5s CMD curl -f http://localhost/api/health || exit 1

Dockerfile: Go Gateway (Multi-stage)

# infrastructure/docker/go/Dockerfile
FROM golang:1.25-alpine AS builder

WORKDIR /build

# Dependencies first (cache layer)
COPY gateway/go.mod gateway/go.sum ./
RUN go mod download

# Build
COPY gateway/ .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
    go build -ldflags="-w -s" -o /app/gateway ./cmd/gateway

# ========== Production stage ==========
FROM alpine:3.21

# Security: non-root user
RUN addgroup -g 1000 app && adduser -u 1000 -G app -D app

# Certificates for HTTPS calls
RUN apk add --no-cache ca-certificates wget

WORKDIR /app
COPY --from=builder /app/gateway .

USER app

EXPOSE 8080
HEALTHCHECK --interval=15s --timeout=5s CMD wget --spider -q http://localhost:8080/health || exit 1

ENTRYPOINT ["./gateway"]

6. Database (RDS PostgreSQL)

# modules/database/main.tf

resource "random_password" "master" {
  length  = 32
  special = false
}

resource "aws_secretsmanager_secret" "db" {
  name = "${var.name_prefix}/database/master"
}

resource "aws_secretsmanager_secret_version" "db" {
  secret_id = aws_secretsmanager_secret.db.id
  secret_string = jsonencode({
    username          = "app_master"
    password          = random_password.master.result
    engine            = "postgres"
    host              = aws_db_instance.main.address
    port              = aws_db_instance.main.port
    dbname            = "${var.project}_db"
    connection_string = "postgresql://app_master:${random_password.master.result}@${aws_db_instance.main.address}:5432/${var.project}_db"
  })
}

resource "aws_db_subnet_group" "main" {
  name       = "${var.name_prefix}-db"
  subnet_ids = var.data_subnet_ids

  tags = { Name = "${var.name_prefix}-db-subnet-group" }
}

resource "aws_db_parameter_group" "postgres18" {
  name   = "${var.name_prefix}-pg18"
  family = "postgres18"

  parameter {
    name  = "shared_preload_libraries"
    value = "pg_stat_statements"
    apply_method = "pending-reboot"
  }

  parameter {
    name  = "log_min_duration_statement"
    value = "500"
  }

  parameter {
    name  = "log_connections"
    value = "1"
  }

  parameter {
    name  = "idle_in_transaction_session_timeout"
    value = "60000"  # 60 seconds
  }
}

resource "aws_db_instance" "main" {
  identifier = "${var.name_prefix}-postgres"

  engine         = "postgres"
  engine_version = "18.1"
  instance_class = var.db_instance_class

  allocated_storage     = var.db_allocated_storage
  max_allocated_storage = var.db_allocated_storage * 2
  storage_type          = "gp3"
  storage_encrypted     = true

  db_name  = "${var.project}_db"
  username = "app_master"
  password = random_password.master.result

  multi_az               = var.environment == "prod"
  db_subnet_group_name   = aws_db_subnet_group.main.name
  vpc_security_group_ids = [var.database_security_group_id]
  parameter_group_name   = aws_db_parameter_group.postgres18.name

  backup_retention_period = var.environment == "prod" ? 30 : 7
  backup_window           = "03:00-04:00"
  maintenance_window      = "Mon:04:00-Mon:05:00"

  deletion_protection       = var.environment == "prod"
  skip_final_snapshot       = var.environment != "prod"
  final_snapshot_identifier = "${var.name_prefix}-final"

  performance_insights_enabled          = true
  performance_insights_retention_period = var.environment == "prod" ? 731 : 7  # Free tier: 7 days

  enabled_cloudwatch_logs_exports = ["postgresql", "upgrade"]

  tags = { Name = "${var.name_prefix}-postgres" }

  lifecycle {
    prevent_destroy = false  # Set to true in real production!
  }
}

# Read Replica (production only)
resource "aws_db_instance" "read_replica" {
  count = var.environment == "prod" ? 1 : 0

  identifier          = "${var.name_prefix}-postgres-rr"
  replicate_source_db = aws_db_instance.main.identifier
  instance_class      = var.db_instance_class
  storage_encrypted   = true

  performance_insights_enabled = true

  tags = { Name = "${var.name_prefix}-postgres-read-replica" }
}

7. Cache (ElastiCache Redis)

# modules/cache/main.tf

resource "aws_elasticache_subnet_group" "main" {
  name       = "${var.name_prefix}-redis"
  subnet_ids = var.data_subnet_ids
}

resource "aws_elasticache_parameter_group" "main" {
  name   = "${var.name_prefix}-redis7"
  family = "redis7"

  parameter {
    name  = "maxmemory-policy"
    value = "allkeys-lru"
  }
}

resource "aws_elasticache_replication_group" "main" {
  replication_group_id = "${var.name_prefix}-redis"
  description          = "Redis cluster for ${var.name_prefix}"

  node_type            = var.redis_node_type
  num_cache_clusters   = var.environment == "prod" ? 2 : 1  # Primary + replica
  port                 = 6379

  engine               = "redis"
  engine_version       = "7.1"
  parameter_group_name = aws_elasticache_parameter_group.main.name

  subnet_group_name    = aws_elasticache_subnet_group.main.name
  security_group_ids   = [var.redis_security_group_id]

  at_rest_encryption_enabled = true
  transit_encryption_enabled = false  # Enable if clients support TLS

  automatic_failover_enabled = var.environment == "prod"

  snapshot_retention_limit = var.environment == "prod" ? 7 : 0
  snapshot_window          = "04:00-05:00"
  maintenance_window       = "Mon:05:00-Mon:06:00"

  tags = { Name = "${var.name_prefix}-redis" }
}

8. Storage (S3)

# modules/storage/main.tf

resource "aws_s3_bucket" "assets" {
  bucket = "${var.name_prefix}-assets"

  tags = { Name = "${var.name_prefix}-assets" }
}

resource "aws_s3_bucket_versioning" "assets" {
  bucket = aws_s3_bucket.assets.id
  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_server_side_encryption_configuration" "assets" {
  bucket = aws_s3_bucket.assets.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}

resource "aws_s3_bucket_public_access_block" "assets" {
  bucket = aws_s3_bucket.assets.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource "aws_s3_bucket_lifecycle_configuration" "assets" {
  bucket = aws_s3_bucket.assets.id

  rule {
    id     = "transition-to-ia"
    status = "Enabled"

    transition {
      days          = 90
      storage_class = "STANDARD_IA"
    }
  }
}

# ALB Logs bucket
resource "aws_s3_bucket" "logs" {
  bucket = "${var.name_prefix}-logs"
  tags   = { Name = "${var.name_prefix}-logs" }
}

resource "aws_s3_bucket_lifecycle_configuration" "logs" {
  bucket = aws_s3_bucket.logs.id

  rule {
    id     = "expire-old-logs"
    status = "Enabled"

    expiration {
      days = var.environment == "prod" ? 365 : 30
    }
  }
}

9. DNS и SSL (Route 53 + ACM)

# modules/dns/main.tf

data "aws_route53_zone" "main" {
  name = var.domain_name
}

# ACM Certificate
resource "aws_acm_certificate" "main" {
  domain_name               = var.domain_name
  subject_alternative_names = ["*.${var.domain_name}"]
  validation_method         = "DNS"

  lifecycle {
    create_before_destroy = true
  }

  tags = { Name = "${var.name_prefix}-cert" }
}

# DNS validation records
resource "aws_route53_record" "cert_validation" {
  for_each = {
    for dvo in aws_acm_certificate.main.domain_validation_options : dvo.domain_name => {
      name   = dvo.resource_record_name
      type   = dvo.resource_record_type
      record = dvo.resource_record_value
    }
  }

  zone_id = data.aws_route53_zone.main.zone_id
  name    = each.value.name
  type    = each.value.type
  records = [each.value.record]
  ttl     = 60

  allow_overwrite = true
}

resource "aws_acm_certificate_validation" "main" {
  certificate_arn         = aws_acm_certificate.main.arn
  validation_record_fqdns = [for r in aws_route53_record.cert_validation : r.fqdn]
}

# A record pointing to ALB
resource "aws_route53_record" "app" {
  zone_id = data.aws_route53_zone.main.zone_id
  name    = var.environment == "prod" ? var.domain_name : "${var.environment}.${var.domain_name}"
  type    = "A"

  alias {
    name                   = var.alb_dns_name
    zone_id                = var.alb_zone_id
    evaluate_target_health = true
  }
}

# API subdomain
resource "aws_route53_record" "api" {
  zone_id = data.aws_route53_zone.main.zone_id
  name    = var.environment == "prod" ? "api.${var.domain_name}" : "api.${var.environment}.${var.domain_name}"
  type    = "A"

  alias {
    name                   = var.alb_dns_name
    zone_id                = var.alb_zone_id
    evaluate_target_health = true
  }
}

10. Monitoring (CloudWatch + SNS)

# modules/monitoring/main.tf

# SNS Topic for alerts
resource "aws_sns_topic" "alerts" {
  name = "${var.name_prefix}-alerts"
}

resource "aws_sns_topic_subscription" "email" {
  topic_arn = aws_sns_topic.alerts.arn
  protocol  = "email"
  endpoint  = var.alert_email
}

# ========== ECS Alarms ==========

resource "aws_cloudwatch_metric_alarm" "ecs_cpu_high" {
  alarm_name          = "${var.name_prefix}-ecs-cpu-high"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = 3
  metric_name         = "CPUUtilization"
  namespace           = "AWS/ECS"
  period              = 300
  statistic           = "Average"
  threshold           = 85
  alarm_description   = "ECS CPU utilization above 85%"
  alarm_actions       = [aws_sns_topic.alerts.arn]

  dimensions = {
    ClusterName = var.ecs_cluster_name
    ServiceName = var.php_service_name
  }
}

# ========== RDS Alarms ==========

resource "aws_cloudwatch_metric_alarm" "rds_cpu_high" {
  alarm_name          = "${var.name_prefix}-rds-cpu-high"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = 3
  metric_name         = "CPUUtilization"
  namespace           = "AWS/RDS"
  period              = 300
  statistic           = "Average"
  threshold           = 80
  alarm_description   = "RDS CPU utilization above 80%"
  alarm_actions       = [aws_sns_topic.alerts.arn]

  dimensions = {
    DBInstanceIdentifier = var.rds_instance_id
  }
}

resource "aws_cloudwatch_metric_alarm" "rds_free_storage" {
  alarm_name          = "${var.name_prefix}-rds-storage-low"
  comparison_operator = "LessThanThreshold"
  evaluation_periods  = 1
  metric_name         = "FreeStorageSpace"
  namespace           = "AWS/RDS"
  period              = 300
  statistic           = "Average"
  threshold           = 5368709120  # 5 GB in bytes
  alarm_description   = "RDS free storage below 5GB"
  alarm_actions       = [aws_sns_topic.alerts.arn]

  dimensions = {
    DBInstanceIdentifier = var.rds_instance_id
  }
}

resource "aws_cloudwatch_metric_alarm" "rds_connections_high" {
  alarm_name          = "${var.name_prefix}-rds-connections-high"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = 2
  metric_name         = "DatabaseConnections"
  namespace           = "AWS/RDS"
  period              = 300
  statistic           = "Average"
  threshold           = 100
  alarm_description   = "RDS connections above 100"
  alarm_actions       = [aws_sns_topic.alerts.arn]

  dimensions = {
    DBInstanceIdentifier = var.rds_instance_id
  }
}

# ========== Redis Alarms ==========

resource "aws_cloudwatch_metric_alarm" "redis_memory_high" {
  alarm_name          = "${var.name_prefix}-redis-memory-high"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = 2
  metric_name         = "DatabaseMemoryUsagePercentage"
  namespace           = "AWS/ElastiCache"
  period              = 300
  statistic           = "Average"
  threshold           = 80
  alarm_description   = "Redis memory usage above 80%"
  alarm_actions       = [aws_sns_topic.alerts.arn]

  dimensions = {
    CacheClusterId = var.redis_cluster_id
  }
}

# ========== ALB Alarms ==========

resource "aws_cloudwatch_metric_alarm" "alb_5xx" {
  alarm_name          = "${var.name_prefix}-alb-5xx-high"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = 2
  metric_name         = "HTTPCode_Target_5XX_Count"
  namespace           = "AWS/ApplicationELB"
  period              = 300
  statistic           = "Sum"
  threshold           = 50
  alarm_description   = "ALB 5xx errors above 50 in 5 minutes"
  alarm_actions       = [aws_sns_topic.alerts.arn]
  treat_missing_data  = "notBreaching"

  dimensions = {
    LoadBalancer = var.alb_arn_suffix
  }
}

resource "aws_cloudwatch_metric_alarm" "alb_response_time" {
  alarm_name          = "${var.name_prefix}-alb-latency-high"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = 3
  metric_name         = "TargetResponseTime"
  namespace           = "AWS/ApplicationELB"
  period              = 300
  statistic           = "p95"
  threshold           = 2.0  # 2 seconds p95
  alarm_description   = "ALB p95 latency above 2 seconds"
  alarm_actions       = [aws_sns_topic.alerts.arn]

  dimensions = {
    LoadBalancer = var.alb_arn_suffix
  }
}

# CloudWatch Dashboard
resource "aws_cloudwatch_dashboard" "main" {
  dashboard_name = "${var.name_prefix}-dashboard"

  dashboard_body = jsonencode({
    widgets = [
      {
        type   = "metric"
        x      = 0
        y      = 0
        width  = 12
        height = 6
        properties = {
          title   = "ECS CPU & Memory"
          metrics = [
            ["AWS/ECS", "CPUUtilization", "ClusterName", var.ecs_cluster_name],
            ["AWS/ECS", "MemoryUtilization", "ClusterName", var.ecs_cluster_name],
          ]
          period = 300
          stat   = "Average"
          region = var.aws_region
        }
      },
      {
        type   = "metric"
        x      = 12
        y      = 0
        width  = 12
        height = 6
        properties = {
          title   = "RDS Performance"
          metrics = [
            ["AWS/RDS", "CPUUtilization", "DBInstanceIdentifier", var.rds_instance_id],
            ["AWS/RDS", "DatabaseConnections", "DBInstanceIdentifier", var.rds_instance_id],
          ]
          period = 300
          stat   = "Average"
          region = var.aws_region
        }
      },
      {
        type   = "metric"
        x      = 0
        y      = 6
        width  = 24
        height = 6
        properties = {
          title   = "ALB Requests & Errors"
          metrics = [
            ["AWS/ApplicationELB", "RequestCount", "LoadBalancer", var.alb_arn_suffix],
            ["AWS/ApplicationELB", "HTTPCode_Target_5XX_Count", "LoadBalancer", var.alb_arn_suffix],
            ["AWS/ApplicationELB", "HTTPCode_Target_4XX_Count", "LoadBalancer", var.alb_arn_suffix],
          ]
          period = 60
          stat   = "Sum"
          region = var.aws_region
        }
      }
    ]
  })
}

11. Root Module -- собираем всё вместе

# environments/prod/main.tf

locals {
  name_prefix = "${var.project}-${var.environment}"
}

module "networking" {
  source = "../../modules/networking"

  project     = var.project
  environment = var.environment
  aws_region  = var.aws_region
  vpc_cidr    = var.vpc_cidr
}

module "storage" {
  source = "../../modules/storage"

  name_prefix = local.name_prefix
  environment = var.environment
}

module "database" {
  source = "../../modules/database"

  name_prefix  = local.name_prefix
  project      = var.project
  environment  = var.environment

  data_subnet_ids            = module.networking.data_subnet_ids
  database_security_group_id = module.networking.database_security_group_id

  db_instance_class    = var.db_instance_class
  db_allocated_storage = var.db_allocated_storage
}

module "cache" {
  source = "../../modules/cache"

  name_prefix  = local.name_prefix
  environment  = var.environment

  data_subnet_ids        = module.networking.data_subnet_ids
  redis_security_group_id = module.networking.redis_security_group_id
  redis_node_type         = var.redis_node_type
}

module "dns" {
  source = "../../modules/dns"

  name_prefix = local.name_prefix
  environment = var.environment
  domain_name = var.domain_name
  alb_dns_name = module.ecs.alb_dns_name
  alb_zone_id  = module.ecs.alb_zone_id
}

module "ecs" {
  source = "../../modules/ecs"

  name_prefix  = local.name_prefix
  project      = var.project
  environment  = var.environment
  aws_region   = var.aws_region

  vpc_id              = module.networking.vpc_id
  public_subnet_ids   = module.networking.public_subnet_ids
  app_subnet_ids      = module.networking.app_subnet_ids
  alb_security_group_id = module.networking.alb_security_group_id
  ecs_security_group_id = module.networking.ecs_security_group_id

  certificate_arn = module.dns.certificate_arn
  logs_bucket_id  = module.storage.logs_bucket_id

  php_image         = var.php_image
  php_cpu           = var.php_cpu
  php_memory        = var.php_memory
  php_desired_count = var.php_desired_count
  php_max_count     = var.php_desired_count * 3

  go_image         = var.go_image
  go_cpu           = var.go_cpu
  go_memory        = var.go_memory
  go_desired_count = var.go_desired_count

  redis_endpoint    = module.cache.primary_endpoint
  db_secret_arn     = module.database.secret_arn
  assets_bucket_arn  = module.storage.assets_bucket_arn
  assets_bucket_name = module.storage.assets_bucket_name
}

module "monitoring" {
  source = "../../modules/monitoring"

  name_prefix      = local.name_prefix
  environment      = var.environment
  aws_region       = var.aws_region
  alert_email      = var.alert_email

  ecs_cluster_name = module.ecs.cluster_name
  php_service_name = module.ecs.php_service_name
  rds_instance_id  = module.database.instance_id
  redis_cluster_id = module.cache.cluster_id
  alb_arn_suffix   = module.ecs.alb_arn_suffix
}

Cost Breakdown

Сравнение стоимости для dev и prod окружений:

┌───────────────────────┬──────────────┬──────────────────┐
│     Component         │     DEV      │    PRODUCTION    │
├───────────────────────┼──────────────┼──────────────────┤
│ ECS Fargate           │              │                  │
│  PHP (0.5vCPU/1GB)x1  │    ~$25/mo   │                  │
│  PHP (0.5vCPU/1GB)x2  │              │    ~$50/mo       │
│  Go (0.25vCPU/0.5GB)x1│    ~$13/mo   │                  │
│  Go (0.25vCPU/0.5GB)x2│              │    ~$26/mo       │
├───────────────────────┼──────────────┼──────────────────┤
│ ALB                   │    ~$22/mo   │    ~$22/mo       │
├───────────────────────┼──────────────┼──────────────────┤
│ RDS PostgreSQL        │              │                  │
│  db.t3.micro          │    ~$15/mo   │                  │
│  db.r6g.large Multi-AZ│              │    ~$380/mo      │
│  + Read Replica       │              │    ~$190/mo      │
├───────────────────────┼──────────────┼──────────────────┤
│ ElastiCache Redis     │              │                  │
│  cache.t3.micro       │    ~$13/mo   │                  │
│  cache.r6g.large (x2) │              │    ~$260/mo      │
├───────────────────────┼──────────────┼──────────────────┤
│ NAT Gateway (x1 / x2) │    ~$45/mo   │    ~$90/mo       │
├───────────────────────┼──────────────┼──────────────────┤
│ S3 (10GB / 100GB)     │     ~$1/mo   │     ~$3/mo       │
├───────────────────────┼──────────────┼──────────────────┤
│ CloudWatch            │     ~$5/mo   │    ~$15/mo       │
├───────────────────────┼──────────────┼──────────────────┤
│ Route53               │     ~$1/mo   │     ~$1/mo       │
├───────────────────────┼──────────────┼──────────────────┤
│ ECR (shared)          │     ~$1/mo   │     ~$1/mo       │
├───────────────────────┼──────────────┼──────────────────┤
│ Secrets Manager       │     ~$1/mo   │     ~$1/mo       │
├───────────────────────┼──────────────┼──────────────────┤
│                       │              │                  │
│ TOTAL (estimated)     │  ~$140/mo    │  ~$1040/mo       │
│                       │              │                  │
└───────────────────────┴──────────────┴──────────────────┘

Notes:
- Prices are approximate for eu-central-1 (Frankfurt)
- NAT Gateway is often the biggest surprise in AWS bills
- Data transfer costs not included (can add 10-20%)
- Reserved Instances can save 30-60% on RDS and ElastiCache

Советы по оптимизации стоимости

Компонент Оптимизация Экономия
NAT Gateway Один NAT GW для dev вместо двух ~$45/mo
RDS Reserved Instances (1 year) ~30%
ElastiCache Reserved Nodes (1 year) ~30%
ECS Fargate Spot для не-критичных задач ~70%
S3 Lifecycle policies (IA → Glacier) ~50% на хранении
Dev окружение Выключать ночью и в выходные ~65%

Проверь себя

5 из 8

Почему image_tag_mutability установлен в IMMUTABLE для ECR-репозитория?

Зачем ECS Task Definition имеет два IAM-роля: execution role и task role?

Какая стратегия auto-scaling используется для ECS-сервисов в этом проекте?

Что делает deployment_circuit_breaker с rollback = true в ECS Service?

Почему приватные подсети разделены на app и data уровни?