Terraform Cloud Run Tasks (GA) と自前の Open Policy Agent サーバーを連携して遊んでみる
ちなみに…
「Terraform Cloud/Enterprise で OPA をネイティブにサポートしてほしい!」というリクエストは数多く寄せられていて、時期こそお伝えできないものの、サポートする動きは進んでいるようです。
Terraform Cloud Run Tasks
先日 Terraform Cloud に新たに Run Tasks という機能が追加され、正式に GA となりました。
ざっくりどういうものかと言うと、plan
と apply
の間のステージで任意のロジックを実行し、その結果によって実行を失敗させたり、そのまま継続させたりすることができる機能です。
上の図でも分かるように、
- Cost estimation
- Security scan
- Policy as Code
などが主なユースケースとして想定されています。
既にいくつかの third-party integrations が公開されていて、例えば Snyk と簡単に連携して Run Tasks によるセキュリティスキャンを行ったり、Infracost によるコスト計算を行ったりすることができます。今後もどんどん追加されていく予定です。
(番外編) HashiCorp Sentinel
Terraform Cloud や Terraform Enterprise を既に利用されている方はご存知かもしれませんが、Policy as Code を実現するツールとして、HashiCorp も Sentinel というツールを公開しています。
利用するには有料プランが必要となりますが、Web ブラウザ上で手軽に Sentinel をデバッグすることができる playground ツール も提供されていて、こちらは誰でも使うことができますので、興味があれば一度試してみてください。
参考: 紹介動画 (YouTube)
Run Tasks integration を自前で作る
さて、上記で説明したサードパーティ製の Run Tasks を使う以外に、自分で Run Tasks を作ることもできます。
流れとしては非常に簡単で、
- まず自分で Webhook サーバーを作る
- 作った Webhook サーバーを Run Tasks に登録する
- Terraform Cloud からの payload を受信する
- payload に含まれる
access_token
,plan_json_api_url
を使って、plan
内容がダンプされた JSON ファイルをダウンロードする - ダウンロードした JSON を解析し、任意のロジックを実行
- 解析した実行結果に基づいて、
task_result_callback_url
に対してリクエストを投げ、処理を失敗、もしくは成功させる
といった一連のステップを踏むだけです。感覚としては、よくある Slack アプリを作るようなものだと思えばそんなにハードルは高くないと思います。
Webhook サーバーは外部に公開する必要がありますが、受信する payload は HMAC キーで検証 できるようになっています。
今回はこの仕組を使って、Webhook サーバー側で OPA (Open Policy Agent) によるスキャンを行う簡単な POC を作ってみます。
OPA の実行方法
OPA を使う方法として、CLI や OPA をホスト上でデーモンとして起動させ、REST API によって操作を行う方法などがありますが、今回は OPA から公式に公開されている github.com/open-policy-agent/opa/rego
package を使い、Go API から直接操作することにします。
こうすれば、Webhook サーバーを Go で作り、リクエストをそのまま OPA のランタイムに投げて処理させることができ、少し楽ができそうです。
Webhook サーバー (サンプル)
サンプルアプリのデフォルトの状態では、run-tasks-demo
という名前で作られた workspace (Terraform Cloud) に対して rego/workspace.run_tasks_demo.rego
に定義されている module (policies) が実行されるようになっています。
かなり無理矢理ですが、クエリを workspace 名を使って動的に組み立てています。
workspace := strings.ReplaceAll(req.WorkspaceName, "-", "_")
q := rego.New(
rego.Query("data.workspace."+workspace),
rego.Load([]string{"./rego"}, nil),
rego.Input(planJSON),
)
package workspace.run_tasks_demo
import data.provisioners.local_exec.disallow as local_exec
default allow := false
allow := true {
count(local_exec.deny_provisioners) == 0
}
reasons[key] := value {
count(local_exec.deny_provisioners) > 0
key := "provisioners.local_exec.disallow"
value := local_exec.deny_provisioners
}
そしてこの module の中では、先日 Mercari Engineering ブログ で紹介されていた、local-exec
provisioner を禁止する policy を例として import
しています。
OPA のエコシステムに全く精通していないので (OPA 入門2日目)、この辺りの運用知見が無く的外れな事をしている可能性が大ですが、今回のサンプルアプリの例では workspace 毎に module を1つ用意し、その中で必要な各共通 module (policies) などを import
してくるイメージです(?)。
ローカルで動かす
みんな大好き ngrok などを使って簡単に手元でテストしてみます。
セットアップについては README を参照ください。
まずは Terraform Cloud の管理画面から対象の Workspace に移動し、Settings -> Run Tasks -> Create a new run task
から Run Task を新規追加します。
そしてこの Workspace に対して新たな Run を実行すると、
上記のように、Policy によって実行が失敗していることが分かります。
この場合、fastly_service_vcl
という resource 内で禁止されている local-exec
provisioner が見つかったため、fail となっています。
もろもろの制限など
message
は最大512文字
Callback に返せる 上の画像を見てもらうと分かるのですが、今回の例だとデバッグの結果を Run Comment にわざわざ追加しています。これは、Callback URL に対して返すことのできる message
フィールドが最大512文字に制限されているためです。
正しいやり方としては、Callback URL に送る payload に data.attributes.url
を含め、当該 Run Task の実行結果が確認できる外部 URL を提供する方法があります。
{
"data": {
"type": "task-results",
"attributes": {
"status": "passed",
"message": "4 passed, 0 skipped, 0 failed",
"url": "https://external.service.dev/terraform-plan-checker/run-i3Df5to9ELvibKpQ"
}
}
}
今回の POC アプリでは URL の発行までは行いませんが、実運用ではこちらが好ましいですね。
Callback には10分以内に結果を返す
10分以上経過しても Callback 先になんの応答もない場合、自動的に失敗となります。
その他
Terraform plan を JSON に変換
# plan.out はバイナリファイル
$ terraform plan -out=plan.out
# JSON に変換
$ terraform show -json plan.out > plan.json
Discussion