GitHub Actions ワークフローを YAML テンプレートツール(ytt)で楽に生成したい!

create
2021年06月18日
update
2021年12月06日

🍨はじめに

GitHub Actions ワークフロー では YAML のアンカーやエイリアスが使えないため同じ設定を何度も記述しなくてはならない場合がある。
とてもつらい。設定を変更したいときには全ての箇所を修正する必要があってつらい。

そこで ytt を使えばテンプレートファイルから YAML ドキュメントを生成できるので楽ができそう。
と思って試してみたサンプルを備忘録として残しておく。

本記事はバージョン 0.37.0 での情報。
GitHub では2021年8月から uses キーワードが複合ステップアクション(Composite Action)で使えるようになったので、ローカルアクションを作成するほうが簡単かもしれない。

使ってみた感想

checkメリット

YAML ドキュメントのある部分を変数化して使い回すような簡単な共通化であれば、さくっと複数ファイルへの適用もできてうれしい。
Overlays がすごい便利。

closeデメリット

複雑な共通化をしようとすると、Starlark 言語を学ぶ必要があってしんどい。
Overlays の挙動がぜんぜんわからん。ふんいきで使ってる。

結論

複数ファイルに対して簡単な共通化をしたいときにとてもうれしい。
GitHub Actions ワークフロー を複数作成する場合、同じ設定を繰り返し記述することが多いので助かる。

🚀 ytt (YAML Templating Tool)

YAML 用のテンプレートエンジン。
yttテンプレート および Data ValuesOverlays ファイルを受け取って YAML ドキュメントを出力する。

特徴
  • Python っぽいプログラミング言語である Starlark 言語 を内蔵している。

  • YAML 構造を把握している。

  • 環境変数やアノテーションを使うことで、設定の一部上書きができる。

  • Go 言語製なのでシングルバイナリで利用できる。使いやすい。


YAML 構造を把握しているので、テンプレートなどを書く際には YAML ドキュメントにアノテーションを付け加える形になる。
このため新たに覚えることが少ないので、さっと使えて便利。

他テンプレートエンジン(Jinja2 とか)との比較
ytt vs x に記載されている。

インストール

GitHub のリリースページからバイナリファイルをダウンロードして Path を通すだけ。
Go 言語製のツールはシングルバイナリで利用できるのが素敵。

コマンドライン(CLI)での実行方法

メインのテンプレートファイルだけでなく、後述する Data ValuesOverlays のファイルも一緒に読み込ませる。

Example 1. ytt コマンド実行例
全てのファイルを指定
ytt template \
  --file main.template.yml \
  --file common_data.yml
ディレクトリを指定
ytt template \
  --file path/to/template/dir \    (1)
  --file-mark '**/*ignore.yml:exclude=true'   (2)
1 指定したディレクトリ以下(サブディレクトリも含む)にある全てのファイルが読み込まれる。
2 除外したいファイルパスを指定。詳しくは File Marks を参照。
ファイルに保存する場合
ytt template \
  --file config.yml \
  --output-files path/to/outputs/dir

📔 ytt の文法についてのサンプル集

とりあえずよく使いそうな分だけ(実装コストが高くなる制御構文(if文やfor文)などは除外)。

ytt を試せる online playground が公式で用意されているため、そこで試してみるといい。

YAMLドキュメント
YAML ドキュメントは1ファイルに複数ふくめることができる(--- 区切り)。
よって ファイルドキュメント の呼び方の違いに気をつける。

