AWS ECS Fargate & Terraform: Hướng dẫn triển khai sẵn sàng cho môi trường Production

DevOps tutorial - IT technology blog
DevOps tutorial - IT technology blog

Sự chuyển dịch sang Serverless Containers

Việc quản lý các instance EC2 để điều phối container thường mang lại cảm giác giống như một vòng lặp không hồi kết của những thông báo cảnh báo lúc 2 giờ sáng. Bạn phải vá lỗi hệ điều hành, theo dõi dung lượng ổ đĩa và tinh chỉnh các scaling group chỉ để giữ cho các Docker container của mình hoạt động bình thường.

Khi tôi chuyển đổi cluster đầu tiên của mình từ EC2 tự quản lý sang AWS ECS Fargate, gánh nặng vận hành gần như biến mất chỉ sau một đêm. Fargate cho phép bạn chạy các container mà không cần chạm vào bất kỳ máy chủ nào. Khi kết hợp với Terraform, bạn sẽ có một môi trường được kiểm soát phiên bản, có khả năng lặp lại và hoạt động ổn định mọi lúc.

Tôi nhận thấy rằng việc làm chủ bộ công cụ này là cách nhanh nhất để xây dựng các hệ thống sẵn sàng cho môi trường production. Bạn có thể tập trung vào mã nguồn của mình trong khi AWS đảm nhận các công việc nặng nhọc trong việc bảo trì hạ tầng. Hướng dẫn này sẽ bỏ qua những lý thuyết suông và chỉ cho bạn cách xây dựng một dịch vụ Fargate có khả năng tự động mở rộng hoàn chỉnh từ con số không.

Bắt đầu nhanh: Cluster trong 5 phút

Mọi thứ bắt đầu với một ECS cluster. Hãy coi đây là một sandbox logic của bạn. Không giống như các cluster truyền thống, một cluster chạy trên nền tảng Fargate không yêu cầu bạn phải cấp phát hoặc trả tiền trước cho tài nguyên EC2 bên dưới.

# provider.tf
provider "aws" {
  region = "us-east-1"
}

# cluster.tf
resource "aws_ecs_cluster" "main" {
  name = "production-cluster"

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

Chạy lệnh terraform initterraform apply. Bây giờ bạn đã có một namespace sẵn sàng hoạt động. Nhưng một cluster đơn lẻ chỉ là một cái vỏ trống rỗng. Chúng ta cần mạng (networking) và các task definition để thực sự chạy một khối lượng công việc.

Xây dựng nền tảng hạ tầng

Một thiết lập Fargate cấp độ production dựa trên ba trụ cột: Mạng (Networking), IAM Roles và Task Definition.

1. Mạng cho Fargate

Các Fargate task phải nằm trong một VPC. Để có một thiết lập bảo mật, hãy đặt các task của bạn trong các private subnet. Sử dụng Application Load Balancer (ALB) trong các public subnet để xử lý lưu lượng truy cập đến. Lưu ý: Vì các task của bạn nằm trong private subnet, bạn sẽ cần NAT Gateway hoặc VPC Endpoints để kéo image từ ECR.

# Cấu hình VPC rút gọn
resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
}

resource "aws_security_group" "ecs_tasks" {
  name        = "ecs-tasks-sg"
  vpc_id      = aws_vpc.main.id

  ingress {
    protocol        = "tcp"
    from_port       = 80
    to_port         = 80
    security_groups = [aws_security_group.alb.id]
  }

  egress {
    protocol    = "-1"
    from_port   = 0
    to_port     = 0
    cidr_blocks = ["0.0.0.0/0"]
  }
}

2. IAM Roles: Execution Role so với Task Role

Đây là nơi nhiều kỹ sư thường gặp khó khăn. Bạn cần hai role riêng biệt để hệ thống hoạt động. Execution Role dành cho ECS agent; nó dùng để kéo image và gửi log đến CloudWatch. Task Role dành cho mã ứng dụng của bạn, cho phép nó giao tiếp với các dịch vụ như S3 hoặc DynamoDB.

resource "aws_iam_role" "ecs_task_execution_role" {
  name = "ecs-task-execution-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action = "sts:AssumeRole"
      Effect = "Allow"
      Principal = { Service = "ecs-tasks.amazonaws.com" }
    }]
  })
}

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

3. Task Definition và Service

Task Definition là DNA của container. Nó xác định image, CPU (ví dụ: 256 cho 0.25 vCPU) và bộ nhớ.

