👮

Conftestでおこなうポリシーチェック

2024/08/13に公開

LAPRAS株式会社でSREをしているyktakaha4ともうします🐧

今回は、実務で発生したプロダクト運用に関する課題をConftestによるポリシーチェックで改善した話について書きたいと思います

なにを作るか

弊社ではエンジニア向けのポートフォリオサービスであるLAPRASや、エンジニア採用プラットフォームであるLAPRAS SCOUTを提供しているのですが、これらのプロダクトのインフラは、Amazon EKSを用いたKubernetesクラスタにて運用しています⚓

https://lapras.com/

https://scout.lapras.com/

以前こちらの記事でクラスタアップデートについて触れたのですが、Kubernetesクラスタを本番環境で安全に運用するには、yamlファイルの適切な管理が欠かせません

https://zenn.dev/yktakaha4/articles/steady_update_of_eks

アプリケーションはPythonのWebフレームワークであるDjangoで実行しているのですが、サービス間通信のためのgRPCサーバーがあったり、非同期処理ににPython向けのタスクキューであるCeleryを利用していたりなど、単一のアプリケーションコードから複数のDeploymentを展開しています
これに加えて、クローラーや社内向けシステムなども含めると数十種類のアプリケーションが存在します

https://www.wantedly.com/companies/lapras/post_articles/191577

システムが事業ドメイン毎に一定の粒度で分割されていると、チームを分けて開発効率を高められたり、コードベースが一定規模に抑えられることでメンテナンスがしやすくなったりと一定のメリットもありますが、
インフラ管理においては、特定の機能で見つかった不具合をいかに全体に展開するか という観点が重要になります

例えば、Kubernetesでありがちな実装不備として、コンテナのpreStopフックが設定されていなかったことで、PodがServiceから切り離される前に終了してしまい、再起動中にトラフィックがエラーになってしまう…というミスがあるものと思います

https://zenn.dev/hhiroshell/articles/kubernetes-graceful-shutdown

これに対する回避策として、以下のようにpreStopフックでsleepコマンドを実行し、切り離しが完了するまでの猶予を稼ぐ方法があります
(ちなみに、1.29よりpreStopフックにSleep Actionなるものが追加されよりシンプルに実装できるようですが、弊社ではまだ未導入のため詳しく紹介しません)

apiVersion: apps/v1
 kind: Deployment
 metadata:
   name: sample-deployment
 spec:
   template:
     spec:
       containers:
         - name: app
           image: nginx
           ......(省略)......
           lifecycle:
             preStop:
               exec:
                 command: ["sh", "-c", "sleep 5"]

もしもシステム全体で運用しているDeploymentがひとつだけならこれで対応は完了ですが、横展開として前述した様々なアプリケーションで同様の設定をおこなう必要があったり、それらのステージング環境についても状況を調査したり…と、修正自体は軽微でもシステム全体への適用を考えるとなかなか大変なものがあります

同じような話として、弊社では歴史的経緯からジョブスケジューラーとしてRundeckを採用しており、KubernetesのJobと組み合わせて使っているのですが、
ジョブのエラー発生時に通知させたり、タイムアウト時間を付与したりといった全ジョブで共通して設定したい内容について強制させる方法がなく、それによってジョブの実行遅延が生じてインシデントが発生してしまうなどの問題が発生していました

https://www.rundeck.com/

RundeckにはGit PluginによるSCM Export機能があり、GitHub等に用意したリポジトリにジョブ定義のyamlファイルを出力できます

https://docs.rundeck.com/docs/manual/projects/scm/git.html

KubernetesのマニフェストもRundeckのジョブ定義もyamlファイルとして考えれば同じものなので、
これらに対して単一の技術による設定値の検証を実現できれば、低コストで運用改善をおこなうことができそうです

どのように作るか

今回選定したConftestは、設定ファイルが一定のポリシーを満たしているかチェックするためのポリシーエンジンです
Go言語で実装されており単一の実行ファイルとして提供されるため、既存のCIに組み込んですぐに利用することができるのがメリットになると思います

