1
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 + Conftest で「許可されていないEC2はデプロイさせない」を実現する

1
Posted at

Terraform + Conftest で「許可されていないEC2はデプロイさせない」を実現する

はじめに

ある日、AWSの請求画面を開いて冷や汗をかいた。誰かが別リージョンにGPUインスタンスを立てていた。Terraformのコードレビューはしていたはずなのに、リージョンとインスタンスタイプの組み合わせまでは目が届いていなかった。

人の目に頼るガードレールは、いつか破れる。ならばコードで止めるしかない。Terraform の plan 出力を Conftest で機械的に検査すれば、ルール違反のデプロイはapplyの前に弾ける。実際にやってみたら、ポリシーのコードは30行に満たなかった。

やりたいこと

ルールは2つだけ。

  • リージョンは ap-northeast-1 だけ許可する
  • インスタンスタイプは t3.microt3.small だけ許可する

これに引っかかる Terraform コードは、plan の時点でブロックする。実際にAWSリソースを作るわけではないので、安全に試せる。

ディレクトリ構成

├─ terraform/
│  ├─ valid/main.tf      # ルールに沿った正常系
│  └─ invalid/main.tf    # わざとルールを破る異常系
├─ policy/
│  ├─ cost_guardrail.rego      # ポリシー本体
│  └─ cost_guardrail_test.rego # ポリシーのテスト

Terraform コード

正常系。東京リージョンに t3.micro を置くだけの最小構成にした。

# terraform/valid/main.tf
provider "aws" {
  region = "ap-northeast-1"
}

resource "aws_instance" "example" {
  ami           = "ami-0123456789abcdef0"
  instance_type = "t3.micro"
}

異常系。バージニアリージョンに GPU インスタンスを置く。明らかに違反する。

# terraform/invalid/main.tf
provider "aws" {
  region = "us-east-1"
}

resource "aws_instance" "example" {
  ami           = "ami-0123456789abcdef0"
  instance_type = "p3.2xlarge"
}

ポリシーを Rego で書く

Conftest は package main の中で deny というルールを定義すると、そこに入ったメッセージを違反として扱ってくれる。メッセージが1件でもあれば exit code 1 で落ちるので、CI に組み込みやすい。

# policy/cost_guardrail.rego
package main

import rego.v1

allowed_region := "ap-northeast-1"

allowed_instance_types := {"t3.micro", "t3.small"}

# リージョンが東京以外なら拒否
deny contains msg if {
    region := input.configuration.provider_config.aws.expressions.region.constant_value
    region != allowed_region
    msg := sprintf("Region '%s' is not allowed. Allowed region: %s", [region, allowed_region])
}

# インスタンスタイプが許可リスト外なら拒否
deny contains msg if {
    some rc in input.resource_changes
    rc.type == "aws_instance"
    some action in rc.change.actions
    action in {"create", "update"}
    instance_type := rc.change.after.instance_type
    not instance_type in allowed_instance_types
    msg := sprintf("Instance type '%s' is not allowed. Allowed types: t3.micro, t3.small",
                   [instance_type])
}

ポリシー自体は30行に満たない。許可値を変数に切り出しておけば、あとから t3.medium を追加したくなったときもすぐ対応できる。

テストを先に書く

ポリシーが正しく動くかどうかは、OPA の組み込みテスト機能で確認できる。test_ で始まるルールを定義し、with input as でモックデータを差し込む。

# policy/cost_guardrail_test.rego(抜粋)
package main

import rego.v1

# バージニアリージョンは拒否される
test_deny_invalid_region if {
    result := deny with input as {
        "configuration": {"provider_config": {"aws": {"expressions": {
            "region": {"constant_value": "us-east-1"}}}}},
        "resource_changes": [{
            "type": "aws_instance",
            "address": "aws_instance.example",
            "change": {"actions": ["create"],
                       "after": {"instance_type": "t3.micro"}},
        }],
    }
    count(result) > 0
    some msg in result
    contains(msg, "us-east-1")
}

# 東京リージョン + t3.micro なら何も引っかからない
test_allow_valid_region if {
    result := deny with input as {
        "configuration": {"provider_config": {"aws": {"expressions": {
            "region": {"constant_value": "ap-northeast-1"}}}}},
        "resource_changes": [{
            "type": "aws_instance",
            "address": "aws_instance.example",
            "change": {"actions": ["create"],
                       "after": {"instance_type": "t3.micro"}},
        }],
    }
    count(result) == 0
}

テストケースは全部で9つ。リージョン違反、インスタンスタイプ違反、両方同時の違反、deleteアクションのスキップ、aws_instance 以外のリソースの無視、複数インスタンスの個別評価をカバーしている。

$ opa test -v policy/
PASS: 9/9

動かしてみる

正常系

cd terraform/valid
terraform init
terraform plan -out=tfplan
terraform show -json tfplan > tfplan.json
conftest test tfplan.json -p ../../policy/
2 tests, 2 passed, 0 warnings, 0 failures, 0 exceptions

exit code は 0。何も引っかからず通過した。

異常系

cd terraform/invalid
terraform init
terraform plan -out=tfplan
terraform show -json tfplan > tfplan.json
conftest test tfplan.json -p ../../policy/
FAIL - tfplan.json - main - Instance type 'p3.2xlarge' is not allowed. Allowed types: t3.micro, t3.small
FAIL - tfplan.json - main - Region 'us-east-1' is not allowed. Allowed region: ap-northeast-1

2 tests, 0 passed, 0 warnings, 2 failures, 0 exceptions

exit code は 1。リージョンとインスタンスタイプの両方で弾かれた。

なぜ素の OPA ではなく Conftest を使うのか

OPA 単体でも同じことはできる。

opa eval -d policy/ -i tfplan.json "data.main.deny"

ただ、この場合は結果がJSON で返ってくるだけで、deny があっても exit code は 0 のまま。CI で落とすには自分で結果をパースして判定するスクリプトが要る。

Conftest はそのあたりを全部やってくれる。deny にメッセージが入っていれば FAIL 表示で exit code 1。入っていなければ PASS で exit code 0。Rego の書き方は変わらないのに、実行と判定の手間がなくなる。

ハマりどころ

delete アクションで落ちる

Terraform Plan JSON の resource_changes には、作成だけでなく削除も含まれる。削除対象のリソースは change.after が null になるので、そこから instance_type を取ろうとすると Rego 上で undefined になって挙動がおかしくなる。

対策は単純で、actionscreateupdate が含まれるリソースだけを見ればいい。

some action in rc.change.actions
action in {"create", "update"}

この1行がないだけで、リソース削除時にポリシーが誤作動する。地味だけど忘れると厄介なポイントだった。

リージョンはリソースの中にない

最初、resource_changes の中にリージョン情報があると思って探したが見つからなかった。Terraform Plan JSON ではリージョンはリソースの属性ではなく、Provider の設定に格納されている。

input.configuration.provider_config.aws.expressions.region.constant_value

このパスを知らないと、リージョンチェックの実装で止まる。terraform show -json の出力を一度じっくり眺めておくと、構造が掴めて楽になる。

replace の扱い

インスタンスタイプを変えるような更新では、Terraform は actions["update"] ではなく ["delete", "create"] にすることがある。リソースを作り直すためだ。今回は createupdate の両方をチェック対象にしているので、replace のケースも create 側で拾える。ここは意識しておかないと抜ける。

使ったツール

ツール バージョン
Terraform v1.5.7
Conftest 0.66.0
OPA v1.14.0

macOS なら brew install terraform conftest opa で揃う。

検証結果

項目 結果
OPA 単体テスト 9/9 PASS
正常系の Conftest 評価 deny なし
異常系の Conftest 評価 deny 2件

Rego のポリシーは30行弱、テストを含めても170行ほど。このくらいの分量で、リージョンとインスタンスタイプの制限が terraform plan の段階で効くようになる。CI/CD パイプラインの中に conftest test を1行入れるだけで、ルール違反のデプロイは自動的に止まる。

さいごに

やってみて感じたのは、Policy as Code は思ったより敷居が低いということだった。Rego の文法に慣れるまで少し戸惑ったものの、ポリシー自体は短く書ける。Conftest のおかげで実行まわりの面倒も省ける。

今回はリージョンとインスタンスタイプの2ルールだけだったが、同じ仕組みでタグの強制やストレージサイズの上限チェックも書ける。ルールを足すたびに .rego ファイルが少し伸びるだけで、テストも同じ要領で追加できる。

コードレビューで「ここ、リージョン合ってる?」と聞く手間がなくなるのは、地味だけど確実に楽になる。

1
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
1
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?