📑

GitHub Actionsにおける一部環境変数の特殊な可視性について

2023/11/14に公開

これはなに?

GitHub Actions(以下、GHA)において、ACTIONS_ から始まる環境変数はGHAの実行中に見えるかどうかが実行される文脈に依存して変化します。
この記事では、この挙動が引き起こす問題として「コンテナイメージビルド時のレイヤキャッシュが効かない」という挙動を紹介しつつ、その原因である環境変数の特殊な挙動とどのようにすればそれが解決するかを紹介します。

3行まとめ

  • GHAのrunステップでbuildkitのキャッシュのtypeに gha を選択している場合、通常ではキャッシュは利用できない
  • 理由は ACTIONS_ から始まる一部の環境変数はrunステップやcompositeアクションには渡されないため
  • これらの環境変数をnodeアクションなどの中で exportVariable で以降のステップで見えるようにすることでキャッシュが利用できない問題は解決できる(crazy-max/ghaction-github-runtimeはそれをやってくれるaction)

話の発端

https://twitter.com/okazu_dm/status/1722289778555843056

これは、状況的にはGHAのjobの中で以下のようなrunステップを定義していたところ、何度実行してもレイヤキャッシュが効いている様子がなかったという状況でした。

run: |
  docker buildx build -f foo/Dockerfile --cache-to=type=gha,mode=max --cache-from=type=gha .

教えてもらった情報をもとに以下のactionを挟んだところ、キャッシュを使うようになりました。
https://github.com/crazy-max/ghaction-github-runtime

また、公式のドキュメントもよく読むと docker buildx built コマンドを自分で叩くようなステップを書く場合は、このactionを使うように書いてありました。
https://docs.docker.com/build/cache/backends/gha/#authentication

crazy-max/ghaction-github-runtimeの中身

https://github.com/crazy-max/ghaction-github-runtime/blob/f7444d9798e4128a4c1335946ea66a4914b395fe/src/github.ts#L4-L9
単純に、ACTIONS_ から始まる以下のような環境変数をワークフローコマンドの exportVariable で環境変数として参照できるようにしているだけでした(exportVariableについてのドキュメント)
この部分だけを見ると、環境変数を同じ名前の環境変数として参照できるようにしているだけなので意味はないように見えます。
ちなみに、このactionを呼び出すと以下のように5つの環境変数がこのactionに渡ってきている事がわかります。

# 公式のCIの実行結果からの引用
ACTIONS_RUNNER_ACTION_ARCHIVE_CACHE=/opt/actionarchivecache
ACTIONS_RUNTIME_URL=https://pipelinesghubeus2.actions.githubusercontent.com/FBdaULJE3PtkjabDRQ4VS7kFhAp9CwewmBkNZE4fazocqWLF10/
ACTIONS_RUNTIME_TOKEN=***
ACTIONS_CACHE_URL=https://acghubeus2.actions.githubusercontent.com/FBdaULJE3PtkjabDRQ4VS7kFhAp9CwewmBkNZE4fazocqWLF10/
ACTIONS_RESULTS_URL=https://actions-results-receiver-production.githubapp.com

しかし、実際にはこうしないと以降のrunステップからはこれらの環境変数は見えないという問題への対応となっています。

試しに同じことをrunステップでやってみる

