ECS Execに対応した ecspresso v1.5.0 をリリースしました

Amazon ECS のデプロイツール、ecspresso の v1.5.0 をリリースしたのでお知らせです。

github.com

今回は Amazon ECS Exec という新機能対応が目玉です。ほかにもお送りいただいたPRを取りこんだ各種修正が入ったリリースになります。どうぞご利用ください。

PRをお送りいただいた皆様、ありがとうございました!

新機能

ECS Exec への対応を追加しました

ホストから docker exec するように、Fargate に対しても起動しているコンテナに入ってコマンドを実行できる ECS Exec 機能がリリースされています。

aws.amazon.com

ecspresso v1.5 では exec コマンドを追加して、AWS CLI なしで exec できるようにしました。session-manager-plugin は必要です。

exec を使用するためには、まずサービス定義で enableExecuteCommand を true に設定して create / deploy します。(起動中の ECS サービスに対してもあとから変更可能です)

{
  "enableExecuteCommand": true
}

必要な権限などが揃っているかどうかは amazon-ecs-exec-checker で確認すると便利です。

その後は ecspresso exec コマンドを使用して、起動中のタスクに対して exec ができます。タスクに複数コンテナがある場合は、更に選択肢がでます。

実際に実行してみた様子は以下をご覧ください。

この例ではタスクやコンテナの絞り込みに peco を使っていますが、絞り込みに使うコマンドは設定ファイルの filter_command で指定可能です。未指定の場合は ID / コンテナ名入力するダイアログが表示されます。

タスクを一覧/詳細表示する tasks コマンドを追加

これまで ecspresso ではサービスとタスク定義の管理に主眼を置いていたため、起動済みのタスクの情報を見る手段がありませんでした。

ecspresso tasks を実行すると、サービスから起動されたタスクと、同じタスク定義を持ったタスク(単体で run されたものも含む) を表示します。

$ ecspresso --config config.yaml
|                ID                |   TASKDEFINITION   | INSTANCE |   LASTSTATUS   | DESIREDSTATUS |         CREATEDAT         |         GROUP         |  TYPE   |
+----------------------------------+--------------------+----------+----------------+---------------+---------------------------+-----------------------+---------+
| 28852184e15e48e0aad6b7549f92ed65 | ecspresso-test:274 |          | RUNNING        | RUNNING       | 2021-04-13T13:45:23+09:00 | service:nginx-local   | FARGATE |
| bed89f2e492e44cd82d75776ff2d8a7f | ecspresso-test:276 |          | RUNNING        | RUNNING       | 2021-04-13T13:48:04+09:00 | service:nginx-local   | FARGATE |
| fad8863654f945588273916a3a56bd95 | ecspresso-test:275 |          | DEPROVISIONING | STOPPED       | 2021-04-13T13:46:21+09:00 | family:ecspresso-test | FARGATE |

--find オプションを付けると exec 同様にタスクを絞り込んで、決定するとそのタスクの情報を JSON で出力もできます。

停止したタスクについても API で取得できる時点までは閲覧できるので、なぜか上手く起動できずに落ちてしまったタスクでも詳細を表示すれば、マネージメントコンソールを見に行かずに理由が分かります。

次の例では、"stoppedReason": "Essential container in task exited" などが読み取れます。

