lesson ── terraform-production ── ~16 мин ── 5 шагов
OPA + Rego, для правил, которых нет в Checkov. Бизнес-инвариант (все ресурсы с тегом CostCenter), cross-resource (Lambda и RDS в одной VPC), conditional (prod-ресурс, только из main-ветки). На этом уроке напишешь Rego-правила, прогонишь conftest на plan.json.
интерактивный sandbox
Поднимется пара контейнеров: terraform 1.9 и localstack 3.8 в одной сети. В браузере откроется терминал, можно сразу terraform init. Каждый шаг проверяется автоматически. TTL 45 минут, без регистрации.
stack ── terraform · localstack · 1 GB RAM · самоуничтожается через 45 мин простоя
Бизнес-правило: все S3-бакеты должны иметь теги CostCenter и Environment. Один из бакетов нарушит.
cd /home/student/tf-opa
cat > main.tf <<'EOF'
resource "aws_s3_bucket" "good" {bucket = "linuxlab-opa-good"
tags = {CostCenter = "ml-team"
Environment = "dev"
}
}
resource "aws_s3_bucket" "missing_tags" {bucket = "linuxlab-opa-missing"
tags = {Owner = "student"
}
}
EOF
terraform init -no-color > /dev/null
terraform plan -no-color -out=plan.tfplan
terraform show -json plan.tfplan > plan.json
plan.json готов. Сейчас напишем правило.
✓ Plan сгенерирован. Бакет missing_tags нарушает политику.
cat > policies/tags.rego <<'EOF'
package terraform.tags
import future.keywords.contains
import future.keywords.if
import future.keywords.in
mandatory_tags := {"CostCenter", "Environment"}deny contains msg if {some resource in input.resource_changes
resource.type == "aws_s3_bucket"
resource.change.actions[_] == "create"
tags := object.get(resource.change.after, "tags", {})some tag in mandatory_tags
not tags[tag]
msg := sprintf("%s missing mandatory tag %q", [resource.address, tag])}
EOF
Что делает: для каждого aws_s3_bucket в plan'е, который будет
создан, проверяет наличие каждого обязательного тега. Если нет
добавляет deny-сообщение.
Сразу прогоним:
opa eval -d policies/ -i plan.json 'data.terraform.tags.deny'
Должно вернуть массив с двумя сообщениями (CostCenter и Environment отсутствуют у missing_tags).
✓ Rego видит нарушение. OPA опознала bucket без тегов.
Тесты на правила, критичны: правило с багом игнорирует нарушения.
cat > policies/tags_test.rego <<'EOF'
package terraform.tags
import future.keywords.if
test_deny_when_missing_tags if { mock := {"resource_changes": [{"address": "aws_s3_bucket.bad",
"type": "aws_s3_bucket",
"change": {"actions": ["create"],
"after": {"tags": {"Owner": "x"}},},
}]}
count(deny) == 2 with input as mock
}
test_no_deny_with_all_tags if { mock := {"resource_changes": [{"address": "aws_s3_bucket.good",
"type": "aws_s3_bucket",
"change": {"actions": ["create"],
"after": {"tags": {"CostCenter": "team",
"Environment": "dev",
}},
},
}]}
count(deny) == 0 with input as mock
}
test_ignores_delete_actions if { mock := {"resource_changes": [{"address": "aws_s3_bucket.gone",
"type": "aws_s3_bucket",
"change": {"actions": ["delete"], "after": null},}]}
count(deny) == 0 with input as mock
}
EOF
opa test policies/
Должно показать «PASS: 3/3». Если бы правило игнорировало
missing tags, test_deny_when_missing_tags упал бы. Это страховка.
✓ Rego-правила протестированы. Безопасно использовать в CI.
opa eval гибкий, но в CI хочется human-friendly формат с pass/fail.
conftest, обёртка с выводом, exit-code'ом, severity.
Сначала, структура папки policies/ под conftest:
# conftest по умолчанию читает namespace "main", переименуем package
sed -i 's/^package terraform.tags$/package main/' policies/tags.rego
sed -i 's/^package terraform.tags$/package main/' policies/tags_test.rego
conftest test plan.json --policy policies/
echo "exit: $?"
Должно показать FAIL с двумя сообщениями и exit 1.
Альтернатива, оставить namespace и использовать --all-namespaces
или --namespace terraform.tags. Это вопрос вкуса; CI один раз
решает.
✓ conftest показывает то же, что opa eval, но в CI-friendly формате.
OpenTofu держит CLI и state совместимыми с Terraform по командам
этого шага: миграция обычно проходит через mv .terraform .terraform.bak; tofu init -upgrade. Но при первом переходе
сделай backup state и прогон на feature-branch - расхождения
концентрируются в новых фичах (variables в backend,
state-encryption, OCI registry-backed модули). См.
tf-opentofu-parity для полной матрицы.
Добавим теги:
sed -i '/"missing_tags"/,/^}$/ s|tags = {|tags = {\n CostCenter = "data-team"\n Environment = "dev"|' main.tfcat main.tf
terraform plan -no-color -out=plan.tfplan
terraform show -json plan.tfplan > plan.json
conftest test plan.json --policy policies/
echo "exit: $?"
Теперь exit 0, gate чистый. PR merge'абельный.
✓ Policy gate проходит. В CI это последний этап перед apply-job.
Простой пример был, все S3 с тегами. Что OPA умеет лучше других:
# «Если в plan'е создаётся Lambda, она должна быть в одной
# VPC с RDS, на которую ссылается через DB_HOST env»
deny contains msg if {some lambda in input.resource_changes
lambda.type == "aws_lambda_function"
lambda.change.actions[_] == "create"
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
# subnets
lambda_subnets := lambda.change.after.vpc_config[0].subnet_ids
db_subnet_group := db.change.after.db_subnet_group_name
count(lambda_subnets) > 0
db_subnet_group != ""
msg := sprintf(
"Lambda %q is in VPC subnet %v but reads RDS %q in subnet group %q, check VPC alignment",
[lambda.address, lambda_subnets, db.address, db_subnet_group],
)
}
Checkov такое не напишет. terraform-compliance, тоже. Это OPA-территория.
Подробнее, tf-policy-as-code.
terraform plan -out=plan.tfplan && terraform show -json plan.tfplan > plan.json.
Полученный JSON прогоняешь через opa eval или conftest test. Правила
в Rego используют input.resource_changes. deny, список сообщений;
непустой = fail.
команды
opa version && opa eval --help | head -5OPA в образе уже стоит.opa eval --data policies/ --input plan.json 'data.terraform.deny'что вернёт rule.terraform show -json plan.tfplan > plan.jsonформат, который OPA понимает.концепции