3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Terraformをyamlで書いて差分管理してみよう!

Last updated at Posted at 2025-12-03

この記事はTerraform Advent Calendarの4日目の記事です。

前回は2日の @keitakei777 さんによる「TerraformでEC2のインスタンスを立ててみる」でした。
次回は6日に @ymasaoka さんによる記事が予定されています。こちらも楽しみです。

はじめに

Terraformはインフラ管理の定番ツールですが、いざ複数環境(dev/prodなど)を扱おうとすると、環境ごとの差分管理が意外と難しいものです。

その解決方法として、共通モジュールを作成し、複数のテナントでこれを利用するといったケースはよくありますが、一部の環境でのみ特殊なオプションを有効化したいといった場合、モジュール側を直接編集するほかないという問題点があります。

kubernetes + kustomizeのように、環境ごとにパッチを当てる仕組みがあれば便利ですが、Terraformにはそのような仕組みはありません。

そこで、本記事では、Terraformの設定をyamlで記述し、環境ごとにパッチを当ててみるといった実験を行った結果を紹介します。
この方法により、環境ごとの差分管理が容易になり、コードの再利用性も向上します。
実用的かどうかはさておき、Terraformの新しい使い方の一例として参考になれば幸いです。

Terraformのjson記法についておさらい

Terraformの入力形式といえば .tf ファイル(HCL)ですが、実はJSONでも書けるのをご存知でしょうか。

例えば、以下のような.tfファイル

terraform {
  required_providers {
    local = {
      source  = "hashicorp/local"
      version = "2.4.0"
    }
  }
}

provider "local" {}

resource "local_file" "hello1" {
  content  = "1st Hello, World!"
  filename = "hello1.txt"
}

resource "local_file" "hello2" {
  content  = "2nd Hello, World!"
  filename = "hello2.txt"
}

は、つぎのようなjsonで記述できます

{
  "provider": {
    "local": {}
  },
  "resource": {
    "local_file": {
      "hello1": {
        "content": "1st Hello, World!",
        "filename": "hello1.txt"
      },
      "hello2": {
        "content": "2nd Hello, World!",
        "filename": "hello2.txt"
      }
    }
  },
  "terraform": {
    "required_providers": {
      "local": {
        "source": "hashicorp/local",
        "version": "2.4.0"
      }
    }
  }
}

これを<filename>.tf.jsonというファイル名で保存しておけば、普段と同じようにterraform applyできます。

しかし、このjsonを1から手で書くのは少々大変です。yamlで記述して、jsonに変換できれば便利そうです。

yisp

yispはyaml中に式を埋め込み、評価することのできるツールです。
例えば次のようなyaml

index.yaml
mynumber: !yisp
  - add
  - 5
  - 3

yisp build .ですると、

result
mynumber: 8

が出力されます。

他にも複数のYAMLを取り込んでまとめたり、オブジェクト同士をマージしたり、出力をJSON形式に変換したりといった機能も備わっています。

今回はこのyispを使って、Terraform JSON構造をYAMLで記述しやすくしたり、環境ごとの差分を扱ってみます。

とりあえず使ってみる

サクっとEC2を立てるだけのTerraform定義をyamlで書いてみましょう。

tf.yaml
terraform:
  required_providers:
    aws:
      source: hashicorp/aws
      version: ">= 3.0.0"
provider:
  aws:
    region: ap-northeast-1
data:
  aws_ami:
    amazon_linux:
      most_recent: true
      owners:
        - "137112412989"  # Amazon
      filter:
        - name: name
          values:
            - "amzn2-ami-hvm-*-x86_64-gp2"
resource:
  aws_instance:
    example:
      ami: "${data.aws_ami.amazon_linux.id}"
      instance_type: t2.micro
      tags:
        Name: example-instance

これをjsonに変換します。何でもいいんですが、ここではyispを使ってみます

yisp build tf.yaml -o json

すると、次のようなjsonが出力されます。

{
  "data": {
    "aws_ami": {
      "amazon_linux": {
        "filter": [
          {
            "name": "name",
            "values": [
              "amzn2-ami-hvm-*-x86_64-gp2"
            ]
          }
        ],
        "most_recent": true,
        "owners": [
          "137112412989"
        ]
      }
    }
  },
  "provider": {
    "aws": {
      "region": "ap-northeast-1"
    }
  },
  "resource": {
    "aws_instance": {
      "example": {
        "ami": "${data.aws_ami.amazon_linux.id}",
        "instance_type": "t2.micro",
        "tags": {
          "Name": "example-instance"
        }
      }
    }
  },
  "terraform": {
    "required_providers": {
      "aws": {
        "source": "hashicorp/aws",
        "version": "\u003e= 3.0.0"
      }
    }
  }
}

