ゼロから始める Terraform Sentinel ポリシー開発

# はじめに

インフラストラクチャの規模が大きくなるにつれて、一貫したポリシーを適用することの重要性も増してきます。
HashiCorp の Sentinel は、Terraform のための Policy as Code フレームワークとして、インフラストラクチャの変更に対して自動的にチェックを行う強力なツールです。

本記事では、Sentinel の基本的な使い方から、ポリシーの実装、テストの方法まで、ステップバイステップで解説します。
AWS S3 バケットの削除を検知するシンプルなポリシーを例に、Sentinel を使った安全なインフラストラクチャ管理の第一歩を踏み出しましょう。

HCP Terraform または Terraform Enterprise を利用している方はもちろん、Terraform OSS の使用者でも Sentinel の概念を理解することで、より安全なインフラストラクチャ管理の方法について考えるきっかけとなれば幸いです。

## Sentinel のインストール

下記からご利用の OS に対応したバイナリをダウンロードし、PATH を通してください。

https://developer.hashicorp.com/sentinel/install

`sentinel` コマンドを単体で実行し、Usage が表示されたら OK です。

```bash
$ sentinel
Usage: sentinel [--version] [--help] <command> [<args>]

Available commands are:
    apply      Execute a policy and output the result
    fmt        Format Sentinel policy to a canonical format
    test       Test policies
    version    Prints the Sentinel runtime version
```

## 基本的なディレクトリ構成の説明

下記のような構成になります。

```
.
├── policy-1.sentinel
├── policy-2.sentinel
├── ...
├── sentinel.hcl
└── test
    ├── policy-1
    │   ├── fail.hcl
    │   ├── mock-tfplan-fail-v2.sentinel
    │   ├── mock-tfplan-pass-v2.sentinel
    │   └── pass.hcl
    ├── policy-2
    │   ├── fail.hcl
    │   ├── mock-tfplan-fail-v2.sentinel
    │   ├── mock-tfplan-pass-v2.sentinel
    │   └── pass.hcl
...
```

### sentinel.hcl

対象ディレクトリ内の Policy 群をとりまとめるファイルです。
このファイルに .sentinel ファイルの参照を記載することで、対象の Policy を適用します。
また、対象の Policy を強制するかどうかも指定します。

### 拡張子が sentinel のファイル群

ルート直下の拡張子が sentinel のファイルにポリシーを記述していきます。

### test ディレクトリ

`sentinel.hcl` の動作をテストする際のテスト定義が含まれています。
`test` ディレクトリ配下に sentinel ファイルの拡張子 sentinel を除いたディレクトリを作成し、その中に対応する sentinel ファイルのテストを記述していきます。

シンプルな例としては、 `pass.hcl` ファイルにポリシーに準拠するテストを記述し、`fail.hcl` ファイルにポリシーに準拠しないテストを記述します。
それらのテストに必要な Mock ファイルを同ディレクトリに含めると便利です。
この例ではそれぞれ `mock-tfplan-pass-v2.sentinel` と `mock-tfplan-fail-v2.sentinel` を含めています。

## ポリシーの実装

では実際にポリシーを実装していきましょう。

### ルート直下の拡張子 sentinel のファイルの作成

下記のドキュメントを参考にしながら組み立てていきます。

https://developer.hashicorp.com/sentinel/docs/language

Sentinel のポリシー言語は、機能が限定されたプログラミング言語のようなイメージです。
代入や簡単な計算などが行えるほか、HCP Terraform 等から実行された場合は Plan 結果を参照して何らかの判定を行うことができます。
最小限のコードは下記です。

```hcl
main = rule { true }
```

これは main 関数を定義しています。
main 関数の戻り値が true ならポリシーに準拠している、false なら準拠していないと判断されます。
この式では常に `true` になるので、「なんでも OK」となりますね。
ここで出てくる `rule` についての詳細は下記になります。

https://developer.hashicorp.com/sentinel/docs/language/rules