$ ecspresso --config config.yaml --id fad8863654f945588273916a3a56bd95 --output json
{
  "attachments": [ ... ],
  "availabilityZone": "ap-northeast-1c",
  "capacityProviderName": "FARGATE",
  "clusterArn": "arn:aws:ecs:ap-northeast-1:123456789012:cluster/ecspresso-test",
  "connectivity": "CONNECTED",
  "connectivityAt": 1618289185.693,
  "containers": [ ... ],
    {
      "containerArn": "arn:aws:ecs:ap-northeast-1:123456789012:container/ecspresso-test/fad8863654f945588273916a3a56bd95/8a04958b-4918-4a15-b6d4-6b40791c4fba",
      "cpu": "0",
      "exitCode": 137,
      "healthStatus": "UNKNOWN",
      "image": "debian:buster-slim",
      "lastStatus": "STOPPED",
      "managedAgents": [
        {
          "lastStatus": "STOPPED",
          "name": "ExecuteCommandAgent"
        }
      ],
      "name": "bash",
      "networkBindings": [],
      "networkInterfaces": [
        {
          "attachmentId": "bcb4aa98-1dfe-4a08-9a29-9db6e15d8334",
          "privateIpv4Address": "10.3.3.161"
        }
      ],
      "runtimeId": "fad8863654f945588273916a3a56bd95-987533343",
      "taskArn": "arn:aws:ecs:ap-northeast-1:123456789012:task/ecspresso-test/fad8863654f945588273916a3a56bd95"
    }
  ],
  "cpu": "256",
  "createdAt": 1618289181.026,
  "desiredStatus": "STOPPED",
  "enableExecuteCommand": true,
  "executionStoppedAt": 1618289229,
  "group": "family:ecspresso-test",
  "healthStatus": "UNKNOWN",
  "lastStatus": "STOPPED",
  "launchType": "FARGATE",
  "memory": "512",
  "overrides": {
    "containerOverrides": [
      {
        "command": [
          "nginx",
          "-V"
        ],
        "name": "nginx"
      },
      {
        "name": "bash"
      }
    ],
    "inferenceAcceleratorOverrides": []
  },
  "platformVersion": "1.4.0",
  "pullStartedAt": 1618289214.039,
  "pullStoppedAt": 1618289223.039,
  "startedAt": 1618289229.039,
  "stopCode": "EssentialContainerExited",
  "stoppedAt": 1618289345.672,
  "stoppedReason": "Essential container in task exited",
  "stoppingAt": 1618289280.886,
  "tags": [],
  "taskArn": "arn:aws:ecs:ap-northeast-1:123456789012:task/ecspresso-test/fad8863654f945588273916a3a56bd95",
  "taskDefinitionArn": "arn:aws:ecs:ap-northeast-1:123456789012:task-definition/ecspresso-test:275",
  "version": 6
}

rollback コマンドが CodeDeploy に対応

これまで ecspresso rollback は CodeDeploy でデプロイされた場合には機能しませんでしたが、機能するようになりました。 Feature Request: Rollback by CodeDeploy by cohalz · Pull Request #261 · kayac/ecspresso · GitHub

タスク定義へのタグ付けに対応

これまではタスク定義ファイルに tags を記述してもタグを付与することができませんでしたが、タグをつけられるようになりました。

これに伴って、タスク定義ファイルを取り扱うときに AWS SDK Go の ecs.TaskDefinition を使用していた部分が、ecs.RegisterTaskDefinitionInput を使用するように変わっています。JSON ファイルとして扱う場合には互換性を持たせていますが、もし何か問題がありましたらお知らせください。

{
  "tags": [
     {
        "key": "TagKey",
        "value": "TagValue"
     }
   ]
}

Feature Request: Support add Tags to TaskDefinition by Hirofumi-Narita · Pull Request #256 · kayac/ecspresso · GitHub

Support tagging for task definition. by fujiwara · Pull Request #260 · kayac/ecspresso · GitHub

バグ修正

init コマンド実行時に ECS サービスの propagateTags 属性を扱えるように

これまでは init コマンドで既存 ECS サービスを定義ファイル化した時点で propagateTags 属性が消えていましたが、保持するようになりました。

Support tags&propagateTags to `init` & `create` by cohalz · Pull Request #270 · kayac/ecspresso · GitHub

verify コマンド実行時に ELB の検証をタスク実行ロールではなく ecspresso の実行権限で行うように

これまでは verify コマンド実行時の ELB (target group) の検証を、タスク実行ロール(にAssumeRoleした状態)で行っていましたが、ecspresso 自体の実行権限で行うように変更しました。

タスク実行ロールには最小限の権限しか持たせない方が望ましい、という理由です。v1.5 の時点ではecspresso の実行権限での検証に失敗した場合、タスク実行ロールでの検証にフォールバックしますが、この挙動は移行措置として次のリリースで削除する予定です。

Don't require `elasticloadbalancing:DescribeTargetGroups` in task execution role to `verify` by cohalz · Pull Request #262 · kayac/ecspresso · GitHub

ecspresso handbook も改訂予定です

すみません、v1.4.0 リリース時に改訂予定としていましたが、まだ handbook は v1.3 対応のままです。v1.4, 1.5 での変更について加筆予定ですので、引き続きよろしく願いします。

zenn.dev

GitHub Sponsors はじめました

github.com

One-time のスポンサーもできるようになっていますので、よろしければこちらもご検討ください。

ecspresso v1.4.0 をリリースしました

Amazon ECS のデプロイツール、ecspresso の v1.4.0 をリリースしたのでお知らせです。

github.com

今回はそこまで大きな機能追加はないのですが、地味に嬉しい機能が入っております。どうぞご利用ください。

run コマンドに --overrides-file が追加

単体タスクを実行する run コマンドに、--overrides-file オプションが追加されました。

タスク実行時に上書きするコマンドや環境変数は、これまで --overrides オプションでJSON文字列として渡す必要がありましたが、これが結構複雑な構造をしているので組み立てが面倒でした。

