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についてはこちらの記事が詳しいです。

kenfdev.hateblo.jp

ConftestはYAMLあるいはJSONで定義された設定ファイルに対してテストを書けるというツールです。
面白いのは、テストに使うのがOpen Policy AgentのRegoというポリシー用の言語だという点です。

ConftestでOpenPolicyAgent/Regoを用いてポリシーを定義しておいてCIでConftestを実行すればポリシーの適用/強制ができそうです。

ConftestのリポジトリにはTerraformのコードをテストする例がありますのでそちらを参考にポリシーを書いてみます。

github.com

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 showJSON出力オプションを指定して実行することでtfファイルを間接的にテストするという方法を取っています。

例えば、以下のようなtfファイルがある場合、

resource sakuracloud_server "server" {
  name   = "example"
  core   = 1
  memory = 1
}

これを元にplanファイル生成〜terraform showJSON出力すると以下のようになります。

# 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出来ると効率がグッと上がりますのでどんどん活用していきたいです。

以上です。

参考情報