Императив тестирования инфраструктуры как кода

Инфраструктура как код (IaC) революционизировала способы развертывания и управления инфраструктурой, но с большой силой приходит большая ответственность. Без надлежащего тестирования IaC может распространять неправильные конфигурации в масштабе, приводя к уязвимостям безопасности, нарушениям соответствия и катастрофическим сбоям. Тестирование IaC больше не является опциональным — это критическое требование для поддержания надежной, безопасной и соответствующей требованиям инфраструктуры.

Сложность современных облачных сред требует сложных стратегий тестирования, выходящих за рамки простой проверки синтаксиса. Нам нужно проверять конфигурации безопасности, обеспечивать соответствие организационным политикам, валидировать межсервисные интеграции и подтверждать, что наша инфраструктура ведет себя правильно в различных условиях. Этот комплексный подход к тестированию IaC предотвращает дорогостоящие ошибки и ускоряет доставку, сохраняя при этом качество.

Пирамида тестирования для инфраструктуры как кода

Статический анализ и линтинг

Основа тестирования IaC начинается со статического анализа. Эти тесты выполняются быстро и обнаруживают распространенные проблемы на ранней стадии цикла разработки:

# terraform/modules/web-server/variables.tf
variable "instance_type" {
  description = "Тип инстанса EC2"
  type        = string
  default     = "t3.micro"

  validation {
    condition = contains([
      "t3.micro",
      "t3.small",
      "t3.medium",
      "t3.large"
    ], var.instance_type)
    error_message = "Тип инстанса должен быть одним из утвержденных размеров."
  }
}

variable "subnet_ids" {
  description = "Список идентификаторов подсетей для развертывания"
  type        = list(string)

  validation {
    condition = length(var.subnet_ids) >= 2
    error_message = "Требуется минимум 2 подсети для высокой доступности."
  }
}

variable "environment" {
  description = "Название окружения"
  type        = string

  validation {
    condition = can(regex("^(dev|staging|prod)$", var.environment))
    error_message = "Окружение должно быть dev, staging или prod."
  }
}

Пайплайн валидации Terraform

# .github/workflows/terraform-validation.yml
name: Пайплайн валидации Terraform

on:
  pull_request:
    paths:
      - 'terraform/**'
      - '.github/workflows/terraform-validation.yml'

jobs:
  validate:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Настройка Terraform
        uses: hashicorp/setup-terraform@v2
        with:
          terraform_version: 1.5.0

      - name: Проверка форматирования Terraform
        run: |
          terraform fmt -check -recursive terraform/

      - name: TFLint - Линтер Terraform
        run: |
          curl -s https://raw.githubusercontent.com/terraform-linters/tflint/master/install_linux.sh | bash

          # Настройка TFLint
          cat > .tflint.hcl <<EOF
          config {
            module = true
            force = false
          }

          plugin "aws" {
            enabled = true
            version = "0.24.0"
            source  = "github.com/terraform-linters/tflint-ruleset-aws"
          }

          rule "aws_instance_invalid_type" {
            enabled = true
          }

          rule "aws_resource_missing_tags" {
            enabled = true
            tags = ["Environment", "Owner", "CostCenter"]
          }
          EOF

          tflint --init
          tflint --recursive

      - name: Сканирование безопасности Checkov
        run: |
          pip install checkov
          checkov -d terraform/ --framework terraform --output json --output-file-path checkov-report.json

          # Парсинг и сбой при проблемах высокой серьезности
          python3 <<EOF
          import json
          with open('checkov-report.json', 'r') as f:
              report = json.load(f)

          failed_checks = report.get('summary', {}).get('failed', 0)
          if failed_checks > 0:
              print(f"Найдено {failed_checks} проблем безопасности")
              for check in report.get('results', {}).get('failed_checks', []):
                  print(f"  - {check['check_id']}: {check['check_name']}")
              exit(1)
          EOF

      - name: Валидация Terraform
        run: |
          for dir in terraform/environments/*/; do
            echo "Валидация $dir"
            cd $dir
            terraform init -backend=false
            terraform validate
            cd -
          done

      - name: Генерация документации
        run: |
          # Установка terraform-docs
          curl -sSLo terraform-docs.tar.gz https://github.com/terraform-docs/terraform-docs/releases/download/v0.16.0/terraform-docs-v0.16.0-linux-amd64.tar.gz
          tar -xzf terraform-docs.tar.gz
          chmod +x terraform-docs

          # Генерация документации для каждого модуля
          for module in terraform/modules/*/; do
            ./terraform-docs markdown table --output-file README.md --output-mode inject $module
          done