{
    "containerOverrides": [
        {
            "name": "foo",
            "command": ["my-command"]
        }
    ]
}

こんな構造なので、CLI で渡すのはだいぶ煩雑です。ということで、あらかじめファイルとして JSON を記述しておいて、それを --overrides-file で指定することができるようになりました。また、このファイルは他の ecspresso の定義ファイルと同様、テンプレートとして解釈されます。

{
    "containerOverrides": [
        {
            "name": "foo",
            "command": ["{{ must_env `COMMAND` | json_escape }}"]
        }
    ]
}

このように環境変数COMMAND という値を展開するように定義しておけば、コマンドだけを変更したい場合に以下のように実行できます。便利!

$ COMMAND="echo hoge" ecspresso run --overrides-file overrides.json ...

verify コマンドに --no-get-secrets オプションが追加

ecspresso verify でのタスク定義の検証時に、これまでは secrets の値が SSM パラメータストアや SecretsManager に存在して読み取れる必要がありました。権限的に読み取れない場合は verify が失敗してしまいますが、値によっては verify を行う環境からは読ませたくない、という要望がありました。

--no-get-secrets オプションを指定すると、secrets が読み取れるかどうかの検証をskipします。

--envfile オプションが追加

ecspresso の各種定義ファイルでは環境変数を展開できる機能があり、動作のカスタマイズには欠かせない機能です。

しかし、複数の環境に対するデプロイを1つの定義ファイルで行う場合には ecspresso 実行環境変数を定義し直す必要があります。これはちょっと面倒な場合があるため、あらかじめ環境変数を定義した envfile を指定できるようにしました。

指定できるファイルは一般的な .env 形式で、export は付いていてもいなくても同様に動作します。

export FOO=foo
BAR=bar

envfile のパースには hashicorp/go-envparse を使用しています。hashicorp のライブラリ、いつもながら渋いけど便利ですね。

github.com

init コマンドに --force-overwrite オプションが追加

指定すると、init コマンドでの定義ファイル出力時に、既にファイルが存在していても確認なしで上書きします。

ecspresso の構成管理ファイルをバックアップ目的でリポジトリ管理していて、それを自動化する場合には確認なしで上書きしたいことはありますね。

テンプレート関数に tfstatef が追加

Terraform state file をテンプレートから参照する機能で tfstate 関数がありますが、tfstatef という関数が追加されました。これは要するに print に対する printf です。resource.name["foo"].id のような参照を行う場合に、部分的に文字列を変数から組み立てたい場合に便利です。

例えば環境変数 AZ を使用して aws_subnet.public['$AZ'].id というリソース名を tfstate から参照したい場合、従来は printf で組み立てた文字列を tfstate 関数で渡す、という若干のテンプレート芸が必要でした。

{{ printf `aws_subnet.public['%s'].id` (must_env `AZ`) | tfstate }}

これが、以下のように分かりやすく記述できます。

{{ tfstatef `aws_subnet.public['%s'].id` (must_env `AZ`) }}

verify 時にタスク実行ロールに assume できない場合の警告を warning から info に変更

verify コマンド実行時、ecspresso はタスク実行ロールに assume role を試みて、成功したらタスク実行ロールで、失敗したら現在の権限で検証を行います。

タスク実行ロールに assume することでより確実な検証が行えるのですが、assume role できる設定をわざわざ行わない場合は毎回以下のような警告が(黄文字で)出力されてしまいます。

WARNING: failed to assume role to taskExecutuionRole.

実際にはこれは害があるとまでは言い切れない警告だったため、ログレベルを INFO に変更しました。

作者の周囲でも、この警告が出ていてもほとんど全員が無視するという結果が観測されたため、WARNING を無視させる習慣を付けてしまうのは望ましくない、という理由もあります。

リリースバイナリを Go 1.16 でビルド

Go 1.15 から Go 1.16 に変更しました。今回から、M1 macdarwin むけ arm64 バイナリも同時にビルドしています。


ecspresso handbook も改訂予定です

zenn.dev

v1.4.0 で追加、変更された機能については、ecspresso handbook でも順次改訂予定です。

ecspresso で CloudFormation のリソース読み込みに対応 + その他機能追加した v1.3.0 をリリースしました

あけましておめでとうございます。Amazon ECS デプロイツール ecspresso の v1.3.0 をリリースしたのでお知らせです。

[2021-01-16 追記] v1.3.0 は init コマンドでクラッシュする問題があります。v1.3.1 をご利用ください。

今回は機能追加が盛り盛りなので、まとめて紹介します。

Release v1.3.0 · kayac/ecspresso · GitHub

【新機能】 cloudformation プラグインを追加

