Протестируйте свой инфраструктурный код с помощью Terratest

Протестируйте свой инфраструктурный код с помощью Terratest

by moiseevrus

С появлением Infrastructure As Code (Ansible, Puppet, Heat или Terraform) мы хотели бы воспользоваться всеми передовыми методами, принесенными движением Software Craftsmanship , чтобы гарантировать качество кода нашей инфраструктуры. Каждый профессиональный разработчик знает, что для обеспечения качества кода нужны тесты. Одной из полученных практик является TDD , также известная как Test Driven Development.

Напомним, TDD состоит из: начните с создания теста; проверка его отказа; написание кода, необходимого для успешного выполнения теста; перезапуск теста и проверка его успешности, а также предыдущих тестов и, наконец, рефакторинг кода перед началом нового цикла.

3 основных шага TDD

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

Если сегодня инструменты, позволяющие разработчикам выполнять TDD, созрели для большинства языков, то это не относится к инструментам Infra as Code. Мы все еще можем прочитать в очень хорошем посте « Мастер », что сегодня мы можем эффективно писать код Ansible в TDD….

… но что касается инструментов подготовки, таких как Terraform , все по-другому.

Боли ручного теста

Terraform , созданный HashiCorp , позволяет нам определять инфраструктуру на языке высокого уровня и развертывать ее в среде облачных провайдеров, таких как Amazon Web Services или Google Cloud Platform.

Сегодня, когда мы хотим протестировать код, мы выполняем тесты вручную. Мы не можем сделать это локально, вы не можете протестировать установку VPC на своем компьютере с помощью localhost . Мы тестируем непосредственно в наших облачных средах.

Они могут длиться несколько минут, часто включают множество шагов, иногда выполняемых вручную, что вынуждает нас либо ждать, либо выполнять регулярное переключение контекста: тем самым задерживается петля обратной связи. Затем мы должны убедиться, что созданная инфраструктура соответствует нашим ожиданиям. Для этого мы используем веб-консоль или командную строку. Иногда мы подключаемся напрямую к машине, чтобы проверить наличие файла. Потом удаляем эти машины и снова перезагружаем…

То, что мы сделали бы вручную, может быть, несколько раз, чтобы исправить свои ошибки, Терратест помогает нам автоматизировать.

Терратест

Что такое Терратест ? Это библиотека Go , которая помогает создавать и автоматизировать тесты для Infra as Code , написанные с помощью Terraform, Packer для поставщиков IaaS, таких как Amazon, Google или для кластера Kubernetes.

 Terratest разработан американским обществом Gruntwork в партнерстве с HashiCorp, которая открыла более 300 000 строк кода и использует Terratest для поддержки своей кодовой базы. Terratest доступен на Github с апреля 2018 года, вы можете перейти сюда , если хотите поиграть с ним.

Почему стоит использовать Терратест?

Тестирование кода с помощью такой библиотеки, как Terratest, дает много преимуществ, вот неполный список:

  • Тестирование поведения сложной инфраструктуры
  • Сквозные тесты
  • Инфраструктурная документация
  • Нет необходимости поддерживать постоянную инфраструктуру iso-prod для тестирования ее модификаций.
  • Быстрая обратная связь, позволяющая эффективно исправлять ошибки
  • Возможность запускать эти тесты быстро и регулярно
  • Обеспечение отказоустойчивости при обновлении версий инструментов
  • Возможность тестировать сценарии, подобные cloud-init.
  • Проверка развернутых образов AMI

Кроме того, Terratest предоставляет множество примеров в своем репозитории Git, упрощая использование библиотеки.

Как это работает ?

Чтобы написать код Terraform в соответствии с принципами Test Driven Infrastructure, мы будем действовать поэтапно:

  • Мы начнем с написания теста с использованием Go в *_test.go , например, instance_test.go . Мы будем внимательны, чтобы делать истинные или ложные утверждения в наших тестах. По принципу TDD сначала проверим, что тест не проходит, выполнив команду «go test instance test_test.go» .
  • Затем мы напишем наш код Terraform, описывающий нашу инфраструктуру, стараясь сохранить модульную структуру проекта.
  • Мы перезапустим Terratest, на этот раз для развертывания вашей инфраструктуры в вашей любимой IaaS. Имейте в виду, что Terratest действительно строит инфраструктуру. Это заставляет применять терраформ , это, очевидно, может потребовать затрат.
  • Terratest проводит тесты. Чтобы проверить соответствие нашей инфраструктуры, библиотека может вызывать конечные точки HTTP, подключаться к машинам через SSH, выполнять команды, загружать файлы, запрашивать API-интерфейсы облачных провайдеров и читать выходные данные Terraform…
  • Наконец, тестовая инфраструктура будет уничтожена с помощью terraform destroy .
  • Результаты теста отобразятся в консоли.

