Terraformのレビューを自動化するために、Conftestを導入してGithub ActionsでCIまで設定してみる
English Version:
はじめに
メリークリスマス。eureka, inc. でSREをやっているnari/wapperです。
こちらは Calendar for terraform | Advent Calendar 2021 - Qiita として書きたかったものを成仏させるために書いています。(登録するのを忘れていました....)
弊社SRE Teamでは、自分たち自身がボトルネックにならないための活動方針推して、来年の大きなキーワードの一つにマイクロサービスレディを掲げる予定です。(マイクロサービス化していくわけではない)
詳しくは以下のSRE Team責任者の marnie0301 (@marnie0301) | Twitterの発表をご覧ください
それに伴い、Production Readiness checkとともに、移譲容易性/Self Serviceを促進するのに重要なピースとなるPolicy as Codeの運用をConftestで試し始めてみたので、そのお話を備忘録的に書いていこうかなと思います。
今回はTerraformのresource tagのルール(AWS reousrce)をRegoでポリシーを書きつつそのポリシー自体のテストまで書いて、その自動実行CIをGithub Actionsで整備する流れで紹介していきます。 (Conftest(Rego)の記述の話は多いけど、CI周りの設定まわりがあまり落ちていなく、何点かハマりポイントがあったので、これから試す方がはまらないように備忘録を残しておきたいと言うのがこの記事を書いている一番大きなモチベーションです)
対象読者
- Policy as Code気になってはいたけど始めるきっかけが欲しかった人
- ConftestをGithub ActionsのCIに落とし込む具体例が知りたい人
OPA/Rego/Conftestとは
OPA(Open Policy Agent) とはOSSの汎用ポリシーエンジンで、現在CNCFのGraduationプロジェクトです。OPAではRegoというポリシー記述言語でポリシーを書きます。OPAではこのRegoで書かれたポリシーとJSONなどの構造化データ(base document)に従って評価し結果も同様にJSONなどの構造化データ(virtual docment)で返します。Regoで記述したルール自体のテストも簡単に記述できるようになっているのも特徴です。
- 今年(2021) CNCFを卒業したてほやほやだったりします: Open Policy Agent Graduates at CNCF
- ソースコード: GitHub - open-policy-agent/opa: An open source, general-purpose policy engine.
ConftestはOPAのRegoで記述されたポリシーを元に、YAMLやJSONだけでなくHCLやDockerfileなどの様々な形式のファイルに対応しコマンドラインから簡単にテストを行うことができるツールです。
- Conftestは、もともとはOPAのRegoを用いたサードパーティツールからOPAコミュニティの傘下に入って正式にCLIツールとして広く利用されているツールのようですね
- ソースコード: GitHub - open-policy-agent/conftest: Write tests against structured configuration data using the Open Policy Agent Rego query language
- 今回はCIでの利用を想定しているので、こちらのツールを採用します
また今年は、25日間ひたすらOPA/Regoについて書く 狂気的な 素敵なアドベントカレンダーがあったりしてとても参考になるので是非そちらを一通り読むことをお勧めします。
Regoでポリシールールを記述して、ルール自体のテストも記述しながらCIへ組み込んでいくまで
ここからのサンプルコードはこちらにすべて置いております:
今回はterraformで作成するリソースが最低限のtag(name/owner/description)を持っていて、かつdata store resourceだった場合にdataという名前のtagにdataの重要性(high/middle/low)が設定されていることを確認するポリシールールを書いてみたいと思います。terraform planの結果をjsonに変換したものを入力値として、それを今から書くRegoのポリシールールでテストできるように、かつそのポリシールール自体もテストできるようにしていきます。(説明していくサンプルはAWSの使用を想定しています)
Conftest(OPA/Rego)のセットアップ
brew install conftest # opaもいれておく brew install opa
- EditorのPluginに関しては、IntelliJならopa plugin 、VSCodeなら opa plugin が使えます
- 他のEditorでもあると思うので探して入れてみてください
前提知識: Terraform plan 結果の構造
(https://speakerdeck.com/ryokbt/terraformfalserebiyuwoconftestdezi-dong-hua-suru?slide=20 参考)
このような構造をしており、このデータをinputとしてその内容をチェックするようなロジックをRegoで書いていきます。
ConftestでTerrafom resource tag ルールを書いてみる
- まず、main.regoでエントリーポイントを記述していきます。
# policy/main.rego package main //❶ import data.tags_validation //❷ ##################################### # Policy as Code Rules ##################################### deny_tags_contain_minimum_set[msg] { # Only target resources that have been changed/added. changeset := resources_not_no_op_action[_] //❸ not tags_validation.contain_proper_keys(changeset.change.after.tags) msg := sprintf("Invalid tags (missing minimum required tags: name,owner,description) for the following resource: %v", [changeset.address]) //❹ } deny_data_store_data_tag_is_proper[msg] { # Only target resources that have been changed/added. changeset := resources_not_no_op_action[_] # Only when resource_type is a data source/store type that can contain sensitive information tags_validation.is_data_tag_required_target_resource(changeset.type) tags_validation.not_has_proper_data_tag(changeset.change.after.tags.data) msg = sprintf("data tag needs to be set to one of [low,high,middle] resource: %v", [changeset.address]) } ##################################### # Utils ##################################### resources_not_no_op_action= {resource | resource := input.resource_changes[_]; resource.change.actions[_] != "no-op"} //❺ resources_with_type(resources, type) = all { all := [item | item := resources[_]; item.type == type] }
❶ ポリシーには必ず package 句が必要です。名前空間を分割するために任意のパッケージ名をつけることができます。
❷ import句で別の名前空間をimportしてきてそちらのリソースを使うことができます。tags_validationという名前空間の方でリソース独自のロジックを書いてそれをmain側で使用していきます
❸ ここで、actionが"no-op"でないリソースにリソースを絞り、それらのリソースをイテレーション([_]で記述する)で回してポリシー違反のリソースがないチェックします。これで新しく変更/追加があったリソースのみを対象とすることができます。また、この操作は基本どのポリシールールでも使用するので、共通の関数にして切り出しています。
❹ 同一ルール内では複数の評価式を記述でき、それらはANDで評価されるので、全てがtrueとなった場合のみdeny/violationと判定されてこのそれぞれのポリシールール用のエラーメッセージ配列にエラーメッセージが登録されテスト実行時に出力されます。
❺ RegoではPythonでおなじみの内包表記が使えるようになっています。文法としては [<結果として返す値> | <ルール>] となっていて、ルールに当てはまるリソースだけを返します。
- 次にmainで使っていた、tags_validationロジックを記述していきます
# policy/tags.rego package tags_validation minimum_tags = {"name", "owner", "description"} contain_proper_keys(tags) { # Subtract the key list of the given tags from the minimum tag list, and if there is no more left, you have the minimum tag. keys := {key | tags[key]} leftover := minimum_tags - keys leftover == set() } not_has_proper_data_tag(value) { // ❶ value != "low" value != "middle" value != "high" } is_data_tag_required_target_resource(type) { // ❷ type == "aws_dynamodb_table" } is_data_tag_required_target_resource(type) { type == "aws_s3_bucket" }
❶ 同一ルール内での評価式はANDになるので"low"でも"middle"でも"high"でもない場合にtrueが返ります
❷ 同一ルール内での評価式はANDで評価されるので、ORの論理を記述したい場合はis_data_tag_required_target_resourceのように同名の複数の処理を書くことで実現が可能です。
ConftestでRegoで書いたルール自体のテストを書いて、実行してみる
- まずmain.regoのテストを書いていきます。こちらはGoなどと同様に${test_target_file_name}_test.regoという風な命名をします。
# policy/main_test.rego package main ##################################### # Tests of Policy as Code Rules ##################################### ... test_tags_contain_minimum_set { plan := ` // ❶ resource_changes: - name: case normal address: module.one type: aws_security_group_rule change: actions: - create after: tags: name: hoge owner: piyo description: for test ` input := yaml.unmarshal(plan) deny_tags_contain_minimum_set == set() with input as input // ❷ } ...
❶ test用のinputは、公式などではjsonで記述されていますが、jsonのメンテナンスはつらいので、yamlで記述管理してjsonに変換して使うと良さそうです。
❷ ルールのテストはこのように記述することができます。配列のメッセージ内容までチェックしなくていい場合は、そのエラーメッセージ配列が空なのか、そうじゃないのかをチェックするだけでも良さそうです。
- 次にtags.regoの単体テストを書いていきます
# tag_rest.rego package tags_validation test_contain_proper_keys { tags := {"name": "test", "owner": "hoge", "description": "normal test"} contain_proper_keys(tags) // ❶ } ...
❶ mainのルール側で使っている関数のユニットテストはこのようにおこない、trueがかえればpassと判断されます
- ここで書いたテストは、以下のコマンドで簡単に実行が可能です
# カレントディレクトリ配下のConftestのルールに対するテストを実行します
conftest verify ./
Conftestを実行するCIをGithub Actionで整備する
Conftest/Regoで書いたポリシールール自体のfmt/verifyのCIの設定
- Regoで記述してConftestのコード自体のfmt/verifyのCIをまず設定します
name: conftest-fmt-and-verify-all on: pull_request: branches: [ main ] paths: - 'policy/**' env: CONFTEST_VERSION: 0.28.3 jobs: terraform: name: fmt-all runs-on: ubuntu-latest defaults: run: shell: bash steps: - name: Checkout uses: actions/checkout@v2 - name: Install conftest run: | wget -O - 'https://github.com/open-policy-agent/conftest/releases/download/v${{ env.CONFTEST_VERSION }}/conftest_${{ env.CONFTEST_VERSION }}_Linux_x86_64.tar.gz' | tar zxvf - ./conftest --version - name: conftest fmt run: | git add . && ./conftest fmt ./ && git diff --exit-code ./. // ❶ - name: conftest verify run: | ./conftest verify ./ // ❷
❶ terraformのvalidationのようなフラグがまだないので、conftest fmtの結果で差分が出たらテストが落ちるようにしています
❷ ここで先ほど書いたRegoで記述したポリシールールのテストを実行しています
Conftest testでTerraform plan結果をテストするCIの設定
- 最後に、GIthub Action CIで記述したConftestのポリシールールで、Terraformコードを自動でチェックするようにしてみます
name: tf-plan-apply on: pull_request: branches: [ main ] env: TF_VERSION: 1.0.0 CONFTEST_VERSION: 0.28.3 WORKING_DIR: ./ jobs: terraform: name: aws-eureka-pairs-etc-s3 runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 - name: Install conftest run: | wget -O - 'https://github.com/open-policy-agent/conftest/releases/download/v${{ env.CONFTEST_VERSION }}/conftest_${{ env.CONFTEST_VERSION }}_Linux_x86_64.tar.gz' | tar zxvf - ./conftest --version //❶ - name: Setup Terraform uses: hashicorp/setup-terraform@v1 with: terraform_wrapper: false //❷ terraform_version: ${{ env.TF_VERSION }} cli_config_credentials_token: ${{ secrets.YOUR_CRED_NAME}} - name: Terraform Init ${{ env.WORKING_DIR }} working-directory: ${{ env.WORKING_DIR }} run: terraform init - name: Terraform Plan ${{ env.WORKING_DIR }} if: github.event_name == 'pull_request' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} working-directory: ${{ env.WORKING_DIR }} id: plan run: terraform plan -out=tfplan -no-color -lock=false -parallelism=50 - name: Convert terraform plan result to json formmat if: github.event_name == 'pull_request' id: convert working-directory: ${{ env.WORKING_DIR }} run: terraform show -json tfplan > tfplan.json - name: conftest test if: github.event_name == 'pull_request' id: conftest run: ./conftest test --no-color ${{ env.WORKING_DIR }}/tfplan.json //❸
❶ 個人の公開している、Actionsできちんとメンテナンスされているものがなかったのでwgetでconftestをインストールするようにしています。
❷ 公式のhashicorp/setup-terraform を使っていると、デフォルトだとこの値がtrueになっていてplan結果が適切なjsonでshowできないのでfalseにしなければいけないことには注意が必要です。
❸ ここで先ほど記述したポリシールールでterraform plan結果をtestしていきます。使用するポリシーの読み込みは、デフォルトではpolicy/
をみにいきます。別のディレクトリを参照させたい場合は、-p
で渡して設定することができます。
終わりに
Conftest/OPA/Rego周りをいじってみての率直な感想としては、型信者なのでデータ定義でスキーマが欲しいなぁとか思ったり、論理評価の癖がかなり強くかつ表現力もかなり豊かなので、裏を返せばわかりやすくシンプルに書こうとしないとすぐにチームでメンテできなくなりそう、、と言う感じなので適切な設計/コード規約とともにメンテナビリティを意識して運用していきたいですね。
あとはなんといっても自分のコーディングスタイル的にもPolicy as Code自体のテストが簡単に書けるのは大きい。自信を持って、Regoのコードをがしがし変更/追加/リファクタしていけます。
また、ディレクトリ構成の戦略もいまだに各社模索していそうな雰囲気を感じるのでここらへんも試行錯誤しながらいい感じのところに落とし込んでいきたいです。
今後はEKSへのPlatform移行とともにKubernetes Manifestにも適応していったり、GitHub - open-policy-agent/gatekeeper: Gatekeeper - Policy Controller for Kubernetes の導入を検討したり、かなりPolicy as Codeを推し進めていきたいと考えているので、ここら辺やっていきたい方は是非弊社で一緒にやっていきましょう〜
Meetyもやってるのでお気軽のどうぞ!:
参考文献
- OPA / Rego Advent Calendar 2021
- TerraformのレビューをConftestで自動化する
- OPA language-cheatsheet
- conftest
- OPA/Rego playground
- terraform plan 結果の検証を自動化するぞ! with Conftest
- validating-terraform-with-conftest
- Terraform x OPA/Conftest の tips
- Quipper における Rego の活用事例
- Open Policy Agent Rego Knowledge Sharing Meetupを開催しました #opa_rego
- Conftestを用いたCIでのポリシーチェックの紹介
- メルペイのエンジニアが考えるOpen Policy AgentとSpinnakerで実現するマイクロサービスの安全な継続的デリバリー