これまで ecspresso は Terraform tfstate を読み込んでそのリソース名から属性値を解決できる機能を持っていましたが、CloudFormation でも同様の処理ができるようになりました。

README に例がありますが、次のように Output と Export を定義したテンプレートで構築された、Stack 名が ECS-ecspresso という CloudFormation stack がある場合に

Outputs:
  SubnetAz1:
    Value: !Ref PublicSubnetAz1
  SubnetAz2:
    Value: !Ref PublicSubnetAz2
  EcsSecurityGroupId:
    Value: !Ref EcsSecurityGroup
    Export:
      Name: !Sub ${AWS::StackName}-EcsSecurityGroupId

ecspresso の設定ファイルで cloudformation plugin を指定すると

plugins:
  - name: cloudformation

定義ファイルで cfn_outputcfn_export という関数が使用可能になります。cfn_output スタック名 OutputKey で Outputs の値を名前で解決できます。cfn_export エクスポート名 で、エクスポートされた値を名前で解決できます。

サービス定義での使用例は次のようになります。

{
  "networkConfiguration": {
    "awsvpcConfiguration": {
      "subnets": [
        "{{ cfn_output `ECS-ecspresso` `SubnetAz1` }}",
        "{{ cfn_output `ECS-ecspresso` `SubnetAz2` }}"
      ],
      "securityGroups": [
        "{{ cfn_export `ECS-ecspresso-EcsSecurityGroupId` }}"
      ]
    }
  }
}

ご意見をくださった @torics さん、@hamako9999 さん、@_sisisin さん、ありがとうございました!

【新機能】scale コマンドを追加

これまでサービスのタスク数を変更するためには deploy --no-update-service --skip-task-definition --tasks タスク数 を指定していましたが、これをショートカットにした scale コマンドを追加しました。

$ ecspresso --config config.yaml scale --tasks 10

これで ecspresso --config config.yaml deploy --no-update-service --skip-task-definition --tasks 10 とおなじ意味になります。

タスク数だけを変えたい場合でも指定するオプションが多いため、うっかりサービスやタスクを更新してしまう事例がありました。scale コマンドを使用すると必要なのは --tasks のみ(必須) になるため、事故を防げます。

【新機能】tfstate plugin が URL を直接読み込めるようになりました

これまで tfstate plugin はローカルファイルの読み込みのみが可能でしたが、直接 s3, http(s) URL からも読み込めるようになりました。デプロイする環境に terraform.tfstate が存在していない場合、これまではわざわざ aws s3 cp などでローカルにコピーする必要がありました。

plugins:
  - name: tfstate
    config:
      url: s3://my-bucket/terraform.tfstate

tfstate-lookup コマンドも v0.1.0 で URL からの tfstate 読み込みをサポートしています。 https://github.com/fujiwara/tfstate-lookup/releases/tag/v0.1.0

【新機能】設定ファイルで required_version が使用可能に

特定のバージョン以降で追加された機能を要求する場合など、ecspresso のバージョンに制約を持たせたい場合に設定ファイルで使用できる機能です。

required_version: ">= v1.3.0, < v2"

このように semver で指定することで、条件を満たしていないバージョンの ecspresso で実行しようとするとエラーになります。hashicorp/go-version を使用しています。

この機能をサポートしていない v1.3.0 未満のバージョンでは無視されるため、v1.3.0 がリリースされた時点では指定する意味はありません。将来新しいバージョンがリリースされ、その時点で入った機能や修正に依存した処理があるためそれ未満のバージョンでは実行させたくないような場合に役に立つ予定です。

PR を送ってくださった mashiike さん (同僚) ありがとうございました!

【新機能】run コマンドでタスクにタグが付けられるように

ecspresso run --tags Foo=Bar で、実行するタスクに Foo という名前のタグが Bar という値で設定されます (複数指定可能)。

ecspresso run --propagate-tags=(SERVICE|TASK_DEFINITION) で、実行するタスクに付けるタグをサービス定義もしくはタスク定義から引き継ぐことができます。

実は現状では、ecspresso でタスク定義を登録する際にはタグを附与することができません。これは定義ファイルを SDKecs.TaskDefinitionマッピングしていて、この構造体にはタグの定義がないためです。(サポート方法を悩みつつ、具体的な需要が今のところないので何もしていません)