Руки вверх

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

Монтаж

Чтобы открыть для себя библиотеку Terratest, вы можете клонировать ее репозиторий Git . Репозиторий кода содержит как модули, так и множество примеров. Примеры дают хорошее представление о том, что может сделать инструмент, и являются хорошей отправной точкой.

Поскольку Terratest является библиотекой Go , очевидно, что она должна быть установлена ​​на вашем компьютере. Для установки библиотечных модулей Terratest предпочтительно использовать менеджер зависимостей, такой как dep .

Поэтому мы можем установить модуль, который будет использоваться для тестирования Terraform, следующим образом:

dep обеспечить -добавить github.com/gruntwork-io/terratest/modules/terraform

Или вот так с go get:

зайдите на github.com/gruntwork-io/terratest/modules/terraform

Все зависимости желательно описывать в файле Gopkg.toml , это также позволяет зафиксировать версию зависимостей.

Затем убедитесь, что у вас есть необходимый доступ к вашему облачному провайдеру, в нашем случае мы используем AWS и загружаем переменные среды AWS_ACCESS_KEY_ID и AWS_SECRET_ACCESS_KEY. Мы также позаботимся о том, чтобы у нас была пара закрытый ключ/открытый ключ для подключения к созданным машинам и чтобы наш открытый ключ был в AWS . В этом примере мы назвали ключ «terratest_key».

Наконец, мы создадим проект с пустыми файлами, структурированными следующим образом:

Структура проекта

Зависимости

Для начала мы используем тестовый пакет Go с хорошо названным «test» и импортируем нужные нам модули в файл instance_test.go :

package test
import (
   "testing"
   "fmt"
   "time"
   "github.com/stretchr/testify/assert"
   "github.com/gruntwork-io/terratest/modules/terraform"
   "github.com/gruntwork-io/terratest/modules/aws"
   "github.com/gruntwork-io/terratest/modules/ssh"
   "github.com/gruntwork-io/terratest/modules/retry"
) 

Первый тест: ключ SSH

Затем мы объявляем нашу первую тестовую функцию, которая должна называться так: func TestXxx(*testing.T) , чтобы ее можно было использовать с командой go test . Мы начинаем с проверки правильности создания машины и наличия ключа SSH.

func TestInstanceSshKey(t *testing.T) {}

Теперь, когда мы написали часть «конфигурации», пришло время действовать. Мы хотим инициализировать наш рабочий каталог Terraform и применить наш код (terraform init + terraform apply) . Именно метод InitAndApply запустит это творение. Мы также хотим уничтожить все наши машины по окончании испытаний (терраформировать уничтожить) . Ключевое слово defer позволяет добавить метод Destroy в список действий, которые будут выполняться при возврате функции.

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

И, наконец, мы получаем вывод terraform, в котором мы находим имя SSH-ключа экземпляра. Этот ключ позволит нам подключиться с Terratest к машине. Поэтому мы сделаем первое утверждение, чтобы можно было определить этот ключ.

instanceSshKey := terraform.Output(t, terraformOptions, "instance_key")
assert.Equal(t, "terratest_key", instanceSshKey)

Вот наша первая полная тестовая функция:

func TestInstanceSshKey(t *testing.T) {
    terraformOptions := configureTerraformOptions(t)
    defer terraform.Destroy(t, terraformOptions)
    terraform.InitAndApply(t, terraformOptions)
    instanceSshKey := terraform.Output(t, terraformOptions, "instance_key")
    assert.Equal(t, "terratest_key", instanceSshKey)
}
  • Запустить тест

Следующая команда:

пройти тест instance_test.go

… запускает наш тест и вознаграждает вас радостным :

FAIL: TestInstanceIp (0.27s)

По умолчанию это не очень многословно, поэтому мы повторно запустим команду с параметром -v , чтобы получить более подробную информацию.

