Sansan Tech Blog

Sansanのものづくりを支えるメンバーの技術やデザイン、プロダクトマネジメントの情報を発信

YAML Anchorsを活用したGitHub Actionsの効率化とその限界

技術本部 研究開発部 Architectグループの島です。

GitHub ActionsでYAML Anchorsがサポートされました。

github.com

これにより、ワークフローの記述において冗長な繰り返し(ボイラープレート)コードをいくらか改善できます。この記事では、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: &not_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. 共通の権限・環境変数設定

これはかなり現実味が乏しく、トップレベルで permissionsenv を書くことで事足りるケースが大多数とは思います。

# 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 は依然として未対応です。次のコメントなどで言及されています。

厄介な観点として、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エンジニアに興味がありましたら、是非ご覧ください。

jp.corp-sansan.com

*1:本題と逸れますが、例えばyqのクエリを頑張ることで平坦化は可能です。

© Sansan, Inc.