service-defを使わずにecspressoとTerraformを併用する

に公開2

こんにちは、TOKIUMでembedded SREをしている對馬です。
ecspressoについて、何か記事を書きたいなと思い投稿しました。

想定読者

  • ecspressoを利用している方
  • ecsの基礎知識をお持ちの方

ecspressoとは

概要

ecspressoは、「ECSにおける、アプリとインフラのデプロイを分離してくれるツール」です。

下記は、ECSにおける定番アーキテクチャパターンの1つです。

ECS アーキテクチャデザインパターン

この図にはECS以外にALBやRDSが登場していますが、アプリのデプロイで更新が必要なのは、だいたいの場合でECS(の中の一部のリソース)だけです。
じゃあそいつをどう更新しようか?と考えた時に、第一にTerraformなどのIaCツールが思い浮かぶかもしれません。
しかし、TerraformにはALBやRDSも管理対象に含まれています。そのため、terraform applyでアプリを更新しようとすると、意図せず他のインフラが更新されて壊れるということが発生しかねません。

予期せぬインフラ変更は地獄への扉ですから、何としてでも避けたいところです。

ecspressoは、ECS上の**アプリケーションの変更に関連するリソースだけを管理対象にするため、こうしたお悩みを解決してくれます。

管理対象

ecspressoは、ECSリソースのうち、サービスタスク定義だけを管理対象とします。

ecspresso handbook

これらの設定は、ecspressoでは、jsonやyamlで管理することができます。

ecspresso.yml
ecs-service-def.json
ecs-task-def.json

設定が用意できたら、あとはecspresso deployecspresso runを実行してやるだけで、タスクのデプロイ、runなどが簡単にできます。

さらなる詳細はhandbookなどをご参照ください。
https://zenn.dev/fujiwara/books/ecspresso-handbook-v2

Terraformを併用している場合の悩みどころ

このように便利なecspressoですが、インフラをTerraformで管理している場合、サービスの管理方法で少し悩むところがあります。

以下はservice-def.jsonの一例です。

{
  "deploymentConfiguration": {
    "deploymentCircuitBreaker": {
      "enable": true,
      "rollback": true
    },
    "maximumPercent": 200,
    "minimumHealthyPercent": 100
  },
  "deploymentController": {
    "type": "ECS"
  },
  "desiredCount": 1,
  "enableECSManagedTags": true,
  "enableExecuteCommand": false,
  "launchType": "FARGATE",
  "networkConfiguration": {
    "awsvpcConfiguration": {
      "assignPublicIp": "ENABLED",
      "securityGroups": [
        "sg-f2ab3796"
      ],
      "subnets": [
        "subnet-6debbe45",
        "subnet-7c52410b",
        "subnet-6438113d"
      ]
    }
  },
  "pendingCount": 0,
  "platformFamily": "Linux",
  "platformVersion": "LATEST",
  "propagateTags": "NONE",
  "runningCount": 0,
  "schedulingStrategy": "REPLICA",
  "tags": [
    {
      "key": "ecs:service:stackId",
      "value": "arn:aws:cloudformation:ap-northeast-1:314472643515:stack/ECS-Console-V2-Service-beaf029c-b87a-463d-9557-0e4d399c240e/9b39b790-7f47-11ed-ae07-0e7eec7bb691"
    }
  ]
}

ecspresso handbook

セキュリティグループやサブネットの値がハードコーディングされていますが、これらの値は作成した時に動的に値が決まるため、事前に値を入れておくことができません。
そのため、インフラ構築 -> service-def編集 -> アプリデプロイ といった流れを踏まざるを得なくなってしまいます。
複数環境を作成したい場合は、service-defを環境ぶん作った上で、この作業をしなくてはならないため、さらに面倒です。

このように、ecspressoでサービスを管理しようとすると、リソース参照にまつわる悩み事が出てくるわけですが、そもそもアプリのデプロイをする時にはサービスを更新することはほとんどないので、Terraform側でサービス管理をすれば良いんじゃないかな?と思ったりもします。

公式での解決方法

この問題の解決方法として、ecspresso作者のfujiwaraさんは、以下の方法を紹介しています。
https://techblog.kayac.com/ecspresso-tf-nullresource

ecspressoにはサービス定義やタスク定義ファイルの中でtfstate(Terraformが構成しているリソースの情報を保持しているファイル)を参照して、その属性を展開する機能があります。

{
 "launchType": "FARGATE",
 "networkConfiguration": {
   "awsvpcConfiguration": {
     "securityGroups": [
       "{{ tfstate `data.aws_security_group.http.id` }}"
     ],
     "subnets": [
       "{{ tfstate `aws_subnet.az-a.id` }}",
       "{{ tfstate `aws_subnet.az-c.id` }}"
     ]
   }
 }
}

