0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ECS Fargate の Blue/Green デプロイを GitHub Actions と Terraform で組んでみた

0
Posted at

はじめに

この記事では、
GitHub に push → GitHub Actions がイメージを作る → ECS が Blue/Green で切り替える
という流れを Terraform で組み、実際に切り替わる様子を確認してみました。

簡易構成図

ECS ネイティブ Blue/Green の構成

ECS ネイティブの Blue/Green

ECS ネイティブ方式では、ALB のリスナールールに 2 つのターゲットグループ(TG)を 重み 100 / 0 でぶら下げておき、ECS がこの重みを書き換えて切り替えます。

CodeDeploy 方式 ECS ネイティブ方式
切り替えるのは CodeDeploy ECS 自身
追加で必要なもの CodeDeploy アプリ / デプロイグループ / AppSpec なし(サービスの設定だけ)
デプロイの呼び出し CodeDeploy のデプロイ作成 通常と同じ UpdateService

AWS からも CodeDeploy 方式からの移行ガイド が出ているので、新しく組むならこちらです。

切り替えは次の順で進みます。

ECS ネイティブ Blue/Green の切り替え

本番より先に、テスト用のリスナー(今回は :8080)に新しい版が出る のがポイントです。

構成

コードは GitHub に置いています。

Blue/Green の本体は ECS サービスの次の部分だけ です。

terraform/ecs.tf(抜粋)
  deployment_configuration {
    strategy             = "BLUE_GREEN"
    bake_time_in_minutes = var.bake_time_in_minutes # 切り替え後に blue を残す時間
  }

  load_balancer {
    target_group_arn = aws_lb_target_group.this["blue"].arn
    container_name   = "app"
    container_port   = var.container_port

    advanced_configuration {
      alternate_target_group_arn = aws_lb_target_group.this["green"].arn
      production_listener_rule   = aws_lb_listener_rule.this["prod"].arn
      test_listener_rule         = aws_lb_listener_rule.this["test"].arn
      role_arn                   = aws_iam_role.ecs_infra_lb.arn
    }
  }

GitHub Actions 側は、普通のデプロイと同じ です。
イメージを ECR に push し、タスク定義のイメージを差し替えて amazon-ecs-deploy-task-definition で UpdateService を呼ぶだけで、あとはサービスの設定に従って ECS が Blue/Green で切り替えます。

①〜④ と ⑥ が GitHub Actions のステップ(.github/workflows/deploy.yml)で、⑤ は ECS が自分で進めます。ワークフローは ⑤ が完了するまで待ちます。

それ以外の部分は、AWS のベストプラクティスに沿って次のようにしています。

対象 設定 対応するコントロール
ネットワーク タスクにパブリック IP を付けない ECS.2
ネットワーク タスクはプライベートサブネットに置き、ECR / Logs へは VPC エンドポイント経由 -
コンテナ 非 root(UID 101) ECS.20
コンテナ 読み取り専用ルートファイルシステム ECS.5
ECS Container Insights(enhanced) ECS.12
ECR プッシュ時スキャン / タグ不変 / ライフサイクルポリシー ECR.1〜3
ALB アクセスログ / 不正ヘッダー破棄 / 削除保護 ELB.4〜6
VPC フローログ / デフォルト SG のルールを空に EC2.6 / EC2.2
GitHub Actions OIDC で AssumeRole(アクセスキーなし)、アクションはコミット SHA で固定 -

HTTPS は独自ドメインが必要なので、今回は HTTP のままにしています。そのため ELB.1(HTTP→HTTPS リダイレクト)と ELB.18(暗号化されたリスナー)は満たしていません。

動作確認

手順 0:自分のリポジトリを作る

GitHub Actions の OIDC は「どのリポジトリから来たか」で AssumeRole を許可する ので、自分のリポジトリとして作り直します。

git clone https://github.com/leomaro7/ecs-bluegreen.git

cd ecs-bluegreen

git remote remove origin

gh repo create ecs-bluegreen --private --source . --remote origin

アカウントに GitHub 用の OIDC プロバイダーがなければ作ります(GitHub の OIDC を AWS で使う手順)。1 アカウントに 1 つなので、Terraform の管理外にしています。

aws iam list-open-id-connect-providers --output text | grep -q token.actions.githubusercontent.com || aws iam create-open-id-connect-provider --url https://token.actions.githubusercontent.com --client-id-list sts.amazonaws.com

手順 1:Terraform state 用のバケットを作る

state は S3 に置き、use_lockfile = true で S3 のロックファイルを使います(S3 backend)。

export AWS_REGION=ap-northeast-1
echo $AWS_REGION

ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
echo $ACCOUNT_ID

STATE_BUCKET=ecs-bluegreen-tfstate-${ACCOUNT_ID}-${AWS_REGION}
echo $STATE_BUCKET

aws s3api create-bucket --bucket $STATE_BUCKET --create-bucket-configuration LocationConstraint=$AWS_REGION

aws s3api put-bucket-versioning --bucket $STATE_BUCKET --versioning-configuration Status=Enabled

手順 2:Terraform でインフラを作る

