
技術本部 研究開発部 Architectグループの島です。
GitHub ActionsでYAML Anchorsがサポートされました。
これにより、ワークフローの記述において冗長な繰り返し(ボイラープレート)コードをいくらか改善できます。この記事では、YAML Anchorsの基本的な仕組みから、思いついた範囲でGitHub Actionsにおける活用例まで解説します。
先に結論
正直、そこまで大きな効き目はない
もしMerge Keysが使えたら結構すごかったかもしれません。
日頃の課題・きっかけ
ステージング環境、本番環境それぞれ向けにサービスをデプロイするようなワークフローがあるとして、素朴に作るとよく似たコードが複数回登場しがちです。特に保守性の面から課題を感じます。解決策は様々ありそうでいて、意外とシンプル・楽になりそうなものは思いつきにくいです(エレガントかもしれないが結局記述量は多い、という方法ばかり)。もしYAMLの書き方1つで楽できるなら良い話だと飛びついた次第です。
こちらはCloud Runにデプロイするワークフローの例です。簡略化しており、実際には gcloud run deploy 以外にも処理が必要で、コードは膨らみます。
name: "Deployment" on: workflow_dispatch: inputs: env: type: choice required: true options: - stg - prod jobs: deploy: runs-on: ubuntu-latest steps: # -- 中略 -- # ステージング環境向けのデプロイメント - if: ${{ github.event.inputs.deploy_environment }} == "stg" run: | gcloud run deploy my-service \ --image "asia-northeast1-docker.pkg.dev/my-project-${{github.event.inputs.env}}/my-repository/my-service:latest" \ --project my-project-${{github.event.inputs.env}} \ --service-account="my-service@my-project-${{github.event.inputs.env}}.iam.gserviceaccount.com" \ --region=asia-northeast1 \ --no-allow-unauthenticated \ --min-instances=0 \ --max-instances=100 \ --memory=4Gi \ --cpu=2 \ --concurrency=2 \ --no-traffic # 本番環境向けのデプロイメント(ステージングとある程度共通で、一部違う設定) - if: ${{ github.event.inputs.deploy_environment }} == "prod" run: | gcloud run deploy my-service \ --image "asia-northeast1-docker.pkg.dev/my-project-${{github.event.inputs.env}}/my-repository/my-service:latest" \ --project my-project-${{github.event.inputs.env}} \ --service-account="my-service@my-project-${{github.event.inputs.env}}.iam.gserviceaccount.com" \ --region=asia-northeast1 \ --no-allow-unauthenticated \ --min-instances=0 \ --max-instances=5 \ --memory=16Gi \ --cpu=4 \ --concurrency=2 \ --no-traffic
YAML Anchorsとは?
YAML Anchorsは、YAML文書内で値を定義し、それを他の場所で再利用できる機能です。重複を減らし、設定の一貫性を保つのに役立ちます。
仕様は以下ページなどをご参照ください: ktomk.github.io
基本的な構文
on: push: # アンカーの定義(&でアンカー名を指定) paths: &common_paths - "src/**.py" - "requirements.txt" pull_request: # アンカーの参照(*でアンカー名を参照) paths: *common_paths
実際にYAML Anchorsがどう展開されるかを確認してみます。ここではyqを使用します。
yq "explode(.)" foo.yaml
on: push: paths: - "src/**.py" - "requirements.txt" pull_request: paths: - "src/**.py" - "requirements.txt"
アンカー参照(*common_paths)が実際の値(配列)に展開されていることが確認できます。
GitHub ActionsでのYAML Anchors活用例
後述する制約から、単純なコピー&ペーストを代替できる ということのみが活用の道だと考えます。私が思いついた範囲で、GitHub Actionsでの具体的な活用例を紹介します。
1. pushとpull_requestで同じpath条件を指定
最もよく使われそうな事例です。
name: "YAML Anchor Sample" on: push: paths: &paths - "src/**.py" - "Dockerfile" - "requirements.txt" branches: &branches - main pull_request: paths: *paths branches: *branches jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Run tests run: python -m pytest
on のトリガー設定に関しては、他に types 等も重複の可能性があり得る箇所ですね。
2. 複雑な条件式の再利用
複雑な条件を何度も使う場合に便利です。
name: "Conditions Anchor Sample" on: [push, pull_request] jobs: test: runs-on: ubuntu-latest if: ¬_draft github.event_name != 'pull_request' || !github.event.pull_request.draft steps: - name: Run tests run: echo "Running tests" deploy: runs-on: ubuntu-latest if: *not_draft needs: test steps: - name: Deploy run: echo "Deploying"
3. 複雑なアクション設定の再利用
Reusable workflow等に渡す複雑な設定を統一できます。
name: "Action Configs Anchor" on: [push] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: &python_config python-version: '3.11' cache: 'pip' cache-dependency-path: 'requirements.txt' - name: Test run: | pip install -r requirements.txt python -m pytest lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: *python_config - name: Lint run: | pip install -r requirements.txt python -m ruff check .
4. 共通のコンテナ設定
同じコンテナ設定を複数のジョブで使用する例です。
name: "Container Configs Anchor" on: [push] jobs: test-api: runs-on: ubuntu-latest container: &python_container image: python:3.11-slim env: PYTHONPATH: /app DATABASE_URL: postgresql://test:test@postgres:5432/testdb FLASK_ENV: testing services: postgres: &postgres_service image: postgres:15 env: POSTGRES_DB: testdb POSTGRES_USER: test POSTGRES_PASSWORD: test steps: - name: Test Python API run: | pip install -r requirements.txt python -m pytest integration-test: runs-on: ubuntu-latest container: *python_container services: postgres: *postgres_service steps: - name: Integration test run: | pip install -r requirements.txt python -m pytest tests/integration/
5. matrixのパラメータの共有
複数のジョブで同じmatrix設定を使用します。
name: "Matrix Anchor Sample" on: [push] jobs: test: strategy: matrix: os: &supported_os [ubuntu-latest, windows-latest, macos-latest] python-version: &supported_python ['3.9', '3.10', '3.11'] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - name: Setup Python uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} deploy: needs: test strategy: matrix: os: *supported_os python-version: *supported_python runs-on: ${{ matrix.os }} steps: - name: Deploy run: echo "Deploying on ${{ matrix.os }} with Python ${{ matrix.python-version }}"
6. 共通の権限・環境変数設定
これはかなり現実味が乏しく、トップレベルで permissions や env を書くことで事足りるケースが大多数とは思います。
# permissions: の例 name: "Permission Anchor Sample" permissions: &read_permissions contents: read packages: read security-events: read on: [push] jobs: security-scan: runs-on: ubuntu-latest permissions: *read_permissions steps: - name: Run security scan run: echo "Scanning..." test: runs-on: ubuntu-latest permissions: <<: *read_permissions checks: write steps: - name: Run tests run: echo "Testing..."
# env: の例 name: "Env Anchor Sample" env: &common_env PYTHON_VERSION: '3.11' DATABASE_URL: 'postgresql://localhost:5432/testdb' FLASK_ENV: 'testing' LOG_LEVEL: 'DEBUG' on: [push] jobs: test: runs-on: ubuntu-latest env: *common_env steps: - name: Run tests run: python -m pytest lint: runs-on: ubuntu-latest env: *common_env steps: - name: Run linter run: python -m flake8
制約事項
これが解決できたらさらに活用の幅が広がるのに、という点をご紹介します。要するに、コピー元のデータに「少し付け足したい」事例が、叶わないということです。
Merge Keys(<<:)
YAMLには Merge Keys という強力な構文があります。これを使用することで、マップ(オブジェクト)の内容を一部上書きしたり、新規のKey-Valueを付け加えたりできます。
default_config: &default_config name: dummy_project adapter: mysql2 encoding: utf8mb4 development_config: <<: *default_config name: dummy_project_dev database: dummy_db_dev
yqで展開すると、次のような結果になります。
default_config: name: dummy_project adapter: mysql2 encoding: utf8mb4 development_config: name: dummy_project_dev # 上書きされた値 adapter: mysql2 encoding: utf8mb4 database: dummy_db_dev # 新しく追加された値
development_configでは、default_configの内容がそのまま入りつつ、nameキーは新しい値で上書きされ、databaseキーは新規追加されています。このように、「大半は流用したいが少しだけ変えたい」ようなシーンに活躍し、例えば本番環境と開発環境それぞれに値を振り分けるなどの用途に便利です。
GitHub Actionsでは使えません
しかしながら残念なことに、2025年9月現在、GitHub Actionsでは Merge Keys の構文(<<:)はサポートされていません。
この制限については、前述のIssue で長年議論されており、2025年8月に基本的なYAML Anchors のサポートが追加されましたが、Merge Keys は依然として未対応です。次のコメントなどで言及されています。
- https://github.com/actions/runner/issues/1182#issuecomment-2810242069
- https://github.com/actions/runner/issues/1182#issuecomment-3182527537
厄介な観点として、Merge Keysの構文はYAML 1.2では非推奨(仕様に含まれない)となっています。今やバージョンごと非推奨となったYAML 1.1のときにWorking Draftとして入っていたのみで、1.2では削られたようです。GitHub Actionsでサポート外とするのも理解はできるところです。明らかに便利機能ゆえ、yq含め多くのParserはサポートしているという現状もまたややこしさの元です。
もしGitHub Actionsで使えたら、の例
エラーになります。
name: "動作しない例" on: workflow_dispatch jobs: deploy-staging: uses: ./.github/workflows/deploy-python.yml with: &deploy_config environment: staging python_version: '3.11' app_module: 'myapp:app' workers: 2 timeout: 300 deploy-production: uses: ./.github/workflows/deploy-python.yml needs: deploy-staging with: <<: *deploy_config # ここがダメ environment: production workers: 4
ちなみにエラーメッセージはこんな感じで出ます。
(Line: 21, Col: 9): Unexpected value '<<', (Line: 21, Col: 9): There's not enough info to determine what you meant. Add one of these properties: run, shell, uses, with, working-directory, (Line: 28, Col: 9): Unexpected value '<<', (Line: 28, Col: 9): There's not enough info to determine what you meant. Add one of these properties: run, shell, uses, with, working-directory
YAML Anchorsのsequence(配列)における課題
もう1つの課題として、sequenceの話題があります。sequenceのAnchorを他のsequenceに結合すると、期待した平坦な配列ではなく、ネストした配列になってしまいます。
base: &base - AAA - BBB extended: [*base, CCC]
展開した結果:
base: - AAA - BBB extended: [[AAA, BBB], CCC] # 期待: [AAA, BBB, CCC]
GitHub Actionsに関係なくYAMLとして、このような入れ子のsequenceを平坦化(flatten)する標準的な方法は、調べた限り存在しないようです。 *1
GitHub Actionsでの影響例
name: "Paths Combination Problem" on: push: paths: &core_paths - "src/**" - "requirements.txt" - "Dockerfile" pull_request: paths: [*core_paths, "docs/**", "tests/**"] # 結果: [["src/**", "requirements.txt", "Dockerfile"], "docs/**", "tests/**"] # 期待: ["src/**", "requirements.txt", "Dockerfile", "docs/**", "tests/**"]
この例に現実味があるかはさておくと、もしpaths指定が少しでも違うならAnchorは使えないということになります。
おわりに
私の経験を思い出す限り、「pull_requestとpullで、pathsを再利用」または「if: の条件式の再利用」に関しては、使える場面がたまにありそうです。
他の例は正直なところ、がんばってひねり出しました。jobを細分化する場合はたまに有用かもしれません。私個人は、可能な限りjobは1つでstepを連ねる主義なので、自分で書いておきながら使いそうにない例となっています。
Merge Keysやsequenceの仕様調査を通じて、YAMLの深淵に少し触れることができました。YAML 1.3にてまた何らか変わるのかもしれませんが、長らく大きな進展はしていないように見えます。
研究開発部では、共に働くメンバーを募集中です!
Sansanのプロダクト価値の根源を担う研究開発。その基盤を支え、成果を最大化するMLOps・Platformエンジニアに興味がありましたら、是非ご覧ください。
*1:本題と逸れますが、例えばyqのクエリを頑張ることで平坦化は可能です。