外部の変数( Data Values

別ファイルに記述した YAML ドキュメントの key:value は、 Data Values として宣言すれば変数として参照することができる。

YAML ドキュメントに @data/values アノテーションをつければ Data Values として宣言したことになる。
また、テンプレートファイルから Data Values を参照するには、ファイル先頭に @ load("@ytt:data", "data") アノテーションをつける。

Example 2. Data Values を使う例
var_1.yml
#@data/values   (1)
---
hoge: hoge value    (2)
foo:
  fuga: fugafuga
bar: old value
1 Data Values ファイルの宣言。
2 それぞれの値を記述。
var_2.yml
#@data/values
---
bar: new value      (1)

#@overlay/remove    (2)
hoge:

#@overlay/match missing_ok=True     (3)
baz: add new key
1 既存の値を上書き。
2 既存の key を削除。
3 新しい key とその値を追加。
template.yml
#@ load("@ytt:data", "data")    (1)
---
var1: #@ data.values.bar        (2)
var2: #@ data.values.foo.fuga
var3: #@ data.values.foo
var4: #@ data.values.baz
1 Data Values を読み込み。
2 登録した Data Values の各値を参照。
実行結果
ytt template \   (1)
  --file template.yml \
  --file var_1.yml \
  --file var_2.yml

var1: new value
var2: fugafuga
var3:
  fuga: fugafuga
var4: add new key
1 テンプレートと使用する Data Values のファイルも一緒に指定する。
Data Values のキーについて
キーは snake_case 形式が推奨(つまり ハイフン(-)は非推奨)。
これは参照時に . を使った参照ができなくなるため。
アノテーションのスペースについて

#@ 後にスペースが必要なものと不要なものがある。
これについては、

  • YAML 要素に働きかけるアノテーション(@data/values@overlay/match)には不要

  • 実際は ytt のディレクティブであるもの(@ load@ if)には必要

ということらしい(詳細)。

JSON データを利用する

コマンドラインオプションの --data-value-file オプションでファイル内容を文字列として読み込み、それを JSON データとしてオブジェクトに変換すればいい。

Example 3. JSONファイルを読み込む
conf.json
{
  "version": "1.2.3",
  "levels": ["info", "warn", "error"]
}
template.yml
#@ load("@ytt:data", "data")
#@ load("@ytt:json", "json")      (1)
#@ load("@ytt:struct", "struct")  (1)

---
#@ config = struct.encode(\   (2)
#@    json.decode(data.values.config)\  (3)
#@  )
json:
  version: #@ config.version
  levels: #@ config.levels
1 JSONstruct 型のモジュールを読み込み。
2 プロパティを . から参照できるように dict 型の値を struct 型に変換。
(末尾の \Starlark 言語における改行のエスケープ)
3 --data-value-file で読み込んだ JSON ファイルの内容(文字列)を dict 型の値として変換。
実行結果
ytt template \
  --file template.yml \
  --data-value-file config=conf.json  (1)

json:
  version: 1.2.3
  levels:
  - info
  - warn
  - error
1 <key>=</path/to/file> の書式。
指定した key の値にファイル内容を文字列として読み込む。

Data Values の型定義(Data Values Schema

version 0.35.0 で正式実装された機能で、Data Values の型定義を宣言する。
YAML ドキュメントに @data/values-schema アノテーションをつけて宣言する。

この Data Values Schema で定義した値はデフォルト値として機能し、 Data Values に定義された値で上書きマージされる。
よって、

  1. Data Values Schema による汎用的な型定義を行い、

  2. 実際に扱う値を Data Values で実装する

という形にするのがよさそう。

使い所として、

  • 複数人による作業

  • 同種の YAML 設定ファイルを複数作成するとき

などに利用するとよさそう。

Example 4. Data Values Schema を使った例
config/schema.yml
#@data/values-schema  (1)
---
cache:
  name: Caching
  id: cache
  uses: actions/cache@v2
  with:
    path: /tmp/cache
    key: ${{ runner.os }}-caching-${{ hashFiles('.lock') }}
    restore-keys: ""
1 Data Values Schema としての宣言。
ここで定義した値はデフォルト値になる。
config/data.yml
#@data/values   (1)
---
cache:
  name: Cache Docker Layer
  with:
    key: ${{ runner.os }}-docker-${{ hashFiles('**/Dockerfile') }}
    restore-keys: |
      ${{ runner.os }}-docker-
1 Data Values で定義した値は一緒に読み込んだ Schema の値を上書きする。
上書きしたいプロパティだけを記述すればOK。
config/template.yml
#@ load("@ytt:data", "data")
---
loading: #@ data.values.cache
ytt template --file=config

loading:
  name: Cache Docker Layer
  id: cache
  uses: actions/cache@v2
  with:
    path: /tmp/cache
    key: ${{ runner.os }}-docker-${{ hashFiles('**/Dockerfile') }}
    restore-keys: |
      ${{ runner.os }}-docker-
複数ファイルに分割して Schema を定義する場合は、Overlays の機能(#@overlay/match missing_ok=True)を利用する必要があるので注意。

Schema の拡張

Schema で定義した型に新しくプロパティを追加したい場合は、

  • その Schema ファイルを修正する

  • 別の Schema ファイルを作成して Overlays による上書きをする

のどちらかを行う必要がある。
(横着して Data Values で追加しようとすると怒られる。)

Example 5. Overlays を利用して Schema を拡張
config/schema.patch.yml
#@data/values-schema
---
cache:
  #@overlay/match missing_ok=True   (1)
  env:
    HOGE: ""
1 Overlays を利用して新しいプロパティを追加する。
config/data.yml (更新)
#@data/values
---
cache:
  name: Cache add env
  env:
    HOGE: hogehoge
ytt template --file=config

loading:
  name: Cache add env
  id: cache
  uses: actions/cache@v2
  with:
    path: /tmp/cache
    key: ${{ runner.os }}-caching-${{ hashFiles('.lock') }}
    restore-keys: ""
  env:
    HOGE: hogehoge
オプショナルなプロパティをもつ型を定義したい場合は #@schema/type any=True を利用する方法がある。

パッチの適用( Overlays

テンプレートや Data Values の設定の一部だけを変更したり、共通の設定を適用したりすることができる。
Overlays として宣言するには、YAML ドキュメントに @overlay/match アノテーションをつける。
また Overlays の関数などを使うには、ファイルの先頭で #@ load("@ytt:overlay", "overlay") アノテーションを記述しておく。

OverlaysYAML テンプレートが描画されたあとに適用される。

他、Overlays についての詳細はこちらの公式ドキュメントを参照。

Example 6. Overlaysを使ったパッチ適用例
config.yml
name: overlay sample
version: 1.2.3
metadata:
  - name: example-ingress1
    tag:
      - "hoge"
    annotations:
      message: removed this message
    overrides:
      - hoge
      - foo
  - name: example2
    tag:
      - foo
    annotations:
      message: left message
    overrides:
      - yoho
patch.yml
#@ load("@ytt:overlay", "overlay")  (1)

#@overlay/match by=overlay.all  (2)
---
metadata:
  #@overlay/match by=overlay.subset({"name": "example-ingress1"})  (3)
  - tag:
      - "fuga"
    annotations:
      #@overlay/remove    (4)
      message:
    #@overlay/match missing_ok=True   (5)
    config:
      var1: hoge
      var2: fuga

#@overlay/match by=overlay.subset({"metadata": []})  (6)
---
metadata:
  #@overlay/match by=overlay.all, expects="1+"  (7)
  - overrides:
      - add value
1 Overlays ライブラリを読み込み。
2 Overlays 用のドキュメントであることを宣言。
かつ Overlays を適用する YAML 要素のパターンマッチ方法を指定(by)。
3 指定した YAML 要素と一致する要素を上書き対象とする。
4 下記のキーを削除する。
5 既存のキーが存在しないときは新しく追加したい場合、missing_ok=True を指定する。
6 詳細な値を指定したくないときは、空の値([], {})を指定すればいい。
が、こういう場合は overlay.all() を使ったほうがいい。
7 expects でマッチすべき回数を指定。この回数に該当しなければエラーとなる。
実行結果
ytt template --file config.yml --file patch.yml

name: overlay sample
version: 1.2.3
metadata:
- name: example-ingress1
  tag:
  - hoge
  - fuga
  annotations: {}
  overrides:
  - hoge
  - foo
  - add value
  config:
    var1: hoge
    var2: fuga
- name: example2
  tag:
  - foo
  annotations:
    message: left message
  overrides:
  - yoho
  - add value
配列の値の置換について
配列の値を追加したり空にしたりすることは簡単にできるが、置換することは難しい。
ある程度あきらめたほうがよさそう。
Table 1. Overlays アノテーション(一部)
アノテーション 説明

@overlay/match

どの要素を修正・上書きするかを指定する。

@overlay/match-child-defaults

expectsmissing_ok の設定値を子要素にデフォルト値としてつける。

@overlay/remove

一致した要素を削除する。

@overlay/replace

一致した要素の値を置換する。

Table 2. Matcher関数(一部)
関数 説明

overlay.all()

記述した要素をすべて含む要素を検索する。

overlay.subset(TARGET)

TARGET で指定した要素の構造と一致する要素を検索する。

overlay.map_key(…​) は理解が不十分なせいで期待通りの動作をしてくれないことが多かった。
なので少し冗長でも overlay.subset(…​) を使ったほうがイライラせずにすんだ。

共通の設定項目を定義する

runs-on とかの共通設定は1回の記述で済ませたい。
そういう場合にも Overlays が有効。

Example 7. Overlays で共通設定を定義

runs-on の値と、ステップの最初に行う uses: actions/checkout@v2 を共通化。

commons-patch.yml
#@ load("@ytt:overlay", "overlay")

#@overlay/match by=overlay.all, expects="1+"
---
jobs:
  #@overlay/match by=overlay.all, expects="1+"
  _:  (1)
    #@overlay/match missing_ok=True
    runs-on: ubuntu-20.04

    steps:
      #@overlay/match by=overlay.index(0)   (2)
      #@overlay/insert before=True  (3)
      - name: Checkout code
        uses: actions/checkout@v2
1 任意のキー名としたい場合、_ と指定する。
2 最初の配列要素を検索。
3 上記で検索された要素の前に、この YAML 要素を挿入させる。

もしあるファイルでは別の設定にしたい場合、それ用の Overlays を設定して上書きさせればいい。

Template モジュール

既存の YAML 要素をまるごと置換する template.replace() 関数が利用できる。

例として、GitHub Actions ワークフロー においては steps キーが配列を格納するので、ここでよく使いそう。

Example 8. GitHub Actions ワークフローにおける利用例
vars.yml
#@data/values
---
setup:
  - name: Checkout code
    uses: actions/checkout@v2
  - name: Set up QEMU
    uses: docker/setup-qemu-action@v1
  - name: Set up Docker Buildx
    uses: docker/setup-buildx-action@v1
workflow-template.yml
#@ load("@ytt:data", "data")
#@ load("@ytt:template", "template")  (1)
---
...
jobs:
  docker:
    name: docker / build
    runs-on: ubuntu-20.04
    steps:
      -  #@ template.replace(data.values.setup)   (2)
      - ...
1 Template ライブラリを読み込み。
2 ここの配列要素を setupYAML 要素で置換している。
これによって二次元配列にならずに済む。
実行結果
ytt template --file workflow-template.yml --file vars.yml

...
jobs:
  docker:
    name: docker / build
    runs-on: ubuntu-20.04
    steps:
    - name: Checkout code
      uses: actions/checkout@v2
    - name: Set up QEMU
      uses: docker/setup-qemu-action@v1
    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v1
    - ...

文字列のテンプレート(Text Templating

#@yaml/text-templated-strings アノテーションをつける。

テンプレート文字列(文字列補間)を使いたいときに使う。

Example 9. テンプレート文字列(文字列補間)

@yaml/text-templated-strings アノテーションをつけた上で (@@) で囲んだ部分が変数展開される。

config.yml
#@ val1 = "value1"  (1)
#@ val2 = "value2"

#@yaml/text-templated-strings   (2)
---
normal: "val1 is (@= val1 @) and val2 is (@= val2 @)"   (3)
no_output: "val1 is (@ val1 @) and val2 is (@ val2 @)"  (4)
trim_spaces: "val1 is (@-= val1 -@) and val2 is (@-= val2 @)"   (5)

key_(@= val1 @): used in key    (6)
1 変数を定義。
2 テンプレート文字列を使うためのアノテーション。
3 通常使用。変数展開された値は文字列として扱われる。
4 = をつけない場合、変数展開されない。
5 - をつけた側のスペースが除去される。
6 キーにも変数展開が使える。
実行結果
ytt template --file config.yml
normal: val1 is value1 and val2 is value2
no_output: 'val1 is  and val2 is '
trim_spaces: val1 isvalue1and val2 isvalue2
key_value1: used in key

コメント

#! で始めるとコメントとして使われる。

Example 10. コメント
comment.yml
---
#! This is  comment
hoge: sample-for-comment
実行結果
ytt template --file comment.yml
hoge: sample-for-comment

その他

Struct とか Library とか if文とか関数とか。

詳しくは公式ドキュメントを参考。

🍣FAQ

anchor/alias は使えるの?

普通に使える。
その上 ytt で出力された YAML ドキュメントは alias が展開されるので、anchor/alias が使えない GitHub Actions ワークフロー でも安心して利用できる。

ただまあ、ytt を使うなら anchor/alias の代わりに関数を利用したほうが筋はいいかも。

data/values で定義した変数を別の data/values から参照したい

現状では data/values のファイルが全て読み込まれるまでは参照できないため、無理らしい(issues #309)。

なので代わりに Overlays でパッチをあてたり、Library を使って参照したりしてみる。

on キーが true に変換されてしまう

YAML Version 1.1 の仕様のせい。
ダブルクォーテーション " で囲み、文字列として扱わせるといい。

onキーが true に変換されないようにする
"on":
  push: {}

Error: use of reserved keyword 'with' is not allowed

ある GitHub ActionData Value にして参照しようとしたときに発生したエラー。

原因

Starlark の予約語(参照)に with が入っているため、ドット表記(~~.with)による利用はできない。

対策

ブラケット表記(~~["with"])で参照すればいい。

✖️NG(ドット表記)
action_name: #@ data.values.<value>.with.name
✔️OK(ブラケット表記)
action_name: #@ data.values.<value>["with"].name
with キーワード以外でもこのエラーは発生するが、同じようにブラケット表記を使えばいい。

😎おわりに

ytt のサンプルがいまいちわからなかったので実際に試してみたのを書いた。
ただ Overlays はすごい便利なんだけど match 条件が全然わからん。ふんいきで使ってる。

なお、作成した ytt 用のテンプレートのままでは GitHub で使えないので、

  • Git Hooksgit push 前にコンパイル

  • 別の GitHub Actions ワークフロー でコンパイルさせてコミットを追加[1]

したりするのがよさそう。


1. ただし GITHUB_TOKEN には workflows 権限が許可されていないため、個人アクセストークンを利用する必要がある。