Terraform 実践 #8 ECS Fargate デプロイ — タスク定義、サービス、同じ ALB の後ろでの切り替え

読了 5分

現在のコンピューティング層は問題なく動いていますが、デプロイの話を始めると限界が見えてきます。アプリを更新するには user_data を書き換えてインスタンスを全部入れ替える必要があり、起動のたびにパッケージインストールが走るので立ち上がりも遅いです。アプリをイメージとしてビルドし、そのままデプロイするコンテナ方式がこの問題の標準的な答えで、AWS でサーバー管理なしにコンテナを動かす選択肢が ECS Fargate です(EC2 モードとの比較は ECS Fargate vs EC2 にまとめてあります)。今回の Terraform 的なハイライトは、稼働中のサービスのコンピューティング層を隣に立てておいて乗り換える切り替えシナリオです。

クラスターとタスク定義 #

アプリのイメージは準備済みという前提で進めます(実習では公開の nginx イメージで代用します。ECR の構築とイメージのプッシュは CI の領域なので、運用シリーズで扱います)。

ecs.tf
# ecs.tf (新しいファイル)
resource "aws_ecs_cluster" "main" {
  name = "myapp-${var.env}"
}

resource "aws_ecs_task_definition" "web" {
  family                   = "myapp-${var.env}-web"
  requires_compatibilities = ["FARGATE"]
  network_mode             = "awsvpc"
  cpu                      = 256
  memory                   = 512

  execution_role_arn = aws_iam_role.ecs_execution.arn
  task_role_arn      = aws_iam_role.ecs_task.arn

  container_definitions = jsonencode([
    {
      name  = "web"
      image = "public.ecr.aws/nginx/nginx:latest"
      portMappings = [{ containerPort = 80 }]
    }
  ])
}

container_definitions はプロバイダーが HCL ブロックに展開していない部分なので、基礎 #4の jsonencode が再登場します。ロールが 2 つあることが ECS の有名な混同ポイントなので、区別しておきます。

  • execution role: ECS のインフラがタスクを立ち上げるために使う権限です。イメージのプル、ログの送信。大半は AWS マネージドポリシー 1 つで足ります。
  • task role: コンテナの中のアプリが使う権限です。SSM パラメータの読み取り、S3 アクセス。前回EC2 ロールに与えた権限がここへ移ってきます。

EC2 時代にインスタンスプロファイル 1 つにまとまっていた権限が、「立ち上げる側」と「アプリ側」に分かれたと理解すれば正確です。ロールのコードは前回のパターンそのままなので省略しますが、ecs_task ロールの権限ポリシーは web ロールのものを再利用します。

サービスと ip タイプのターゲットグループ #

ECS サービスは ASG のコンテナ版です。望むタスク数を維持し、落ちたら再び立ち上げ、ALB に登録します。ここでターゲットグループを新しく作らなければならない理由があります。#4 のターゲットグループはインスタンスを登録するデフォルトのタイプですが、Fargate タスクはインスタンスではなく ENI(IP)で登録されるため、target_type = "ip" のターゲットグループが必要です。

ecs.tf
resource "aws_lb_target_group" "ecs" {
  name_prefix = "ecs-"
  port        = 80
  protocol    = "HTTP"
  target_type = "ip"
  vpc_id      = aws_vpc.main.id

  health_check {
    path = "/"
  }

  lifecycle {
    create_before_destroy = true
  }
}

resource "aws_ecs_service" "web" {
  name            = "web"
  cluster         = aws_ecs_cluster.main.id
  task_definition = aws_ecs_task_definition.web.arn
  desired_count   = 2
  launch_type     = "FARGATE"

  network_configuration {
    subnets         = [for s in aws_subnet.private : s.id]
    security_groups = [aws_security_group.web.id]
  }

  load_balancer {
    target_group_arn = aws_lb_target_group.ecs.arn
    container_name   = "web"
    container_port   = 80
  }
}

ネットワーク構成には見覚えがあるはずです。同じプライベートサブネット、同じ web セキュリティグループです。awsvpc モードのタスクは自分の ENI を持つ一人前のネットワーク構成員なので、#3で組んだセキュリティグループのチェーンがそのまま適用されます。コンテナに替えてもネットワーク設計が再利用できるのは、その設計が層を基準にしていたからです。

切り替え — 同じ ALB の後ろで乗り換える #

現在の状態を見ると、ALB の後ろにターゲットグループが 2 つあります。リスナーはまだ EC2 側(aws_lb_target_group.web)を向いていて、ECS タスクは自分のターゲットグループでヘルスチェックだけ受けながら待機中です。切り替えはリスナーの 1 行です。

lb.tf
# lb.tf を修正
resource "aws_lb_listener" "http" {
  # ...

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.ecs.arn   # web → ecs
  }
}

plan にはリスナーの修正 1 件だけが現れ、apply した瞬間からトラフィックがコンテナに流れます。問題が起きたら参照を戻して apply すれば即座にロールバックです。乗り換えが確認できたら、EC2 層(ASG、launch template、旧ターゲットグループ、スケーリングポリシー)をコードから削除します。#4 でインスタンス 1 つを削除したときに見た流れの拡張版ですが、今回は消えるリソースが複数なので、plan の destroy 一覧を 1 行ずつ確認してから apply します。本番運用ならこの切り替えの間に観察期間を置きますが、手順そのものは同じです。

まとめ #

今回扱った内容です。

  • user_data 方式はデプロイのたびにインスタンスの入れ替えと長い起動を要求します。イメージとしてビルドしてデプロイするコンテナが標準的な答えで、サーバー管理がない側が Fargate です
  • タスク定義の execution role は ECS インフラの権限、task role はアプリの権限です。アプリの権限は EC2 ロールから task role へ移ってきます
  • Fargate タスクは IP で登録されるため、target_type が ip のターゲットグループを新しく作ります
  • awsvpc モードのタスクは、既存のプライベートサブネットとセキュリティグループのチェーンをそのまま再利用します
  • 切り替えはリスナーのターゲットグループ参照 1 行です。確認後に EC2 層をコードから削除し、plan の destroy 一覧を検討しながら締めくくります

次回(#9 環境分離)では、ここまで dev 1 つで育ててきたコードをモジュールにリファクタリングし、prod 環境を隣に立てます。先送りにしてきた「実習と本番の値の違い」が、すべてそこで解消されます。

X