こしあん
2025-02-14

ECRプッシュでFargateを自動更新するCodePipelineによるCI/CD


71{icon} {views}

CodePipelineとCodeDeployのECSアクションプロバイダーを使い、ECRにイメージがプッシュされるたびにFargateを自動デプロイする仕組みを構築。Terraformのignore_changes設定でデプロイ後のドリフトも防ぎ、継続的な更新を安全かつスムーズに実行できる。

はじめに

  • Fargateのアプリに対して、固定のECRのイメージタグがあり、ECRのPushされたらFargateに自動デプロイされるのを作ってみた
  • CodePipelineとCodeDeployのECSのアクションプロバイダーを使ってできる。CodeDeployのデフォルトのECSプロバイダーを使うことで、デプロイ用のインスタンスやイメージを管理することなくシステムを単純化できる

背景

  • 通常Fargateだと、既存のイメージタグに対してPushされたら、タスクがKillされない限り最新のイメージに更新されない。Pushとアプリ側の更新に対して自動化は完全にできてはいない
  • 一番簡単なのは(SLAとか無視していいなら)GitHub ActionでPushしたら既存のタスクをKillしてしまうようなパイプラインを作る。そのあとはECS側で勝手にドレインされる
  • 一方で開発時はローカルでDockerをビルドすることも多く、ECRへのPushをトリガーとして更新したいことも多い。GitHub Actionsにしてしまうと、DockerのビルドはActionsからしかできなくなってしまう。
  • このCodePipelineを使うやり方がAWSのお作法的には正しいやり方で、ECRへのPushをトリガーとして、CodePipelineを実行する。CodePipelineの管理が面倒なら実践的には、EventBridgeだけ残してタスクをKillするようなLambdaを走らせるというのもあり。

コード

以下のGitHubにある

https://github.com/koshian2/fargate-codepipeline

結果

DockerイメージをPushする

aws ecr get-login-password --region ap-northeast-1 --profile {aws_profile_name} | docker login --username AWS --password-stdin {aws_account_id}.dkr.ecr.ap-northeast-1.amazonaws.com

docker build -t {aws_account_id}.dkr.ecr.ap-northeast-1.amazonaws.com/streamlit-app:latest .

docker push {aws_account_id}.dkr.ecr.ap-northeast-1.amazonaws.com/streamlit-app:latest

イメージがPushされた

CodePipelineが走る

元はこうだったのが

アップデートされる

ドリフトを予防するには

CodePipelineはECSのタスク定義をアップデートしている。CodePipelineのアップデート後にterraform applyをすると、以下のようなドリフトが発生する。

Terraform will perform the following actions:

  # aws_ecs_service.fargate_service will be updated in-place
  ~ resource "aws_ecs_service" "fargate_service" {
        id                                 = "arn:aws:ecs:ap-northeast-1:{aws_account_id}:service/example-spot-cluster/fargate-service-spot"
        name                               = "fargate-service-spot"
        tags                               = {}
      ~ task_definition                    = "arn:aws:ecs:ap-northeast-1:{aws_account_id}:task-definition/streamlit-task:14" -> "arn:aws:ecs:ap-northeast-1:{aws_account_id}:task-definition/streamlit-task:12"
        # (15 unchanged attributes hidden)

        # (5 unchanged blocks hidden)
    }

Plan: 0 to add, 1 to change, 0 to destroy.

これを予防するには、Fargateサービスに対してignore_changesをつける。これにより、CodePipelineによるドリフトを予防しつつ、Terraform側でのタスク定義の更新を併用することが可能になる。

resource "aws_ecs_service" "fargate_service" {
  name            = "fargate-service-spot"
  # 中略
  lifecycle {
    // 外部(CodeDeploy 等)で更新された task_definition の変更は無視する
    ignore_changes = [ task_definition ]
  }
}

試しにTerraform側でメモリサイズやイメージタグを編集してapplyするときちんと変更が走る。

解説(By o3-mini-high)

このコードは、AWS上でCI/CDパイプラインを構築するためのTerraform定義です。全体としては、ソース(コンテナイメージ)の取得 → ビルド(イメージ定義ファイルの生成) → デプロイ(ECSサービスの更新)という流れを自動化しています。以下、ハイレベルなコンセプトと技術的な背景、そして特に「ECSのアクションプロバイダー」と「BuildステージからのJSON連携」について解説します。

