[OpenPolicyAgent] TerraformでEC2を構築する際のポリシーをチェックする

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 の一覧で typeaws_instance のリソースを取得するため、構築するEC2の一覧を取得することができます。 詳しくは公式の内容を見ていただけるとわかるかと思います。

www.openpolicyagent.org

では本題の タグに「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の値が設定されているかをチェックすることができます。 この書き方は公式に記載がありますので、詳しく知りたい方は公式を見てみてください。

www.openpolicyagent.org

これで、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 になります。(ほかにいい方法があれば教えていただけると嬉しいです!)

この書き方は公式に載っていますので、詳しくはこちらを参考にしてください。

www.openpolicyagent.org

では、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とかのポリシーについても実装してみたいですね。