ConftestでOpenPolicyAgent/Regoを使いTerraformのコードにポリシーを適用してみる
今日はConftestを用いてTerraformでのインフラコードにポリシーを適用してみます。
TerraformでのインフラコードのUnitTest
terraform validate
での構文チェック
Terraformではtfファイルの構文チェックを行ってくれるterraform validate
コマンドが提供されています。
実行するとtfファイルの構文誤りやパラメータ名間違いなどを検出してくれます。
$ terraform validate Error: Unsupported argument ← パラメータ名間違い on test.tf line 6, in data "sakuracloud_server" "server": 6: name_selectorsa = ["sakura-dev"] An argument named "name_selectorsa" is not expected here. Did you mean "name_selectors"?
これを利用すれば最低限tfファイルとして正しく書けているかのテストが行えます。
特にTerraform v0.12からは以下のように変数に型情報を与えることが出来るようになっており、モジュール利用時などにより厳密なチェックが行えるようになりました。
# var.nodesはaddressとuserというフィールドを持つオブジェクトのリストしか指定できない variable "nodes" { type = list(object({ address = string, user = string, })) }
ポリシーの適用
テストの際、terraform validate
での構文チェック以外にも様々な制約を課したい/ポリシーを適用したいことが多々あります。
例えば、
といった場合です。
Terraform EnterpriseであればSentinelを用いてポリシーの適用が行えるのですが、OSS版の場合、現時点ではSentinelを利用できません。
そこで今回はConftestを用いてポリシーの適用を行ってみました。
Conftest
Conftestについてはこちらの記事が詳しいです。
ConftestはYAMLあるいはJSONで定義された設定ファイルに対してテストを書けるというツールです。
面白いのは、テストに使うのがOpen Policy AgentのRegoというポリシー用の言語だという点です。
ConftestでOpenPolicyAgent/Regoを用いてポリシーを定義しておいてCIでConftestを実行すればポリシーの適用/強制ができそうです。
ConftestのリポジトリにはTerraformのコードをテストする例がありますのでそちらを参考にポリシーを書いてみます。
conftestでのterraformインフラコードのテスト
Conftestはコマンドラインツールとなっており、以下のように実行することでYAML/JSONファイルのテストが行えます。
$ conftest test <file>
デフォルトではカレントディレクトリのpolicy
ディレクトリ配下を参照するようになっていますのでこちらにポリシーファイル(.rego)を作成していきます。
(この挙動は-p
または--policy
オプションで変更可能です)
tfファイルをどうやってテストするの?
TerraformでのインフラコードはJSONでも記載できますが、通常はHCL(.tf)で記載していると思います。 conftestはHCLを読んでくれませんのでどうにかしてJSON/YAMLに変換する必要があります。
ConftestのリポジトリにあるTerraformの例ではplanファイルを出力した上でterraform show
にJSON出力オプションを指定して実行することでtfファイルを間接的にテストするという方法を取っています。
例えば、以下のようなtfファイルがある場合、
resource sakuracloud_server "server" { name = "example" core = 1 memory = 1 }
これを元にplanファイル生成〜terraform show
をJSON出力すると以下のようになります。
# planファイルを出力 $ terraform plan --out plan.tfplan # terraform showをJSONで出力 $ terraform show -json plan.tfplan | jq . { "format_version": "0.1", "terraform_version": "0.12.1", "planned_values": { "root_module": { "resources": [ { "address": "sakuracloud_server.server", "mode": "managed", "type": "sakuracloud_server", "name": "server", "provider_name": "sakuracloud", "schema_version": 1, "values": { "additional_nics": null, "commitment": "standard", "core": 2, "description": null, "disable_pw_auth": null, "graceful_shutdown_timeout": 60, "hostname": null, "icon_id": null, "interface_driver": "virtio", "memory": 4, "name": "test", "nic": "shared", "note_ids": null, "password": null, "private_host_id": null, "ssh_key_ids": null } } ] } }, "resource_changes": [ { "address": "sakuracloud_server.server", "mode": "managed", "type": "sakuracloud_server", "name": "server", "provider_name": "sakuracloud", "change": { "actions": [ "create" ], "before": null, "after": { "additional_nics": null, "commitment": "standard", "core": 2, "description": null, "disable_pw_auth": null, "graceful_shutdown_timeout": 60, "hostname": null, "icon_id": null, "interface_driver": "virtio", "memory": 4, "name": "test", "nic": "shared", "note_ids": null, "password": null, "private_host_id": null, "ssh_key_ids": null }, "after_unknown": { "additional_display_ipaddresses": true, "cdrom_id": true, "disks": true, "display_ipaddress": true, "dns_servers": true, "gateway": true, "id": true, "ipaddress": true, "macaddresses": true, "nw_address": true, "nw_mask_len": true, "packet_filter_ids": true, "private_host_name": true, "tags": true, "vnc_host": true, "vnc_password": true, "vnc_port": true, "zone": true } } } ], "configuration": { "root_module": { "resources": [ { "address": "sakuracloud_server.server", "mode": "managed", "type": "sakuracloud_server", "name": "server", "provider_config_key": "sakuracloud", "expressions": { "core": { "constant_value": 2 }, "memory": { "constant_value": 4 }, "name": { "constant_value": "test" } }, "schema_version": 1 } ] } } }
これをconftestコマンドに読ませることでテストを行います。
ポリシーの作成
今回は試しにサーバのコア数は2以上、メモリは4GB以上を指定しないといけないというポリシーにしてみます。
ポリシーファイルは以下の内容でpolicy/instance_type.rego
というファイルを作成します。
package main # コア数は2以上であること deny[msg] { resource := input.resource_changes[index] resource.type == "sakuracloud_server" resource.change.after.core < 2 msg = "sakuracloud_server.core must be greater than 2" } # メモリは4GB以上であること deny[msg] { resource := input.resource_changes[index] resource.type == "sakuracloud_server" resource.change.after.memory < 4 msg = "sakuracloud_server.memory must be greater than 4" }
テスト実行(ポリシー適用)
それでは早速conftestでテストを実行(ポリシーを適用)してみます。
Makefileの作成
プランファイルの出力~JSONへの変換は毎回コマンド入力するのも大変なのでMakefileでまとめておきます。
NAME := myproject COMMAND := terraform PLAN = $(NAME)-plan.tfplan SHOW = $(NAME)-show.json CODE = $(NAME).tf all: test plan: $(PLAN) $(PLAN): $(CODE) $(COMMAND) plan -out $(PLAN) show: $(SHOW) $(SHOW): plan $(COMMAND) show -json $(PLAN) > $(SHOW) test: show cat $(SHOW) | conftest test - clean: @rm -f $(PLAN) $(SHOW) .PHONY: plan show test all clean
これでmake
するだけでテスト可能になります。
なお、このMakefileだと中間ファイルが作成されますので必要に応じてcleanを実行したり.gitignoreに追記したりしておいてください。
実行
現時点でのtfファイルは以下の通りです。
resource sakuracloud_server "server" { name = "example" core = 1 memory = 1 }
ポリシーである2コア以上かつ4GBメモリ以上に反しているのでエラーとなるはずです。
$ make # [中略] cat example-show.json | conftest test - sakuracloud_server.core must be greater than 2 sakuracloud_server.memory must be greater than 4 make: *** [test] Error 1
エラーになりましたね!
今度はtfファイルを修正して実行してみます。
resource sakuracloud_server "server" { name = "example" core = 2 memory = 4 }
# [中略] cat example-show.json | conftest test -
今度はエラーとなりませんでしたね。
あとは必要に応じてポリシーを充実させ、CIに組み込めば良さそうです。
終わりに
Open Policy Agent/Regoを初めて使ってみましたがなかなか面白いですね。 TerraformのようなインフラコードのテストでShift left testing出来ると効率がグッと上がりますのでどんどん活用していきたいです。
以上です。
参考情報
- Conftest: https://github.com/instrumenta/conftest
- Open Policy Agent: https://www.openpolicyagent.org/
- Rego Deep Dive: https://www.slideshare.net/TorinSandall/rego-deep-dive
- Open Policy Agentを始めてみよう: https://kenfdev.hateblo.jp/entry/2019/03/19/092927
- Unit Test好きにはたまらない!Conftestで設定ファイルのテストをしてみた: https://kenfdev.hateblo.jp/entry/2019/05/31/194614