その他修正いろいろ

  • [Fix] Unable to update platformVersion and networkConfiguration for CodeDeploy services. #209
    • CodeDeploy でデプロイする際に、--update-service を付けていても platformVersion と networkConfiguration が変更できなかった問題を修正しています。
  • [Fix] diff command becomes to compare local task definition and remote task definition. #218
    • diff コマンドが比較するタスク定義のリモート(ECS)側が、これまで latest (一番リビジョンが大きい) のものになっていましたが、サービスが現在使用しているリビジョンのものと比較するようになりました。
  • [Fix] Support to Docker Registry V2 API. Enables to read any public image repository using V2 API. (public.ecr.aws, gcr.io, ghcr.io and etc.) #214 #213
    • ECR Public repo など、一般の公開 image を verify できなかった問題を修正しています。Docker Registry API V2 を利用している任意の公開リポジトリの image が存在検証できます。
    • この修正に伴って、verify に 必要な IAM 権限から ecr:ListImages が不要になりました。

ecspresso handbook も改訂予定

昨年末に公開した ecspresso handbook ですが、こちらも v1.3.0 の新機能へ追従して近日改訂予定です。ご購入の方はそのままお読みいただけます。まだの方は是非この機会にお求めください。

zenn.dev

ecspresso handbookをZennで公開しました

激動の2020年もいよいよ押し詰まってきましたが、皆様いかがお過ごしでしょうか。

今年は ecspresso advent calendar 2020 というひとりアドベントカレンダーを唐突に思いついて始めてしまったのですが、なんとか完走することができました。

拙作の OSS である Amazon ECS 用のデプロイツール ecspresso の基本的な使い方から応用編、コマンドリファレンス、設計思想までを一通り書ききることができたので、せっかくなので Zenn で本としてまとめることにしました。

zenn.dev

9万字弱とそこそこのボリュームになり、書くのも大変だったしなあということで、500円の有料本としています。

内容自体はアドベントカレンダーに書いたものを再編集(章立ての変更と校正など)したものなので、特に新しい情報が入っているわけではないですが、もしよろしかったらお買い求め頂けると大変ありがたいです。売れると開発にもやる気が出ます(重要)。冬休みのお供にいかがでしょうか!

ecspresso の今後ですが、CloudFormation との連携 (OutputとExportの論理名を指定して物理名を解決できる) の実装の目処が付いたので、年明けぐらいにリリースできるといいなと思っております。

それでは、皆様良いお年を (よろしければ ecspresso handbook と一緒に) お迎えください。

Amazon Lightsail Container を Mackerel で監視する

Mackerel Advent Calendar 2020 8日目の参加記事です。

2020年11月に、Amazon Lightsail Container というサービスがリリースされました。

dev.classmethod.jp

最低月額 $7 で、コンテナを気軽に(ロードバランサー込みで!) Amazon Lightsail 上にデプロイできるというものです。普通に AWS で Fargate のコンテナとロードバランサー(ALB)を動かすと最低でも$25/月程度はかかってしまうので、これはお手軽でよさそうですよね。

ということで、この Lightsail Container を Mackerel で監視してみましょう。

Lightsail Container の実体は?

なにはともあれ、Ligtsail Container に環境変数を出力する Web アプリケーションをデプロイして素性を探ってみます。環境変数JSON で出力する HTTP サーバを起動してくれる便利な phamviet/printenv という Public Image があるので、これを適当に Lightsail Container にデプロイしてアクセスしてみると、以下のようなレスポンスが得られました。

{
  "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI": "/v2/credentials/03aed4fb-11f7-4c84-967a-34389ff48412",
  "AWS_DEFAULT_REGION": "ap-northeast-1",
  "AWS_EXECUTION_ENV": "AWS_ECS_FARGATE",
  "AWS_REGION": "ap-northeast-1",
  "ECS_CONTAINER_METADATA_URI": "http://169.254.170.2/v3/3715f73c-a351-4df9-a923-02163d4df095",
  "ECS_CONTAINER_METADATA_URI_V4": "http://169.254.170.2/v4/3715f73c-a351-4df9-a923-02163d4df095",
  "HOME": "/root",
  "HOSTNAME": "ip-172-26-26-31.ap-northeast-1.compute.internal",
  "NODE_VERSION": "10.16.3",
  "PATH": "/usr/src/app/node_modules/.bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
  "PWD": "/usr/src/app",
  "SHLVL": "1",
  "YARN_VERSION": "1.17.3"
}

"AWS_EXECUTION_ENV": "AWS_ECS_FARGATE" なるほどですね。

Lightsail Container = Fargate を簡単に使えるようにいい感じにしてくれているなにか、のようです(あくまで推測ですよ?)

ということは普通に mackerel-container-agent で ECS / Fargate を監視するのと同じ方法でいけそうですよね。サイドカーを追加してみましょう。

mackerel-container-agent のサイドカーを追加する

公式のドキュメントをもとに、以下のような設定でコンテナを追加しました Amazon ECSにmackerel-container-agentをセットアップする