Модульное тестирование с Terratest

Комплексная реализация Terratest

// test/terraform_aws_vpc_test.go
package test

import (
    "fmt"
    "strings"
    "testing"
    "time"

    "github.com/gruntwork-io/terratest/modules/aws"
    "github.com/gruntwork-io/terratest/modules/random"
    "github.com/gruntwork-io/terratest/modules/retry"
    "github.com/gruntwork-io/terratest/modules/terraform"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)

func TestTerraformAwsVpc(t *testing.T) {
    t.Parallel()

    // Генерация уникальных идентификаторов
    uniqueId := random.UniqueId()
    region := "us-east-1"
    vpcCidr := "10.0.0.0/16"

    terraformOptions := &terraform.Options{
        TerraformDir: "../terraform/modules/vpc",

        Vars: map[string]interface{}{
            "vpc_cidr":     vpcCidr,
            "environment":  "test",
            "name":        fmt.Sprintf("test-vpc-%s", uniqueId),
            "region":      region,
            "enable_nat":  true,
            "single_nat":  false,
        },

        EnvVars: map[string]string{
            "AWS_DEFAULT_REGION": region,
        },
    }

    defer terraform.Destroy(t, terraformOptions)
    terraform.InitAndApply(t, terraformOptions)

    // Валидация выходных данных
    vpcId := terraform.Output(t, terraformOptions, "vpc_id")
    assert.NotEmpty(t, vpcId)

    publicSubnetIds := terraform.OutputList(t, terraformOptions, "public_subnet_ids")
    privateSubnetIds := terraform.OutputList(t, terraformOptions, "private_subnet_ids")

    assert.Equal(t, 3, len(publicSubnetIds))
    assert.Equal(t, 3, len(privateSubnetIds))

    // Проверка конфигурации VPC
    vpc := aws.GetVpcById(t, vpcId, region)
    assert.Equal(t, vpcCidr, vpc.CidrBlock)
    assert.True(t, vpc.EnableDnsSupport)
    assert.True(t, vpc.EnableDnsHostnames)

    // Тестирование конфигураций подсетей
    for _, subnetId := range publicSubnetIds {
        subnet := aws.GetSubnetById(t, subnetId, region)
        assert.True(t, subnet.MapPublicIpOnLaunch)
        assert.Contains(t, subnet.AvailabilityZone, region)
    }

    for _, subnetId := range privateSubnetIds {
        subnet := aws.GetSubnetById(t, subnetId, region)
        assert.False(t, subnet.MapPublicIpOnLaunch)

        // Проверка подключения NAT gateway
        routeTable := aws.GetRouteTableForSubnet(t, subnet, region)
        hasNatRoute := false
        for _, route := range routeTable.Routes {
            if route.DestinationCidrBlock == "0.0.0.0/0" && route.NatGatewayId != "" {
                hasNatRoute = true
                break
            }
        }
        assert.True(t, hasNatRoute, "Приватная подсеть должна иметь маршрут NAT gateway")
    }

    // Тестирование сетевых ACL
    testNetworkAcls(t, vpcId, region)

    // Тестирование групп безопасности
    testSecurityGroups(t, terraformOptions, region)
}

