🍵

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

2021/04/29に公開
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の併用に関して、自分なりの方法を解説しました。

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

Discussion

fujiwarafujiwara

ecspresso 作者です。記事ありがとうございます!

ecspresso はサービスの新規作成用に create というコマンドを用意しているのですが、それでは用が足りないでしょうか?
https://zenn.dev/fujiwara/articles/ecspresso-20201215

初回だけ deploy ではなくcreate を実行しなければいけないので、何度も環境を自動的に作り直しつつ ECS サービスも立てるような場合には不便、というのはありそうですが…
deploy で新規作成ができないというのは他にもハマった事例を見たことがあるので、新規作成も自動的に実行するようにできないか検討してみます。

Takashi YamaharaTakashi Yamahara

はじめまして!
ecspressoをいつも便利に使っております!
素晴らしいツールをありがとうございます!

おっしゃる通りcreateコマンドを使う方法がありますね。

記事中では触れていなかったのですが、私の場合、以下の事象に遭遇してcreateコマンドの利用を見送った経緯がありました(長文ご容赦ください)。

前提

  • ecs-service-def.jsonでは、ターゲットグループのARN、セキュリティグループやサブネットのIDを指定する必要がありました
  • 上記はリソースを作成してみるまで値が不定なので、ecs-service-def.json内ではtfstate読み込みが必須という認識です
    • e.g.) ターゲットグループ : arn:aws:elasticloadbalancing:ap-northeast-1:{{ AWS_ACCOUNT_ID }}:targetgroup/example-prod/6373336eb3c5543a
  • 一方、私の構成ではTerraform側でのtfstate管理を以下のように分割していました
    • ターゲットグループを含むtfstate
    • セキュリティグループやサブネットを含むtfstate
  • ecspressoで読み込めるtfstateは1個までという認識のため(こちら間違っていたらすみません)、ecspressoのconfig.yamlでは、まずいったんターゲットグループを含むtfstateを指定することにしました
plugins:
  - name: tfstate
    config:
      url: s3://my-bucket/prod/apps/example_v0.14.3.tfstate // ターゲットグループを含むtfstate
  • その上で、ターゲットグループを含むコンポーネントでは、terraform_remote_stateにてセキュリティグループやサブネットを管理しているtfstateを読み込ませるようにしました
ターゲットグループを含むコンポーネント側での記述
data "terraform_remote_state" "network" {
  backend = "s3"

  config = {
    bucket = "my-bucket"
    key    = "prod/network_v0.14.3.tfstate" // セキュリティグループやサブネットを管理しているtfstate
    region = "ap-northeast-1"
  }
}
  • 上記で指定している、セキュリティグループやサブネットを管理しているコンポーネントでは以下のようにoutputしています
セキュリティグループやサブネットを含むコンポーネント側での記述
output "security_group_vpc" {
  value = aws_security_group.vpc // idだけではなくresource全体をoutputしています
}

output "subnet_private" {
  value = aws_subnet.private // idだけではなくresource全体をoutputしています
}
  • 結果として、ecs-service-def.jsonは以下の記述となりました
ecs-service-def.json
{
  // 略
  "loadBalancers": [
    {
      "containerName": "nginx",
      "containerPort": 80,
      "targetGroupArn": "{{ tfstate `aws_lb_target_group.this.arn` }}"
    }
  ],
  "networkConfiguration": {
    "awsvpcConfiguration": {
      "assignPublicIp": "DISABLED",
      "securityGroups": [
        "{{ tfstate `data.terraform_remote_state.network.outputs.security_group_vpc.id` }}`"
      ],
      "subnets": [
        "{{ tfstate `data.terraform_remote_state.network.outputs.subnet_private[0].id` }}",
        "{{ tfstate `data.terraform_remote_state.network.outputs.subnet_private[1].id` }}",
        "{{ tfstate `data.terraform_remote_state.network.outputs.subnet_private[2].id` }}"
      ]
    }
  },
  // 略
}

ecspresso createでエラー

  • 上記の前提のもと、ecspresso createを行うとエラーになりました