Бегло взглянув на логи, мы быстро понимаем, что terratest ничего не создавал, это нормально, мы еще ничего не реализовывали. С другой стороны, мы замечаем в журналах, что он инициализировал рабочий каталог (terraform init) , и теперь мы находим файлы Terraform .tfstate .

  • Реализация

Теперь мы хотим использовать Terraform для создания нашего экземпляра EC2. В файле main.tf объявляем следующее:

resource "aws_instance" "example" {
    ami           = "ami-5026902d"
    instance_type = "t2.micro"
    key_name = "terratest_key"
}

Этот простой фрагмент кода описывает экземпляр Centos 7 (описываемый свойством «ami»), t2.micro . И настраивает ключ экземпляра SSH, этот ключ уже есть в AWS, мы его создали и скопировали при установке).

Теперь добавим в файл output.tf , в котором мы указываем вывод кода, имя ключа экземпляра:

output "instance_key" {
    value = "${aws_instance.example.key_name}"
}
  • Новая попытка

Мы перезапускаем тесты с параметром -v , что позволит нам получить более полный отчет.

Логи показывают этап инициализации Terraform (init), затем приложение (apply). Мы также видим запрошенный вывод : имя ключа экземпляра. Затем наблюдаем за разрушением машины.

И, наконец, облегчение:

--- PASS: TestInstanceSshKey (73.80s)
PASS
ok      command-line-arguments  73.812s

Есть кеш , поэтому, если вы дважды запустите одну и ту же команду без каких-либо изменений в коде Go, ответ будет мгновенным, но тесты не будут воспроизводиться повторно. Мы по-прежнему можем форсировать выполнение, изменив следующую переменную среды: GOCACHE=off.

Никакого рефакторинга, наш код очень прост.

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

Второй тест: публичный IP

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

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

Давайте создадим новую тестовую функцию: TestInstanceIp. Структура нашего файла instance_test.go теперь выглядит так:

func configureTerraformOptions(t *testing.T) *terraform.Options {...}
func TestInstanceSshKey(t *testing.T) {...}
func TestInstanceIp(t *testing.T) {...}

Мы хотим убедиться, что наш экземпляр имеет общедоступный IP-адрес. И мы хотели бы использовать для этой цели aws- модуль Terratest. Как и в первом тесте, добавим методы для создания и уничтожения нашей небольшой архитектуры:

terraformOptions := configureTerraformOptions(t)
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)

Давайте сначала убедимся, что тест красный. Чтобы получить IP-адрес с AWS, нам нужен идентификатор экземпляра. ID — это выходной параметр Terraform, давайте протестируем и реализуем его восстановление, идентичное SSH-ключу.

Получаем следующее утверждение:

instanceID := terraform.Output(t, terraformOptions, "instance_id")
instanceIPFromInstance := aws.GetPublicIpOfEc2Instance(t, instanceID, awsRegion)
assert.Equal(t, “fake_ip”, instanceIPFromInstance)

Давайте проверим, что наш тест красный: (между двумя результатами много логов)

--- PASS: TestInstanceSshKey (45.02s)
=== RUN   TestInstanceIp
---
--- FAIL: TestInstanceIp (52.22s)
instance_test.go:50:
Error Trace: instance_test.go:50
Error: Not equal:
expected: "fake_ip"
actual : "35.180.230.122"

Действительно, IP машины не «fake_ip» . Наш тест провален, мы можем положиться на него.

  • Реализация

Затем мы понимаем, что по умолчанию экземпляры AWS создаются с общедоступным IP-адресом; мы проверим, что IP-адрес, возвращаемый выводом Terraform, идентичен IP-адресу Terratest.

Добавим следующий вывод в output.tf:

output "instance_id" {
    value = "${aws_instance.example.id}"
}
output "instance_public_ip" {
    value = "${aws_instance.example.public_ip}"
}

Затем давайте изменим нашу тестовую функцию TestInstanceIp() , чтобы сравнить два значения.

Вся функция:

func TestInstanceIp(t *testing.T) {
    terraformOptions := configureTerraformOptions(t)
    defer terraform.Destroy(t, terraformOptions)
    terraform.InitAndApply(t, terraformOptions)
    instanceIP := terraform.Output(t, terraformOptions, "instance_public_ip")
    instanceID := terraform.Output(t, terraformOptions, "instance_id")
    instanceIPFromInstance := aws.GetPublicIpOfEc2Instance(t, instanceID, awsRegion)
    assert.Equal(t, instanceIP, instanceIPFromInstance)
}