f:id:sfujiwara:20201205022844p:plain

  • image: mackerel/mackerel-container-agent:v0.5.0
  • environment
    • MACKEREL_APIKEY: *************
    • MACKEREL_CONTAINER_PLATFORM: ecs

デプロイしてしばらく待ってみると…

f:id:sfujiwara:20201205022918p:plain

普通に Fargate task として認識され、同様のメトリックが得られました。よかったですね。

[PR] デプロイツール sailtrim 開発中

現在、fujiwara/sailtrim という、Lightsail Container 用のデプロイツールを開発しています。

まだまだ荒削りですが、ECSのデプロイツール ecspresso や Lambda のデプロイツール lambroll と同じような使い勝手で Lightsail Container をコード管理してデプロイできるツールです。よろしくどうぞ。

ECS task definition を Jsonnet で生成する

Amazon ECS でタスクの構成定義に使う task definition をリポジトリで管理する場合、JSON 形式で取り扱うことが多いと思います。

ECS デプロイツールである ecspresso でも、task definition は環境変数等を展開できる記法があるものの、基本的には JSON ファイルとして扱っています。

これは AWS コンソールで作成した task definition を aws describe-task-definition や ecspresso init コマンドでファイルとして取り出して利用することを前提としているためで、これを YAML や別の形式で扱うことは考えていません。

ところで、実際に ECS でそれなりのサービスを運用すると、task definition JSON の管理で悩むことが増えてきました。

  • JSON は人間が編集するのに便利ではない
    • コメントが書けない、配列末尾の , の有無で余分な diff が発生、key をいちいち " " で括るのが面倒…
  • サイドカーなどがだいたい同じで、一部だけ異なる task definition を複数運用する場面がある
    • 複雑な構造の一部が異なるような場合、環境変数展開ではカバーできないので、コピペせざるを得ない
  • タスク内の各コンテナで共通で埋め込みたい要素がそこそこあるが、揃えて変更するコストが高い
    • environment 要素などが典型

例として、nginx にサイドカーとして mackerel-container-agent を追加した task definition が以下のようにあるとします。

{
  "containerDefinitions": [
    {
      "name": "nginx",
      "image": "nginx:latest",
      "environment": [
        {
          "name": "AWS_REGION",
          "value": "ap-northeast-1"
        },
        {
          "name": "TZ",
          "value": "Asia/Tokyo"
        }
      ],
      "essential": true,
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "ecspresso-test",
          "awslogs-region": "ap-northeast-1",
        }
      },
      "portMappings": [
        {
          "containerPort": 80,
          "hostPort": 80,
          "protocol": "tcp"
        }
      ]
    },
    {
      "name": "mackerel-container-agent",
      "image": "mackerel/mackerel-container-agent:v0.4.0",
      "environment": [
        {
          "name": "AWS_REGION",
          "value": "ap-northeast-1"
        },
        {
          "name": "TZ",
          "value": "Asia/Tokyo"
        },
        {
          "name": "MACKEREL_CONTAINER_PLATFORM",
          "value": "ecs"
        }
      ],
      "essential": true,
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "ecspresso-test",
          "awslogs-region": "ap-northeast-1",
        }
      },
      "secrets": [
        {
          "name": "MACKEREL_APIKEY",
          "valueFrom": "/MACKEREL_APIKEY"
        }
      ]
    }
  ],
  "cpu": "256",
  "executionRoleArn": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole",
  "family": "ecspresso-test",
  "memory": "512",
  "networkMode": "awsvpc",
  "requiresCompatibilities": [
    "EC2",
    "FARGATE"
  ],
  "taskRoleArn": "arn:aws:iam::123456789012:role/ecsTaskRole"
}

environment には AWS_REGION と TZ が共通で設定されています (mackerel-container-agent には追加で MACKEREL_CONTAINER_PLATFORM)。

共通で設定したい環境変数に変更がある場合、nginx と mackerel-container-agent の両方のコンテナ定義を変更する必要があります。この例ではタスク内に 2コンテナしかないですが、実際に稼働するタスクでは多数のサイドカーがあったりして、人手で漏れなく変更するのは大変ダルいことになります。

Jsonnet で JSON を生成する

JSON をテンプレートから生成する処理系として、Jsonnet というものがあります。これを使って task definition JSON を生成して、辛さが軽減できないか試してみました。

jsonnet.org

qiita.com

共通要素の括りだし

まず environment 要素の共通部分を抜き出してファイルにします。

[
    {
        "name": "AWS_REGION",
        "value": "ap-northeast-1"
    },
    {
        "name": "TZ",
        "value": "Asia/Tokyo"
    }
]

