# OPA + Rego, policy as code для Terraform plan _CI/CD · TerraformLab Knowledge Base_ **TL;DR:** Policy-as-code = правила («все S3 шифрованы», «никаких IAM с *») написаны кодом, гонятся в CI, fail'ят PR. OPA, стандарт, Rego, язык. conftest, обёртка с CLI-friendly выводом; читает plan.json, прогоняет правила, exit 0/1. Зрелее и дороже на старте, чем Checkov, но позволяет cross-resource правила любой сложности. ## Когда нужен OPA Checkov ловит шаблонные правила, terraform-compliance, простые утверждения над plan'ом. Когда нужны **cross-resource** проверки или **сложные условия с lookup'ами**, OPA. Примеры что OPA умеет, а другие нет: - «У каждого `aws_db_instance` должен быть `aws_db_subnet_group` в той же VPC, что и Lambda, которая его читает». - «Если ресурс затронут plan'ом и тэг `Environment=prod`, то approver должен быть из списка senior'ов». - «Имя бакета должно следовать паттерну `---`». - «Стоимость нового ресурса меньше $200/мес» (с внешним lookup в Infracost). Прикинул, нужно cross-resource или lookup, берёшь OPA. Иначе хватает более простых. ## Минимальное правило OPA-policy для Terraform пишутся в Rego. Файл `policies/s3.rego`: ```rego package terraform.s3 import future.keywords.if import future.keywords.in # Deny S3 buckets without encryption deny contains msg if { some resource in input.resource_changes resource.type == "aws_s3_bucket_server_side_encryption_configuration" # ... } deny contains msg if { some resource in input.resource_changes resource.type == "aws_s3_bucket" resource.change.actions[_] == "create" bucket_name := resource.change.after.bucket not has_encryption(bucket_name) msg := sprintf("S3 bucket %q must have server_side_encryption_configuration", [bucket_name]) } has_encryption(bucket_name) if { some enc in input.resource_changes enc.type == "aws_s3_bucket_server_side_encryption_configuration" enc.change.after.bucket == bucket_name } ``` Что делает: ищет все `aws_s3_bucket`, для каждого проверяет, есть ли отдельный `aws_s3_bucket_server_side_encryption_configuration` с тем же bucket'ом. ## Запуск через conftest conftest, CLI-обёртка над OPA, специально под policy-testing. ```bash # подготовить plan.json terraform plan -out=plan.tfplan terraform show -json plan.tfplan > plan.json # натравить policies conftest test plan.json ``` Вывод: ``` FAIL - plan.json - terraform.s3 - S3 bucket "logs" must have server_side_encryption_configuration 2 tests, 1 passed, 1 failure ``` Exit 1 при FAIL, CI ломается. ## Структура plan.json Что conftest видит: ```json { "resource_changes": [ { "address": "aws_s3_bucket.logs", "type": "aws_s3_bucket", "change": { "actions": ["create"], "before": null, "after": { "bucket": "linuxlab-logs", "tags": {"Owner": "student"} } } }, ... ] } ``` Этот формат, то, что Rego «видит» через `input`. Ключевые поля: - `resource_changes[]`, все изменения. - `.type`, `aws_s3_bucket` и т.д. - `.change.actions`, `["create"]`, `["update"]`, `["delete"]`, `["create", "delete"]` (replace). - `.change.after`, желаемое состояние (то что станет). - `.change.before`, что есть сейчас. - `.address`, `module.x.aws_s3_bucket.this`. ## Тесты на сами правила Rego поддерживает unit-тесты. Для каждого правила, свой тест: ```rego # policies/s3_test.rego package terraform.s3 test_encryption_required_when_bucket_created if { mock := {"resource_changes": [ { "address": "aws_s3_bucket.foo", "type": "aws_s3_bucket", "change": {"actions": ["create"], "after": {"bucket": "foo"}}, }, ]} count(deny) > 0 with input as mock } test_no_deny_when_encryption_present if { mock := {"resource_changes": [ { "address": "aws_s3_bucket.foo", "type": "aws_s3_bucket", "change": {"actions": ["create"], "after": {"bucket": "foo"}}, }, { "address": "aws_s3_bucket_server_side_encryption_configuration.foo", "type": "aws_s3_bucket_server_side_encryption_configuration", "change": {"actions": ["create"], "after": {"bucket": "foo"}}, }, ]} count(deny) == 0 with input as mock } ``` Запуск: ```bash opa test policies/ ``` ## Cross-resource правило Реальный пример: «если plan создаёт `aws_lambda_function`, который читает RDS, RDS должен быть в той же VPC». ```rego package terraform.lambda_rds import future.keywords.contains import future.keywords.if import future.keywords.in deny contains msg if { some lambda in input.resource_changes lambda.type == "aws_lambda_function" # Lambda's vpc_config subnet some subnet_id in lambda.change.after.vpc_config[_].subnet_ids # RDS environment-var DB_HOST refers to which DB? db_host := lambda.change.after.environment[0].variables.DB_HOST db_address := substr(db_host, 0, indexof(db_host, ".")) some db in input.resource_changes db.type == "aws_db_instance" db.change.after.address == db_address lambda_vpc := vpc_for_subnet(subnet_id) rds_vpc := vpc_for_subnet(db.change.after.db_subnet_group_name) lambda_vpc != rds_vpc msg := sprintf("Lambda %q in VPC %q reads RDS %q in different VPC %q", [lambda.address, lambda_vpc, db.address, rds_vpc]) } ``` Это **невозможно** написать в Checkov или terraform-compliance. OPA это делает естественно. ## CI-интеграция ```yaml - name: Install conftest run: | wget -O conftest.tgz https://github.com/open-policy-agent/conftest/releases/download/v0.55.0/conftest_0.55.0_Linux_x86_64.tar.gz tar xzf conftest.tgz sudo mv conftest /usr/local/bin/ - name: Generate plan.json run: | terraform plan -out=plan.tfplan terraform show -json plan.tfplan > plan.json - name: Policy check run: conftest test plan.json -p policies/ ``` Можно использовать `--all-namespaces`, чтобы прогнать все packages из `policies/`. По дефолту conftest читает только default-namespace. ## Структура реальной policy-suite ``` policies/ ├── main.rego # cross-cutting (mandatory tags) ├── s3/ │ ├── encryption.rego │ ├── public_access.rego │ └── encryption_test.rego ├── iam/ │ ├── no_wildcards.rego │ ├── no_admin_attach.rego │ └── no_wildcards_test.rego ├── network/ │ ├── vpc_consistency.rego │ └── vpc_consistency_test.rego └── lib/ └── helpers.rego # shared functions ``` Тесты рядом с правилами. Helpers, переиспользуемые функции. ## Soft-fail и severity Иногда нужна warning'и, не fail'ы. conftest: ```rego warn contains msg if { # ... checks } ``` По умолчанию warns не валят CI, видны в выводе. Управляется через `--no-fail` или конфигом. ## Подводные камни - **Rego, отдельный язык.** Кривая обучения реальная. Многоэтапные `some x in ...; some y in ...` сложные для tutorial-знатока. Начни с одного правила, разогрейся. - **plan.json формат меняется.** Между minor-версиями Terraform поля в `resource_changes[].change` иногда переименовываются. Rego-правила нужно обновлять. Прибей версию терраформа в CI. - **Compute-cost.** OPA быстрая, но 500+ resources × 50 правил = пару секунд. На больших mono-state'ах заметно. Профилируй (`opa eval --profile`) и оптимизируй cross-resource правила. - **Cross-resource не всегда возможна.** В plan.json не всегда есть данные обо всём, некоторые computed-атрибуты остаются `null` до apply. Правила пиши осторожно: `not x` для null = true, не желаемое поведение. - **conftest и OPA, разные CLI.** Чёткая граница: opa, general-purpose, conftest, для CI-friendly testing с разделением по namespaces. Документация Rego показывает примеры на `opa eval`, на conftest надо адаптировать. - **Maintenance cost.** Policy-suite это код. Его нужно ревьювить, обновлять, тестировать. Сделал, назначь owner. Без owner'а через год вместо «policy-gate» получишь «gate, который все skip'ают suppression'ами». - **Не заменяет статический анализ.** OPA на plan.json, runtime-policy. Стиль HCL, синтаксис, deprecated-args, это `terraform fmt`, `validate`, `tflint`. Слои не конкурируют, дополняют. ## Команды ```bash conftest test plan.json ``` Прогнать policies/ против plan.json. Exit 1 на любую deny. ```bash conftest test plan.json --all-namespaces ``` Все packages в policies/. Без флага, только default-namespace. ```bash opa test policies/ ``` Unit-тесты на сами правила. Должны pass'нуть перед использованием в CI. ```bash opa eval --data policies/ --input plan.json 'data.terraform.s3.deny' ``` Прямой OPA-запуск, показывает что вернёт rule, удобно для debug. ## См. также - [terraform-compliance: BDD-проверки на plan-файл](/terraform/kb/terraform-compliance.md) - [Checkov: статический анализ HCL](/terraform/kb/tf-checkov.md) - [Plan-as-artifact и automation mode в CI](/terraform/kb/tf-plan-apply-ci.md)