$ ecspresso create --config=config.yaml --dry-run
2021/05/01 14:35:28 example-prod/example-prod Starting create service DRY RUN
2021/05/01 14:35:28 create FAILED. failed to load service definition: ecs-service-def.json load failed: custom failed: template attach failed: template: conf:25:12: executing "conf" at <tfstate `data.terraform_remote_state.network.outputs.security_group_vpc.id`>: error calling tfstate: data.terraform_remote_state.network.outputs.security_group_vpc.id is not found in tfstate
  • terraform state showにて、stateファイル中に値はあることは確認しました
$ terraform state show data.terraform_remote_state.network
data "terraform_remote_state" "network" {
    backend = "s3"
    // 略
    outputs = {
    // 略
        security_group_vpc    = {
        // 略
            id                     = "sg-0e65d961c4637990f"
            // 略
  • 今回outputがmapだったので、これをidのみのstringに変えてみましたが同様のエラーになりました
セキュリティグループやサブネットを含むコンポーネント側での記述
output "security_group_vpc_id" {
  value = aws_security_group.vpc.id
}
ecs-service-def.json
      "securityGroups": [
        "{{ tfstate `data.terraform_remote_state.network.outputs.security_group_vpc_id` }}`"
      ],
$ ecspresso create --config=config.yaml --dry-run
2021/05/01 14:47:50 example-prod/example-prod Starting create service DRY RUN
2021/05/01 14:47:50 create FAILED. failed to load service definition: ecs-service-def.json load failed: custom failed: template attach failed: template: conf:25:12: executing "conf" at <tfstate `data.terraform_remote_state.network.outputs.security_group_vpc_id`>: error calling tfstate: data.terraform_remote_state.network.outputs.security_group_vpc_id is not found in tfstate
  • ecsoressoではdata.terraform_remote_stateの先にあるoutputsが読み込めない?ように思えました
  • ecspressoのバージョンはv1.5.1となります

以上となります。

createでエラーになりましたが、tfstateの読み込みの問題となると、これはdeployコマンドでも同様になると思われるので、ecspressoでのecs-service-def.jsonの利用を見送る理由にもなりますね。

お時間のある時にご確認いただけたら幸いです。

余談ですが、tfstate-lookup単体で色々検証してみようと思ったのですが、インストールできず見送りました。

$ brew install tfstate-lookup
==> Searching for similarly named formulae...
Error: No similarly named formulae found.
Error: No available formula or cask with the name "tfstate-lookup".
==> Searching for a previously deleted formula (in the last month)...
Error: No previously deleted formula found.
==> Searching taps on GitHub...
Error: No formulae found in taps.

deploy で新規作成ができないというのは他にもハマった事例を見たことがあるので、新規作成も自動的に実行するようにできないか検討してみます。

ありがとうございます!
今後のリリースを楽しみにしております!

fujiwarafujiwara

なるほど、事情を把握しました。
tfstate を分割していて、data.terraform_remote_state の先が参照できなかったのが ECS サービスを ecspresso での管理から除外した根本理由、ということなのですね。

ecspresso の想定ユースケースはタスク定義とサービスを両方管理下に置くもので、tfstate を分割していなければ create の利用におそらく問題はないと思われますので、terraform_remote_state を使えるようにできないか検討してみますね。

余談ですが、tfstate-lookup単体で色々検証してみようと思ったのですが、インストールできず見送りました。

すみません、こちらREADMEが間違っていました。
brew install fujiwara/tap/tfstate-lookup
でインストールできるかと思います。(現時点では terraform_remote_state 先の参照はできないと思います)

Takashi YamaharaTakashi Yamahara

tfstate を分割していて、data.terraform_remote_state の先が参照できなかったのが ECS サービスを ecspresso での管理から除外した根本理由、ということなのですね。

そうですね、よくよく考えてみれば私のTerraformの構成の場合ではこちらが根本理由となりますので、初めから記事中にその旨も書くべきでしたね。

ecspresso の想定ユースケースはタスク定義とサービスを両方管理下に置くもので

なるほど!本記事でのサービスを管理下に置かない手法はイレギュラーなものとわかりましたので、記事を読んだ方が誤解されないよう、冒頭にその旨を追記させていただきます。

terraform_remote_state を使えるようにできないか検討してみますね。

ありがとうございます!もし使えるようになれば、大変嬉しいです!

tfstate-lookupのインストールの件もありがとうございました!