これをrendered.tf.jsonとして保存しておけば、あとは通常通りterraform initterraform applyで適用できます。

makefileを作る

本当はterraformコマンドが標準入力からマニフェストを受け取ってくれたら楽だったのですが、適用するためにはファイルとして実体がある必要がある為、makeファイルにまとめておくと楽です。

つぎのようにシンプルなmakefileを作成しておくことで、terraform planをした際にまだjsonが生成されていなければyamlから自動的にjsonが生成されたのち、terraformコマンドが実行されるようになります。

SOURCES := $(wildcard *.yaml)

rendered.tf.json: $(SOURCES)
	yisp build . -o json > rendered.tf.json

init: rendered.tf.json
	terraform init

plan: rendered.tf.json
	terraform plan

apply: rendered.tf.json
	terraform apply

モジュール化する

次にモジュールを作成し、dev環境とprod環境から、それぞれ異なるパラメーターで呼び出せるようにしてみます。
この構成はkustomizeを利用したkubernetesマニフェスト管理の構成方法に強く影響を受けています。

次のようなディレクトリ構造を作ります。

📁 .
├── 📁 base
│   └── 📄 ec2.yaml
└── 📁 env
    ├── 📁 dev
    │   ├── 📄 index.yaml
    │   └── 📄 main.yaml
    │   ├── 📄 ec2.yaml
    └── 📁 prodなど...

base/ec2.yaml

例えば、今回の例では、同じAMIのEC2インスタンスを複数建てたいので、これをモジュール化するとしましょう。
base/ec2.yamlを次のように作成します。

base/ec2.yaml
!yisp &default
- lambda
- [props]
- !quote
  # こうするとモジュールを呼び出すたびにこのdataが定義されるが、
  # 最終的にマージされると1つにまとまるので問題ない
  data:
    aws_ami:
      amazon_linux:
        most_recent: true
        owners:
          - "137112412989"  # Amazon
        filter:
          - name: name
            values:
              - "amzn2-ami-hvm-*-x86_64-gp2"
  resource:
    aws_instance: !yisp
      - maps.make # オブジェクトのキーを変数から作る為に、maps.makeを使う
      - *props.name
      - !quote
        ami: "${data.aws_ami.amazon_linux.id}"
        instance_type: !yisp [default, *props.instance_type, "t2.micro"]
        tags:
          Name: *props.name

yispに慣れてない人には少々分かりづらいかもしれませんが、ここではlambda式を使って、propsという引数を受け取り、その中のnameとinstance_typeを使ってEC2インスタンスを定義しています。

env/dev/ec2.yaml

次に、baseで作ったモジュールをdev環境から呼び出してみましょう。

2つのEC2インスタンスを立てる例を示します。

env/dev/ec2.yaml
!yisp
- import
- [ec2, ../base/ec2.yaml] # baseのec2モジュールを名前付きでimportする
---

!yisp
- *ec2.default # baseのec2モジュールの関数defaultを呼び出す
- name: dev-server
  instance_type: t3.medium

---
!yisp
- *ec2.default
- name: dev-database
  instance_type: t3.medium

ここでyisp build ec2.yamlを実行すると、たしかに2つのEC2インスタンスが定義されたyamlが出力されます。

output
$ yisp build ec2.yaml
data:
  aws_ami:
    amazon_linux:
      most_recent: true
      owners:
        - "137112412989" # Amazon
      filter:
        - name: name
          values:
            - "amzn2-ami-hvm-*-x86_64-gp2"
resource:
  aws_instance:
    dev-server:
      ami: "${data.aws_ami.amazon_linux.id}"
      instance_type: t3.medium
      tags:
        Name: dev-server
---
data:
  aws_ami:
    amazon_linux:
      most_recent: true
      owners:
        - "137112412989" # Amazon
      filter:
        - name: name
          values:
            - "amzn2-ami-hvm-*-x86_64-gp2"
resource:
  aws_instance:
    dev-database:
      ami: "${data.aws_ami.amazon_linux.id}"
      instance_type: t3.medium
      tags:
        Name: dev-database

しかし、このyamlは2つのドキュメントに分かれてしまっているので、後でこれをマージして1つのオブジェクトにまとめる必要があります。

env/dev/main.yaml

dev環境用にterraformの基本設定を書いておきます

env/dev/main.yaml
terraform:
  required_providers:
    aws:
      source: hashicorp/aws
      version: ">= 3.0.0"
provider:
  aws:
    region: ap-northeast-1

env/dev/index.yaml

env/dev/index.yamlでは、main.yamlとec2.yamlを読み込み、1つのオブジェクトにまとめます。

env/dev/index.yaml
!yisp
- lists.reduce
- - include
  - main.yaml
  - ec2.yaml
- maps.merge