リポジトリの owner 名と ID を terraform.tfvars に書いてから apply します。

2026 年 7 月 15 日より後に作った(または名前変更・移管した)リポジトリは、OIDC トークンの sub が ID 付きの形式になります(GitHub の changelog)。
sub はどのリポジトリ・ブランチから来たかを表す値で、IAM ロールの信頼ポリシーはこの値で呼び出し元を絞ります。旧形式(repo:<owner>/<repo>:ref:refs/heads/main)だけを許可していると AssumeRole に失敗するので、このリポジトリでは新形式(repo:<owner>@<owner_id>/<repo>@<repo_id>:ref:refs/heads/main)も受けられるよう ID も渡しています。

cd terraform

OWNER=$(gh api user --jq .login)
echo $OWNER

cat > terraform.tfvars <<EOF
github_owner       = "$OWNER"
github_repo        = "ecs-bluegreen"
github_owner_id    = "$(gh api repos/$OWNER/ecs-bluegreen --jq .owner.id)"
github_repo_id     = "$(gh api repos/$OWNER/ecs-bluegreen --jq .id)"
test_listener_cidr = "$(curl -s https://checkip.amazonaws.com)/32"
EOF

terraform init -backend-config="bucket=$STATE_BUCKET"
terraform apply

ECS サービスはタスク数 0 で作られるので、この時点ではまだアプリは動いていません。

手順 3:GitHub Actions でアプリを初めてデプロイする

手順 2 で作ったロールの ARN をリポジトリ変数に入れて push すると、GitHub Actions がイメージを作って ECS にデプロイします。

gh variable set AWS_ROLE_ARN --body "$(terraform output -raw github_actions_role_arn)"
git push -u origin main

初回は Blue/Green の切り替えが終わってから、ワークフローがタスク数を 0 → 2 に上げて、自前のイメージで起動します。

ワークフローが終わったら、本番の URL を開きます。

curl $(terraform output -raw prod_url)
<h1>ecs-bluegreen</h1><p>version: 8e8a6c5...</p>

version には push したコミットの SHA が入るので、環境ごとに異なります。

手順 4:2 回目のアプリのデプロイで切り替わりを見る

ページのタイトルを変えて push し、本番(:80)とテスト(:8080)の応答を 10 秒おきに並べて 見ます。

cd ..

sed -i '' 's|<h1>ecs-bluegreen</h1>|<h1>ecs-bluegreen v2</h1>|' app/Dockerfile

git commit -am "v2" && git push

URL=$(terraform -chdir=terraform output -raw prod_url)
echo $URL

while true; do
  echo "$(date +%T)  :80 $(curl -s $URL/ | grep -o '<h1>[^<]*')  :8080 $(curl -s ${URL}:8080/ | grep -o '<h1>[^<]*')"
  sleep 10
done

手元の検証では、リスナールールの重みと :80 / :8080 の応答を 10 秒おきに記録して、次のように切り替わりました。

2 回目のデプロイの実測タイムライン

新しい版がテストリスナーだけに出ていたのは約 25 秒 でした。フックなしだとすぐ本番に進むので、確認の時間を取りたいならライフサイクルフックを入れる必要がありそうです。

ハマりどころ

起きたこと 対処
非 root のコンテナが /tmp に書けず起動失敗(バインドマウントは root 所有) Dockerfile に VOLUME ["/tmp"]
そのため公開イメージのままでは初回起動できない サービスはタスク数 0 で作り、初回 push で起動
初回に新しいタスク定義と台数を同時に渡すと、旧リビジョン(起動できない初回の枠)まで起動しようとして pull 失敗が続く 0 台のまま切り替えてから台数を上げる
デプロイのたびに ECS が TG の役割と重みを入れ替える Terraform 側は ignore_changes で管理から外す

後片付け

ALB の削除保護を外してから destroy し、Terraform の管理外のもの(GitHub Actions が登録したタスク定義は登録解除、Container Insights が自動で作ったロググループ、state バケット)を消します。

cd terraform

STATE_BUCKET=ecs-bluegreen-tfstate-$(aws sts get-caller-identity --query Account --output text)-ap-northeast-1
echo $STATE_BUCKET

terraform apply -var enable_deletion_protection=false
terraform destroy -var enable_deletion_protection=false

for td in $(aws ecs list-task-definitions --family-prefix ecs-bluegreen --query 'taskDefinitionArns[]' --output text); do aws ecs deregister-task-definition --task-definition $td --query 'taskDefinition.revision' --output text; done

aws logs delete-log-group --log-group-name /aws/ecs/containerinsights/ecs-bluegreen/performance
aws s3api delete-objects --bucket $STATE_BUCKET --delete "$(aws s3api list-object-versions --bucket $STATE_BUCKET --query '{Objects: Versions[].{Key: Key, VersionId: VersionId}}' --output json)"
aws s3api delete-objects --bucket $STATE_BUCKET --delete "$(aws s3api list-object-versions --bucket $STATE_BUCKET --query '{Objects: DeleteMarkers[].{Key: Key, VersionId: VersionId}}' --output json)"
aws s3api delete-bucket --bucket $STATE_BUCKET
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?