GitHub Actions で Terraform の plan 結果を Conftest(OPA)でポリシーチェックしてみた
GitHub Actions で terraform plan の JSON を Conftest(OPA)でポリシーチェックし、違反があれば CI を落とす仕組みを作ってみました。
「本番のセキュリティグループを 0.0.0.0/0 に開けない」「必須タグが無いリソースは作らせない」のような組織の決まりを、機械的に強制する仕組みです。
以前OpenTaco + Conftest を使ってやってみましたが、今回はOpenTaco無しConftest単体でやってみます。
前提
このワークフローは aws-actions/configure-aws-credentials で OIDC 認証し、GitHub Actions から IAM ロールを AssumeRole する構成です。
実行する前に、AWS側でOIDCのIAMロールを準備し、ロールのARNをリポジトリのSecret AWS_ROLE_TO_ASSUME に登録しておく必要があります。
IAMロールの作成手順は以下の記事にまとめています。
この記事は環境ごとにロールを分ける構成ですが、OIDCプロバイダーの参照とIAMロールの信頼ポリシー作成の部分は、今回のような単一ロール構成でもそのまま使えます。
ポリシー(Rego)作成
今回はルールを3つ用意しました。
- セキュリティグループの ingress に
0.0.0.0/0があれば deny - 必須タグ(
Environment/Owner)が無い AWS リソースを deny - 暗号化されていない EBS ボリュームを deny
各ルールは以下です。
package main
deny contains msg if {
some change in input.resource_changes
change.type == "aws_security_group"
some action in change.change.actions
action in {"create", "update"}
some ingress in change.change.after.ingress
some cidr in ingress.cidr_blocks
cidr == "0.0.0.0/0"
msg := sprintf("%s: ingress open to 0.0.0.0/0 (port %d)", [change.address, ingress.from_port])
}
package main
required_tags := {"Environment", "Owner"}
taggable_types := {"aws_vpc", "aws_security_group", "aws_ebs_volume", "aws_instance"}
deny contains msg if {
some change in input.resource_changes
taggable_types[change.type]
some action in change.change.actions
action in {"create", "update"}
tags := object.get(change.change.after, "tags", {})
missing := required_tags - {k | some k, _ in tags}
count(missing) > 0
msg := sprintf("%s %s: required tags missing: %v", [change.type, change.address, missing])
}
package main
deny contains msg if {
some change in input.resource_changes
change.type == "aws_ebs_volume"
some action in change.change.actions
action in {"create", "update"}
not change.change.after.encrypted
msg := sprintf("%s: EBS volume not encrypted", [change.address])
}
3つとも input.resource_changes[] の change.after を見て、actions に create または update が含まれる変更を対象にしています。新規作成や置き換えだけでなく、既存の SG に 0.0.0.0/0 の ingress を追記するような in-place update も検知対象です。
GitHub Actions ワークフロー作成
実際に動かしたワークフローです。
name: terraform-opa-check
on:
pull_request:
types: [opened, synchronize]
paths:
- "terraform-conftest-opa/**"
permissions:
contents: read
pull-requests: write
id-token: write
jobs:
opa-check:
runs-on: ubuntu-latest
defaults:
run:
working-directory: terraform-conftest-opa
steps:
- uses: actions/checkout@v7
- uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
aws-region: ap-northeast-1
- uses: hashicorp/setup-terraform@v4
- run: terraform init
- run: terraform plan -out=tfplan
- run: terraform show -json tfplan > plan.json
- shell: bash
run: |
docker run --rm -v "$PWD":/project \
openpolicyagent/conftest:v0.68.2 \
test plan.json --policy policy --output json | tee conftest-result.json
- if: always()
uses: actions/upload-artifact@v7
with:
name: conftest-result
path: terraform-conftest-opa/conftest-result.json
working-directory: terraform-conftest-opa にしているので、$PWD はこのサブディレクトリを指します。
Conftest 専用の公式 GitHub Actions アクションはありません。OPA org が出している setup-opa は opa バイナリ向けで、conftest は別バイナリのため入りません。
そこで OPA org 公式の Docker イメージ openpolicyagent/conftest を docker run で実行することにしました。
conftest test の結果は --output json で conftest-result.json に残し、actions/upload-artifact でアーティファクト化しています。
| tee conftest-result.json の部分に注意です。
デフォルトでは、bash -e {0}で実行され pipefail が無効になります。
パイプの終了コードは常に exit 0 で終わる tee のものになり、Conftest が違反で失敗してもジョブに伝わりません
shell: bash を明示すると pipefail が自動で有効になるため、上記の対策として設定しました。
GitHub Docs: defaults.run.shell
動作確認
違反ありと違反なしのTerraformコードを用意して動作を確認しました。差分は main.tf の以下の部分です。
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
tags = {
- Name = "${var.instance_name}-vpc"
+ Name = "${var.instance_name}-vpc"
+ Environment = "dev"
+ Owner = "sato.masaki"
}
}
resource "aws_security_group" "web" {
name_prefix = "${var.instance_name}-sg-"
description = "Security group for ${var.instance_name}"
vpc_id = aws_vpc.main.id
ingress {
- description = "HTTP"
+ description = "HTTP from office"
from_port = 80
to_port = 80
protocol = "tcp"
- cidr_blocks = ["0.0.0.0/0"]
+ cidr_blocks = ["203.0.113.0/24"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
- Name = "${var.instance_name}-sg"
+ Name = "${var.instance_name}-sg"
+ Environment = "dev"
+ Owner = "sato.masaki"
}
}
resource "aws_ebs_volume" "data" {
availability_zone = "ap-northeast-1a"
size = 10
+ encrypted = true
tags = {
- Name = "${var.instance_name}-vol"
+ Name = "${var.instance_name}-vol"
+ Environment = "dev"
+ Owner = "sato.masaki"
}
}
| 内容 | 結果 |
|---|---|
違反あり plan(- 側) |
failure(意図通り) |
必須タグ・SG ingress・EBS暗号化をすべて修正した plan(+ 側) |
success(意図通り) |
違反あり plan(- 側)では、conftest-result.json に5件の failures が出ます。
[
{
"filename": "plan.json",
"namespace": "main",
"successes": 0,
"failures": [
{
"msg": "aws_security_group.web: ingress open to 0.0.0.0/0 (port 80)",
"metadata": { "query": "data.main.deny" }
},
{
"msg": "aws_ebs_volume.data: EBS volume not encrypted",
"metadata": { "query": "data.main.deny" }
}
]
}
]
修正後の plan(+ 側)では、conftest-result.json の中身もシンプルになりました。
[
{
"filename": "plan.json",
"namespace": "main",
"successes": 3
}
]
おわりに
GitHub Actions で terraform plan の JSON を Conftest 単体でポリシーチェックする仕組みを試してみました。
OPA org 公式の Docker イメージを使うだけで動くので、専用アクションが無くても導入のハードルは低かったです。






