Когда нужен OPA
Checkov ловит шаблонные правила, terraform-compliance, простые утверждения над plan'ом. Когда нужны cross-resource проверки или сложные условия с lookup'ами, OPA.
Примеры что OPA умеет, а другие нет:
- «У каждого
aws_db_instanceдолжен бытьaws_db_subnet_groupв той же VPC, что и Lambda, которая его читает». - «Если ресурс затронут plan'ом и тэг
Environment=prod, то approver должен быть из списка senior'ов». - «Имя бакета должно следовать паттерну
<team>-<env>-<purpose>-<hash>». - «Стоимость нового ресурса меньше $200/мес» (с внешним lookup в Infracost).
Прикинул, нужно cross-resource или lookup, берёшь OPA. Иначе хватает более простых.
Минимальное правило
OPA-policy для Terraform пишутся в Rego. Файл policies/s3.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.
# подготовить 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 видит:
{"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-тесты. Для каждого правила, свой тест:
# 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
}
Запуск:
opa test policies/
Cross-resource правило
Реальный пример: «если plan создаёт aws_lambda_function, который
читает RDS, RDS должен быть в той же VPC».
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-интеграция
- 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:
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. Слои не конкурируют, дополняют.