AWS ECS Fargate & Terraform:本番環境レベルのデプロイメントガイド

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

サーバーレスコンテナへの移行

コンテナオーケストレーションのためにEC2インスタンスを管理することは、深夜2時のアラート対応という終わりのないサイクルに陥ることを意味しがちです。Dockerコンテナを稼働させ続けるためだけに、OSのパッチ適用、ディスク容量の監視、スケーリンググループの微調整に追われることになります。

私が最初のクラスターを自己管理型のEC2からAWS ECS Fargateに移行したとき、運用上の負担は一夜にしてほぼ消え去りました。Fargateを使えば、サーバーを一台も触ることなくコンテナを実行できます。これをTerraformと組み合わせることで、常に安定して動作する、再現可能でバージョン管理された環境を手に入れることができます。

このスタックをマスターすることは、本番環境レベルのシステムを構築するための最短ルートだと私は確信しています。インフラのメンテナンスという重労働をAWSに任せ、自分はコードに集中できるからです。このガイドでは余計な説明を省き、ゼロから実用的でオートスケーリング機能を備えたFargateサービスを構築する方法を紹介します。

クイックスタート:5分で構築するクラスター

すべてはECSクラスターから始まります。これは論理的なサンドボックスと考えてください。従来のクラスターとは異なり、Fargateベースのクラスターでは、基礎となるEC2のキャパシティを事前にプロビジョニングしたり、その料金を支払ったりする必要はありません。

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

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

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

terraform initterraform apply を実行してください。これで、アクションを開始する準備が整ったネームスペースが作成されました。しかし、クラスターだけでは単なる空の殻にすぎません。実際にワークロードを動かすには、ネットワークとタスク定義が必要です。

インフラストラクチャ基盤の構築

本番グレードেরFargateセットアップは、「ネットワーキング」「IAMロール」「タスク定義」の3つの柱で成り立っています。

1. Fargate用のネットワーキング

FargateタスクはVPC内に配置する必要があります。安全なセットアップのために、タスクはプライベートサブネットに配置しましょう。パブリックサブネットにApplication Load Balancer (ALB)を配置して、着信トラフィックを処理します。注意:タスクがプライベートサブネットにあるため、ECRからイメージをプルするにはNAT GatewayまたはVPCエンドポイントが必要になります。

# 簡略化されたVPCセットアップ
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ロール:実行ロール vs タスクロール

ここで多くのエンジニアが躓きます。これを機能させるには、2つの異なるロールが必要です。実行ロール (Execution Role) はECSエージェント用で、イメージをプルし、ログをCloudWatchに送信するために使用されます。タスクロール (Task Role) はアプリケーションコード用で、S3や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. タスク定義とサービス

タスク定義はコンテナの「設計図」です。イメージ、CPU(例:0.25 vCPUなら256)、メモリを定義します。そしてサービスが「マネージャー」として機能し、指定した数のタスクが正常に動作し続けるように管理します。

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]
  }
}

ピーク需要に合わせたスケーリング

トラフィックが急増した際、固定のタスク数では対応できません。アプリが主要なニュースサイトで取り上げられた場合、即座にスケーリングする必要があります。AWS Application Auto Scalingは、リアルタイムのメトリクスに基づいて desired_count を調整します。

ここでは「ターゲット追跡 (Target tracking)」が最もスマートなアプローチです。これはインフラのサーモスタットのように機能し、負荷が高まればキャパシティを追加し、トラフィックが落ち着けば冷却(削減)します。

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
  }
}

平均CPU使用率が70%に達すると、ECSは自動的にタスクを増やします。負荷が下がれば、予算を守るために余分なタスクを正常に終了させます。

現場で得た教訓

数年間、本番環境でFargateを運用してきた中で、ドキュメントには必ずしも書かれていない重要な教訓をいくつか学びました。

「:latest」タグの罠

単に新しいイメージを同じ :latest タグでプッシュしただけでは、Terraformは変更を検出しません。aws_ecs_serviceforce_new_deployment = true を設定してください。これにより、terraform apply を実行するたびに強制的にロールアウトが行われ、最新のコードが確実に本番環境に反映されます。

可視性がすべて

FargateコンテナにはSSHで入ることはできません。ログだけが唯一の命綱です。必ず awslogs ドライバを設定してください。これがないと、500エラーのデバッグは技術的なプロセスではなく、単なる「当てずっぽう」になってしまいます。

SIGTERMを適切に処理する

ECSが新しいバージョンをデプロイするとき、古いコンテナに SIGTERM を送信します。AWSがプロセスを強制終了するまで、アプリケーションには現在のリクエストを完了させるための猶予が正確に30秒与えられます。長時間実行されるジョブを扱うアプリの場合は、データ破損を避けるためにコンテナ定義の stopTimeout を増やしてください。

Fargate Spotでコストを削減する

大規模なクラスターを24時間365日稼働させると、Fargateは高額になる可能性があります。開発環境や重要度の低いバックグラウンドワーカーには、Fargate Spot を使用しましょう。2分前の終了通知を許容できるのであれば、AWSの余剰キャパシティを最大70%割引で利用できます。

Terraformのボイラープレートは最初は重く感じるかもしれません。しかし、その見返りは、手動の介入なしでスケールする堅牢な環境です。HCLファイルが一度整えば、新しいマイクロサービスの立ち上げは数分ではなく、数分で完了するようになります。

Share: