🛡️

Terraform Cloud Run Tasks (GA) と自前の Open Policy Agent サーバーを連携して遊んでみる

2022/06/06に公開

ちなみに…

「Terraform Cloud/Enterprise で OPA をネイティブにサポートしてほしい!」というリクエストは数多く寄せられていて、時期こそお伝えできないものの、サポートする動きは進んでいるようです。

Terraform Cloud Run Tasks

先日 Terraform Cloud に新たに Run Tasks という機能が追加され、正式に GA となりました。

https://www.hashicorp.com/blog/terraform-cloud-run-tasks-are-now-generally-available

ざっくりどういうものかと言うと、planapply の間のステージで任意のロジックを実行し、その結果によって実行を失敗させたり、そのまま継続させたりすることができる機能です。

上の図でも分かるように、

  • 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 というツールを公開しています。

https://www.hashicorp.com/sentinel

利用するには有料プランが必要となりますが、Web ブラウザ上で手軽に Sentinel をデバッグすることができる playground ツール も提供されていて、こちらは誰でも使うことができますので、興味があれば一度試してみてください。
参考: 紹介動画 (YouTube)

Run Tasks integration を自前で作る

さて、上記で説明したサードパーティ製の Run Tasks を使う以外に、自分で Run Tasks を作ることもできます。

https://www.terraform.io/cloud-docs/integrations/run-tasks

流れとしては非常に簡単で、

  1. まず自分で Webhook サーバーを作る
  2. 作った Webhook サーバーを Run Tasks に登録する
  3. Terraform Cloud からの payload を受信する
  4. payload に含まれる access_token, plan_json_api_url を使って、plan 内容がダンプされた JSON ファイルをダウンロードする
  5. ダウンロードした JSON を解析し、任意のロジックを実行
  6. 解析した実行結果に基づいて、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 サーバー (サンプル)

https://github.com/smaeda-ks/tf-cloud-opa-server

サンプルアプリのデフォルトの状態では、run-tasks-demo という名前で作られた workspace (Terraform Cloud) に対して rego/workspace.run_tasks_demo.rego に定義されている module (policies) が実行されるようになっています。
かなり無理矢理ですが、クエリを workspace 名を使って動的に組み立てています。

main.go
workspace := strings.ReplaceAll(req.WorkspaceName, "-", "_")
q := rego.New(
    rego.Query("data.workspace."+workspace),
    rego.Load([]string{"./rego"}, nil),
    rego.Input(planJSON),
)
rego/workspace.run_tasks_demo.rego
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 となっています。

もろもろの制限など

Callback に返せる message は最大512文字

上の画像を見てもらうと分かるのですが、今回の例だとデバッグの結果を 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