この実装では検証にならないので、Plan 結果を読み込んで評価する形にしてみましょう。
下記を記述することで、Plan 結果を `tfplan` という名前で参照できるようになります。

```hcl
import "tfplan/v2" as tfplan
```

`tfplan/v2` と命名された構造が含まれており、下記のドキュメントに仕様が記載されています。

https://developer.hashicorp.com/terraform/cloud-docs/policy-enforcement/import-reference/tfplan-v2

例えば、`resource_changes` という要素にどのリソースに変更が発生するのか、どの種類の変更(作成、削除、変更)が発生するのかの情報があります。
例えば下記のように記載すると、削除が計画されている AWS の S3 バケットの情報を `delete_planned_s3_buckets` 変数に含むことができます。

```hcl
delete_planned_s3_buckets = filter tfplan.resource_changes as _, rc {
    rc.mode is "managed" and
        rc.type is "aws_s3_bucket" and
        rc.change.actions is ["delete"]
}
```

S3 バケットの削除を計画したら警告するようにしてみましょう。
`delete_planned_s3_buckets` にはリストが含まれるので、リストが 0 件より大きければ警告すれば良いですね。
リストの件数は `length` 関数で確認できます。

https://developer.hashicorp.com/sentinel/docs/functions/length

そうすると、下記のようなポリシーを用意すれば OK ですね。

```hcl:alert-delete-s3.sentinel
import "tfplan/v2" as tfplan

delete_planned_s3_buckets = filter tfplan.resource_changes as _, rc {
    rc.mode is "managed" and
        rc.type is "aws_s3_bucket" and
        rc.change.actions is ["delete"]
}

main = rule { length(delete_planned_s3_buckets) == 0 }
```

こちらの内容で、 `alert-delete-s3.sentinel` というファイル名で保存してみます。

### sentinel.hcl の作成