func testNetworkAcls(t *testing.T, vpcId string, region string) {
    nacls := aws.GetNetworkAclsForVpc(t, vpcId, region)

    for _, nacl := range nacls {
        // Проверка правил входящего трафика
        for _, rule := range nacl.IngressRules {
            if rule.RuleNumber == 100 {
                assert.Equal(t, "tcp", rule.Protocol)
                assert.Equal(t, "0.0.0.0/0", rule.CidrBlock)
            }
        }

        // Проверка правил исходящего трафика
        for _, rule := range nacl.EgressRules {
            if rule.RuleNumber == 100 {
                assert.Equal(t, "-1", rule.Protocol) // Все протоколы
                assert.Equal(t, "0.0.0.0/0", rule.CidrBlock)
            }
        }
    }
}

func TestTerraformAwsEcs(t *testing.T) {
    t.Parallel()

    terraformOptions := &terraform.Options{
        TerraformDir: "../terraform/modules/ecs-cluster",

        Vars: map[string]interface{}{
            "cluster_name":         fmt.Sprintf("test-cluster-%s", random.UniqueId()),
            "capacity_providers":   []string{"FARGATE", "FARGATE_SPOT"},
            "container_insights":   true,
            "enable_execute_command": true,
        },
    }

    defer terraform.Destroy(t, terraformOptions)
    terraform.InitAndApply(t, terraformOptions)

    // Тестирование создания кластера
    clusterArn := terraform.Output(t, terraformOptions, "cluster_arn")
    assert.Contains(t, clusterArn, "cluster/test-cluster")

    // Тестирование развертывания сервиса
    deployTestService(t, terraformOptions)
}

func deployTestService(t *testing.T, terraformOptions *terraform.Options) {
    serviceOptions := &terraform.Options{
        TerraformDir: "../terraform/modules/ecs-service",

        Vars: map[string]interface{}{
            "cluster_id":     terraform.Output(t, terraformOptions, "cluster_id"),
            "service_name":   "test-service",
            "task_cpu":       256,
            "task_memory":    512,
            "desired_count":  2,
            "container_port": 8080,
        },
    }

    defer terraform.Destroy(t, serviceOptions)
    terraform.InitAndApply(t, serviceOptions)

    // Ожидание стабилизации сервиса
    retry.DoWithRetry(t, "Ожидание сервиса ECS", 30, 10*time.Second, func() (string, error) {
        // Проверка статуса сервиса
        return "", nil
    })
}

Тестирование Ansible с Molecule

Конфигурация тестов Molecule

# molecule/default/molecule.yml
---
dependency:
  name: galaxy

driver:
  name: docker

platforms:
  - name: ubuntu-2204
    image: ubuntu:22.04
    pre_build_image: false
    dockerfile: Dockerfile.j2
    privileged: true
    command: /lib/systemd/systemd
    volumes:
      - /sys/fs/cgroup:/sys/fs/cgroup:ro
    tmpfs:
      - /tmp
      - /run
    capabilities:
      - SYS_ADMIN

  - name: centos-8
    image: centos:8
    pre_build_image: false
    dockerfile: Dockerfile.j2
    privileged: true
    command: /usr/sbin/init
    volumes:
      - /sys/fs/cgroup:/sys/fs/cgroup:ro
    tmpfs:
      - /tmp
      - /run

provisioner:
  name: ansible
  config_options:
    defaults:
      callback_whitelist: profile_tasks
      fact_caching: jsonfile
      fact_caching_connection: /tmp/ansible_cache
  inventory:
    host_vars:
      ubuntu-2204:
        ansible_python_interpreter: /usr/bin/python3
  lint:
    name: ansible-lint
  playbooks:
    prepare: prepare.yml
    converge: converge.yml
    verify: verify.yml

verifier:
  name: testinfra
  options:
    verbose: true
  lint:
    name: flake8

scenario:
  name: default
  test_sequence:
    - dependency
    - lint
    - cleanup
    - destroy
    - syntax
    - create
    - prepare
    - converge
    - idempotence
    - side_effect
    - verify
    - cleanup
    - destroy

Тестирование Ansible Playbook

