🏝️

Jsonnetでatlantis.yamlを書く

に公開

Atlantis v0.35での破壊的な変更

カンムではAtlantisというアプリケーションを使ってPull Requestでterraformを実行するようにしています。

https://www.runatlantis.io/

https://tech.kanmu.co.jp/entry/2024/10/28/150000

Atlantisではatlantis.yamlというファイルでリポジトリの設定を管理していますが、最近のアップデートでyamlライブラリの変更により、既存のYAMLアンカーを使ったテンプレートでエラーが出るようになりました。

YAML anchor configurations in atlantis.yaml files that worked in Atlantis 0.34.0 now fail with duplicate key errors in Atlantis 0.35.0. This is a breaking change that affects users who utilize YAML anchors and aliases to reduce duplication in their Atlantis configurations.
The root cause is the migration from gopkg.in/yaml.v3 to github.com/goccy/go-yaml in version 0.35.0, which introduced stricter YAML parsing that now detects duplicate keys that were previously allowed.
Atlantis v0.35 has breaking changes around YAML anchor · Issue #5665 · runatlantis/atlantis

テンプレートを使ったatlantis.yamlの書き方は以下のようになります。

atlantis.yaml
projects:
  - &template
    name: template
    dir: template
    autoplan:
      when_modified:
        - "**/*.tf"
        - ".terraform.lock.hcl"
  - <<: *template
    name: ue1-prod-titan
    dir: ./terraform/titan
    workspace: ue1-prod
  - <<: *template
    name: ue1-stage-titan
    dir: ./terraform/titan
    workspace: ue1-stage

from https://www.runatlantis.io/docs/repo-level-atlantis-yaml#example-of-drying-up-projects-using-yaml-anchors

- &templateの設定が<<: *templateの箇所に差し込むことができるため、when_modifiedなどの重複する設定を一箇所にまとめでDRYに書くことができます。
しかし、Atlantis v0.35から使われるようになった github.com/goccy/go-yaml ではキーの重複を許していない実装のため、上記の書き方ではエラーが発生します。

parsing atlantis.yaml: [10:5] duplicate key "name"
   8 |         - ".terraform.lock.hcl"
   9 |   - <<: *template
> 10 |     name: ue1-prod-titan
           ^
  11 |     dir: ./terraform/titan
  12 |     workspace: ue1-prod

書き方を変えればエラーを回避できるようですが、同様の問題に困っている方が多いようで、変更のRevertやyaml/go-yamlの利用についてのコメントが寄せられています。

YAMLテンプレートの問題

カンムでもAtlantisをv0.35にあげたのですが、幸いこの変更の影響を受けることはありませんでした。これはatlantis.yamlでのテンプレート利用にすこし問題があったため、Jsonnetからatlantis.yamlに変換するようにしていたためです。

問題の一つはアンカーでの配列のマージです。
when_modifiedは「どのファイルが変更されたらterraform planを実行するのか」という設定ですが、トリガにしたいファイルはプロジェクトによって異なり、*.tfだけで十分のこともあれば、*.sqlを含める必要があることもあります。
テンプレートでは「すべてのプロジェクトに共通するトリガファイル」を書き、個別のプロジェクトで必要なファイルを追加するようにしたいのですが、以下のような書き方はできません。

atlantis.yaml
projects:
  - &template
    name: template
    dir: template
    autoplan:
      when_modified: &when_modified
        - "**/*.tf"
        - ".terraform.lock.hcl"
  - <<: *template
    name: ue1-prod-titan
    dir: ./terraform/titan
    workspace: ue1-prod
    autoplan:
      when_modified: *when_modified
        - "**/*.sql" # このような書き方はできない

YAMLでの配列の結合についていろいろと調べたのですが、実装特有のやり方を除いて汎用的な方法を見つけることはできませんでした。

https://github.com/yaml/yaml/issues/35
https://stackoverflow.com/questions/24090177/how-to-merge-yaml-arrays

もう一つの問題は些細なことですが、- &templateのアンカーの部分はテンプレートとしてだけ機能するわけではなく実態があることです。
テンプレートを使ったatlantis.yamlをJSONに展開してみると "name": "template"というプロジェクトが存在することが分かります。

{
  "projects": [
    {
      "autoplan": {
        "when_modified": [
          "**/*.tf",
          ".terraform.lock.hcl"
        ]
      },
      "dir": "template",
      "name": "template"
    },
    {
      // 略

templateというディレクトリが存在しないため単に無視されているだけですが、あまりきれいなやり方とは思えず、また、トラブルの元になりそうな懸念もありました。

Jsonnetで書く

AtlantisにはPre Workflow Hooksというフックがあり、ワークフロー実行前に処理を差し込むことができます。ドキュメントにはこのフックを使ってatlantis.yamlを生成する例が書かれています。

https://www.runatlantis.io/docs/pre-workflow-hooks.html#customizing-the-shell

カンムでは以下の設定とスクリプトを使ってatlantis.jsonnetからatlantis.yamlを生成しています。

repos.yaml
    pre_workflow_hooks:
      - run: repo-config-generator.sh
        description: Generating atlantis.yaml
repo-config-generator.sh
#!/bin/bash
set -ex

# atlantis.jsonnetがなかったらatlantis.yamlを使う
if [ -e atlantis.jsonnet ]; then
  exec 9>atlantis.jsonnet.lock
  flock -w 60 9 # 念のため排他制御
  jsonnet atlantis.jsonnet > atlantis.json
  # yqでYAMLに変換
  # https://github.com/mikefarah/yq
  yq -MPo yaml atlantis.json > atlantis.yaml.new
  mv -f atlantis.yaml.new atlantis.yaml
  rm -f atlantis.json
fi

atlantis.jsonnetは以下のようになります。

atlantis.jsonnet
local project = {
  branch: '/main/',
  autoplan: {
    when_modified: [
      '.terraform.lock.hcl',
      '**/*.tf',
    ],
  },
};

{
  version: 3,
  projects: [
    project {
      name: 'hello',
      dir: './project/hello',
    },
  ],
}

local projectはただの変数なのでatlantis.yamlに展開されることはありません。
もし、個別のプロジェクトでwhen_modifiedを書き換えたい場合、Jsonnetを機能で配列をマージすることができます。

atlantis.jsonnet
{
  version: 3,
  projects: [
    project {
      name: 'hello',
      dir: './project/hello',
      autoplan+: {
        when_modified+: [ // project の when_modified に追加される
          '**/*.sql',
        ],
      },
    },
  ],
}

https://jsonnet.org/

株式会社カンム

Discussion