https://www.conftest.dev/

以下のExampleで示されるように、yamlだけでなくTerraformで用いられているHCLやXMLなどにもポリシーチェックを書くことができます

https://www.conftest.dev/examples/

ポリシーはRegoという宣言型言語を用いて記述します
理解するまで最初は少し苦労したのですが、以下のBook公式ドキュメントなど丁寧に解説された資料が多くあります

https://zenn.dev/mizutani/books/d2f1440cfbba94

https://www.openpolicyagent.org/docs/latest/policy-language/

個人的に気に入っている点として、ポリシーに対して単体テストを記述できることもポイントです

https://www.conftest.dev/#testingverifying-policies

なお、選定にあたってはKyvernoGatekeeperも調査したのですが、稼働環境に対するポリシーチェックを主眼に置いていたりなどKubernetesに特化したユースケースを想定しているように思ったため、導入コストを鑑みてConftestを採用することとしました

https://kyverno.io/

https://open-policy-agent.github.io/gatekeeper/website/

できたもの

ポリシーチェックのサンプルと動作イメージを以下に示します🚓

Kubernetes

リポジトリルートに policies ディレクトリを作成し、サービスごとに固有のテストと共通のテストを分けて格納します

$ tree policies 
policies
├── ingress.rego
├── ingress_test.rego
├── job.rego
├── job_test.rego
├── lapras
│   ├── pod.rego
│   └── pod_test.rego
├── pod.rego
├── pod_test.rego
├── scout
│   ├── pod.rego
│   └── pod_test.rego
├── settings
│   ├── default.yaml
│   └── helmfile.yaml
├── utils.rego
└── utils_test.rego

3 directories, 14 files

例として、pod.rego ファイルの内容の一部を示します
これは、Podを持つ各種リソースに対して、以下のようなポリシーチェックを提供します

  • CPUの要求量を明示させる
  • メモリのオーバーコミットを禁止する
  • 一部リポジトリを除き、latestタグの設定を禁止する
  • ポートを公開しているリソースについて
    • Probeの指定を必須にする
    • PreStop hookの指定を必須にする
    • terminationGracePeriodSecondsを明示する
pod.rego
package pod

import data.utils.get_all_containers
import data.utils.is_helmfile

target_resource_types := {"DaemonSet", "Deployment", "StatefulSet", "Job", "CronJob", "ReplicaSet", "ReplicationController"}

denied_tags := {":latest"}

denied_tags_exception_repositories := {"123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/xxxxx"}

warn_cpu_requests_not_set[msg] {
	not is_helmfile

	target_resource_types[input.kind]
	container := input.spec.template.spec.containers[_]
	not container.resources.requests.cpu

	msg := sprintf("%s: コンテナの必要CPUリソースを明示するため、コンテナ %s にCPU要求を指定してください", [input.metadata.name, container.name])
}

deny_memory_requests_not_set[msg] {
	not is_helmfile

	target_resource_types[input.kind]
	container := input.spec.template.spec.containers[_]
	not container.resources.requests.memory

	msg := sprintf("%s: コンテナが意図せずOOMされるのを防ぐため、コンテナ %s にメモリ要求を指定してください", [input.metadata.name, container.name])
}

deny_memory_limits_not_set[msg] {
	not is_helmfile

	target_resource_types[input.kind]
	container := input.spec.template.spec.containers[_]
	not container.resources.limits.memory

	msg := sprintf("%s: コンテナが意図せずOOMされるのを防ぐため、コンテナ %s にメモリ上限を指定してください", [input.metadata.name, container.name])
}

deny_image_tag_not_set[msg] {
	target_resource_types[input.kind]
	container := get_all_containers(input.spec.template.spec)[_]
	not contains(container.image, ":")

	msg := sprintf("%s: コンテナが意図せず更新されるのを防ぐため、イメージ名 %s にタグを指定してください", [input.metadata.name, container.image])
}

deny_image_tag_disallow[msg] {
	target_resource_types[input.kind]
	container := get_all_containers(input.spec.template.spec)[_]

	strings.any_suffix_match(container.image, denied_tags)
	not strings.any_prefix_match(container.image, denied_tags_exception_repositories)

	msg := sprintf("%s: コンテナのイメージ名 %s に許可されないタグが使用されています", [input.metadata.name, container.image])
}

warn_probes_not_set[msg] {
	not is_helmfile

	target_resource_types[input.kind]
	container := input.spec.template.spec.containers[_]
	container.ports

	not has_valid_probes(container)

	msg := sprintf("%s: ポートを公開している場合、コンテナ %s は livenessProbe に加えて、readinessProbe または startupProbe を設定してください", [input.metadata.name, container.name])
}

warn_termination_grace_period_seconds_not_set[msg] {
	not is_helmfile

	target_resource_types[input.kind]
	container := input.spec.template.spec.containers[_]
	container.ports

	not input.spec.template.spec.terminationGracePeriodSeconds

	msg := sprintf("%s: ポートを公開している場合、terminationGracePeriodSeconds を指定してください", [input.metadata.name])
}

warn_prestop_hook_not_set[msg] {
	not is_helmfile

	target_resource_types[input.kind]
	container := input.spec.template.spec.containers[_]
	container.ports

	not container.lifecycle.preStop.exec.command

	msg := sprintf("%s: ポートを公開している場合、コンテナ %s はpreStopフックを設定してください", [input.metadata.name, container.name])
}

has_valid_probes(container) {
	container.livenessProbe
	container.readinessProbe
} else {
	container.livenessProbe
	container.startupProbe
}

複数のポリシーチェックで共有したい記述については共通関数を作ることができます

utils.rego
package utils

import data.settings

is_empty(value) {
	is_null(value)
} else {
	count(value) == 0
}

has_any_value(value) {
	value
	not is_null(value)
}

get_all_containers(value) = containers {
	value.initContainers
	value.containers
	containers = array.concat(value.initContainers, value.containers)
} else = containers {
	value.containers
	containers = value.containers
} else = containers {
	value.initContainers
	containers = value.initContainers
} else = containers {
	containers = []
}

is_in_namespaces(value, namespaces) {
	value
	namespaces[value.metadata.namespace]
}

is_helmfile {
	settings.test_type == "helmfile"
}

ポリシーチェック実行時に設定情報を --data 引数で渡すことができます

default.yaml
settings:
    test_type: default

実行イメージを示します
manifests/ ディレクトリ配下に処理対象のyamlファイルがある場合、以下のようなコマンドでポリシーのテストがおこなえます

$ conftest test \
    --all-namespaces \
    --strict \
    --data policies/settings/default.yaml \
    --policy policies/ \
    manifests/ \
    --parser yaml

(省略)

WARN - manifests/xxx/yyy.yaml - pod - yyy: ポートを公開している場合、コンテナ zzz はpreStopフックを設定してください

6721 tests, 6692 passed, 29 warnings, 0 failures, 0 exceptions

--fail-on-warn を加えると fail_ のものに加えて warn_ で始まるポリシーチェックに合格しなかった場合に終了ステータスが 1 になります
これにより、問題のある既存のマニフェスト群にまずポリシーチェックを実装してから、漸進的にプロダクトを改善していくことが可能です


CIのイメージ

上記のポリシーチェックに対して、単体テストを書くこともできます
一例として、以下は pod.regowarn_prestop_hook_not_set に対する単体テストです

https://www.conftest.dev/#writing-unit-tests

pod_test.rego
package pod

import data.utils.is_empty

cfg_ok := parse_config("yaml", `
apiVersion: apps/v1
kind: Deployment
metadata:
  name: deployment
  namespace: test
spec:
  replicas: 1
  strategy:
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1
  template:
    spec:
      initContainers:
        - name: python
          image: python:v1
      containers:
        - name: nginx
          image: 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/nginx:v1
          ports:
            - name: http-port
              containerPort: 80
              protocol: TCP
          resources:
            requests:
              memory: 3000Mi
              cpu: 600m
            limits:
              memory: 3000Mi
          readinessProbe:
            httpGet:
              path: /health
              port: 80
          livenessProbe:
            httpGet:
              path: /health
              port: 80
          lifecycle:
            preStop:
              exec:
                command: ["sh", "-c", "sleep 40"]
      terminationGracePeriodSeconds: 45
`)

# ------------------------------
# warn_prestop_hook_not_set
# ------------------------------

test_warn_prestop_hook_not_set_ok {
	is_empty(warn_prestop_hook_not_set) with input as cfg_ok
}

test_warn_prestop_hook_not_set_ng {
	cfg := parse_config("yaml", `
apiVersion: apps/v1
kind: Deployment
metadata:
  name: deployment
  namespace: test
spec:
  template:
    spec:
      initContainers:
        - name: node
          image: node:v1
      containers:
        - name: nginx
          image: nginx:v1
          ports:
            - name: http-port
              containerPort: 80
              protocol: TCP
        - name: python
          image: python:v2
      terminationGracePeriodSeconds: 30
  `)
	warn_prestop_hook_not_set["deployment: ポートを公開している場合、コンテナ nginx はpreStopフックを設定してください"] with input as cfg
	not warn_prestop_hook_not_set["deployment: ポートを公開している場合、コンテナ python はpreStopフックを設定してください"] with input as cfg
}

上記のようなテストコードがあったとき、以下のコマンドでポリシーチェックのテストを実行することができます

$ conftest verify \
    --strict \
    --report full \
    --policy policies/

(略)

policies/pod_test.rego:
data.pod.test_warn_cpu_requests_not_set_ok: PASS (342.212µs)
data.pod.test_warn_cpu_requests_not_set_ng: PASS (269.531µs)
data.pod.test_deny_memory_requests_not_set_ok: PASS (321.981µs)
data.pod.test_deny_memory_requests_not_set_ng: PASS (246.496µs)
data.pod.test_deny_memory_limits_not_set_ok: PASS (670.714µs)
data.pod.test_deny_memory_limits_not_set_ng: PASS (728.324µs)
data.pod.test_deny_image_tag_not_set_ok: PASS (1.117798ms)
data.pod.test_deny_image_tag_not_set_ng: PASS (1.09238ms)
data.pod.test_deny_image_tag_disallow_ok: PASS (436.79µs)
data.pod.test_deny_image_tag_disallow_ng: PASS (1.325713ms)
data.pod.test_warn_probes_not_set_ok: PASS (349.649µs)
data.pod.test_warn_probes_not_set_ng: PASS (339.81µs)
data.pod.test_warn_termination_grace_period_seconds_not_set_ok: PASS (344.542µs)
data.pod.test_warn_termination_grace_period_seconds_not_set_ng: PASS (251.95µs)
data.pod.test_warn_prestop_hook_not_set_ok: PASS (282.142µs)
data.pod.test_warn_prestop_hook_not_set_ng: PASS (381.049µs)

(略)
--------------------------------------------------------------------------------
PASS: 65/65

その他、一例として以下のようなポリシーチェックをおこなっています

  • Ingress
    • AWS Load Balancer ControllerのIngress Groupを設定して、Ingress毎にALBが生成されないようになっていること
    • 社外公開しているエンドポイントについてAWS WAFを適用していること
  • Job
  • サービス固有
    • 設定必須としたい特定の環境変数が設定されていること

https://creators.oisixradaichi.co.jp/entry/2022/05/30/132412

https://kubernetes.io/ja/docs/concepts/workloads/controllers/job/#ttl-mechanism-for-finished-jobs

