コンテナイメージのタグをよしなに探すツールを作りました

SRE チームの市川恭佑です。

今回はコンテナに関連するDevOpsツールを作った話です。ecresolve と書いて「イーシーリゾルブ」と読みます*1

github.com

ecresolve は、ざっくり言うとコンテナイメージのタグが上書きされる前提で、複数タグを指定すると一番先頭で見つかったものを出力するツールです。 基本的な使い方としては、たとえば dogcat のタグのみが存在する ECR リポジトリ animal があるとき、以下のコマンドを実行すると標準出力に dog が出力されます*2

ecresolve monkey bird dog human cat --repository-name=animal --format=tag-only
# dog

なお、本記事の大前提として、もちろん本番環境において、特にお客様のデータを取り扱うようなコンポーネントについては、コンテナイメージの「上書き前提のmutableな運用」はおすすめしません。

しかし、検証環境*3にあるジョブやミドルウェアをはじめとして、「最新版を指し示すタグ( latest など)を決め打ちで指定すると面倒ごとが少ない!」という場面もそれなりにあると思います。 こういったコンテナですが、たま〜にイジりたくなること、ありません?そのときに非互換な変更があると、動作確認で厄介なことになります。

ケーススタディー: ジョブの開発

以下では、いったん検証環境に「検証環境を更新するためのジョブ」を動かしているコンテナがあると仮定して話を進めます。

【前提】このジョブは、Amazon ECS 上で日常的に稼働していて、CI(GitHub Actions)からキックされています。 ソースコードは標準的なOLTPアプリケーションと同じリポジトリに置いてあります。

【運用】ジョブの内容変更は頻繁ではありませんが、依存ライブラリのアップデートもあるので、 main ブランチが更新されるたびにコンテナイメージがビルドされます。 そのとき、タグは latest を毎度更新する形で push されます。 ジョブは main 以外のブランチでも実行されますが、ビルドにはそれなりに時間もかかるし、レジストリの容量が爆発するのも嫌なので latest を参照してもらっています。

【拡張性】ビルドのスクリプトや ecspresso の設定ファイル*4では、一応 REVISION という環境変数でタグを上書きできるようにしています。 なお、補足として JOB_REPO_URL は 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/job といった形式で、Amazon ECR リポジトリのURLを指します。

# コンテナのビルド・push(他のフラグは省略)
docker buildx build \
  --tag "${JOB_REPO_URL}:${REVISION:latest}" \
  --push \
  .

以下はタスク定義の一部です。

{
    ..., // 略
    "containerDefinitions": [{
        "name": "job",
        "image": "{{ must_env `JOB_REPO_URL` }}:{{ env `REVISION` `latest` }}",
        ... // 略
    }]
}

変更作業の動作確認がしたいだけなのに・・・

さて、この状況で、ジョブに非互換な変更を加えて動作確認をしたいとき、どうしたら良いでしょうか?

(ジョブの変更作業は feature/enhance-job ブランチで行なっているものとします。)

特に何も手を加えなければ、以下の図のように feature/enhance-job で発火されるジョブも latest を参照します。

パワー系ソリューションとしては、 feature/enhance-job ブランチでコンテナのビルド・push を行なって latest を更新することも考えられますが、もし動作確認やレビューに時間が掛かった場合、他のブランチで実行されるジョブが正常に動作しなくなります。

流石にそれでは困るので、「ビルド・push」および「ジョブ発火」の両方について、スクリプトないしワークフローにパラメータをねじ込んで挙動を変更する方針にしましょう。

ここから先は複数ファイルに跨ってコードを確認する必要がありますが、その結果 「あれ、、、実際に REVISION で上書きしようと思うと、、、可能だけど若干しっくりこないぞ・・・?」 という気持ちになるかもしれません。

より具体的には、feature/enhance-job ブランチで CI の YAML ファイルに REVISION 指定を記述して、マージ前に消す方針を取ればワークアラウンドとしては機能します。 もしくは、より根本的にジョブを発火する根本的なコンポーネントまで手を加えることでマニュアル感を減らすことも可能でしょう。しかし、それがパラメータのバケツリレーを増やして認知負荷に影響する可能性は否めません。 これが冒頭で紹介した厄介な問題です。

嗚呼・・・ただ単に、変更作業の動作確認がしたいだけなのに・・・。

手軽に可変タグを運用する方法

