SREチームの藤原です。
この記事はTech KAYAC Advent Calendar 2022 5日目の記事です。
この記事では筆者が開発しているAmazon ECSデプロイツール ecspresso (v2)と、Terraformのnull_resourceを組み合わせて、 TerraformによるECS関連リソース作成とecspressoによるECSサービスのデプロイを一発で実行する手法について説明します。
ecspresso とは
ecspressoは筆者(fujiwara)が開発している、Amazon ECS用のデプロイツール(OSS)です。ECSにタスク定義とサービスをデプロイするツールはAWSが作っているものを含めて世の中に多々ありますが、ecspressoは次のような特徴を持っています。
- Go 言語で書かれた OSS (MIT LICENSE) です
- ECS サービスとタスクに関わる最小限のリソースをコード管理し、デプロイを実行するためのツールです
- ECS サービスとタスクが動作するにあたって必要な関連リソース(例: IAM Role, ELB target group, VPC, Subnet, Security gruop など)を作成/管理する機能はありません
- Terraform の State を管理している tfstate ファイルを読み、その情報を使うことができます
- 既存の ECS サービスとタスクの情報を元に、構成ファイルを生成する機能があります
- AWS コンソールや他のツールでデプロイしている既存サービスを、あとから ecspresso で管理するように変更できます
詳しくはecspresso handbookの1章(無料公開)も参照して下さい。 zenn.dev
ecspressoは、基本的にECSのタスク定義とサービスしか管理しないため、ECSが動作する際に必要な他のAWSのリソース (IAM RoleやVPC、ロードバランサーなど)は、別の手段で管理する必要があります。 *1
Terraformと組み合わせる
ECSサービスが必要とする関連リソースについては、カヤックでは主にTerraformによる管理を行っています。
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のコード上で管理しているリソース名で記述できます。
環境ごとにtfstateを分割して同一のリソース名にしておくことで、参照するtfstateを切り替えるだけで複数環境へのECSデプロイを統一的に管理することもできます。
ecspresso + Terraform構成の面倒なところ
ECSへのアプリケーションのデプロイと、ECSが要求する依存リソースは更新のサイクルが異なることが多いため、長期運用する場合や管理するチームが分かれている場合などは特に、2つのツールを使い分けることにメリットがあります。
しかし、例えば検証用の環境を1から立ち上げて不要になったらすぐに削除したい、というような場合には1つのツールで完結するほうが便利です。
また、ECSが依存するリソース(例: IAM Role)と、ECSに依存するリソース(例: Application Auto Scaling)を両方1つのTerraformで管理しようとすると、terraform apply
を1回で済ませることができません。
- TerraformでIAM Roleを作成
- この時点ではECSサービスはないため、Application Auto Scalingはコメントアウトするなどして作成しないようにする
- ecspressoでECSサービスを作成
- TerraformでApplication Auto Scalingを作成
と順に手作業をする必要があり、これが面倒なところでした。
ecspresso v2とTerraform null_resourceによる一発構築手法
ところでecspressoでは、もうすぐv2をリリースする予定です。v2での変更点は、以下の記事を参照して下さい。
v1ではECSサービスを新規に作成する場合にはecspresso create
、作成済みのサービスに変更やデプロイを行う場合にはecspresso deploy
と、サブコマンドを使い分ける必要がありました。
この仕様は冪等性がなく不便なことが多かったため、v2では新規作成も更新もecspresso deploy
コマンドのみで実行できるようになりました。
そして、この挙動とTerraformのnull_resourceを組み合わせることで、terraform apply
を1回実行するだけで関連リソースとECSへのデプロイが完結できるようになりました!
具体的には、次のようなことが可能です。
- 関連リソースの初期構築とECSへのデプロイを
terraform apply
のみで行える - Terraformで管理しているリソースに変更があった場合も
terraform apply
からecspresso deploy
が呼ばれるため自動でデプロイされる- ECSに関連しないリソースの変更ではデプロイは発生しません
- もちろん単独で
ecspresso deploy
することも可能 terraform destroy
するとECSサービスも関連リソースも削除される
null_resourceとは
The null_resource resource implements the standard resource lifecycle but takes no further action.
とあるように、それ自身はTerraformリソースとしてのライフサイクルを持ちますが、単体では「何もしない」リソースです。
ただし、他のリソースの変更をトリガーにしてprovisioner
でTerraformから外部コマンドを実行できます。そこでecspressoを実行することで、ECSサービスが依存するリソースも、ECSサービス自身も、ECSサービスに依存するリソースも、terraform apply
一撃で構築できますし、terrraform destroy
一撃で削除できます。
具体例
Terraformで管理するものは以下とします。
- ECSが依存するリソース
- IAM Role (タスク実行ロール)
- ECSクラスタ
- ECSに依存するリソース
- Application Auto Scaling target
ECSが依存するリソースの.tfは普通に記述します。
resource "aws_iam_role" "ecs-task-execution" { name = "ecs-task-execution-oneshot" assume_role_policy = data.aws_iam_policy_document.ecs.json } resource "aws_iam_role_policy_attachment" "ecs-task-execution" { role = aws_iam_role.ecs-task-execution.name policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" } resource "aws_ecs_cluster" "oneshot" { name = "oneshot" }
null_resourceによってecspressoでデプロイする記述は次のようになります。
triggers
にECSが依存するリソースを記述provisioner "local-exec"
でecspresso deploy
を実行するenvironment
で依存リソースの値を渡す
when = destroy
条件でECSタスクを0にしてサービスを削除する記述も
ポイントは「environment
で依存リソースの値を渡す」点です。最初にterraform apply
を実行してる途中にはtfstateに値が書き込まれていないため、ecspresso側からtfstate参照でIDなどを解決できません。しかしterraformの中では実行中でも作成済みのリソースの値は参照できるため、それを環境変数でecspresso側に渡してあげることで解決します。
// ECSが依存するリソースの変更でトリガーされるnull_resource resource "null_resource" "ecspresso" { triggers = { cluster = aws_ecs_cluster.oneshot.name, execution_role_arn = aws_iam_role.ecs-task-execution.arn, } provisioner "local-exec" { command = "ecspresso deploy" working_dir = "." environment = { // 環境変数で依存リソースの値(ecspressoで参照するもの)を渡す ECS_CLUSTER = aws_ecs_cluster.oneshot.name, EXECUTION_ROLE_ARN = aws_iam_role.ecs-task-execution.arn } } provisioner "local-exec" { command = "ecspresso scale --tasks 0 && ecspresso delete --force" working_dir = "." when = destroy // terraform destroy時に発動する条件 } }
ecspressoが使用するタスク定義は、次のようにします。ポイントは「 環境変数があればその値、なければtfstateを参照する」ためにテンプレート関数を{{ or }}
を使って記述することです。
terraform apply
によってnull_resourceがトリガーされて実行される場合は環境変数が使われます。Terraform経由ではなくecspresso deploy
を単独で実行した場合は、tfstateを参照することになります。
{ containerDefinitions: [{ essential: true, image: 'nginx:latest', name: 'nginx', }], cpu: '256', executionRoleArn: '{{or (env `EXECUTION_ROLE_ARN` ``) (tfstate `aws_iam_role.ecs-task-execution.arn`)}}', family: 'nginx', memory: '512', networkMode: 'awsvpc', requiresCompatibilities: ['FARGATE'], }
最後に、ECSサービスに依存するリソースを記述します。data resourceでecspressoで管理しているサービスを参照し、それに依存する形でApplication Auto Scaling targetを定義しています。
data "aws_ecs_service" "oneshot" { cluster_arn = aws_ecs_cluster.oneshot.name service_name = "nginx" depends_on = [ null_resource.ecspresso, ] } resource "aws_appautoscaling_target" "nginx" { max_capacity = 10 min_capacity = 1 resource_id = "service/${aws_ecs_cluster.oneshot.name}/${data.aws_ecs_service.oneshot.service_name}" scalable_dimension = "ecs:service:DesiredCount" service_namespace = "ecs" }
実行結果の例
terraform apply
で一撃構築されている様子がこちらです。IAM Role、ECS Cluster、ecspresso deploy、Application Auto Scaling targetの順で作成されていることが分かります。
aws_ecs_cluster.oneshot: Creating... aws_iam_role.ecs-task-execution: Creating... aws_iam_role.ecs-task-execution: Creation complete after 2s [id=ecs-task-execution-oneshot] aws_iam_role_policy_attachment.ecs-task-execution: Creating... aws_iam_role_policy_attachment.ecs-task-execution: Creation complete after 0s [id=ecs-task-execution-oneshot-20221202070518071000000001] aws_ecs_cluster.oneshot: Still creating... [10s elapsed] aws_ecs_cluster.oneshot: Creation complete after 11s [id=arn:aws:ecs:ap-northeast-1:314472643515:cluster/oneshot] null_resource.ecspresso: Creating... null_resource.ecspresso: Provisioning with 'local-exec'... null_resource.ecspresso (local-exec): Executing: ["/bin/sh" "-c" "ecspresso deploy"] null_resource.ecspresso (local-exec): 2022/12/02 16:05:26 nginx/oneshot Starting deploy null_resource.ecspresso (local-exec): 2022/12/02 16:05:27 nginx/oneshot Service nginx not found. Creating a new service null_resource.ecspresso (local-exec): 2022/12/02 16:05:27 nginx/oneshot Starting create service null_resource.ecspresso (local-exec): 2022/12/02 16:05:27 nginx/oneshot Registering a new task definition... null_resource.ecspresso (local-exec): 2022/12/02 16:05:27 nginx/oneshot Task definition is registered nginx:19 null_resource.ecspresso (local-exec): 2022/12/02 16:05:27 nginx/oneshot Service is created null_resource.ecspresso (local-exec): 2022/12/02 16:05:30 nginx/oneshot Waiting for service stable...(it will take a few minutes) null_resource.ecspresso: Still creating... [10s elapsed] null_resource.ecspresso (local-exec): 2022/12/02 16:05:31 (service nginx) has started 1 tasks: (task 911019d2120b41ffa3b50da400cf5cb3). null_resource.ecspresso (local-exec): 2022/12/02 16:05:40 nginx/oneshot PRIMARY nginx:19 desired:1 pending:1 running:0 null_resource.ecspresso: Still creating... [20s elapsed] null_resource.ecspresso (local-exec): 2022/12/02 16:05:50 nginx/oneshot PRIMARY nginx:19 desired:1 pending:0 running:1 null_resource.ecspresso: Still creating... [30s elapsed] null_resource.ecspresso (local-exec): 2022/12/02 16:06:06 nginx/oneshot Service is stable now. Completed! null_resource.ecspresso: Creation complete after 39s [id=7075482505817107284] data.aws_ecs_service.oneshot: Reading... data.aws_ecs_service.oneshot: Read complete after 0s [id=arn:aws:ecs:ap-northeast-1:314472643515:service/oneshot/nginx] aws_appautoscaling_target.nginx: Creating... aws_appautoscaling_target.nginx: Creation complete after 1s [id=service/oneshot/nginx] Apply complete! Resources: 5 added, 0 changed, 0 destroyed.
まとめ
ecspresso v2 とTerraform null_resourceを組み合わせることで、関連リソースもECSサービスも1コマンドの実行で構築/削除が完結する手法を紹介しました。
ecspresso は現在 v1.99.6をプレリリースしています。 試していただけると嬉しいです。
*1:この設計には意図があります。ecspresso handbookの設計思想と実装 の章を参照して下さい