このように記述すると、実際にTerraformによって構築されたリソースのIDをハードコードすることなく、Terraformのコード上で管理しているリソース名で記述できます。

現在構築中のプロダクトでは、この方法を採用すること自体が難しい状況でした。
というのも、subnetのtfstateに、subnetIDが含まれてしまっていたからです。

module.network.aws_subnet.app["subnet-xxxxxxxxxxxxxxxxx"]
module.network.aws_subnet.app["subnet-yyyyyyyyyyyyyyyyy"]
module.network.aws_subnet.app["subnet-zzzzzzzzzzzzzzzzz"]

terraformの書き方を変えれば修正すること自体はできそうではありました。
しかし、修正が若干面倒になりそうだったのと、私がサービス管理はterraform側に寄せたい派閥であったため、公式が推奨する方法は見送ることにしました。

解決方法

service-def.jsonは作らず、terraform上でサービスを管理する方式をとることにしました。

この方法を選んだ理由は以下2つです。

  • アプリのデプロイ時にサービスを変えることはほとんどないため、ecspressoに管理させるメリットが大きくない
  • service-def.jsonを作らずとも、ecspresso deployは実行可能だった

自分の宗教を信じきった形になります。
タスク定義さえ更新できればアプリのデプロイに支障はないですし、ecspresso側としてもサービス管理を外せるとなれば、特に問題ないだろうと判断しました。

後述するデメリットは存在するものの、今のところこの方法で問題なくecspressoを利用できます。

service-defを作らない場合のデメリット

タスク定義が二重管理になる

Terraformのaws_ecs_serviceでは、task_definitionの設定がrequiredになっています。

task_definition - (Optional) Family and revision (family:revision) or full ARN of the task definition that you want to run in your service. Required unless using the EXTERNAL deployment controller. If a revision is not specified, the latest ACTIVE revision is used.
aws_ecs_service

「EXTERNAL deploymentを選ばない限りtask_definitionはrequiredだよ。(意訳)」と言っています。

タスク定義はecspresso側でも管理しているため、Terraformにもtask_definitionが存在するし、ecspressoにもtask-def.jsonが存在するという形になってしまいます。
とはいえ、Terraform側はタスク定義をlifecycleでignoreの対象にしておけば、運用に支障が出ることはないと思います。

ecspresso runが使えなくなる

service-defを作らない場合、ecspresso runができなくなってしまいます。
https://github.com/kayac/ecspresso/issues/405

ECSで単体タスクを起動するためには、タスク定義だけではなく他の情報(特にnetwork modeがawsvpcの場合 subnet, security groupなど)が必要になることがあります。

タスク定義だけだと、runするのに必要な情報が揃わないわけですね。

そのため、もしタスクのrunを実行したい場合、aws cli(aws ecs run-task)を使うことになります。
cli用のスクリプトを書くのが若干面倒ではあるものの、致命的な問題ではないと判断しています。
(昨今だと、こういうスクリプトはAIがサッと作ってくれますし。)

感想

記事を書いてみて、サービス管理をecspresso側に寄せる方法も意外と悪くないかもと思い始めました。
現在構築中のサービスでは見送ったものの、別のサービスではservice-defを使う方向性でいくかもしれません。

何にせよ、ecspressoはとても良いツールなので、ECSをご利用されている方は、利用を検討してみてください。

TOKIUMプロダクトチーム テックブログ

Discussion

fujiwarafujiwara

ecspresso作者です。

というのも、subnetのtfstateに、subnetIDが含まれてしまっていたからです。

v2.6ではTerraform上で for_each / count で定義されたリソースに対してindexを指定しないで object / list として扱えるようになったので、Jsonnet を使って
https://github.com/kayac/ecspresso?tab=readme-ov-file#tfstate-jsonnet-function

subnets: [
  subnet.id for subnet in std.objectValues(tfstate('module.network.aws_subnet.app'))
],

このように書けば index をハードコードしないでもすべてのsubnetを列挙してidだけ抜き出すことができそうな気がします。よろしければお試し下さい。

tsushima_mtsushima_m

ご覧いただきありがとうございます。

ecspressoは本当にかゆいところに手が届くツールで、本当にすごいなと思います。
(すでにECS B/Gデプロイに対応されていて驚きです。。。)

すでに構築済みのものは、運用に出てしまっているためこのままにしますが、次のサービス構築でecspressoを使う際は、いただいた方法で構築を進めようかと思います。🙇