ハイレベルなコンセプトと技術的背景

  1. CI/CDパイプラインの自動化
    • 目的: 新しいコードやコンテナイメージがリポジトリにプッシュされると、自動的にビルド・テスト・デプロイが実行され、アプリケーションの更新が迅速かつ確実に反映されます。
    • 構成要素:
      • CodePipeline: 全体のフロー(ソース、ビルド、デプロイ)を管理します。
      • CodeBuild: ビルドステージで実際の処理(ここではJSONファイルの生成)を実行します。
      • ECS (Elastic Container Service): コンテナ化されたアプリケーションを実行・管理します。
      • ECR (Elastic Container Registry): コンテナイメージのリポジトリとして利用され、ソースとしても使われます。
      • S3: パイプラインの中間成果物(アーティファクト)を保存します。
  2. IAMロールとセキュリティ
    • 各サービス(CodePipeline、CodeBuild、EventBridgeなど)が連携して動作するために、必要な権限(S3アクセス、CodeBuildの起動、ECSの操作など)をIAMポリシーとして細かく設定しています。
  3. イベントドリブンのトリガー
    • EventBridge(旧CloudWatch Events) を利用し、ECRに新しいイメージがプッシュされた際にパイプラインを自動起動する仕組みを作っています。

ECSのアクションプロバイダーとBuildステージからのJSON連携

BuildステージでのJSON生成

  • 役割:
    CodeBuildプロジェクト(imagedefinitions-generator)は、コンテナイメージの情報をもとに、imagedefinitions.json というファイルを生成します。このファイルは、ECSに対して「どのコンテナを、どのイメージに更新するか」を伝えるための設定ファイルです。

  • 生成内容の例:

    [
    {
      "name": "streamlit",
      "imageUri": "【ECRリポジトリURL】:latest"
    }
    ]
    

    ここで、name はECSタスク定義内のコンテナ名、imageUri は更新対象の新しいコンテナイメージのURIとなります。これにより、常に最新のイメージでデプロイが行われるように設定されます。

  • 流れ:

    1. ソースステージでECRから最新のイメージが取得され、CodeBuildに渡される。
    2. CodeBuild内で上記のbuildspecに従い、imagedefinitions.json が生成され、ビルドアーティファクトとして出力される。
    3. このJSONファイルが次のDeployステージに引き渡されることで、ECSの更新に利用されます。

ECSアクションプロバイダーの動作

  • 目的:
    CodePipelineのDeployステージでは、ECSアクションプロバイダーが利用されます。このプロバイダーは、事前にビルドステージで生成されたimagedefinitions.json を読み込み、ECSサービスのタスク定義を更新する役割を持ちます。

  • 動作の流れ:

    1. 入力アーティファクトとしてimagedefinitions.jsonを受け取ります。
    2. JSONの内容から、どのコンテナのイメージをどのURIに更新するかを把握。
    3. 必要に応じて、新しいタスク定義を登録(ecs:RegisterTaskDefinition)し、既存のサービスに対して更新(ecs:UpdateService)を行います。
    4. この更新により、ECSクラスター内のサービスが新しいイメージを利用して再起動され、アプリケーションが最新の状態に更新されます。
  • JSON連携の重要性:
    • 柔軟性: ビルドステージで生成されるJSONにより、複数のコンテナや複雑なタスク定義の変更を柔軟に扱うことが可能になります。
    • 自動化の一環: 手動でタスク定義を更新する手間を排除し、コードと設定の変更に連動してデプロイが自動実行されるため、ヒューマンエラーを低減します。
    • IAMのPassRole権限: ECS更新時にecsTaskExecutionRoleを渡すための権限も設定されており、セキュアな運用が可能です。

まとめ

  • 全体の流れ:
    ECRに新しいイメージがプッシュされると、EventBridgeがCodePipelineを起動します。CodePipelineはソース→ビルド→デプロイの各ステージを順次実行し、CodeBuildがimagedefinitions.jsonを生成します。このJSONファイルを基に、ECSアクションプロバイダーが自動的にタスク定義を更新し、最新のイメージでサービスを再起動します。

  • ECSアクションプロバイダーのポイント:
    Buildステージで生成されたJSON連携により、ECSの更新が自動かつ正確に行われる仕組みが実現されます。これにより、CI/CDパイプライン全体の効率性と信頼性が向上し、迅速なデプロイが可能となります。

このように、Terraformで各AWSサービスのリソースと権限を整え、連携させることで、エンドツーエンドの自動デプロイパイプラインを実現しています。

所感

  • 割とローカルからPushすることが多いので、テスト用としてはかなり有用な気がする
  • CodePiplineの管理が煩雑だってのは認める。ロールの管理や各ステージの管理など、明らかにコード数が長い。ただ、CodePipeline上で手動承認を挟むことができるので、そこをAWS側でログを残しつつやりたいってケースだと有効かな。
  • Terraform前提でECSのCodePipelineをデプロイする場合、このやり方が一番簡単そうな気がする。下手にカスタムのCodeDeployを使うと余計煩雑になりそう。
  • GitHub Actionsとの住み分けがとても悩ましい


Shikoan's ML Blogの中の人が運営しているサークル「じゅ~しぃ~すくりぷと」の本のご案内

技術書コーナー

北海道の駅巡りコーナー


Add a Comment

メールアドレスが公開されることはありません。 が付いている欄は必須項目です