このファイルを envs.libsonnet として保存して、taskdef.json をコピーして taskdef.jsonnet を以下のように用意します。もとの taskdef.json をちょっといじるだけです。

  • local envs = import 'envs.libsonnet'; を追加
  • "environment": envs, として import した値を展開
# taskdef.jsonnet
local envs = import 'envs.libsonnet'; # envs テンプレートの読み込み
{
  "containerDefinitions": [
    {
      "name": "nginx",
      "image": "nginx:latest",
      "environment": envs, # 読み込んだ envs をここに展開
      "essential": true,
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "ecspresso-test",
          "awslogs-region": "ap-northeast-1",
        }
      },
      "portMappings": [
        {
          "containerPort": 80,
          "hostPort": 80,
          "protocol": "tcp"
        }
      ]
    },
    {
      "name": "mackerel-container-agent",
      "image": "mackerel/mackerel-container-agent:v0.4.0",
      "environment": envs + [  # envs 配列への追加
        {
          "name": "MACKEREL_CONTAINER_PLATFORM",
          "value": "ecs"
        }
      ],
      "essential": false,
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "ecspresso-test",
          "awslogs-region": "ap-northeast-1",
        }
      },
      "secrets": [
        {
          "name": "MACKEREL_APIKEY",
          "valueFrom": "/MACKEREL_APIKEY"
        }
      ]
    }
  ],
  "cpu": "256",
  "executionRoleArn": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole",
  "family": "ecspresso-test",
  "memory": "512",
  "networkMode": "awsvpc",
  "requiresCompatibilities": [
    "EC2",
    "FARGATE"
  ],
  "taskRoleArn": "arn:aws:iam::123456789012:role/ecsTaskRole"
}

これを jsonnet コマンドで処理すると、taskdef.json と同一のものが生成されます。

