
conftest は、Open Policy Agent (OPA) の Rego 言語を使って、構造化データ(YAML、JSON、Terraform、Dockerfile など)に対するポリシーテストを実行するCLIツールです。
よくあるユースケースとして、Kubernetes マニフェストや Terraform の設定ファイルに対して「コンテナは root で実行してはいけない」「すべてのリソースにタグが必要」といったルールの適用です。これらのルールを組織ポリシーとして定義して、それに適合しているかをconftestで検査できます。conftestをCIに組み込めば、PRに含まれた変更が問題ないかどうかを機械的にチェックできるというわけです。
これまでの10Xでも、Regoを書きconftestを使って検査してきていましたが、Rego言語のとっつきにくさ/学習コストの高さがネックでポリシーの追加が億劫でした。しかし最近はLLMがかなりマチュアになり記述コストがゼロになったので年末に一気に再整備しました。
conftest以外にも社内の組織ポリシーを評価してconfig類にapplyする手段はありそうですが、自然言語(AIによるRego記述)を通して「こう書いてほしい」を強制できているため今はconftestがお気に入りです。AIによる検査は、自然言語のみだと評価結果にゆらぎが出る可能性がありますが、RegoというDSLを通すことでAIと人間の共通プロトコルができ、毎回必ず同じ評価結果を得られます。conftestを使うことでレビューで人間が同じ指摘をする必要もなくなるし、例えば「ここハイフンを使ってほしくないんだよなぁ」みたいなもう申し訳ないnitsレビューもconftestを通して代弁することができます。裏を返せばtestが通ってconftestも通っているなら、問答無用にApproveできる状態というのが理想かもしれません。
今回は再整備して追加したポリシーなどを含めて、10Xでどんなポリシーを定めているのかを一部紹介します。
HCLファイル検証 vs plan検証
Terraformの設定ファイルに対するポリシーチェックを実行する場合、conftestをいつ実行するかというのは悩めるポイントの1つです。1つはtfファイルに対する実行で、これはコードの構造そのものをチェックするHCLファイル検証(Static HCL validation)です。もう1つは、plan結果(tfplan.json)に対する実行で、これは実際の変更内容を踏まえた動的な検証となるplan検証(Plan-time validation)になります。
10Xでは、チェック内容に応じて以下のように使い分けています。
HCLファイル検証は terraform plan を実行する前の段階で、Terraformのコード(.tfファイル)を直接読み込んでチェックします。リソース定義の妥当性を検証するもので、ファイル名とリソースタイプの一致、リソース名の命名規則、必須フィールドの有無、セキュリティ設定の不備などをチェックします。terraform planにいく前に実行するため違反があった場合CIの早い段階でフィードバックできます。
一方、plan検証は terraform plan の実行結果に対してチェックします。実際の実行計画に基づいた検証ができるため、変更アクション(create/update/delete)に応じた異なる検証ロジックを適用できます。例えば、重要なリソースが削除される際の承認フロー確認、既存のIAMメンバーが誤って削除されていないかの検証、リソース間の依存関係を考慮した変更順序のチェックなどです。ただし plan の実行が必要なので結果を見るにはplanの終了を待つ必要があります。
Policy warning vs Policy violation
warning (warn) と violation (deny) の使い分けはCIを落としたいかどうかです。このポリシーは絶対に守ってほしいというものはdenyとして設定し、なんとなく知っておいてほしいというものはwarnにすることでインラインコメントは残すけどexit 0として通すようにしています。

10Xで利用しているポリシー
ファイル名とリソースタイプの一致チェック (file_resource_match.rego)
Terraformファイルの命名規則を強制するポリシーです。例えば google_storage_bucket リソースは google_storage_bucket.tf に配置する必要があります。ファイル名とリソースタイプが一致していない場合に違反として検出します。
10XではTerraform導入時からリソース名とファイル名の一致を徹底してきました。このルールにより、どのファイルにどのリソースが定義されているかが明確になり、コードを追加・変更する際も迷いがなくなるし探す手間もなくなる重要なルールです。

リソース名のハイフン使用チェック (hyphen_in_resource_name.rego)
Terraformリソース名にハイフンが含まれている場合に違反として検出します。Terraformのベストプラクティスでは、リソース名にはアンダースコアを使用し、ハイフンは避けるべきとされています。
参考:
- Style Guide - Configuration Language | Terraform
- Best practices for general style and structure | Terraform

重要リソースのprevent_destroy設定チェック (missing_prevent_destroy.rego)
重要なリソースに対して prevent_destroy が設定されているかをチェックします。対象リソースは google_storage_bucket、google_secret_manager_secret、google_compute_network、google_service_account、google_bigquery_dataset など、誤って削除するとサービスに重大な影響を与える可能性があるものです。
このポリシーにより、terraform apply 時の誤操作によるリソース削除を防止できます。特に本番環境では、データやネットワーク構成の削除は致命的な障害につながるため、lifecycle { prevent_destroy = true } の設定を必須化しています。

パートナー名のコード混在チェック (partner_name_in_code.rego)
stailer ディレクトリ内のコードにパートナー名が含まれていないかをチェックします。パートナー固有のリソースは stailer-{partner_name} のような専用ディレクトリに配置すべきで、共通の stailer ディレクトリにパートナー名を含めるべきではありません。
このポリシーは、マルチテナント構成におけるコードの整理と保守性を目的としています。パートナー固有のコードが汎用ディレクトリに混在すると、コードベースが複雑になり、他のパートナーへの影響範囲の把握が困難になります。既存の違反リソースについては除外リストで管理し、新規追加を防いでいます。
Storageバケットのライフサイクルルール設定チェック (storage_bucket_lifecycle.rego)
Cloud Storage バケットにライフサイクルルールが設定されているかをチェックします。対象は backup、log、archive、temp などの名前を含むバケットで、時間経過とともにデータが蓄積されることが想定されるものです。これらは10X内でよく使われるテンポラリなユースケースのバケット名から採取しました。
ライフサイクルルールを設定することで、古いオブジェクトの自動削除やストレージクラスの変更が可能になり、ストレージコストを最適化できます。

Storageバケットの統一アクセス設定チェック (storage_bucket_uniform_access.rego)
Cloud Storage バケットで uniform_bucket_level_access が有効になっているかをチェックします。Google Cloud では、ACL(アクセスコントロールリスト)は legacy な方式とされており、IAM のみで統一的にアクセス管理を行う uniform bucket-level access が推奨されています。

Storageバケットのバージョニング設定チェック (storage_bucket_versioning.rego)
Cloud Storage バケットでバージョニングが有効になっているかをチェックします。ただし temp や tmp、dataflow、staging といった一時的なデータを扱うバケットは除外されます。これらのデータの過去バージョンは不要だからですね。
バージョニングを有効にすることで、誤削除や意図しない上書きからデータを保護できます。削除されたオブジェクトや上書きされたオブジェクトの以前のバージョンを保持し、必要に応じて復元が可能になります。

個人ユーザーへの権限付与チェック (user_primitive_role.rego)
roles/owner1やroles/editor2といった強力な権限が個人ユーザーに付与されていないかをチェックします。基本的に、人間プリンシパルが持つ強力な権限はすべてPAM(Privileged Access Management)に寄せていき、定常状態で付与されっぱなしになることを避けるようにしています。どうしてもという場合はconditionでexpireを設定し時限的になるように努めています。

プリンシパルの重複チェック (duplicate_iam_member_consolidation.rego)
google_project_iam_member リソースで同じメンバーに対して複数のロールを付与している場合に、for_each を使って1つのリソースにまとめることを強制するポリシーです。
例えば、同じGoogleグループやサービスアカウントに対して roles/viewer、roles/editor、roles/storage.admin の3つのロールを付与する場合、3つの個別リソースとして定義するのではなく、for_each を使って1つのリソースで管理する必要があります。個別のリソースとして定義してしまうと、権限の全体像が把握しづらくなり、変更時に漏れが発生しやすくなります。
このポリシーにより、同じメンバーの権限を1箇所で管理でき、コードの可読性と保守性が向上します。なお、IAMの条件付きバインディング(期限付き権限など)で condition ブロックを使用している場合は、for_each では個別の条件を設定できないため、このポリシーの対象外となります。

「こう書いたら良いよ」のような例を示したほうが良い場合はポリシーガイドへのリンクを付与するようなメッセージを出しています:

こうすることで、terraformのstateでも、
- google_project_iam_member.developer_viewer
- google_project_iam_member.developer_editor
- google_project_iam_member.developer_storage_admin
ではなく、
- google_project_iam_member.developer["roles/viewer"]
- google_project_iam_member.developer["roles/editor"]
- google_project_iam_member.developer["roles/storage.admin"]
のようにインデックス管理になるため見通しが良くなります(現時点ではremovedブロックでインデックス個別に消せないという気になりポイントもありますが見通し優先してのこのルールです)。
GitHub組織外ユーザーチェック (invalid_github_member.rego)
GitHub 組織のメンバーリストと照合し、組織外のユーザーがリポジトリやチームに追加されていないかをチェックします。このポリシーは少し特殊で、事前にGitHub組織の人間リストを貰う必要があります。conftest自体にロジックを書くことはできないので、外部で計算したリストを渡す必要があるためです。conftest実行前に gh api "/orgs/myorg/members" --jq '.[].login' を実行してメンバーリストのJSONを作成し、 --data ${{ inputs.data_file }} で受け取るようにしています。

まとめ
LLMのおかげでRegoの記述コストが劇的に下がったことで、これまで億劫だったポリシーの追加を一気に進めることができました。今回このブログ記事にて紹介したポリシーはすべてHCLファイル検証の例ですが、plan検証ではどんなチェックをしているか今回の記事の反応が良ければ書いてみようと思います😄
ポリシーによる自動レビューを導入したことで、人間のレビュアーはより本質的な設計やアーキテクチャの議論に集中できるようになりました。また、PRを出す側も機械的にチェックできる項目は事前に検証されているため、安心してレビューを依頼できます。今後はゼロコストになったRegoを使って人間レビューゼロを目指す気持ちでポリシー追加していきます。Regoをどう書くかという話ではなく、この組織にはどんなポリシーがあるかというのは組織によって十人十色だと思うので、みなさんの組織ポリシーもぜひ教えてください。
-
Owner は Legacy basic role となり、代わりに
roles/adminの使用が推奨されています。 https://docs.cloud.google.com/iam/docs/roles-overview#legacy-basic↩ -
Editor は Legacy basic role となり、代わりに
roles/writerの使用が推奨されています。 https://docs.cloud.google.com/iam/docs/roles-overview#legacy-basic↩