Перезапускаем тесты:

--- PASS: TestInstanceIp (79.52s)
PASS
ok      command-line-arguments  124.562s

Победа, он зеленый!

Хорошо, он зеленый, но детали не очень явные.

Третий тест: содержимое файла

Мы убедились, что экземпляр создан с нашим ключом SSH и общедоступным IP-адресом. Воспользуемся этими тестами сейчас, чтобы протестировать запись в файл скриптом, запускаемым при инициализации машины.

Мы хотим проверить, что файл «/tmp/salut» содержит строку «Hello World».

Мы используем пакет ssh и функцию CheckSshCommandE() для выполнения команды «cat /tmp/salvation» на машине и сравнения ее со строкой символов.

Что дает нам утверждение:

expectedText := "Hello, World"
command := fmt.Sprintf("cat /tmp/salut") // Command executed on target machine
actualText, err := ssh.CheckSshCommandE(t, publicHost, command)
assert.Equal(t, expectedText, actualText)

Теперь мы должны указать Terratest, как подключиться к рассматриваемой машине, чтобы получить ее адрес в качестве вывода. Мы используем пользователя по умолчанию «ec2-user» и ключ SSH нашего агента.

publicIP := terraform.Output(t, terraformOptions, "instance_public_ip")
publicHost := ssh.Host{ Hostname:  publicIP, SshUserName: "ec2-user", SshAgent: true, }

Поэтому мы просим 30 попыток, 1 каждые 5 секунд , игнорируя исключения, чтобы иметь возможность продолжить. Наконец, поскольку запуск экземпляра может занять до нескольких минут, нам нужно убедиться, что Terratest несколько раз попытается подключиться к машине, прежде чем объявить об ошибке.

maxRetries := 30
timeBetweenRetries := 5 * time.Second
description := fmt.Sprintf("SSH to public host %s", publicInstanceDNS)
retry.DoWithRetry(t, description, maxRetries, timeBetweenRetries, func() (string, error) {
    actualText, err := ssh.CheckSshCommandE(t, publicHost, command)
    assert.Equal(t, expectedText, actualText)
    return "", err
})

Вот тестовая функция TestFileContent() :

func TestFileContent(t *testing.T) {
    terraformOptions := configureTerraformOptions(t)
    terraform.InitAndApply(t, terraformOptions)
    defer terraform.Destroy(t, terraformOptions)
    publicIP := terraform.Output(t, terraformOptions, "instance_public_ip")
    publicHost := ssh.Host{
        Hostname:  publicIP,
        SshUserName: "ec2-user",
        SshAgent: true,
    }
    maxRetries := 30
    timeBetweenRetries := 5 * time.Second
    description := fmt.Sprintf("SSH to public host %s", publicIP)
    expectedText := "Hello, World"
    command := fmt.Sprintf("cat /tmp/salut")
    retry.DoWithRetry(t, description, maxRetries, timeBetweenRetries, func() (string, error) {
        actualText, err := ssh.CheckSshCommandE(t, publicHost, command)
        assert.Equal(t, expectedText, actualText)
        return "", err
    })
}

Это довольно утомительно и требует знаний в области программирования на Go. С другой стороны, мы автоматически достигаем того, что сделали бы вручную ( кот на файле).

  • Что мы забыли?

Мы запускаем тест: go test -v instance_test.go -run TestFileContent

Running command cat /tmp/salut on ec2-user@35.180.190.131:22
"returned an error: dial tcp 35.180.190.131:22: i/o timeout. Sleeping for 5s and will try again."

К сожалению , порт экземпляра не открыт…

Мы реализуем и назначаем нашему экземпляру группу безопасности, разрешающую подключение SSH к машине с любого IP в файле main.tf :

resource "aws_security_group" "ssh" {
    ingress {
        from_port = "22"
        to_port   = "22"
        protocol  = "tcp"
        cidr_blocks = ["0.0.0.0/0"]
    }
}

Попробуем еще раз: go test -v instance_test.go -run TestFileContent

Новая ошибка:

Running command cat /tmp/salut on ec2-user@35.180.190.131:22
"returned an error: Process exited with status 1. Sleeping for 5s and will try again."