$ jsonnet taskdef.jsonnet | head
{
   "containerDefinitions": [
      {
...

$ # 差分確認
$ diff -w <(jsonnet taskdef.jsonnet | jq --sort-keys .) <(cat taskdef.json | jq --sort-keys .)

JSON より扱いやすい記法への変換

Jsonnet は JSON の superset なので JSON 記法そのままでも扱えますが、jsonnetfmt コマンドを通すと素の JSON よりも人間が扱いやすい Jsonnet の記法に変換してくれます。

$ jsonnetfmt envs.libsonnet
[
  {
    name: 'AWS_REGION',
    value: 'ap-northeast-1',
  },
  {
    name: 'TZ',
    value: 'Asia/Tokyo',
  },
]

key のクォートが不要だったり、配列やオブジェクト末尾の , が許可されたりと、すっきり書けるようになります。

コンテナ定義の共通化

タスク内に複数あるコンテナ定義をテンプレートで共通化してみます。各コンテナの共通要素を定義したファイルを用意して container.libsonnet というファイルに保存します。

# container.libsonnet
local envs = import 'envs.libsonnet';
{
  essential: true,
  environment: envs,
  logConfiguration: {
    logDriver: 'awslogs',
    options: {
      'awslogs-group': 'ecspresso-test',
      'awslogs-region': 'ap-northeast-1',
    },
  },
}

このコンテナ定義テンプレートを使って、nginx コンテナの定義を生成する jsonnet は次のようになります。 import したコンテナ定義テンプレートに、必要な要素を上書き(追加)するだけです。

# nginx.libsonnet
local container = import 'container.libsonnet';
container {
  name: 'nginx',
  image: 'nginx:latest',
  portMappings: [
    {
      containerPort: 80,
      hostPort: 80,
      protocol: 'tcp',
    },
  ],
}

同様に mackerel-container-agent のコンテナ定義も container 定義テンプレートから生成します。environment は配列要素への追加になるため、container.environment + [ ] という形で定義しています。

# mackerel.libsonnet
local container = import 'container.libsonnet';
container {
  name: 'mackerel-container-agent',
  image: 'mackerel/mackerel-container-agent:v0.4.0',
  environment: container.environment + [
    {
      name: 'MACKEREL_CONTAINER_PLATFORM',
      value: 'ecs',
    },
  ],
  secrets: [
    {
      name: 'MACKEREL_APIKEY',
      valueFrom: '/MACKEREL_APIKEY',
    },
  ],
}

この各コンテナ定義を import して、taskdef を生成する jsonnet は以下のようになりました。すっきり!

# taskdef.jsonnet
local mackerel = import 'mackerel.libsonnet';
local nginx = import 'nginx.libsonnet';
{
  containerDefinitions: [
    nginx,
    mackerel,
  ],
  cpu: '256',
  executionRoleArn: 'arn:aws:iam::123456789012:role/ecsTaskExecutionRole',
  family: 'ecspresso-test',
  memory: '512',
  networkMode: 'awsvpc',
  requiresCompatibilities: [
    'EC2',
    'FARGATE',
  ],
  taskRoleArn: 'arn:aws:iam::123456789012:role/ecsTaskRole',
}

この taskdef.jsonnet を jsonnet コマンドでレンダリングすると、もとの taskdef.json と同一内容の JSON が出力されます。

更に似たような task definition を増やしたい場合には taskdef の定義テンプレートを用意して共通項目を括りだし、それを基にそれぞれの JSON を生成してもいいでしょう。

まとめ

取り扱いが煩雑になりがちな JSON による ECS task definition を Jsonnet というテンプレート言語で生成してみました。

Jsonnet のサイトを見るとあまりにいろいろな機能があってびっくりしますが、まずは既存 JSON の共通部分を括り出す程度のテンプレートとして最小限で使うのをお勧めします。

Jsonnet は独自関数定義なども使えるので、やり過ぎると読めなくなる可能性はありそうです。が、生成されるものは plain な JSON なので、最悪読めなくなっても生成後の JSON からリファクタリングをやり直すことは可能です。

デプロイに ecspresso を利用する場合は、デプロイ前に ecspresso diff コマンドを使用すると、現在稼働中の task との差分があるかを検出できます。Jsonnet でリファクタリングした結果意図せず壊していないかの検知に便利だと思います。

ecspresso v1 をリリースします(v0.99.x をリリースしているのでお試しください)

おかげさまで各所でご利用頂いている(模様の) Amazon ECS デプロイツール ecspresso ですが、リリースしてからもうすぐ3年になりますし、ここで一部非互換の修正を入れた v1 を出そうとしています。

非互換な点は少ないのでほとんどの方にはそのまま使って頂けると思いますが、下記を確認した上で v0.99.x を試して頂いて、もし何か問題があればフィードバックを頂けると幸いです。

(2020-10-30 追記) v1.0.0 をリリース済です。

github.com

(2020-09-25 23:00 追記) Homebrew で 0.99.x がインストールできるようになりました。 tap ecspresso@0.99 by mashiike · Pull Request #9 · kayac/homebrew-tap · GitHub

$ brew install kayac/tap/ecspresso@0.99

既に v0.x がインストールされている場合は conflict するので、一旦 unlink して link し直してください。

$ brew unlink ecspresso
$ brew link ecspresso@0.99

v0.x に戻したい場合は逆操作です。

$ brew unlink ecspresso@0.99
$ brew link ecspresso

v1 リリース後は @0.99 は削除されるので、単に brew upgrade ecspresso で v1 へ更新されるようになる予定です。 (追記ここまで)

v1 で非互換になる点

deploy --update-service (true) がデフォルトになります

従来 、service 定義ファイルを更新した場合にそれを ECS service に反映するには、ecspresso deploy --update-service とオプションを指定する必要がありました。

v1 では --update-service はデフォルトで true になります。v0.x と同様、service 定義に変更があっても deploy 時に適用したくない場合、deploy --no-update-service として明示する必要があります。

次に説明する、service 定義の desiredCount が反映されるようになる件との組み合わせで、意図しない挙動になる場合がありますのでご注意ください。

service 定義に記述された desiredCount を解釈するようになります

Does not `desiredCount` field work? · Issue #131 · kayac/ecspresso · GitHub

これまでは service 定義に desiredCount 属性が記述されていても無視されていました。v1 では service 定義に desiredCount 属性が存在して null ではない数値が記載されている場合に限り、deploy 時にその値を使用するようになります。deploy --tasks N が指定されていればそちらが優先されます。

ecspress deploy 実行時に実際に ECS task に指定される desiredCount は、以下の優先順位で決定されます。

  1. --tasks で指定された値
  2. service 定義に desiredCount で定義されている値
  3. (現在の service の desiredCount と変更なし)

これまで通りの挙動を期待する場合は、service 定義に desiredCount が指定されていないことを確認してください。

service / task 定義ファイルの path を config ファイルからの相対で解釈するようになります

feature request: resolve definition files path relatively from config.yml · Issue #140 · kayac/ecspresso · GitHub

例えば現在 config.yaml に以下のように記述されていている場合、

service_definition: ecs-service-def.json
task_definition: ecs-task-def.json

ecs-(service|task)-def.json は ecspresso 実行時のカレントディレクトリに存在している必要があります。

v1 では config ファイルからの相対で解釈するようになるため、

$ ecspresso --config path/to/config.yml

として実行した場合は ecs-(service|task)-def.json は ecspresso 実行時のカレントディレクトリではなく、config.yml と同一ディレクトリ (path/to) に存在しているものとして実行します。


以上、よろしくお願いします!