ecspressoのcfn_outputで参照先スタック名を環境変数で切り替える

ecspressoのcfn_outputで参照先スタック名を環境変数で切り替える

2026.04.16

ecspressoのタスク定義やサービス定義でCloudFormationの出力値を参照するときは、cfn_outputテンプレート関数を使います。ただ、参照元のスタック名は環境ごとに変えたいケースがよくあります。

環境変数で切り替える方法を試してみました。Go text/templateprintfmust_envを組み合わせると、envファイルを肥大化させずにスタック参照を環境別に切り替えられました。

なお、サンプルのecspresso設定は以下の記事で作成したものをベースにしています。

https://dev.classmethod.jp/articles/cdk-ecs-fargate-ecspresso-migration-removal-policy-retain/

前提

  • ecspresso v2
  • CloudFormation plugin を有効化
  • 環境ごとにスタック名へサフィックスが付く構成(例: EcspressoDemoInfraStack-devEcspressoDemoInfraStack-prod

ecspresso.ymlでCloudFormation pluginを有効にしておきます。

ecspresso.yml
region: ap-northeast-1
cluster: ecspresso-demo-cluster
service: ecspresso-demo-service
service_definition: ecs-service-def.json
task_definition: ecs-task-def.json
timeout: "10m0s"
plugins:
  - name: cloudformation

やりたいこと

cfn_outputのスタック名をハードコードしていると、環境を切り替えるときにファイルを分けたりsedで書き換えたりする必要があります。

ecs-task-def.json
"awslogs-group": "{{ cfn_output `EcspressoDemoInfraStack-dev` `LogGroupName` }}"

スタック名は環境変数から取りたいところです。ecspressoで環境変数を埋め込むときは{{ must_env STACK_NAME }}と書きますが、これをcfn_outputの第1引数にそのまま差し込もうとすると、{{ }}をネストする書き方になってしまいます。

// これは動かない({{ }} のネストは Go text/template で許されない)
"awslogs-group": "{{ cfn_output {{ must_env `STACK_NAME` }} `LogGroupName` }}"
$ ecspresso --envfile .env.dev render task-definition
# エラーメッセージ
2026-04-16T16:40:28.446+09:00 [ERROR] FAILED. failed to load task definition ecs-task-def.json: config parse by template failed: template: conf:11: unexpected "{" in operand

Go text/templateでは{{ }}のネストが構文エラーになります。代わりに「1つの{{ }}の中で、関数呼び出しの結果を別関数の引数に渡す」書き方が必要です。

cfn_outputの引数に関数を渡す

Go text/templateでは、丸括弧 () で関数呼び出しを別の関数の引数に渡せます。{{ }} のネスト(二重中括弧)は使えませんが、() による関数合成は標準の構文です。

ecspressoにはmust_envprintfといった関数が組み込まれているので、これらをcfn_outputの引数に渡します。

must_envだけで渡す場合

スタック名そのものを環境変数に入れる書き方です。

ecs-task-def.json
"awslogs-group": "{{ cfn_output (must_env `STACK_NAME`) `LogGroupName` }}"

実行時はSTACK_NAME=EcspressoDemoInfraStack-devのように設定します。

STACK_NAME=EcspressoDemoInfraStack-dev ecspresso diff

シンプルですが、参照するスタックが複数ある場合は環境変数も複数必要になります。envファイルにスタック名が並んでいくと管理が煩雑です。

printfで組み立てる場合

スタック名の命名規則が <ベース名>-<環境> のように規則的であれば、printfでサフィックスを組み立てたほうが楽です。

ecs-task-def.json
"awslogs-group": "{{ cfn_output (printf `EcspressoDemoInfraStack-%s` (must_env `ENV`)) `LogGroupName` }}"

ENV=devを1つ設定するだけで、すべてのcfn_outputで使い回せます。

ENV=dev ecspresso diff

printfはGo text/templateの組み込み関数で、fmt.Sprintf相当の書式指定ができます。

タスク定義全体を書き換えると、たとえば以下のようになります。

ecs-task-def.json
 {
   "containerDefinitions": [
     {
       "logConfiguration": {
         "logDriver": "awslogs",
         "options": {
-          "awslogs-group": "{{ cfn_output `EcspressoDemoInfraStack` `LogGroupName` }}",
+          "awslogs-group": "{{ cfn_output (printf `EcspressoDemoInfraStack-%s` (must_env `ENV`)) `LogGroupName` }}",
           "awslogs-region": "ap-northeast-1",
           "awslogs-stream-prefix": "nginx"
         }
       }
     }
   ],
-  "executionRoleArn": "{{ cfn_output `EcspressoDemoInfraStack` `TaskExecutionRoleArn` }}",
+  "executionRoleArn": "{{ cfn_output (printf `EcspressoDemoInfraStack-%s` (must_env `ENV`)) `TaskExecutionRoleArn` }}",
-  "taskRoleArn": "{{ cfn_output `EcspressoDemoInfraStack` `TaskRoleArn` }}"
+  "taskRoleArn": "{{ cfn_output (printf `EcspressoDemoInfraStack-%s` (must_env `ENV`)) `TaskRoleArn` }}"
 }

printfの第1引数に書式文字列、第2引数以降に埋め込む値を渡します。書式を変えれば<環境>-<ベース名>のような逆順のパターンにも対応できます。

動作確認

EcspressoDemoInfraStack-devがデプロイ済みの状態で試します。

デプロイ前にテンプレート置換の結果だけ見たい場合は、ecspresso renderが使えます。タスク定義やサービス定義をレンダリングして標準出力に書き出すサブコマンドです。cfn_outputの解決はCloudFormationのDescribeStacks APIを呼ぶため、AWS認証情報と参照先スタックの存在が必要です。

ENV=dev ecspresso render task-definition

出力を抜粋すると、cfn_outputの箇所が実際のARNに置き換わっています。

"executionRoleArn": "arn:aws:iam::123456789012:role/EcspressoDemoInfraStack-d-TaskExecutionRole-XXXXXXXX",
"taskRoleArn": "arn:aws:iam::123456789012:role/EcspressoDemoInfraStack-dev-TaskRole-XXXXXXXX"

ECSサービスとの差分を確認するならecspresso diffです。

ENV=dev ecspresso diff

ENV=prodに変えて同じコマンドを叩くと、今度はEcspressoDemoInfraStack-prodの出力値で解決されます。

ENVを未設定のまま実行すると、must_envがpanicで強制停止します。

envとmust_envの使い分け

ecspressoには似た関数としてenvもあります。違いはデフォルト値の有無です。

  • env: 第2引数にデフォルト値を取る。環境変数が未設定ならデフォルト値が使われる
  • must_env: デフォルト値なし。未設定ならpanicで停止する

今回のように環境(dev / prod)の識別子として使う値は、うっかり設定を忘れたままデプロイが進むと事故になります。デフォルト値を用意せず、must_envで明示的な設定を強制するのが安全です。

一方、欠けてもデフォルトで問題ない値にはenvが向いています。イメージタグをlatestに固定しておきたい場面などです。

ecs-task-def.json
"image": "public.ecr.aws/nginx/nginx:{{ env `IMAGE_TAG` `latest` }}"

値が抜けるのが致命的かどうかで選ぶ、というのが基本方針です。

envfileで環境変数をまとめて渡す

ENVだけならENV=dev ecspresso diffのようにシェルで直接渡せますが、IMAGE_TAGなど他の値も一緒に渡したくなったら--envfileオプションでまとめて読み込めます。dotenv形式(KEY=VALUE)のファイルを指定します。

.env.dev
ENV=dev
IMAGE_TAG=1.26
ecspresso --envfile .env.dev diff

--envfileは複数指定でき、後から渡したファイルで同名キーが上書きされます。共通設定と環境別設定を分けて書く使い方もできます。

ecspresso --envfile .env.common --envfile .env.dev diff

ecspresso内部では go-envparse でパースしos.Setenvで値をセットするので、シェルで事前にexportしてあった環境変数もenvfileの値で上書きされます。「envfile は常に勝つ」と覚えておくと混乱しません。

ファイル名の付け方

.env系のファイル名は.gitignoreのデフォルトパターンに含まれているリポジトリが多いので、envfileをgit管理したい場合(秘匿情報を含めない前提)はenvrc.devのような命名で.gitignoreルールとの衝突を避けられます。

envrc.dev
export ENV=dev
export IMAGE_TAG=1.26

go-envparseはexportプレフィックスを許容するので、source envrc.devでシェルに読み込ませたいケースにも両対応できます。命名や書式は運用に合わせて選べます。

他の方法との比較

環境差分が「スタック名のサフィックスだけ」なら、ここまでのprintfで組み立てる方法が一番シンプルです。設定値の差分が増えてきたら、以下のような構成も選択肢になります。

  • config を環境別に分ける: ecspresso-dev.yml / ecspresso-prod.yml のようにconfig自体を分割する。タスク定義やサービス定義の構造そのものに環境差があるときに向いています
  • タスク定義・サービス定義まで環境別に分ける: ディレクトリごとecspresso/dev/, ecspresso/prod/のように分離する。環境ごとに全く違う構成になる場合の選択肢です

おわりに

printfmust_envの組み合わせは、Go text/templateの機能をそのまま使っているだけなので、ecspresso以外のtext/templateベースの設定ファイルでも応用できそうです。

この記事をシェアする

関連記事