# molecule/default/tests/test_nginx.py
import os
import pytest

import testinfra.utils.ansible_runner

testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner(
    os.environ['MOLECULE_INVENTORY_FILE']
).get_hosts('all')


def test_nginx_установлен(host):
    """Проверка установки nginx"""
    nginx = host.package('nginx')
    assert nginx.is_installed
    assert nginx.version.startswith('1.')


def test_сервис_nginx(host):
    """Проверка работы и включения сервиса nginx"""
    nginx = host.service('nginx')
    assert nginx.is_running
    assert nginx.is_enabled


def test_конфигурация_nginx(host):
    """Проверка конфигурации nginx"""
    config = host.file('/etc/nginx/nginx.conf')
    assert config.exists
    assert config.is_file
    assert config.user == 'root'
    assert config.group == 'root'
    assert oct(config.mode) == '0o644'

    # Валидация синтаксиса конфигурации
    cmd = host.run('nginx -t')
    assert cmd.rc == 0
    assert 'syntax is ok' in cmd.stderr
    assert 'test is successful' in cmd.stderr


def test_порты_nginx(host):
    """Проверка прослушивания nginx на ожидаемых портах"""
    assert host.socket('tcp://0.0.0.0:80').is_listening

    # Тестирование SSL если настроен
    if host.file('/etc/nginx/sites-enabled/ssl').exists:
        assert host.socket('tcp://0.0.0.0:443').is_listening


def test_процессы_nginx(host):
    """Проверка работы процессов nginx"""
    master = host.process.filter(comm='nginx', user='root')
    assert len(master) == 1

    workers = host.process.filter(comm='nginx', user='www-data')
    assert len(workers) >= 1


def test_лог_файлы_nginx(host):
    """Проверка создания лог-файлов с правильными разрешениями"""
    access_log = host.file('/var/log/nginx/access.log')
    error_log = host.file('/var/log/nginx/error.log')

    for log in [access_log, error_log]:
        assert log.exists
        assert log.is_file
        assert log.user == 'www-data'
        assert oct(log.mode) == '0o640'


def test_заголовки_безопасности_nginx(host):
    """Проверка установки заголовков безопасности"""
    response = host.run('curl -I http://localhost')
    assert response.rc == 0

    headers = response.stdout
    assert 'X-Frame-Options: SAMEORIGIN' in headers
    assert 'X-Content-Type-Options: nosniff' in headers
    assert 'X-XSS-Protection: 1; mode=block' in headers


@pytest.mark.parametrize('site', [
    'default',
    'app.example.com'
])
def test_сайты_nginx(host, site):
    """Проверка конфигураций сайтов nginx"""
    site_config = host.file(f'/etc/nginx/sites-available/{site}')
    assert site_config.exists

    site_enabled = host.file(f'/etc/nginx/sites-enabled/{site}')
    assert site_enabled.exists
    assert site_enabled.is_symlink
    assert site_enabled.linked_to == f'/etc/nginx/sites-available/{site}'

Политики как код с Open Policy Agent

Политики OPA для Terraform

# policies/terraform/security.rego
package terraform.security

import future.keywords.if
import future.keywords.in

default allow = false

# Запретить публичные S3 бакеты
deny[msg] {
    resource := input.resource_changes[_]
    resource.type == "aws_s3_bucket_public_access_block"
    resource.change.after.block_public_acls == false
    msg := sprintf("S3 бакет %s должен блокировать публичные ACL", [resource.address])
}

# Требовать шифрование для инстансов RDS
deny[msg] {
    resource := input.resource_changes[_]
    resource.type == "aws_db_instance"
    not resource.change.after.storage_encrypted
    msg := sprintf("RDS инстанс %s должен иметь зашифрованное хранилище", [resource.address])
}

# Применение требований к тегированию
required_tags := ["Environment", "Owner", "CostCenter", "Project"]

deny[msg] {
    resource := input.resource_changes[_]
    required_tag := required_tags[_]
    not resource.change.after.tags[required_tag]
    msg := sprintf("У ресурса %s отсутствует обязательный тег: %s", [resource.address, required_tag])
}

# Валидация правил групп безопасности
deny[msg] {
    resource := input.resource_changes[_]
    resource.type == "aws_security_group_rule"
    resource.change.after.type == "ingress"
    resource.change.after.cidr_blocks[_] == "0.0.0.0/0"
    resource.change.after.from_port == 22
    msg := sprintf("Правило группы безопасности %s разрешает SSH откуда угодно", [resource.address])
}

# Ограничения типов инстансов
allowed_instance_types := [
    "t3.micro", "t3.small", "t3.medium",
    "m5.large", "m5.xlarge"
]

deny[msg] {
    resource := input.resource_changes[_]
    resource.type == "aws_instance"
    not resource.change.after.instance_type in allowed_instance_types
    msg := sprintf("Инстанс %s использует неодобренный тип: %s", [
        resource.address,
        resource.change.after.instance_type
    ])
}

# Требования сегментации сети
deny[msg] {
    resource := input.resource_changes[_]
    resource.type == "aws_instance"
    resource.change.after.associate_public_ip_address == true
    contains(resource.change.after.tags.Environment, "prod")
    msg := sprintf("Продакшн инстанс %s не может иметь публичный IP", [resource.address])
}

Политики Sentinel для Terraform Cloud

# policies/контроль-затрат.sentinel
import "tfplan/v2" as tfplan
import "decimal"

# Максимальный порог месячных затрат
макс_месячная_стоимость = decimal.new(1000)

# Расчет предполагаемой месячной стоимости
расчетная_стоимость = decimal.new(tfplan.cost_estimate.total_monthly_cost)

# Главное правило
main = rule {
    расчетная_стоимость.less_than_or_equal_to(макс_месячная_стоимость)
}

# Лимиты стоимости инстансов по типу
лимиты_стоимости_инстансов = {
    "t3.micro":  50,
    "t3.small":  100,
    "t3.medium": 200,
    "t3.large":  300,
    "m5.large":  400,
    "m5.xlarge": 800,
}

# Валидация индивидуальных затрат инстансов
валидировать_стоимость_инстансов = func() {
    валидировано = true

    for tfplan.resource_changes as _, rc {
        if rc.type is "aws_instance" and rc.change.actions contains "create" {
            тип_инстанса = rc.change.after.instance_type

            if тип_инстанса in keys(лимиты_стоимости_инстансов) {
                # Оценка почасовой стоимости (упрощенно)
                почасовая_стоимость = лимиты_стоимости_инстансов[тип_инстанса] / 730

                if почасовая_стоимость > лимиты_стоимости_инстансов[тип_инстанса] / 730 {
                    print("Инстанс", rc.address, "превышает лимит стоимости")
                    валидировано = false
                }
            }
        }
    }

    return валидировано
}

# Проверки соответствия
правило_соответствия = rule {
    валидировать_стоимость_инстансов()
}

Интеграционное тестирование с Kitchen-Terraform

Конфигурация Kitchen

# .kitchen.yml
---
driver:
  name: terraform
  root_module_directory: test/fixtures/wrapper
  command_timeout: 1800

provisioner:
  name: terraform

verifier:
  name: terraform
  systems:
    - name: default
      backend: ssh
      hosts_output: public_ip
      user: ubuntu
      key_files:
        - test/assets/id_rsa

    - name: aws
      backend: awspec

platforms:
  - name: ubuntu-2204
    driver:
      variables:
        platform: ubuntu-2204

  - name: amazon-linux-2
    driver:
      variables:
        platform: amazon-linux-2

suites:
  - name: default
    driver:
      variables:
        instance_count: 2
        enable_monitoring: true
    verifier:
      inspec_tests:
        - test/integration/default
    lifecycle:
      pre_converge:
        - local: echo "Выполнение задач pre-converge"
      post_converge:
        - local: echo "Выполнение задач post-converge"

Интеграционные тесты InSpec

# test/integration/default/controls/infrastructure.rb
control 'инфраструктура-01' do
  title 'Проверка конфигурации VPC'
  desc 'Убедиться, что VPC настроена правильно'

  describe aws_vpc(vpc_id: attribute('vpc_id')) do
    it { should exist }
    its('cidr_block') { should eq '10.0.0.0/16' }
    its('state') { should eq 'available' }
    its('enable_dns_support') { should eq true }
    its('enable_dns_hostnames') { should eq true }
  end
end

control 'инфраструктура-02' do
  title 'Проверка групп безопасности'
  desc 'Убедиться, что группы безопасности настроены безопасно'

  aws_security_groups.where(vpc_id: attribute('vpc_id')).entries.each do |sg|
    describe aws_security_group(group_id: sg.group_id) do
      it { should_not allow_ingress_from_internet_on_port(22) }
      it { should_not allow_ingress_from_internet_on_port(3389) }

      # Кастомные матчеры
      its('ingress_rules') { should_not include(
        from_port: 0,
        to_port: 65535,
        cidr_blocks: ['0.0.0.0/0']
      )}
    end
  end
end

control 'инфраструктура-03' do
  title 'Проверка инстансов EC2'
  desc 'Убедиться, что инстансы EC2 соответствуют требованиям безопасности'

  aws_ec2_instances.where(vpc_id: attribute('vpc_id')).entries.each do |instance|
    describe aws_ec2_instance(instance_id: instance.instance_id) do
      it { should be_running }
      it { should have_encrypted_root_volume }
      its('monitoring_state') { should eq 'enabled' }
      its('instance_type') { should be_in %w[t3.micro t3.small t3.medium] }

      # Проверка, что IMDSv2 требуется
      its('metadata_options.http_tokens') { should eq 'required' }
      its('metadata_options.http_endpoint') { should eq 'enabled' }
    end
  end
end

control 'инфраструктура-04' do
  title 'Проверка S3 бакетов'
  desc 'Убедиться, что S3 бакеты безопасны'

  aws_s3_buckets.entries.each do |bucket|
    describe aws_s3_bucket(bucket_name: bucket.name) do
      it { should have_default_encryption_enabled }
      it { should have_versioning_enabled }
      it { should_not be_public }

      its('bucket_acl.grants') { should_not include(
        grantee: { type: 'Group', uri: 'http://acs.amazonaws.com/groups/global/AllUsers' }
      )}
    end
  end
end

control 'инфраструктура-05' do
  title 'Проверка инстансов RDS'
  desc 'Убедиться, что инстансы RDS настроены безопасно'

  aws_rds_instances.entries.each do |db|
    describe aws_rds_instance(db_instance_identifier: db.db_instance_identifier) do
      it { should be_encrypted }
      it { should have_automated_backups_enabled }
      its('backup_retention_period') { should be >= 7 }
      its('multi_az') { should eq true }
      its('publicly_accessible') { should eq false }
      its('deletion_protection') { should eq true }
    end
  end
end

Пайплайн непрерывного соответствия

Полный CI/CD пайплайн для IaC

# .github/workflows/iac-pipeline.yml
name: Пайплайн инфраструктуры как кода

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
  schedule:
    - cron: '0 2 * * *'  # Ежедневная проверка соответствия

env:
  TF_VERSION: '1.5.0'
  ANSIBLE_VERSION: '2.15.0'
  AWS_DEFAULT_REGION: 'us-east-1'

jobs:
  статический-анализ:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Настройка инструментов
        run: |
          # Установка необходимых инструментов
          pip install checkov ansible-lint yamllint

          # Установка tflint
          curl -s https://raw.githubusercontent.com/terraform-linters/tflint/master/install_linux.sh | bash

          # Установка tfsec
          curl -s https://raw.githubusercontent.com/aquasecurity/tfsec/master/scripts/install_linux.sh | bash

      - name: YAML Lint
        run: yamllint -c .yamllint .

      - name: Ansible Lint
        run: ansible-lint ansible/

      - name: Сканирование безопасности Terraform
        run: |
          tfsec terraform/ --format json --out tfsec-report.json
          checkov -d terraform/ --output json --output-file-path checkov-report.json

      - name: Загрузка отчетов безопасности
        uses: actions/upload-artifact@v3
        with:
          name: отчеты-безопасности
          path: |
            tfsec-report.json
            checkov-report.json

  модульные-тесты:
    runs-on: ubuntu-latest
    needs: статический-анализ

    strategy:
      matrix:
        набор_тестов: [vpc, ecs, rds, s3]

    steps:
      - uses: actions/checkout@v3

      - name: Настройка Go
        uses: actions/setup-go@v4
        with:
          go-version: '1.20'

      - name: Запуск Terratest
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        run: |
          cd test
          go mod download
          go test -v -timeout 30m -run Test${{ matrix.набор_тестов }}

  валидация-политик:
    runs-on: ubuntu-latest
    needs: статический-анализ

    steps:
      - uses: actions/checkout@v3

      - name: Настройка OPA
        run: |
          curl -L -o opa https://openpolicyagent.org/downloads/latest/opa_linux_amd64_static
          chmod +x opa
          sudo mv opa /usr/local/bin/

      - name: План Terraform
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        run: |
          cd terraform/environments/dev
          terraform init
          terraform plan -out=tfplan.binary
          terraform show -json tfplan.binary > tfplan.json

      - name: Проверка политик OPA
        run: |
          opa eval -d policies/ -i terraform/environments/dev/tfplan.json \
            "data.terraform.security.deny[_]" --format pretty

          # Сбой при нарушении политик
          НАРУШЕНИЯ=$(opa eval -d policies/ -i terraform/environments/dev/tfplan.json \
            "data.terraform.security.deny" --format json | jq '.result[0].expressions[0].value | length')

          if [ "$НАРУШЕНИЯ" -gt 0 ]; then
            echo "Обнаружены нарушения политик!"
            exit 1
          fi

  интеграционные-тесты:
    runs-on: ubuntu-latest
    needs: [модульные-тесты, валидация-политик]
    if: github.ref == 'refs/heads/main'

    steps:
      - uses: actions/checkout@v3

      - name: Настройка Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: '3.0'
          bundler-cache: true

      - name: Запуск тестов Kitchen
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        run: |
          bundle exec kitchen test --parallel

  сканирование-соответствия:
    runs-on: ubuntu-latest
    needs: интеграционные-тесты

    steps:
      - uses: actions/checkout@v3

      - name: Запуск InSpec соответствия
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        run: |
          # Установка InSpec
          curl https://omnitruck.chef.io/install.sh | sudo bash -s -- -P inspec

          # Запуск профилей соответствия
          inspec exec compliance/ --reporter json:compliance-report.json

      - name: Генерация отчета соответствия
        run: |
          python3 scripts/генерация_отчета_соответствия.py \
            --input compliance-report.json \
            --output отчет-соответствия.html

      - name: Загрузка отчета соответствия
        uses: actions/upload-artifact@v3
        with:
          name: отчет-соответствия
          path: отчет-соответствия.html

Заключение

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

Комбинация статического анализа, модульного тестирования, интеграционного тестирования и валидации политик создает защитную сеть, которая позволяет командам двигаться быстро, не ломая при этом ничего. Внедряя эти практики, организации могут достичь святого Грааля управления инфраструктурой: самодокументирующейся, самовалидирующейся и самовосстанавливающейся инфраструктуры, которая надежно масштабируется.

Помните, что тестирование IaC - это развивающаяся дисциплина. По мере роста сложности вашей инфраструктуры должны развиваться и ваши стратегии тестирования. Начните с основ - линтинга и валидации - затем постепенно добавляйте более сложные слои тестирования. Инвестиции в комплексное тестирование IaC окупаются сокращением инцидентов, более быстрыми развертываниями и повышением уверенности команды.