TerraformにおけるOPAのユースケース

## はじめに

この記事はCyberAgent Group SRE Advent Calendar 2023の18日目の記事です。


Open Policy Agent(OPA)は、JSONやYAMLベースのInfrastructure as Code(IaC)管理において、近年広く用いられるようになっています。特に、Kubernetesのマニフェストに対するOPAとGateKeeperの導入が標準化されつつあります。一方で、Terraformのような、複雑なクラウドインフラの記述に長けたツールとの統合は、まだ挑戦的であり、実際の連携例は少ない状況です。Terraform CloudやStyraとの組み合わせでの利用事例は存在しますが、純粋なTerraform環境（例えば、バックエンドがS3の場合）でのOPAの活用は、まだそれほど多くはありません。この記事では、Terraform環境でのOPAのユースケースとその実践的な利用方法に焦点を当てていきます。

## OPAとRegoについて

OPAは、Regoポリシーを集中的に管理するエンジンです。JSON形式の入力データに対して、複数のシステム間で一貫性のあるポリシーを適用することが可能です。RegoはOPA用のドメイン固有言語であり、データに対するポリシーを表現するために使用されます。

Regoを用いた複雑な論理処理や動的なJSON構造の処理は難易度が高いです。開発体制が整っていない状況でRegoの開発に着手すると、メンテナンスの面で大きな負担を背負うことになる可能性があります。そのため、コミュニティによって既に作成されたRegoを利用することを推奨します。

