2023/10/24

Github Actionsで動的なVariablesを実現する

Variables

Github ActionsにはWorkflow内で環境変数をいい感じに扱う方法としてVairablesがあります。 ドキュメントにある通り1つのWorkflowに対する環境変数の扱い方は以下のようになります。 1つのWorkflow内で静的な値を参照したい場合は上記のようにWorkflow内にベタ書きで必要十分と思っています。

example.yaml
name: Test

on:
pull_request:

env:
ENV_NAME: production

jobs:
test:
runs-on: ubuntu-latest
steps:
- run: echo ${{ env.ENV_NAME }}
example.yaml
name: Test

on:
pull_request:

env:
ENV_NAME: production

jobs:
test:
runs-on: ubuntu-latest
steps:
- run: echo ${{ env.ENV_NAME }}

ただ複数のWorkflow内で環境変数を共有したい場合はVariableを積極的に使うことになると思います。 このとき下記の3つの選択肢があります。

  1. organization
  2. repository
  3. environment

この場合は組織、リポジトリ、環境のレベルでそれぞれ値を共有することができて、またリポジトリ内で同じkeyを上書きすることことができます。 つまりenviroment > repository > organizationの優先順位です。

この前提の元に複数のWorkflow内で値を共有する方法です。 よりDRYにWorklfowを記述する方法について軽くまとめています。

静的な値を複数のWorkflowで参照したい場合

この場合はorganizationもしくはrepositoryレベルを使うのが良いでしょう。 Settings > Secrets and variables > Variablesから環境変数は登録が可能です。 この場合変数はvars contextからkey名で参照可能です。

example.yaml
name: Test

on:
pull_request:

jobs:
test:
runs-on: ubuntu-latest
steps:
- run: echo ${{ vars.ENV_NAME }}
example.yaml
name: Test

on:
pull_request:

jobs:
test:
runs-on: ubuntu-latest
steps:
- run: echo ${{ vars.ENV_NAME }}

環境ごとに静的な値を複数のWorkflowで参照したい場合

この場合はenvironmentレベルを使うのが良いでしょう. environmentはドキュメントを読む限りにおいてはWorkflowを動かすことができるるbranch, tagを制御したり,Workflowの実行に承認を入れるなど保護ルールを適用するための仕組みです. つまりアプリケーションのデプロイなど保護ルールをどうしても適用したいユースケースが想定された機能です.

Environments are used to describe a general deployment target like production, staging, or development. ... You can configure environments with protection rules and secrets. When a workflow job references an environment, the job won't start until all of the environment's protection rules pass. A job also cannot access secrets that are defined in an environment until all the deployment protection rules pass.

環境ごとに複数のWorkflow内で一意の値を参照する一例としては,production環境とdevelopmentな環境が異なるAWSのアカウントで運用されているとすると下記のようなWorkflowがありえるでしょう. このような場合はsettings > EnvironmentsからEnvironmentを作成し,そこで環境変数が設定できます.

example.yaml
name: Test

on:
pull_request:

jobs:
test:
runs-on: ubuntu-latest
environment: production
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ vars.AWS_ACCOUNT_ID }}:role/my-github-actions-role
aws-region: ${{ vars.AWS_REGION }}
example.yaml
name: Test

on:
pull_request:

jobs:
test:
runs-on: ubuntu-latest
environment: production
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ vars.AWS_ACCOUNT_ID }}:role/my-github-actions-role
aws-region: ${{ vars.AWS_REGION }}

特定の値を元に静的な値を参照したい場合

例えばgit tagがpushされたことをトリガーとしたWorkflowを作るとして,このときtagがv1.0.0-dev.1のようなpre-release versionやv1.0.0のようにsufixなしでpushされたらどうでしょう. 素直にWorkflowを作るのであればこれもenvironmentレベルを使うことになります.

ただし問題はenvironmentレベルはデプロイを主なユースケースにしていることです. つまりデプロイは保護ルールを適用したいが,アプリケーションのビルドには適用したくない場合などには保護ルールが邪魔くさく感じることになるでしょう.

このようなとき素直に書くならば下記のようになるのかもしれません.

example.yaml
name: Test

on:
push:
tags:
- v*

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: aws-actions/configure-aws-credentials@v4
if: ${{ contains(github.ref, 'dev') }}
with:
role-to-assume: arn:aws:iam::${{ env.DEV_ACCOUNT_ID }}:role/my-github-actions-role
aws-region: ${{ env.DEV_REGION }}
- uses: aws-actions/configure-aws-credentials@v4
if: ${{ !contains(github.ref, 'rc') }}
with:
role-to-assume: arn:aws:iam::${{ env.ACCOUNT_ID }}:role/my-github-actions-role
aws-region: ${{ env.REGION }}
example.yaml
name: Test

