SREチームの藤原です。
この記事はTech KAYAC Advent Calendar 2022 5日目の記事です。
この記事では筆者が開発しているAmazon ECSデプロイツール ecspresso (v2)と、Terraformのnull_resourceを組み合わせて、 TerraformによるECS関連リソース作成とecspressoによるECSサービスのデプロイを一発で実行する手法について説明します。
ecspresso とは
github.com
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
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デプロイを統一的に管理することもできます。
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をリリースする予定です。v2での変更点は、以下の記事を参照して下さい。
sfujiwara.hatenablog.com
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
を実行する
when = destroy
条件でECSタスクを0にしてサービスを削除する記述も
ポイントは「environment
で依存リソースの値を渡す」点です。最初にterraform apply
を実行してる途中にはtfstateに値が書き込まれていないため、ecspresso側からtfstate参照でIDなどを解決できません。しかしterraformの中では実行中でも作成済みのリソースの値は参照できるため、それを環境変数でecspresso側に渡してあげることで解決します。
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 = {
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
}
}
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をプレリリースしています。 試していただけると嬉しいです。