Сообщение не очень значимо, но мы знаем, что не создавали файл. Итак, мы заканчиваем наш main.tf , добавляя скрипт, который пишет в файле /tmp/salut при создании машины:

resource "aws_instance" "example" {
    ami           = "ami-5026902d"
    instance_type = "t2.micro"
    key_name = "terratest_key"
    vpc_security_group_ids = ["${aws_security_group.ssh.id}]"
    user_data = <<-EOF
    #!/bin/bash
    echo 'Hello, World!' > /tmp/salut
    EOF
}

« Вот!» :

Running command cat /tmp/salut on ec2-user@35.180.208.120
--- PASS: TestFileContent (0.61s)
PASS
ok      command-line-arguments  0.623s

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

Идти дальше

Эти неполные тесты позволили нам увидеть некоторые аспекты Terratest. Немного покопавшись в репозитории, мы нашли примеры более сложных тестов для проверки поведения вашей архитектуры с течением времени, таких как развертывание без прерывания обслуживания.

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

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

Может быть интересно интегрировать Terratest в платформу непрерывной интеграции и запускать тесты при каждом обновлении кода инфраструктуры. Создание сред только на время проведения тестов более экономично, чем постоянное создание выделенных сред.

Наконец, тот же репозиторий включает тестовые примеры и модули, охватывающие другие сервисы AWS, такие как S3, ARDS, CloudWatch, IAM и VPC.

Не говоря уже о том, что Terratest охватывает и другие инструменты: Packer, GCP и K8.

Бонус: Облачная ядерная бомба

Риск, выявленный Gruntwork , заключается в том, что ресурсы не будут уничтожены после неудачной серии тестов и, следовательно, будут иметь застойные экземпляры среди тех, которые действительно используются. Это представляло для них значительные затраты: ~85%. Поэтому они разработали инструмент Cloud Nuke , который регулярно очищает среду от этих экземпляров, конфигураций запуска, балансировщиков нагрузки и потерянных EIP . Все, что было создано более часа назад, считается потерянным, что дольше выполнения всех тестов. Их тестовая среда использует учетную запись AWS, независимую от других, чтобы избежать риска уничтожения компьютеров в других средах.

Бронирование

  • В отличие от других инструментов тестирования кода, таких как кухня или молекула, библиотека не абстрагируется от логики создания, тестирования или разрушения своей среды. Из этого следует, что операторы должны реализовать определенные механизмы, такие как повторные попытки в своих тестах.
  • Еще раз: будьте осторожны, облачная среда, в которой Terratest строит тестовую инфраструктуру, должна быть разделена и отделена от других сред. Было бы позором уничтожать производственные машины, пытаясь протестировать свою инфраструктуру…
  • В-третьих, для взаимодействия Terratest с тестируемыми облачными сервисами он должен иметь соответствующие права. Это потенциально подразумевает необходимость управлять новым пользователем или мощной ролью и предоставлять ему аккредитацию.
  • Отчет о результатах теста либо слишком лаконичен: строка FAIL/PASS в неподробном режиме, либо слишком многословен, отображая все детали создания и уничтожения терраформ, которые переполняют информацию.
  • Библиотека не очень хорошо представлена ​​​​на менее «традиционных» сервисах AWS, но, в свою очередь, их чрезвычайно много.

А как насчет других во всем этом?

Есть и другие инструменты для тестирования инфраструктуры, такие как kitchen-terraform , набор плагинов для тестовой кухни, написанных на Ruby, а также совсем молодой ruby ​​rspec-terraform или фреймворк для тестирования Terraform .

Terratest фокусируется на функциональном аспекте всей инфраструктуры, а не на отдельных свойствах этих компонентов. Библиотека фокусируется на автоматизации задач, которые подтверждают поведение, а не наблюдают за ним. Например, мы предпочли бы делать настоящие http-вызовы и анализировать код возврата, чем проверять, работает ли служба httpd на сервере.

Вывод

Мы видели, что возможно, хотя и сложно, создавать тесты, написанные на Go, чтобы гарантировать свойства инфраструктуры, созданной кодом Terraform.

Terratest выполняет свое обещание создания эфемерных сред и автоматизации тестирования. Это гарантирует, что в конце выполнения машины будут уничтожены. Он также позволяет тестировать большое количество параметров на инстансах AWS EC2.

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

Статья является переводом blog.octo.com

You may also like

Leave a Comment