on:
push:
tags:
- v*

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: aws-actions/configure-aws-credentials@v4
if: ${{ contains(github.ref, 'dev') }}
with:
role-to-assume: arn:aws:iam::${{ env.DEV_ACCOUNT_ID }}:role/my-github-actions-role
aws-region: ${{ env.DEV_REGION }}
- uses: aws-actions/configure-aws-credentials@v4
if: ${{ !contains(github.ref, 'rc') }}
with:
role-to-assume: arn:aws:iam::${{ env.ACCOUNT_ID }}:role/my-github-actions-role
aws-region: ${{ env.REGION }}

上記はtagがpushされたことをトリガーとして保護ルールが不要なWorkflowを動かすことを想定しています.tagがpre-release versionならばdevelopmentアカウントに,安定版であるならばprodcutionに対してWorkflowを動かすことを想定しています. このようにifで制御しかつ環境変数はrepositoryレベルに保持しておけば目的は達成できます.

上記の方法でも良いのですが,同じようなstepが重複することが問題になることもあると思います. 例えば重複するstepの結果をoutputに出力する時は後続のstepが複雑になることは想像できます. そのような時には設定用のjsonファイルをrepositoryに含めてjqを使って操作することや,個人的にはreposioryレベルでjson形式で登録すれば良いのではないかと考えています. つまり上記Workflowを書き直すと下記のようになります.

example.yaml
name: Test

on:
push:
tags:
- v*

jobs:
evaluate:
runs-on: ubutns-latest
outputs:
env_name: ${{ steps.evaluate-env.outputs.name }}
steps:
- name: evaluate env
id: evaluate-env
shell: bash
env:
is_rc: ${{ contains(github.ref, 'rc') }}
run: |
if $is_rc; then
echo "name=rc" >> "$GITHUB_OUTPUT"
else
echo "name=default" >> "$GITHUB_OUTPUT
test:
runs-on: ubuntu-latest
needs: evaluate
steps:
- uses: aws-actions/configure-aws-credentials@v4
env:
AWS_ACCOUNT_ID: ${{ fromJson(vars.AWS_ACCOUNT_IDS)[needs.evaluate.outputs.env_name] }}
AWS_REGION: ${{ fromJson(vars.AWS_REGIONS)[needs.evaluate.outputs.env_name] }}
with:
role-to-assume: arn:aws:iam::${{ env.AWS_ACCOUNT_ID }}:role/my-github-actions-role
aws-region: ${{ env.AWS_REGION }}
example.yaml
name: Test

on:
push:
tags:
- v*

jobs:
evaluate:
runs-on: ubutns-latest
outputs:
env_name: ${{ steps.evaluate-env.outputs.name }}
steps:
- name: evaluate env
id: evaluate-env
shell: bash
env:
is_rc: ${{ contains(github.ref, 'rc') }}
run: |
if $is_rc; then
echo "name=rc" >> "$GITHUB_OUTPUT"
else
echo "name=default" >> "$GITHUB_OUTPUT
test:
runs-on: ubuntu-latest
needs: evaluate
steps:
- uses: aws-actions/configure-aws-credentials@v4
env:
AWS_ACCOUNT_ID: ${{ fromJson(vars.AWS_ACCOUNT_IDS)[needs.evaluate.outputs.env_name] }}
AWS_REGION: ${{ fromJson(vars.AWS_REGIONS)[needs.evaluate.outputs.env_name] }}
with:
role-to-assume: arn:aws:iam::${{ env.AWS_ACCOUNT_ID }}:role/my-github-actions-role
aws-region: ${{ env.AWS_REGION }}

evaluateというmeta的なjobをしれっと追加していますが,概ね実現したいことはfromJsonを利用してrepositoryレベルに登録されているjson文字列からパースして取得するということです. このときAWS_ACCOUNT_IDS={ "rc": 123456789012, "default": 123456789013 }のようにreposoitryには登録しています.

終わりに

Github Actionsで構造化されたvariablesやsecretsが明示的にサポートしているわけではないので,この方法が意図している利用方法かは怪しいのが若干考えものです. できればActionsが構造化されたvariablesなどを正式にサポートしてくれれば良いのですが...