тестирование IaC на Terraform, Test Kitchen и InSpec

Тестирование инфраструктуры как кода: надёжные релизы с Terraform и Kitchen-Terraform

9 минут

Ошибки в IaC по-прежнему дают простои и перерасход. В материале — слоистая стратегия тестов, пошаговый пример Kitchen-Terraform и InSpec для модуля AWS S3 и практики, чтобы проверки инфраструктуры оставались честными в CI.

Почему IaC ломается, если тестирование остаётся «по остаточному принципу»

Инфраструктура как код давно стала опорой DevOps, но ошибочные конфигурации по-прежнему ведут к простоям, дырам в безопасности и неожиданным счетам от облака. Привычные подходы к тестам часто относят инфраструктуру к второму плану по сравнению с приложением, поэтому команды упускают поведение, зависящее от среды, стыковку модулей и дрейф между объявленным в коде и фактически развёрнутым состоянием. Без системных проверок возникает та же логика «у меня работает», но уже в масштабе инфраструктуры: локально всё выглядело корректно, а в продакшене падает из-за версии провайдера, нюансов API или скрытых зависимостей.

Слой стратегии: модульные, интеграционные, сквозные проверки и дрейф

Относитесь к IaC как к критичному коду. «Модульный» уровень проверяет синтаксис и изоляцию ресурсов внутри модуля. Интеграционный прогон показывает, что модули согласованно работают в изолированном аккаунте или проекте. Сквозной сценарий поднимает полный стек во временной среде. Обнаружение дрейфа непрерывно сравнивает живое состояние с тем, что зафиксировано в репозитории. Kitchen-Terraform подключает Terraform к Test Kitchen: вы реально применяете конфигурацию и проверяете результат через InSpec на живом или имитируемом провайдере и получаете воспроизводимый цикл вместо разовых ручных проверок.

Пример структуры репозитория с модулем Terraform

Дерево ниже отражает типичное разделение: код модуля в корне, Gemfile для Test Kitchen, при необходимости корневой kitchen.yml, а интеграционные тесты лежат в test/integration/default вместе с controls и kitchen.yml для suite.

Repository layout
terraform-module-s3-bucket/
├── main.tf
├── variables.tf
├── outputs.tf
├── test/
│   ├── integration/
│   │   └── default/
│   │       ├── controls/
│   │       │   └── s3_bucket.rb
│   │       └── kitchen.yml
│   └── unit/
│       └── test_s3_bucket.rb
├── Gemfile
└── kitchen.yml

Модуль Terraform: корзина S3 с версионированием и SSE

Минимальный модуль соответствует актуальному разбиению в AWS provider (начиная с v4): в `aws_s3_bucket` остаются имя и теги, версионирование задаёт ресурс `aws_s3_bucket_versioning`, шифрование по умолчанию — отдельный `aws_s3_bucket_server_side_encryption_configuration` с SSE-AES256. Вложенные блоки `versioning` и `server_side_encryption_configuration` на самой корзине в новых версиях провайдера только для чтения; попытка управлять ими там приведёт к ошибке. Переменные по-прежнему можно передавать из Kitchen для уникальных одноразовых ресурсов.

main.tf
variable "bucket_name" {
  description = "Name of the S3 bucket"
  type        = string
}

variable "tags" {
  description = "Tags to apply to the bucket"
  type        = map(string)
  default     = {}
}

resource "aws_s3_bucket" "this" {
  bucket = var.bucket_name
  tags   = var.tags
}

resource "aws_s3_bucket_versioning" "this" {
  bucket = aws_s3_bucket.this.id

  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_server_side_encryption_configuration" "this" {
  bucket = aws_s3_bucket.this.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}

Драйвер Kitchen-Terraform и верификатор

В suite заданы драйвер и provisioner Terraform с корнем в каталоге модуля, в variables передаётся имя корзины с меткой времени и теги, верификатор InSpec указывает на каталог интеграционных тестов. Платформа ubuntu-22.04 — распространённая метка в Kitchen, даже если сами проверки выполняются по API облака.

test/integration/default/kitchen.yml
driver:
  name: terraform

provisioner:
  name: terraform
  root_folder: ../..
  variables:
    bucket_name: "test-kitchen-s3-bucket-<%= Time.now.to_i %>"
    tags:
      Environment: test
      Kitchen: verifies

verifier:
  name: inspec

platforms:
  - name: ubuntu-22.04

suites:
  - name: default
    verifier:
      inspec_tests:
        - test/integration/default

Control InSpec для уже созданной корзины

После apply InSpec через AWS API проверяет, что корзина существует, включено версионирование и задана конфигурация шифрования. Падение теста блокирует продвижение изменений, потому что это сигнал о реальной ошибке конфигурации, а не о расхождении с моком.

test/integration/default/controls/s3_bucket.rb
control 's3-bucket-basics' do
  impact 1.0
  title 'Ensure S3 bucket has versioning and encryption'

  describe aws_s3_bucket(bucket_name: input('bucket_name')) do
    it { should exist }
    its('versioning') { should eq true }
    its('server_side_encryption_configuration') { should_not be_empty }
  end
end

Создание, проверка, уничтожение

Цикл создаёт реальную корзину в AWS на короткоживущих учётных данных, прогоняет Terraform, выполняет InSpec и затем удаляет ресурсы. Это медленнее моков, но ловит поведение, специфичное для провайдера, которое видно только на живом API.

Kitchen CLI
# Install dependencies
bundle install

# Create and converge the test instance
bundle exec kitchen create

# Run the verification
bundle exec kitchen verify

# Clean up
bundle exec kitchen destroy

Практики, из-за которых тесты IaC остаются полезными

Изолируйте среды через workspace или случайные суффиксы, чтобы параллельные прогоны не пересекались по именам. На интеграционном и сквозном уровнях по возможности используйте реальных провайдеров или высокоточные эмуляторы вроде LocalStack, оставив быстрые статические проверки ближе к началу пайплайна. Добавьте policy-as-code до apply — OPA с Conftest или Sentinel. Встройте набор в pull request и настройте кэширование под баланс скорости и глубины. Версионируйте модули по semver и публикуйте в частный реестр, чтобы потребители фиксировались на проверенных релизах. Отслеживайте флаки, вводите ограниченные повторы при транзиентных ошибках облака и отделяйте шум от регрессий. Контролируйте стоимость временных стендов. И наконец, документируйте назначение каждого control, чтобы набор тестов оставался понятным по мере роста команды.

Чтобы встроить такие проверки в поставку без наугад, согласуйте набор тестов с диагностикой из гайда по узким местам release-пайплайна.

Если тесты инфраструктуры ловят нестабильность, опирайтесь на наблюдаемость и культуру контролируемых отказов, как в статье про хаос-инжиниринг в DevOps.