先ほどの話はあくまでケーススタディなので、ご自身のプロジェクトにピッタリと当てはまる事はほとんど無いでしょう。

しかし、一般論として、コンテナイメージの運用は掘り下げるとキリがないのも事実です。 可変性以外に、ロールバックや自動削除の問題もあれば、同じリビジョンのイメージでもパラメータによって挙動が異なることさえあります。 本番環境におけるコンテナイメージ運用の深掘りは信頼性の追求に繋がりますが、検証環境においては必ずしもそのメリットが大きいとは限りません。

むしろ筆者は、検証環境のうち特に恒常的かつ柔軟に使うもの(prd,stg,dev構成であればdev)の運用は、無理に本番環境に寄せるべきでないと考えています。 そのような環境では開発者*5が直感的に運用できることが最優先事項です。 それにも拘らず本番環境に寄せるための無理な共通化を施して、逆に本番環境の運用が複雑になってしまったら本末転倒です。

結論: ecresolve でサクッと探索

ということで、検証環境を想定して、可変タグをgitブランチに対応させて手軽に運用する方法を提案します。 実際にはブランチ以外の管理単位を設定しても良いのですが......そもそもブランチ自体が本質的には可変なリビジョン情報なので、素直に対応させるのも悪くないと思います。

コンセプトとしては以下の図のとおりです。ブランチ名の / はタグ名として使えないので s/\//___/g で加工するものとします。また、先程のケーススタディから main ブランチが更新されるたびにコンテナイメージがビルドされる」という状況は変えないものとします。

とはいえ「RunTask APIを実行時に現在のブランチ名に対応したタグを参照 → CannotPullContainerErrorだったらmainに向ける」スタイルだとエラー時のフィードバックが遅すぎます。 そこで、 ecresolve で事前に Amazon ECR にアクセスして存在するタグを突き止めておきます。

CI に仕込んでおくコードとしては、ざっとこんな調子です。こうすると、ECRリポジトリ job から現在のブランチに対応するタグを探し、存在しなければ main を参照させることができます。

serialized_branch=$(git rev-parse --abbrev-ref HEAD | sed 's/\//___/g')
image_tag=$(ecresolve "$serialized_branch" main \
    --repository-name=job --format=tag-only)
export REVISION=$image_tag

なお、--format フラグのデフォルトは json です。その場合はタグ名ではなくECR APIと互換なJSONが出力されます。 詳しくは README をご参照ください。

おまけ

ecspresso をお使いの場合、v2.5で追加予定の External Plugins を用いることで、ecresolve をタスク定義のテンプレートに組み込むこともできます。

その場合の例として、タスク定義を以下のように記述できますが、

{
    ..., // 略
    "containerDefinitions": [{
        "name": "job",
        "image": "{{ must_env `JOB_REPO_URL` }}:{{ ecresolve-job `$SERIALIZED_BRANCH` `main` }}",
        ... // 略
    }]
}

ecsoresso.yml の記述に関して、少々知識が必要になってくるので、好みの分かれるところかと思います*6

plugins:
  - name: external
    config:
      name: ecresolve-job
      command: ["ecresolve", "--format=tag-only", "--repository-name", "job"]
      parser: string # デフォルト(json)の場合は↑の--formatを削った上で {{ (ecresolve_job `$SERIALIZED_BRANCH` `main`).imageId.imageTag }} でもアクセス可能
      num_args: 2
      timeout: 5

それでは、よいコンテナライフをお過ごしください! 👋

カヤックでは、コンテナと程よく付き合いたいエンジニアも募集しています!

hubspot.kayac.com

*1:ユースケースは Amazon ECR に限られていますが、超絶簡単なソフトウェアなので、他のコンテナレジストリをご利用の方は同じようなものを適宜つくってみても良いかもしれません。

*2:なぜ cat ではないかと言うと、引数に指定した複数のタグのうち「最初に見つかったイメージ」の情報が出力されるからです。

*3:語弊を防ぐため、本記事では「本番以外の環境」のことをひっくるめて"検証環境"と呼ぶことにします。

*4:ecspresso使っていないよ、という方は他のツールにおけるタスク定義の扱いに置き換えて想像してください。

*5:機能開発エンジニアもそうですし、QAエンジニアや企画チームの方々も含まれます

*6:commandをbash -cにするなど、より汎用的なPlugin設計も可能ですが、濫用のリスクと照らし合わせてご検討ください。