index.yamlでは、yispのlists.reduce関数を使って、includeした複数のyamlドキュメントをmaps.mergeでマージしています。

ここで、yisp build .を実行すると、次のような出力が得られます。

output
terraform:
  required_providers:
    aws:
      source: hashicorp/aws
      version: ">= 3.0.0"
provider:
  aws:
    region: ap-northeast-1
data:
  aws_ami:
    amazon_linux:
      most_recent: true
      owners:
        - "137112412989" # Amazon
      filter:
        - name: name
          values:
            - "amzn2-ami-hvm-*-x86_64-gp2"
resource:
  aws_instance:
    dev-server:
      ami: "${data.aws_ami.amazon_linux.id}"
      instance_type: t3.medium
      tags:
        Name: dev-server
    dev-database:
      ami: "${data.aws_ami.amazon_linux.id}"
      instance_type: t3.medium
      tags:
        Name: dev-database

複数に分かれていたオブジェクトが、1つにマージされて出力されていますね。
途中で重複して存在していたdataモジュールも、1つに集約されていることが確認できます。

特定リソースにパッチを当てる

例えば、dev-databaseインスタンスでmonitoring設定を有効にしたいとします。
通常であれば、baseモジュールを直接編集する必要がありましたが、今回はyispを使っているためこの環境のみにパッチを当てることができます。

次のようにec2.patch.yamlを作成します。

yispでは、maps.patch関数を使うことで、RFC 6902で定義されているJSON Patchと近い形式でオブジェクトにパッチを当てることができます。 (※maps.patchはRFC6902に加えて独自拡張が含まれております(後述))

env/dev/ec2.patch.yaml
op: add
path: "/resource/aws_instance/dev-database/monitoring"
value: true

そして、index.yamlでこのパッチを取り込みます。

!yisp
- maps.patch
- - lists.reduce
  - - include
    - main.yaml
    - ec2.yaml
  - maps.merge
- - include
  - ec2.patch.yaml
ちなみに、よりオススメな書き方

今回はパッチを当てるのみですが、次々に様々な操作を加えたい場合は、pipeline式を使うとネストを避けられて見通しが良くなります。

env/dev/index.yaml
!yisp
- pipeline
# 使用リソースを集める
- - lists.reduce
  - - include
    - main.yaml
    - ec2.yaml
  - maps.merge

# パッチを適用する
- - lambda
  - [x]
  - - maps.patch
    - *x
    - - include
      - ec2.patch.yaml

すると、yisp build .の出力は次のようになります。

output(一部抜粋)
resource:
  aws_instance:
    dev-server:
      ami: "${data.aws_ami.amazon_linux.id}"
      instance_type: t3.medium
      tags:
        Name: dev-server
    dev-database:
      ami: "${data.aws_ami.amazon_linux.id}"
      instance_type: t3.medium
      tags:
        Name: dev-database
      monitoring: true

たしかに、baseモジュールを直接編集することなく、dev環境のみにmonitoring設定を追加できました。

同種のリソースに一括でパッチを当てる

今回は特定のリソースにのみパッチを当てましたが、例えば、dev環境の全てのEC2インスタンスに対して一括でパッチを当てたい場合もあるでしょう。
これも、maps.patchを使うことで実現可能です。
ec2.patch.yamlに次のように追記します。

env/dev/ec2.patch.yaml
op: add
path: "/resource/aws_instance/dev-database/monitoring"
value: true
---
op: add
path: "/resource/aws_instance/*/tags"
value:
  Environment: Production
  Owner: TeamA
  Project: ProjectX

RFC6902ではワイルドカードはサポートされていませんが、yispのmaps.patchではpath中に*を使うことで、該当する全てのキーに対してパッチを適用できます。

これを適用すると、dev-serverとdev-databaseの両方に対してtagsが追加されます。

output(一部抜粋)
resource:
  aws_instance:
    dev-server:
      ami: "${data.aws_ami.amazon_linux.id}"
      instance_type: t3.medium
      tags:
        Name: dev-server
        Environment: Production
        Owner: TeamA
        Project: ProjectX
    dev-database:
      ami: "${data.aws_ami.amazon_linux.id}"
      instance_type: t3.medium
      tags:
        Name: dev-database
        Environment: Production
        Owner: TeamA
        Project: ProjectX
      monitoring: true

このように、yispを活用することでモジュールをある程度共通化しつつ、環境ごとの差分を柔軟に管理できるようになりました。

おわりに

今回はyispを利用してTerraformの定義をYAMLで記述することで、環境ごとの差分をパッチで管理する方法を紹介しました。

正直、実用的かどうかはまだ未知数ですが、Kubernetesのkustomizeのように「環境差分を後から吸収できる」仕組みをTerraformにも持ち込めるのは面白い発見でした。

興味があればぜひ手元で遊んでみてください。

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?