Open Policy Agentを使うことでTerraformでEC2を構築する際にインスタンス設定が想定されているものなのかチェックができるため実装してみました。
やってみた内容について簡単にまとめていきたいと思います。
実装方法などはまだまだ勉強中ですのであくまでも参考程度に見ていただければと思います。(よい実装方法などあれば教えてくれると嬉しいです!)
動作環境
OS: CentOS 7 Terraform : 0.12.26 opa: Version: 0.21.0
Open Policy AgentでTerraformのポリシーチェック
まずはOpen Policy AgentでTerraformのポリシーをチェックする方法を見ていきます。
Terraformは .tf という拡張子でHCLで記述していきます。このままですとOpen Policy Agentでポリシーチェックができませんので、json形式に変換する必要があります。
変換方法としては下記のようにすることでjsonファイルに変換することができます。
terraform plan --out tfplan.binary terraform show -json tfplan.binary > tfplan.json
terraform plan --out ftplan.binary でTerraformのプラン結果をファイルに出力することができます。
そして terraform show -json tfplan.binary でプランで出力した内容をjson形式としてファイルに出力することができます。
これでOpen Policy Agentでポリシーチェックをする対象の準備ができました。
ポリシーチェックするTerraformのファイル
では、早速ポリシーチェックをするTerraformを見ていこうと思います。
resource "aws_instance" "web" {
ami = "ami-0717724173e4d9989"
instance_type = "t3.micro"
tags = {
Env = terraform.workspace
}
root_block_device {
volume_type = "gp2"
volume_size = "20"
}
}
今回は上記のtfファイルの内容が下記のポリシー通りになっているのかを見ていきたいと思います。
- タグに「Env」が存在しており、stagging or production のどちらかが設定されているか
- ステージング環境の場合、インスタンスタイプが t3.micro か
- ルートボリュームでサイズが20以上、100以下になっているか、typeがgp2になっているか
前提として Env タグに設定されている値は環境を表しており、staggingの場合はステージング環境、productionの場合はプロダクション環境となります。 また、Env タグに workspaceの値を入れるようにしているため、EC2を複数構築ときでもステージング環境とプロダクション環境の二つの環境に同時にデプロイすることはできない想定です。
このtfファイルをjson形式に変換した時の内容は下記のようになります。
{
・・・
"resource_changes": [
{
"address": "aws_instance.web",
"mode": "managed",
"type": "aws_instance",
"name": "web",
"provider_name": "aws",
"change": {
"actions": [
"create"
],
"before": null,
"after": {
"ami": "ami-0717724173e4d9989",
"credit_specification": [],
"disable_api_termination": null,
"ebs_optimized": null,
"get_password_data": false,
"hibernation": null,
"iam_instance_profile": null,
"instance_initiated_shutdown_behavior": null,
"instance_type": "t3.micro",
"monitoring": null,
"root_block_device": [
{
"delete_on_termination": true,
"volume_size": 20,
"volume_type": "gp2"
}
],
"source_dest_check": true,
"tags": {
"Env": "stagging"
},
"timeouts": null,
"user_data": null,
"user_data_base64": null
},
・・・
resource_changes に構築するEC2の情報が記述されています。構築時のポリシーチェックになるのでここの要素に対してポリシーを作成していきます。
EC2のポリシーをチェックする
ポリシーは下記のように実装しました。
package terraform.aws
import input as tfplan
stagging_instance_type = ["t3.micro"]
production_instance_type = ["m5.large"]
env_tags = ["stagging", "production"]
default allow = false
allow {
valid_instance_tag
valid_instance_root_block_device
valid_environment_instance_type
}
valid_instance_root_block_device {
instances := aws_instances
count({instance|
instances[instance]; valid_instance_root_block_device_filter(instance)
}) == count(instances)
}
valid_instance_root_block_device_filter(instance) {
instance.change.after.root_block_device[_].volume_size >= 20
instance.change.after.root_block_device[_].volume_size <= 100
instance.change.after.root_block_device[_].volume_type == "gp2"
}
valid_instance_tag {
instances := aws_instances
count({instance|
instances[instance]; instance_tag_filter(instance)
}) == count(instances)
}
instance_tag_filter(instance) {
instance.change.after.tags.Env == env_tags[_]
}
default valid_environment_instance_type = false
valid_environment_instance_type = true {
valid_stagging_instance_type
}
valid_environment_instance_type = true {
valid_production_instance_type
}
aws_instances[instance] {
instance := tfplan.resource_changes[_]
instance.type == "aws_instance"
}
stagging_instances[instance] {
instances := aws_instances
instance := instances[_]
instance.change.after.tags.Env == "stagging"
}
valid_stagging_instance_type {
instances := stagging_instances
count({instance|
instances[instance]; stagging_instance_type_filter (instance)
}) == count(instances)
}
stagging_instance_type_filter (instance) {
instance.change.actions[_] == "create"
instance.change.after.instance_type == stagging_instance_type[_]
}
production_instances[instance] {
instances := aws_instances
instance := instances[_]
instance.change.after.tags.Env == "production"
}
valid_production_instance_type {
instances := production_instances
count({instance|
instances[instance]; production_instance_type_filter(instance)
}) == count(instances)
}
production_instance_type_filter(instance) {
instance.change.actions[_] == "create"
instance.change.after.instance_type == production_instance_type[_]
}
ここでは
- タグに「Env」が存在しており、stagging or production のどちらかが設定されているか
- ステージング環境の場合、インスタンスタイプが t3.micro か
- ルートボリュームでサイズが20以上、100以下になっているか、typeがgp2になっているか
をチェックしています。
では、 どのようにしてチェックしているか を順を追ってみていきます。
allow
allowというRuleの中で複数のRuleを記述しています。allowの中に記述しているRule内で細かくポリシーのチェックを行い、すべてのRuleがtrueになった場合にallowもtrueになります。
default allow = false
allow {
valid_instance_tag
valid_instance_root_block_device
valid_environment_instance
}
タグに「Env」が存在しており、stagging or production のどちらかが設定されているか
EC2に「Env」タグが存在しているか、「Env」タグが stagging か production かどうかは valid_instance_tag でチェックしています。 実装している部分は下記のところとなります。
valid_instance_tag {
instances := aws_instances
count({instance|
instances[instance]; instance_tag_filter(instance)
}) == count(instances)
}
instance_tag_filter(instance) {
instance.change.after.tags.Env == env_tags[_]
}
instances := aws_instances ですがこちらは構築対象となるインスタンス一覧を取得しています。
aws_instances のRuleの内容ですが下記のようになっています。
aws_instances[instance] {
instance := tfplan.resource_changes[_]
instance.type == "aws_instance"
}
簡単にまとめると resource_changes の一覧で type が aws_instance のリソースを取得するため、構築するEC2の一覧を取得することができます。
詳しくは公式の内容を見ていただけるとわかるかと思います。
では本題の タグに「Env」が存在しており、stagging or production のどちらかが設定されているか をチェックしている Rule を見てみたいと思います。
内容を一部抜粋して下記に記載してあります。
env_tags = ["stagging", "production"]
instances := aws_instances
count({instance| instances[instance]; instance_tag_filter(instance) }) == count(instances)
instance_tag_filter(instance) {
instance.change.after.tags.Env == env_tags[_]
}
ここで重要になるのが instance_tag_filter 関数の内容になります。 instance_tag_filter関数でtagsにEnvがあり、Envの値が env_tags 内のものと一致しているインスタンス一覧を返す処理をしています。
そして count({instance| instances[instance]; instance_tag_filter(instance) }) の処理ですが instance_tag_filter関数から取得したインスタンス一覧の数を取得し、count(instances) で構築するEC2インスタンスの数を取得しています。
つまり instance_tag_filter関数から取得したインスタンス一覧の数と構築するEC2インスタンス数が同数かをチェックしています。
こうすることで、すべてのEC2インスタンスがタグに「Env」があり、 stagging or productionの値が設定されているかをチェックすることができます。 この書き方は公式に記載がありますので、詳しく知りたい方は公式を見てみてください。
これで、EC2に「Env」タグがあり、stagging or production の値が設定されているかのチェックができました。
ステージング環境の場合、インスタンスタイプが t3.micro か
次にステージング環境の場合、インスタンスタイプが t3.micro かのチェック方法を見ていきたいと思います。 っとその前に、「Env」タグの値が「stagging」なのか「production」なのかで分岐する必要があります。ただ、Ruleとして下記のように記載してしまうと誤った挙動となります。
valid_environment_instance_type {
valid_stagging_instance_type
valid_production_instance_type
}
valid_stagging_instance_type が staggingの場合のインスタンスタイプチェック、valid_production_instance_type が production の場合のインスタンスタイプチェックとなります。
こうしてしまうと、両方のRuleがtrueになったときにしか valid_environment_instance_type の Ruleがtrueしません。
前提としてステージング環境とプロダクション環境を同時にデプロイすることはないので、仮にステージング環境にデプロイしようとすると Envタグに production が設定されているものがなく、valid_production_instance_type の結果が undefined となり、valid_environment_instance_type が falseになります。
理想はstaggingとproductionが同時に指定されることはないため valid_stagging_instance_type と valid_production_instance_type でどちらかがtrueになった場合、valid_environment_instance_type がtrueになるようにしたいです。 そのためには下記のように設定する必要があります。
default valid_environment_instance_type = false
# ステージング環境
valid_environment_instance_type = true {
valid_stagging_instance_type
}
# プロダクション環境
valid_environment_instance_type = true {
valid_production_instance_type
}
こうすることで、どちらかのRuleがtrueになれば valid_environment_instance_type が true になります。(ほかにいい方法があれば教えていただけると嬉しいです!)
この書き方は公式に載っていますので、詳しくはこちらを参考にしてください。
では、valid_stagging_instance_type のRuleの内容を見ていきたいと思います。
stagging_instance_type = ["t3.micro"]
valid_stagging_instance_type {
instances := stagging_instances
count({instance|
instances[instance]; stagging_instance_type_filter(instance)
}) == count(instances)
}
stagging_instances[instance] {
instances := aws_instances
instance := instances[_]
instance.change.after.tags.Env == "stagging"
}
stagging_instance_type_filter(instance) {
instance.change.actions[_] == "create"
instance.change.after.instance_type == stagging_instance_type[_]
}
ここでもタグのポリシーチェックのときと同様に条件に当てはまるもの(stagging_instance_type_filter 関数で取得したインスタンス一覧)が作成するEC2インスタンス数と同数になっているかをチェックしています。
これをもう少しかみ砕いて整理していきたいと思います。
まず、 stagging_instances でEnvタグにstaggingが設定されているインスタンスの一覧を取得します。
そして、stagging_instance_type_filter 関数で条件に当てはまるインスタンス一覧(ここではインスタンスタイプ)を取得します。関数の中で、
instance.change.after.instance_type == stagging_instance_type[_] とありますが、ステージング環境に構築できるインスタンスタイプかどうかを判定しています。
stagging_instance_type には t3.micro のみ設定しているため、ステージング環境では t3.micro のみ許可します(もちろん、複数指定することも可能です)。
こうすることで、環境ごとでインスタンスタイプが想定されているものなのかをチェックすることができました。
ルートボリュームでサイズが20以上、100以下になっているか、typeがgp2になっているか
最後のチェック項目として、 root_block_device でサイズが 20以上、100以下になっているか、typeがgp2になっているか をチェックしていきます。
valid_instance_root_block_device {
instances := aws_instances
count({instance|
instances[instance]; valid_instance_root_block_device_filter(instance)
}) == count(instances)
}
valid_instance_root_block_device_filter(instance) {
instance.change.after.root_block_device[_].volume_size >= 20
instance.change.after.root_block_device[_].volume_size <= 100
instance.change.after.root_block_device[_].volume_type == "gp2"
}
ここまで記事を読んでいただいた方には上記内容で伝わるかと思いますが、、簡単に説明していきたいと思います。
valid_instance_root_block_device の Rule内 構築するインスタンス一覧と valid_instance_root_block_device_filter 関数から取得したインスタンス一覧が同数になっているかをチェックしています。
valid_instance_root_block_device_filter 関数では root_block_device の値で volume_size と volume_type が想定されているものかどうかをチェックし、該当するインスタンス一覧を取得します。 これで、 root_block_device 内の値がポリシー通りになっているかをチェックすることができました。
まとめ
以上がTerraformで構築するEC2インスタンスのポリシーチェックになります。ポリシーを機械的にチェックできるので設定ミスや不正なもののデプロイを防ぐことができますし、レビューの負担も減らすことができると思います。 また、今回は構築(actionsがcreate)時のみインスタンスタイプのチェックしていました。同様にアクションごとでポリシーのチェックを利用することで更新や削除についてもチェックすることができるかと思いますので、別の機会に試してみたいと思います。 S3とかのポリシーについても実装してみたいですね。