https://kubernetes.io/ja/docs/concepts/workloads/controllers/job/#job-termination-and-cleanup

余談ですが、リソース毎に一般的にチェックすべき項目はKubernetesの知識地図という書籍が参考になるものと思います
Conftestについても少しですが触れられています

https://www.amazon.co.jp/dp/4297135736

Rundeck

まず、RundeckからGitへのSCM Exportを設定して、GitHubの特定のリポジトリにジョブをexportします

https://qiita.com/aki-nasu/items/066bdec24bf6a457f065

SCM Settingにて Job Source Files > Formatyaml を選択していると、リポジトリに以下のようなファイルが格納されます

example_job.yaml
- defaultTab: nodes
  description: |-
    test command description1
    test command description2
  executionEnabled: true
  id: 00000000-0000-0000-0000-000000000000
  loglevel: INFO
  name: example_job
  nodeFilterEditable: false
  notification:
    onfailure:
      plugin:
        configuration:
          slack_template: ''
          webhook_url_override: ''
        type: SlackNotification
  retry: '0'
  schedule:
    month: '*'
    time:
      hour: '*'
      minute: 6/15
      seconds: '0'
    weekday:
      day: '*'
    year: '*'
  scheduleEnabled: true
  sequence:
    commands:
    - exec: ls -l
    keepgoing: false
    strategy: node-first
  uuid: 00000000-0000-0000-0000-000000000000
  timeout: '6h'

上記のようなファイルに対して、以下の仕様のポリシーをチェックを実装するRegoを示します

  • 自動リトライを禁止する
  • タイムアウトを明示させる
  • エラー通知をSlackに送る
scm.rego
package scm

deny_retry_setted[msg] {
	input.retry
	input.retry != "0"

	msg := sprintf("%s: 多重実行されることを防ぐため、リトライ設定に '0' を明示してください", [input.name])
}

deny_timeout_not_set[msg] {
	not input.timeout

	msg := sprintf("%s: ハングアップを抑止するため、タイムアウトを設定してください", [input.name])
}

deny_timeout_invalid_value[msg] {
	input.timeout
	valid_timeout := get_timeout_value(data.conftest.file)
	input.timeout != valid_timeout

	msg := sprintf("%s: ハングアップを抑止するため、タイムアウトに '%s' を設定してください", [input.name, valid_timeout])
}

deny_failure_notification_not_set[msg] {
	not input.notification.onfailure

	msg := sprintf("%s: ジョブの実行結果を通知するため、失敗時通知設定を行ってください", [input.name])
}

deny_failure_slack_notification_not_set[msg] {
	input.notification.onfailure
	not input.notification.onfailure.plugin.type == "SlackNotification"

	msg := sprintf("%s: ジョブの失敗時通知はSlackに送信してください", [input.name])
}

deny_multiple_executions[msg] {
	input.multipleExecutions

	msg := sprintf("%s: 多重実行を防ぐため、ジョブの並列実行設定はおこなわないでください", [input.name])
}

# 自動実行されるジョブのみを対象とする
is_scheduled_job(definition) {
	definition.executionEnabled
	definition.scheduleEnabled
}

# 特定のパス配下のyamlのみ異なるタイムアウトを変更する
get_timeout_value(fileInfo) = "1d" {
	fullPath := concat("/", [fileInfo.dir, fileInfo.name])
	contains(fullPath, "/long-job/")
} else = "6h"

自動テストは以下のようになります
Conftestにおいて data.conftest.file 配下に処理対象のファイルパスやファイル名が設定された状態でチェックがおこなわれるため、ポリシーチェック時に活用できます
Regoにおいては with 句で特定の値をモックすることができるので、これを複数指定してテストパターンを網羅します

https://www.openpolicyagent.org/docs/latest/policy-language/#with-keyword

scm_test.rego
package scm

import data.utils.is_empty

mocked_conftest := {"file": {
	"dir": "/path/to/file",
	"name": "example_job.yaml",
}}

