ecspresso利用を考慮したTerraformのECS(Fargate)構築

6 min read読了の目安(約5800字 4

はじめに

ecspressoでは、デプロイの過程でECSのタスク定義の新規リビジョン作成とECSサービスの更新を行います。

そのため、インフラのコード管理をTerraformで行なっている場合、ECSに関してはクラスターまでをTerraformで管理し、サービスとタスク定義は管理しないという方針が考えられます。

ただ、実際に構築・運用をしてみて後述するいくつかの課題にぶつかり、最終的に以下の方法を採ることにしました。

  • Terraformではサービスを管理させる。また、初期構築時用のダミーのタスク定義を管理させる。
  • ecspressoではタスク定義のみを管理させ、サービスを管理させない。

そのような結論に至った背景を説明します。

(2021/5/1追記)
ecspresso作者のfujiwaraさんにコメントをいただきましたが、ecspressoで本来想定しているユースケースはタスク定義とサービスの両方をecspressoの管理下に置くとのことなので、本記事で最終的に取り扱っている方法は特殊ケースと認識いただければと思います。

環境

  • ecspresso v1.5.1

前提

説明を開始する前に、どのようなファイル構成を想定しているかを示します(説明のために簡略化はしています)。

ポイントとして、ecspressoのecs-service-def.jsonecs-task-def.jsonは環境ごとにファイルを分けず、環境変数を使ってdev環境とprod環境双方で共用することを想定しています。

.
├── Terraformのリポジトリ
│   ├── dev
│   │   └── dev環境用の各種リソース
│   ├── prod
│   │   └── prod環境用の各種リソース
│   └── modules
│       └── 各種モジュール
└── アプリケーションのリポジトリ
    ├── .github/workflows 
    │   └── deploy.yml // ecspresso deployを実行
    ├── ecspresso
    │   ├── config_dev.yaml 
    │   ├── config_prod.yaml
    │   ├── ecs-service-def.json // 本記事最後に説明するパターン3では削除し、使用しない
    │   └── ecs-task-def.json // devとprodで共用
    └── src
	└── アプリケーションのコード

なお、ecspressoでの環境変数の使用方法は以下を参考にしてください。

パターン1 : ECSサービスとタスク定義の初期構築をマネジメントコンソールで行う

最初に実行したのが、このパターンです。

  1. TerraformでECSクラスターやその他必要なリソース(ALBやIAMロール)などを構築(terraform apply)する
  2. マネジメントコンソールで、ECSサービスとタスク定義を作成する
  3. (まだ一度も実行していなければ)ecspresso initを実行し、2で作成したECSサービスとタスク定義からecs-service-def.jsonecs-task-def.jsonを生成する
  4. 以後、 ecs-service-def.jsonecs-task-def.jsonを用いてecspresso deployを継続的に行う

いったん構築してしまえば、その後は特に問題は無いのですが、2でマネジメントコンソールを使っているため、本番環境や検証環境といった複数環境を構築する場合や、環境構築途中で何か問題があって12を始めからやり直すことになった場合、その分だけマネジメントコンソールを操作する必要があり、あまり効率的ではありません。

パターン2 : ECSサービスとタスク定義の初期構築もTerraformで行う

マネジメントコンソールの使用を止めたのが、以下のパターンです。

  1. TerraformでECSクラスターやその他必要なリソース(ALBやIAMロール)などのほか、ECSサービスとタスク定義を構築(terraform apply)する
  2. (まだ一度も実行していなければ)ecspresso initを実行し、1で作成した既存のECSサービスとタスク定義からecs-service-def.jsonecs-task-def.jsonを生成する
  3. 以後、 ecs-service-def.jsonecs-task-def.jsonを用いてecspresso deployを継続的に行う

マネジメントコンソールの操作が不要となり、初期構築はTerraformだけで完結します。

パターン2の課題1 : コードの二重管理

ただ、この状況だとECSサービスのコードがTerraform側とecspresso側で二重管理になっています。

そのため、ecspresso側のECSサービスのコード(ecs-service-def.json)に何か変更を加えてデプロイした場合、Terraform側でECSサービスを含むstateに対してterraform planを行うと、差分となって現れてしまい、Terraformの運用が煩雑になってしまいます。

パターン2の課題2 : Terraformだけで環境を繰り返し新規構築できない

前述の課題1を解消する方法として、追加で以下を行うことが考えられます。

  1. ECSサービスをTerraform管理外にする(terraform state rmの実行とtfファイルからの該当コード削除)

ただし、これにも問題があります。

4が完了したTerraformのコードの状態で、環境の作り直しや、別環境の構築を行おうとすると以下の流れとなります。

  1. TerraformでECSクラスターやその他必要なリソース(ALBやIAMロール)などのほか、ECSサービスとタスク定義を構築(terraform apply)する
  2. ecspresso initは前回実行済みなのでecs-service-def.jsonecs-task-def.jsonは生成済み
  3. 以後、 ecs-service-def.jsonecs-task-def.jsonを用いてecspresso deployを継続的に行う
    • ecspresso deployがエラーとなる
2021/04/28 15:09:14 {クラスター名}/{サービス名} Starting deploy 
2021/04/28 15:09:15 deploy FAILED. failed to describe current service status: service is not found

ecspresso deploy

  1. 新しいタスク定義を ECS に登録 (register)
  2. サービスに設定されているタスク定義を新しく登録したものに変更

といったように、既存のECSサービスを読み取って更新する動きを取り、ECSサービスが無い状態から新規作成はしません。

そのため、AWS側にECSサービスが存在していないとエラーとなります。

結果、前述の4を行った状態からそのまま環境の作り直しや、別環境の構築を行うということができません(構築そのものはできますが、ecspresso deployのエラー問題にぶつかってしまう)。

パターン3 ECSサービスとダミーのタスク定義の初期構築および管理をTerraformで行う

以上の課題を踏まえて、以下の方法を採ることにしました。

  • Terraformではサービスを管理させる。また、初期構築時用のダミーのタスク定義を管理させる。
  • ecspressoではタスク定義のみを管理させ、サービスを管理させない。

パターン3のポイント1 : ecspressoにサービスを管理させない

まず、ecspressoでは、ECSサービスの定義ファイル(ecs-service-def.json)を指定しなくてもecspresso deployは正常に動作します。

ecspresso/config_prod.yaml
region: ap-northeast-1
cluster: example-prod
service: example-prod
# service_definition: ecs-service-def.json
task_definition: ecs-task-def.json
timeout: 10m0s

そのため、ecs-service-def.jsonは削除可能であり、ECSサービスの管理を一方(今回はTerraform)に寄せることができます。

パターン3のポイント2 : Terraformにダミーのタスク定義を管理させる

ECSサービスを新規作成、更新する際には、タスク定義のARNの指定が必須です。

そのため、Terraformで管理するECSサービスにもタスク定義を指定しますが、これにダミーのタスク定義のARNを指定するようにします。

// ECSサービス
resource "aws_ecs_service" "this" {
  // 略
  task_definition     = aws_ecs_task_definition.dummy.arn
  // 略
}

// ダミーのECSタスク定義
resource "aws_ecs_task_definition" "dummy" {
  container_definitions = jsonencode([
    {
      name  = "nginx"
      image = "nginx:latest" // 適当なイメージを使用
      
      // 略
  ])
  // 略
}

これにより、初期構築時のterraform applyでECSサービスを作成できます。

ECSサービスがAWS側に作成できているということは、ecspressoによるデプロイが前述のエラーにならずに済みます。

初期構築時にダミーのタスク定義もAWS側に作成されてしまいますが、ecspressoによるデプロイでは、ecspresso側のecs-task-def.jsonに基づいて新規リビジョンのタスク定義が都度作成されますので、ダミーのタスク定義の存在が問題になることはありません。

パターン3のポイント3 : Terraformでignore_changeを使う

また、TerraformのECSサービスでは、lifecycleignore_changestask_definitionを指定しておきます。

// ECSサービス
resource "aws_ecs_service" "this" {
  // 略
  task_definition     = aws_ecs_task_definition.dummy.arn
  // 略
  lifecycle {
    ignore_changes = [
      task_definition,
    ]
  }
}

ecsoressoによる継続的デプロイを開始した以降は、ECSサービス上のタスク定義指定(task_definition)は、常にecspressoが作成した最新リビジョンのタスク定義のARNとなっています。

しかし、ignore_changesを指定することで、TerraformでECSサービスを含むstateに対してapplyしたとしても、AWS側のECSサービスのタスク定義指定がダミーのタスク定義のARNに戻ることはありません。

よって、ECSサービスのtask_definitionの更新はecspressoに任せ、それ以外の属性に関してはTerraform側だけで管理することができます。

終わりに

以上、ecspressoとTerraformの併用に関して、自分なりの方法を解説しました。

また何か課題が見つかれば記事にしていきたいと思います。