作成したポリシーを適用するように、sentinel.hcl を作成してみましょう。
policy ブロックで設定を定義して、ポリシーとその[適用レベル: Enforcement Levels](https://developer.hashicorp.com/sentinel/docs/concepts/enforcement-levels) を記載します。
ポリシーは `source` 引数で相対パスで指定可能です。
適用レベルは、`enforcement_level` という引数で指定し、下記の 3 つのうちどれかを指定できます。

- `advisory`: 警告表示のみ
- `soft-mandatory`: ポリシーをパスしないと Apply が実施できないが、ポリシーの結果はユーザーで上書き可能
- `hard-mandatory`: ポリシーをパスしないと絶対に Apply できない

今回の S3 バケットの削除を警告するポリシーは誤削除を防ぐ安全装置のようなものなので、試しに `soft-mandatory` で設定してみましょう。

```hcl:sentinel.hcl
policy "alert-delete-s3" {
  source            = "./alert-delete-s3.sentinel"
  enforcement_level = "soft-mandatory"
}
```

### テストの作成

ポリシーの記述が完了したら、テストを実装して意図した動作をするか確認しましょう。
テストの実装といっても、テスト対象の Mock を用意し、それがどのような動作をすると想定するかを定義するだけなので簡単です。
テストのファイルは `test` ディレクトリ内に配置します。

#### Mock の作成

テスト対象となる Mock を作成する場合、まずはどの Mock が必要かを確認します。
今回はポリシーの記述で `import "tfplan/v2" as tfplan` と記載し、`tfplan/v2` を使用していますので、この Mock が必要となります。
[`tfplan/v2` のドキュメント](https://developer.hashicorp.com/terraform/cloud-docs/policy-enforcement/import-reference/tfplan-v2) を参照して 1 から記載しても良いのですが大変です。
HCP Terraform / Terraform Enterprise を利用している場合は、Run の結果から Mock を出力できますので、こちらを利用するのが確実です。

#### 1. Mock の想定する実装を行う

Mock を生成するため、テストしたい修正を行った terraform コードを記述します。
今回だと構築済みの S3 バケットを削除する実装になるので、`aws_s3_bucket` リソースの実装を削除すれば良いですね。
例え下記のように構築済みの S3 バケットの resource ブロックをコメントアウトして対応しましょう。

```hcl:s3.tf
# resource "aws_s3_bucket" "example" {
#   bucket_prefix = "sentinel-test"
# }
```

#### 2. Mock を生成する

対象の terraform コードに対して、HCP Terraform / Terraform Enterprise の Workspace で Plan 実行します。
Plan 結果を開くと、「Download Sentinel mocks」というボタンがあるので、こちらをクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3769407/5851cee4-6e87-487f-bbc1-9857fdd4215d.png)

そうすると様々な Mock をダウンロードすることができます。
`tfplan/v2` の Mock は `mock-tfstate-v2.sentinel` に格納されているので、こちらを利用します。
注意点として、Mock 内の機密情報はダミーの文字列に置換するなどしてコード管理して問題ない状態に整えてください。

ポリシーでエラーにしたい Plan 結果から取得した Mock を `test/alert-delete-s3/mock-tfplan-fail-v2.sentinel` に、ポリシーをパスする Plan 結果から取得した Mock を `test/alert-delete-s3/mock-tfplan-pass-v2.sentinel` に保存してください。

#### テスト定義の hcl ファイルの作成

Mock を用いたテスト定義の場合、mock ブロックと test ブロックで構成します。
test ブロックには Mock に対してどのような結果を想定するかを記載します。
今回だと、main 関数の結果がエラーのパターンは `false` 、検証をパスするパターンでは `true` を想定するので、下記の 2 つのファイルを用意すれば良いです。

```test/alert-delete-s3/fail.hcl
mock "tfplan/v2" {
  module {
    source = "./mock-tfplan-fail-v2.sentinel"
  }
}

test {
  rules = {
    main = false
  }
}
```

```test/alert-delete-s3/pass.hcl
mock "tfplan/v2" {
  module {
    source = "./mock-tfplan-pass-v2.sentinel"
  }
}

test {
  rules = {
    main = true
  }
}
```

### テストの実行

ここまでの対応で必要なファイルが揃い、下記のディレクトリ構成になっていますね。

```
.
├── alert-delete-s3.sentinel
├── sentinel.hcl
└── test
    └── alert-delete-s3
        ├── fail.hcl
        ├── mock-tfplan-fail-v2.sentinel
        ├── mock-tfplan-pass-v2.sentinel
        └── pass.hcl
```

テストを実行してみましょう。
`sentinel.hcl` があるディレクトリに移動して、`sentinel test` コマンドを実行してください。
適切に実装できていれば、下記のような出力になるはずです。

```bash
$ sentinel test
PASS - alert-delete-s3.sentinel
  PASS - test/alert-delete-s3/fail.hcl
  PASS - test/alert-delete-s3/pass.hcl
1 tests completed in 6.545917ms
```

Mock を正しく構成できていれば、すべてのテストが成功することが確認できると思います。
お疲れさまでした !

# まとめ

本記事では、HashiCorp Sentinel を使用して Terraform のポリシーを実装する基本的な手順を解説しました。

- Sentinel のインストール方法
- 基本的なディレクトリ構成
- ポリシーの実装（AWS S3 バケットの削除を検知する例）
- sentinel.hcl の作成とポリシーの適用レベルの設定
- Mock を使ったテストの実装と実行

Sentinel を活用することで、インフラストラクチャの変更に対する一貫したルールを適用し、誤った操作や望ましくない設定変更を防ぐことができます。
今回紹介した S3 バケットの削除検知は単純な例ですが、同様の方法でより複雑なポリシーを実装することも可能です。

例えば、以下のようなポリシーも実装できます：

- リソースに特定のタグが付与されていることを確認する
- インスタンスタイプを組織の標準に準拠させる
- ネットワーク設定がセキュリティ要件を満たしていることを確認する

Policy as Code の考え方はインフラストラクチャ管理の自動化において重要な概念であり、Sentinel はその実装を支援する優れたツールです。
皆さんの Terraform 環境に Sentinel を導入して、より安全で一貫性のあるインフラストラクチャ管理を実現してください。