- [Kubernetes shared policies](https://github.com/instrumenta/policies)
    Kubernetesのセキュリティに関するポリシーセット
- [Rego Library](https://github.com/open-policy-agent/library)
    OPAコミュニティで共有されているポリシーの例

OPAを実装しているリポジトリは多くありませんが、特におすすめのツールとして[Regula for IaC](https://github.com/fugue/regula)があります。これはTerraform、CloudFormation、Kubernetes manifestsなどの検証とセキュリティ検知ツールです。このツールにはすでにいくつかの事前定義されたポリシーが含まれており、そのまま利用可能です。

- [Regula Terraform RuleSets](https://github.com/fugue/regula/tree/master/rego/rules/tf)

**ただし、これら事前定義のポリシーは、セキュリティチェックのベストプラクティスに基づいており、tfsecやtflintの機能と一部重複する場合があります。**

Regoのベストプラクティスについては、OPAの開発会社が提供する[StyleGuide](https://docs.styra.com/opa/rego-style-guide)と[よくあるRego bugs](https://docs.styra.com/regal/category/bugs)を参照すると良いでしょう。また、優れたLinterである[regal](https://docs.styra.com/regal)も利用可能ですので、ぜひ活用してみてください。

## Terraform OPA連携のユースケース

OPAの検証はJSONベースなので、Terraformの各検証対象に対し、JSONとして出力する必要があります。

Terraformの検証対象は概ね以下の種類があります。
- state JSON
    - `tfstate`、定期実行ジョブで現時点のインフラ構成を検証する用途です
- plan JSON
    - `terraform plan`のJSON出力、検証目的によって利用する部分も違います。
    - 詳細は以下のドキュメントをご参考ください。
        - [resource_changes](https://developer.hashicorp.com/terraform/internals/json-format#plan-representation)
        - [configuration](https://developer.hashicorp.com/terraform/internals/json-format#configuration-representation)
- output JSON
    - [planned_values.outputs](https://developer.hashicorp.com/terraform/internals/json-format#values-representation)

### 組織固有ポリシーの定義と適用

組織固有のポリシー、例えば統計用のタグ指定やマルチ環境対応のフォーマット指定、特定リソースへのIAM割り当てなどは、OPAを用いて柔軟に設定できます。これらのポリシーは組織の実際の要件に沿って慎重に検討する必要があります。

例えば、以下のような例があります。

全てのVariableは、Terrafrom Workspaceごとに区別できるmap構成

```terraform
variable "min_size" {
  type = map(number)

  default = {
    "default" = 1
    "prd"     = 3
    "stg"     = 3
    "dev"     = 1
  }
}

resource "aws_autoscaling_group" "main" {
# ...
  min_size = var.min_size[terraform.workspace]
# ...
}
```

```rego
package terraform
import future.keywords.if
import future.keywords.in
import input as tfplan # terraform planの結果をjsonとして保存する

variable_compliance if {
    required_keys := {"dev", "stg", "prd"} # 全てのvariableは、これらのKeyを所持する
    all_variables_have_required_keys(tfplan.variables, required_keys)
}

all_variables_have_required_keys(variables, required_keys) if {
    count(variables) == count({var_name |
        var_value := variables[var_name].value
        is_map(var_value)
        has_all_required_keys(var_value, required_keys)
    })
}

is_map(object) if {
    not null in object
}

has_all_required_keys(object, keys) if {
    count(keys) == count({key | object[key] != null})
}

violation[msg] if {
	not variable_compliance
	msg := "Validation failed: Not all variables are map or have the required keys (dev, stg, prd)."
}
```

```bash
$ opa eval --format pretty --data policy.rego --input input.json "data.terraform.violation"

{
  "Validation failed: Not all variables are map or have the required keys (dev, stg, prd).": true
}
```

### 整合性チェック

Providerがまだ提供していないバリデーションを補完する目的で、細かい値の制御を検証する用途です。
簡単な値チェックなら、`Terraform Variables`の`Validation`ブロックで簡単にできますが、複雑条件の制御はValidationブロックだけでは難しいので、この場合Regoを使ってもいいと考えています。

もちろん、ほとんどの場合、変数構成の再設計とValidationブロックやpre/postconditionブロックなど組み合わせによって、解決できると思います。Regoを使って、複雑条件の制御を書くのに向いています。

以下のような例があります。
- 二つVariableのお互いに影響する条件設定

```rego
package terraform

import future.keywords.if
import future.keywords.in

import input as tfplan

variable1 := tfplan.variables.variable1.value

variable2 := tfplan.variables.variable2.value

vars_pattern_is_valid if {
	variable1 == "A"
	variable2 > 10
} else if {
	variable2 == 10
}

violation[msg] if {
	not vars_pattern_is_valid
	msg := "variable1が'A'の場合、variable2は10以上でなければなりません。それ以外、variable2は10でなければなりません。"
}
```

```bash
$ opa eval --format pretty --data policy.rego --input input.json "data.terraform.violation"

{
  "variable1が'A'の場合、variable2は10以上でなければなりません。それ以外、variable2は10でなければなりません。": true
}
```

その他には、目で確認していたロジックもRegoで書くことができます。
- AWS Network FirewallのSubnetとRouteTable設定が正しいかどうか
- AWS WAFのRuleは規定の必須Ruleを実装しているかどうか

### 自動化フローの一環

JSON Parserとして使うというのは酷いかもしれませんが、企業固有のポリシーの実施に加えて、OPAをJSON Parserとして活用することは有効です。

ただし、現時点でOPAはデバッグやエラーメッセージ以外の目的での出力使用はまだ限定的ですので、CIフローの中で特定のコンプライアンスにおけるポリシーとルールを評価し、真偽値（True/False）の結果のみを利用することをお勧めします。

以下のような例があります。

```rego
package terraform

import future.keywords.if
import future.keywords.in

plan_has_destroy[resource_type] {
	some resource_changes in input.resource_changes
	"delete" in resource_changes.change.actions
	resource_type := resource_changes.type
}

warn[msg] if {
	some resource_type in plan_has_destroy
	msg := sprintf(
		"Please be cautious: the resource '%s' is scheduled for destruction. Verify if this action is intentional.",
		[resource_type],
	)
}
```
リソースが削除される予定である場合、このポリシーは適切な警告メッセージを生成し、ユーザーに対してその操作が意図的であるかどうかを再確認するよう促します。

```bash
$ opa eval --format pretty --data policy.rego --input input.json "data.terraform.warn"

{
  "Please be cautious: the resource 'aws_db_instance' is scheduled for destruction. Verify if this action is intentional.": true,
  "Please be cautious: the resource 'aws_ecr_lifecycle_policy' is scheduled for destruction. Verify if this action is intentional.": true,
  "Please be cautious: the resource 'aws_elasticache_cluster' is scheduled for destruction. Verify if this action is intentional.": true
}
```

### セキュリティとコンプライアンス

ポリシーに基づいた自動化により、セキュリティコンプライアンスの一貫性を保つための検証です。
RegoとOPAで定義しても良いが、定義と更新管理が非常に手間がかかることもあります。`tfsec`と`tflint`を使う前提で、組織固有のポリシーとして改めて定義する場合に使っても良いと思います。

[Regula Terraform RuleSets](https://github.com/fugue/regula/tree/master/rego/rules/tf)が一例になります。


## まとめ
OPAとRego言語の統合は、Terraformによるクラウドインフラの管理を効率的にする大きな潜在能力を持っています。しかし、Regoの開発には相応の労力が必要であり、これがTerraform環境におけるOPAの導入の障壁の一つとなっています。この記事を通じて、TerraformとOPAを統合することの重要性と、そのためのベストプラクティスを理解いただけたことでしょう。現在、TerraformとOPAの実際の連携事例はまだ少ないですが、この分野の将来的な発展が非常に期待されています。