では、同じことを自分でやっても同じ効果が得られるだろうと思い、試しに以下のようなrunステップを定義してみます

      # とりあえず渡ってきている環境変数を表示するところまで 
      - name: Display env with node
        run: |
          node -e "
            Object.keys(process.env).forEach(key => {
              if (key.startsWith('ACTIONS_')) {
                console.log(\`\${key}: \${process.env[key]}\`)
              }
            })
          "

すると、以下のように一つだけしか環境変数が渡ってきていないことがわかります。

ACTIONS_RUNNER_ACTION_ARCHIVE_CACHE=/opt/actionarchivecache

# 比較用: ghaction-github-runtimeのCIでは以下のように5つ返ってくる
# ACTIONS_RUNNER_ACTION_ARCHIVE_CACHE=/opt/actionarchivecache
# ACTIONS_RUNTIME_URL=https://pipelinesghubeus2.actions.githubusercontent.com/FBdaULJE3PtkjabDRQ4VS7kFhAp9CwewmBkNZE4fazocqWLF10/
# ACTIONS_RUNTIME_TOKEN=***
# ACTIONS_CACHE_URL=https://acghubeus2.actions.githubusercontent.com/FBdaULJE3PtkjabDRQ4VS7kFhAp9CwewmBkNZE4fazocqWLF10/
# ACTIONS_RESULTS_URL=https://actions-results-receiver-production.githubapp.com

前述のとおり、ghaction-github-runtimeには5つの環境変数が渡ってきているため、これでは不十分であると考えられます。
この時点で立てた予測は「runステップでは見えないが、action中では見える環境変数があるのではないか」というものでした(結果的にこの予測は概ね正しかったが、もう少しトリッキーだった)
この予測を確かめるために、もう少し細かく実験をしていきます。

与えられる環境変数の状況別整理

前述したように、ACTIONS_ から始まる環境変数の渡ってくる数としては、ghaction-github-runtimeだと5つ、runステップだとほぼ同じ内容のスクリプトを実行した場合でも1つでした。これは単純にactionかどうかの違いなのではないかという予測を立てましたが、その予測を確かめるために実際に自分でactionを定義し、実行しました。

GHAのアクションの種類について

GHAにおけるアクションの種類について簡単に触れておきます。
公式ドキュメント に書いてあるように、GHAには3つのアクションがあります。設定ファイル中の記述に寄せて、本文中では以下のように分類します。

  • dockerアクション: Dockerfileに基づいてコンテナをビルドして実行する
  • nodeアクション: JavaScriptファイルを実行する
  • compositeアクション: 指定されたシェル上でコマンドを実行する

検証内容と結果

最終的に3つのアクションの種類と、それぞれの呼び出し方(ディレクトリとして参照するか、他のリポジトリを参照する際の指定方法で呼び出すか)ごとに渡ってきている環境変数を表示しました(実行ログ)

  • composite: sort | env | grep -e '^ACTIONS_' をして環境変数が1つだけ表示された
  • docker: sort | env | grep -e '^ACTIONS_' をして環境変数が4つだけ表示された
  • node: このスクリプト を実行した結果、環境変数が5つ表示された

最終的な結論を出すまでに、渡ってくる環境変数の違いの原因として思いついた仮説は以下のようなものがありました。

  • 参照方法による違い => これは実際に試してみて差がないことを確認済み
  • Marketplaceにリスティングされているか否かか: これも試してみたが、やはり差はなかった
  • Github Actions Toolkitを使っているか: これを使ったスクリプトをrunステップで試しに実行してみても、ACTIONS_から始まる環境変数は一つしか見えなかったためやはり違うことがわかった

などいくつかの説を考えましたが、actionの種類によって渡ってくる環境変数の数が違う、というのが挙動から確認できる結論でした。

実際にキャッシュの挙動が変化するかの確認

では、当初の目的であった「コンテナイメージのビルド時にキャッシュが使えるか」という点を改めて確認してみます。
nodeアクションを使うとghaction-github-runtimeと同じになるため、今回はdockerアクションで以下のように、同様の処理を実装しました。
https://github.com/okazu-dm/gha-envvar-visibility-sample/blob/670b28222e3dd62594ba746d5fe2d971385f4ece/export-env-docker/entrypoint.sh

まず、今回問題となったケースと同じように、何もせずにrunステップでキャッシュのオプションだけ与えた方のジョブは以下のように、通常のビルドを実行し、これはリトライしても同様でした。(job定義)

しかし、環境変数をセットするactionを挟んだ方のジョブは二度目の実行以降は以下のようにキャッシュが使われるようになりました。(job定義)

まとめと感想

今回の挙動の違いは単純にgithub actions側の実装上の違いに起因するものと考えられますが、そもそもこの違いはどうして生まれているのか、というのは挙動の違いからは読み取れず、結論としては謎は残ったままとなりました。
また、buildkit側のエラーメッセージとして情報が出ていないか、試しにGHAのデバッグログを有効化して確認してみましたが、特に関連する情報は出ておらず、このあたりはどのような扱いになっているのかというのもやや気になるポイントではあります(そもそも一部のパラメータが渡ってきていないからキャッシュ周りの処理が発火していないのか、キャッシュの取得/保存に失敗してもwarnとしないのか)

その他

  • そういえ環境変数を入力として渡すような処理は何も書いてないけど、自動でgithubの環境変数読むのかな?と思って調べてみたが、どうも読みそう

https://github.com/moby/buildkit/blob/a9a5aaf23f8ad77b1a461f4aec7edce19bf4a4c3/cmd/buildctl/build/util.go#L17-L34

Discussion