cfg_ok := parse_config("yaml", `
- defaultTab: nodes
  description: |-
    test command description1
    test command description2
  executionEnabled: true
  id: 00000000-0000-0000-0000-000000000000
  loglevel: INFO
  name: example_job
  nodeFilterEditable: false
  notification:
    onfailure:
      plugin:
        configuration:
          slack_template: ''
          webhook_url_override: ''
        type: SlackNotification
  retry: '0'
  schedule:
    month: '*'
    time:
      hour: '*'
      minute: 6/15
      seconds: '0'
    weekday:
      day: '*'
    year: '*'
  scheduleEnabled: true
  sequence:
    commands:
    - exec: ls -l
    keepgoing: false
    strategy: node-first
  uuid: 00000000-0000-0000-0000-000000000000
  timeout: '6h'
  `)

# ------------------------------
# deny_timeout_invalid_value
# ------------------------------

test_deny_timeout_invalid_value_ok {
	cfg := cfg_ok[_]
	is_empty(deny_timeout_invalid_value) with input as cfg with data.conftest as mocked_conftest
}

test_deny_timeout_invalid_value_ng {
	cfg := parse_config("yaml", `
- defaultTab: nodes
  executionEnabled: true
  name: example_job
  scheduleEnabled: true
  timeout: '10h'
  `)[_]
	deny_timeout_invalid_value["example_job: ハングアップを抑止するため、タイムアウトに '6h' を設定してください"] with input as cfg with data.conftest as mocked_conftest
}

test_deny_timeout_invalid_value_ng_long_job {
	mocked_conftest := {"file": {
		"dir": "/path/to/file/long-job",
		"name": "example_job.yaml",
	}}

	cfg := parse_config("yaml", `
- defaultTab: nodes
  executionEnabled: true
  name: example_job
  scheduleEnabled: true
  timeout: '6h'
  `)[_]
	deny_timeout_invalid_value["example_job: ハングアップを抑止するため、タイムアウトに '1d' を設定してください"] with input as cfg with data.conftest as mocked_conftest
}

今回の記事の主旨とそれるため詳細は割愛しますが、弊社ではRundeckからGitHubへのジョブエクスポート状況を週次でチェックし、エクスポート結果に対してConftestを実行するGitHub Actionsを設定して運用しています

プロジェクトのステータスはRundeck API経由で /api/15/project/[PROJECT]/scm/[INTEGRATION]/status にアクセスすることで取得できます
https://docs.rundeck.com/docs/api/#get-project-scm-status

本当はExport処理自体を自動化したいのですが、少し調べた限りには具体的な方法が見つけられなかったため、まずは手動で運用することとしました
今後の課題としたいと思います


通知の様子

番外編: Terraform

こちらは同僚のnappaさんが作ってくれたものですが、せっかくなのでご紹介します

ConftestはTerraform(HCL)についても対応していますが、弊社ではtflintを導入していたため、tflint-ruleset-opaを用いてtflint経由で実行します

https://github.com/terraform-linters/tflint

https://github.com/terraform-linters/tflint-ruleset-opa

tflint-ruleset-awsと組み合わせて使った場合の設定ファイルのサンプルを示します

https://github.com/terraform-linters/tflint-ruleset-aws

.tflint.hcl
# -------------------------
# Plugins
# -------------------------

plugin "aws" {
    enabled = true
    version = "0.32.0"
    source  = "github.com/terraform-linters/tflint-ruleset-aws"
}

plugin "opa" {
  enabled = true
  version = "0.7.0"
  source  = "github.com/terraform-linters/tflint-ruleset-opa"
}

# -------------------------
# 除外ルール
# -------------------------

rule "terraform_typed_variables" {
  enabled = false
}

ルールの一例を示します

以下は、aws_iam_policy_attachmentの使用を禁止するルールです
aws_iam_policy_attachmentは、適切に利用できないと異なるtfstateで管理しているIAMポリシーの紐づけを壊してしまうケースがあるため、弊社では利用しないようにしています

https://qiita.com/bilzard/items/8b54c40351e2ff39afa0

aws_iam.rego
package tflint

import rego.v1

deny_using_aws_iam_policy_attachment contains issue if {
	resources := terraform.resources("aws_iam_policy_attachment", {}, {})

	not count(resources) == 0

	issue := tflint.issue("aws_iam_policy_attachment は使用せず aws_iam_role_policy_attachment 等を使用してください", resources[_].decl_range)
}

実行イメージは以下です

$ tflint --init
Installing "aws" plugin...
Installed "aws" (source: github.com/terraform-linters/tflint-ruleset-aws, version: 0.32.0)
Installing "opa" plugin...
Installed "opa" (source: github.com/terraform-linters/tflint-ruleset-opa, version: 0.7.0)

# base_dirはリポジトリルート
$ base_dir="$(pwd)"
$ cd target_directory/
$ TFLINT_OPA_POLICY_DIR="${base_dir}/.tflint.d" tflint \
    --config="${base_dir}/.tflint.hcl" \
    --minimum-failure-severity=warning \
    -f compact
1 issue(s) found:

aws_iam_policy_attachment.tf:22:1: Error - aws_iam_policy_attachment は使用せず aws_iam_role_policy_attachment 等を使用してください (opa_deny_using_aws_iam_policy_attachment)

単体テストもConftestと同様に書くことができます

aws_iam_test.rego
package tflint

import rego.v1

failed_resources(type, schema, options) := terraform.mock_resources(type, schema, options, {"main.tf": `
    resource "aws_iam_policy_attachment" "ecs_instance" {
        name = "ecs-instance"
        roles = [
            aws_iam_role.ecs_ec2_role.name,
        ]
        policy_arn = aws_iam_policy.ecs_instance_policy.arn
    }
`})

test_deny_using_aws_iam_policy_attachment_failed if {
    issues := deny_using_aws_iam_policy_attachment with terraform.resources as failed_resources

    count(issues) == 1
    issue := issues[_]
    issue.msg == "aws_iam_policy_attachment は使用せず aws_iam_role_policy_attachment 等を使用してください"
}

単体テストの実行は以下で可能です

https://github.com/terraform-linters/tflint-ruleset-opa/pull/15

$ TFLINT_OPA_TEST=1 tflint

公式からsetup-tflintというActionsが出ているため、GitHub Actionsにも組み込みやすいです

https://github.com/marketplace/actions/setup-tflint

所感

しばらく運用してみての所感としては以下がありました

  • ポリシーチェックにより対応の横展開チェックが不要になり気が楽になった
    • 今後新しく作成されるリソースに対しても過去の知見が適用されるのには大きなメリットを感じる
  • 大量の設定ファイルのチェックに悩まされている組織には効果的かも
    • アーキテクチャ面での複雑度が低く、漸進的に問題を解決していけるのが個人的に好み
    • ポリシーの単体テストも書けるので退行にも強い
  • Conftest / Rego言語そのものについてはまだ試行錯誤中
    • Conftestの単体テストについて、アサーションを書いたテストケースを作るというよりも、テストデータでコードを実行してチェックを通過したor通過しなかったという書き方になるため、失敗時に何が原因だったか調査するのが難しい
    • Regoのデバッグ用にトレースログを出すことができるものの内容が直感的でない
  • 一方で、達成したいポリシーを宣言的に記述するため、辛みが出てきて他の実装に変えたくなった場合も移行は容易に思えた
    • リポジトリ内にある程度サンプルができてしまえば書き味は気にならなくなる
      • GitHub Copilotに これこれの仕様のポリシーチェックをConftest用のRego言語で書いて と質問したら大体解決した

ということでまだまだ課題もありますが、低コストで改善の仕組みを回し始めることができたので満足です
よければ使ってみてください🐣

LAPRAS Inc.

Discussion