resource "aws_ecs_task_definition" "app" {
  family                   = "my-app"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  cpu                      = "256"
  memory                   = "512"
  execution_role_arn       = aws_iam_role.ecs_task_execution_role.arn

  container_definitions = jsonencode([
    {
      name      = "app-container"
      image     = "nginx:latest"
      essential = true
      portMappings = [{
        containerPort = 80
        hostPort      = 80
      }]
      logConfiguration = {
        logDriver = "awslogs"
        options = {
          "awslogs-group"         = "/ecs/my-app"
          "awslogs-region"        = "us-east-1"
          "awslogs-stream-prefix" = "ecs"
        }
      }
    }
  ])
}

resource "aws_ecs_service" "main" {
  name            = "my-service"
  cluster         = aws_ecs_cluster.main.id
  task_definition = aws_ecs_task_definition.app.arn
  desired_count   = 2
  launch_type     = "FARGATE"

  network_configuration {
    security_groups = [aws_security_group.ecs_tasks.id]
    subnets         = [aws_subnet.private.id]
  }
}

Tự động mở rộng khi nhu cầu tăng cao

Số lượng task cố định sẽ thất bại khi lưu lượng truy cập tăng đột biến. Nếu ứng dụng của bạn được giới thiệu trên một trang tin tức lớn, bạn cần mở rộng quy mô ngay lập tức. AWS Application Auto Scaling sẽ điều chỉnh desired_count của bạn dựa trên các chỉ số thời gian thực.

Target tracking là cách tiếp cận thông minh nhất ở đây. Nó hoạt động như một bộ điều nhiệt cho hạ tầng của bạn, tự động thêm tài nguyên khi mọi thứ trở nên “nóng” và giảm bớt khi lưu lượng truy cập giảm xuống.

resource "aws_appautoscaling_policy" "ecs_policy_cpu" {
  name               = "cpu-autoscaling"
  policy_type        = "TargetTrackingScaling"
  resource_id        = "service/${aws_ecs_cluster.main.name}/${aws_ecs_service.main.name}"
  scalable_dimension = "ecs:service:DesiredCount"
  service_namespace  = "ecs"

  target_tracking_scaling_policy_configuration {
    predefined_metric_specification {
      predefined_metric_type = "ECSServiceAverageCPUUtilization"
    }
    target_value = 70.0
  }
}

Nếu mức sử dụng CPU trung bình đạt 70%, ECS sẽ khởi tạo thêm các task. Khi tải giảm bớt, nó sẽ dừng các task dư thừa một cách khéo léo để bảo vệ ngân sách của bạn.

Những bài học kinh nghiệm thực tế đắt giá

Vận hành Fargate trong môi trường production nhiều năm đã dạy cho tôi một vài bài học quan trọng mà không phải lúc nào cũng xuất hiện trong tài liệu hướng dẫn.

Bẫy của tag ‘:latest’

Terraform sẽ không nhận thấy sự thay đổi nếu bạn chỉ đơn giản là push một image mới với cùng tag :latest. Hãy thiết lập force_new_deployment = true trong aws_ecs_service của bạn. Điều này bắt buộc một đợt triển khai mới mỗi khi bạn chạy terraform apply, đảm bảo mã nguồn mới nhất của bạn thực sự được đưa lên production.

Khả năng quan sát là yếu tố sống còn

Bạn không thể SSH vào một Fargate container. Log là cứu cánh duy nhất của bạn. Luôn luôn cấu hình driver awslogs. Nếu không có nó, việc gỡ lỗi lỗi 500 sẽ trở thành một trò chơi đoán mò thay vì một quy trình kỹ thuật chuẩn xác.

Xử lý SIGTERM một cách mượt mà

Khi ECS triển khai một phiên bản mới, nó sẽ gửi một tín hiệu SIGTERM đến các container cũ. Ứng dụng của bạn có chính xác 30 giây để hoàn thành request hiện tại trước khi AWS dừng tiến trình. Nếu ứng dụng của bạn xử lý các tác vụ chạy lâu, hãy tăng stopTimeout trong container definition để tránh mất mát dữ liệu.

Cắt giảm chi phí với Fargate Spot

Fargate có thể trở nên đắt đỏ nếu bạn chạy các cluster lớn 24/7. Đối với môi trường phát triển hoặc các worker chạy ngầm không quan trọng, hãy sử dụng Fargate Spot. Nó cho phép bạn sử dụng tài nguyên dư thừa của AWS với mức giảm giá tới 70%, miễn là bạn có thể xử lý thông báo dừng trước hai phút.

Các mã nguồn Terraform mẫu có thể tạo cảm giác nặng nề lúc đầu. Tuy nhiên, sự đánh đổi là một môi trường cực kỳ vững chắc, có khả năng tự mở rộng mà không cần can thiệp thủ công. Một khi các file HCL của bạn đã sẵn sàng, việc khởi chạy một microservice mới chỉ mất vài phút chứ không phải vài ngày.

Share: