Императив тестирования инфраструктуры как кода
Инфраструктура как код (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 окупаются сокращением инцидентов, более быстрыми развертываниями и повышением уверенности команды.