linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
Intro
Lessons
Footer
linuxlab-УчебникиЦеныО платформеКонфиденциальность и куки
Copyright © 2026 LinuxLab. Все права защищены.
linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
  • Введение
  • Уроки
  • How it works
  • База знаний
  • Шпаргалка
  • Capstone
  • Собеседование
home/terraform/kb/CI/CD/tf-policy-as-code

kb/cicd ── CI/CD ── intermediate

OPA + Rego, policy as code для Terraform plan

Policy-as-code = правила («все S3 шифрованы», «никаких IAM с *») написаны кодом, гонятся в CI, fail'ят PR. OPA, стандарт, Rego, язык. conftest, обёртка с CLI-friendly выводом; читает plan.json, прогоняет правила, exit 0/1. Зрелее и дороже на старте, чем Checkov, но позволяет cross-resource правила любой сложности.

view as markdownaka: terraform-opa, terraform-policy-as-code, conftest-terraform

Когда нужен 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:

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-complianceterraform-compliance: BDD-проверки на plan-файлУтилита terraform-compliance читает plan-файл (`plan.json`) и применяет к нему BDD-правила в Gherkin. «Given a resource of type X, it must contain a property Y», читаемо для не-инженеров, гарантирует политику до apply. Альтернатива OPA/Rego для команд, которые предпочитают естественный язык, но менее мощная, нельзя писать сложные cross-resource проверки.
  • tf-checkovCheckov: статический анализ HCLCheckov, Python-сканер от Prisma Cloud, проверяет `.tf` и `plan.json` против ~2000 встроенных правил (CKV_AWS_*, CKV_K8S_*, etc.). Запускается на HCL до plan'а (быстрее) или на JSON-plan (богаче, видит вычисленные значения). Suppressions делаются комментарием в HCL или в файле конфигурации; baseline-файл фиксирует найденные issues как «приемлемые на сейчас», чтобы новые сразу ломали CI.
  • tf-plan-apply-ciPlan-as-artifact и automation mode в CIВ CI plan-job создаёт `plan.tfplan`, грузит как артефакт. Apply-job скачивает артефакт и применяет ровно его, никакого нового plan на apply-стадии. Это гарантирует: то что показали reviewer'у в PR ровно то, что применилось. Все команды с `-input=false`, `-no-color`, `-lock-timeout` чтобы автоматизация не зависала. Plan-файл содержит sensitive, обращайся с ним как с секретом.
Footer
linuxlab-
Copyright © 2026 LinuxLab. Все права защищены.
Учебники
Цены
О платформе
Конфиденциальность и куки