Как стать автором
Обновить

Книга «Terraform: инфраструктура на уровне кода. 3-е межд. изд.»

Время на прочтение26 мин
Количество просмотров7.6K
image Привет, Хаброжители!

Terraform — настоящая звезда в мире DevOps. Эта технология позволяет управлять облачной инфраструктурой как кодом (IaC) на облачных платформах и платформах виртуализации, включая AWS, Google Cloud, Azure и др. Третье издание было полностью переработано и дополнено, чтобы вы могли быстро начать работу с Terraform.

Евгений (Джим) Брикман знакомит вас с примерами кода на простом декларативном языке программирования Terraform, иллюстрирующими возможность развертывания инфраструктуры и управления ею с помощью команд. Умудренные опытом системные администраторы, инженеры DevOps и начинающие разработчики быстро перейдут от основ Terraform к использованию полного стека, способного поддерживать трафик огромного объема и большую команду разработчиков.
Целевая аудитория книги
Книга предназначена для всех, кто отвечает за уже написанный код. Это относится к сисадминам, специалистам по эксплуатации, релиз-инженерам, инженерам по мониторингу, инженерам DevOps, разработчикам инфраструктуры, разработчикам полного цикла, руководителям инженерной группы и техническим директорам. Какой бы ни была ваша должность, если вы занимаетесь инфраструктурой, развертываете код, конфигурируете серверы, масштабируете кластеры, выполняете резервное копирование данных, мониторите приложения и отвечаете на вызовы в три часа ночи, эта книга для вас.

В совокупности эти обязанности обычно называют операционной деятельностью (или системным администрированием). Раньше часто встречались разработчики, которые умели писать код, но не разбирались в системном администрировании; точно так же нередко попадались сисадмины без умения писать код. Когда-то такое разделение было приемлемым, но в современном мире, который уже нельзя представить без облачных вычислений и движения DevOps, практически любому разработчику необходимы навыки администрирования, а любой сисадмин должен уметь программировать.

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

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

— Зачем вообще использовать IaC?

— Какая разница между управлением конфигурацией, оркестрацией, выделением ресурсов и шаблонизацией серверов?

— Когда следует использовать Terraform, Chef, Ansible, Puppet, Pulumi, CloudFormation, Docker, Packer или Kubernetes?

— Как работает система Terraform и как с ее помощью управлять инфраструктурой?

— Как создавать многократно используемые модули Terraform?

— Как безопасно управлять конфиденциальной информацией при работе с Terraform?

— Как использовать Terraform с несколькими регионами, учетными записями и облаками?

— Как писать код для Terraform, который будет достаточно надежным для практического применения?

— Как тестировать свой код для Terraform?

— Как внедрить Terraform в свой процесс автоматического развертывания?

— Как лучше всего использовать Terraform в командной работе?

— Вам понадобятся лишь компьютер (Terraform поддерживает большинство операционных систем), интернет-соединение и желание учиться.
Структура издания
Вот список тем, которые освещаются в книге.

Глава 1 «Почему Terraform». Как DevOps меняет наш подход к выполнению ПО; краткий обзор инструментов IaC, включая управление конфигурацией, шаблонизацию серверов, оркестрацию и выделение ресурсов; преимущества IaC; сравнение Terraform, Chef, Puppet, Ansible, Pulumi, OpenStack Heat и CloudFormation; как сочетать такие инструменты, как Terraform, Packer, Docker, Ansible и Kubernetes.

Глава 2 «Приступаем к работе с Terraform». Установка Terraform; краткий обзор синтаксиса Terraform; обзор утилиты командной строки Terraform; как развернуть один сервер; как развернуть веб-сервер; как развернуть кластер веб-серверов; как развернуть балансировщик нагрузки; как очистить созданные вами ресурсы.

Глава 3 «Как управлять состоянием Terraform». Что такое состояние Terraform; как хранить состояние, чтобы к нему имели доступ разные члены команды; как блокировать файлы состояния, чтобы предотвратить состояние гонки; как изолировать файлы состояния, чтобы смягчить последствия ошибок; как использовать рабочие области Terraform; рекомендуемая структура каталогов для проектов Terraform; как работать с состоянием, доступным только для чтения.

Глава 4 «Многократное использование инфраструктуры с помощью модулей Terraform». Что такое модули; как создать простой модуль; как сделать модуль конфигурируемым с помощью входящих и исходящих значений; локальные переменные; версионирование модулей; потенциальные проблемы с модулями; использование модулей для описания настраиваемых элементов инфраструктуры с возможностью повторного применения.

Глава 5 «Работа с Terraform: циклы, условные выражения, развертывание и подводные камни». Циклы с параметром count, выражения for_each и for, строковая директива for; условный оператор с параметром count, выражениями for_each и for, строковой директивой if; встроенные функции; развертывание с нулевым временем простоя; часто встречающиеся подводные камни, связанные с ограничениями count и for_each, развертываниями без простоя; как хорошие планы могут провалиться и как безопасно осуществлять рефакторинг кода Terraform.

Глава 6 «Управление конфиденциальными данными». Введение в управление секретами; обзор различных типов секретов, разных способов их хранения и доступа к ним; сравнение распространенных инструментов управления секретами, таких как HashiCorp Vault, AWS Secrets Manager и Azure Key Vault; как управлять секретами при работе с провайдерами, включая аутентификацию через переменные окружения, роли IAM и OIDC; управление секретами при работе с ресурсами и источниками данных, в том числе с использованием переменных окружения, зашифрованных файлов и централизованных хранилищ секретов; безопасное обращение с файлами состояния и файлами планов.

Глава 7 «Работа с несколькими провайдерами». Подробный обзор особенностей работы с провайдерами, поддерживающими Terraform, в том числе порядок выбора и установки определенной версии и ее использование в коде. Применение нескольких копий одного и того же провайдера, в том числе развертывание в нескольких регионах AWS, развертывание в нескольких учетных записях AWS и создание повторно используемых модулей, способных использовать несколько провайдеров. Как задействовать несколько разных провайдеров вместе, например, для запуска кластера Kubernetes (EKS) в AWS и развертывания контейнеров Docker в кластере.

Глава 8 «Код Terraform промышленного уровня». Почему проекты DevOps всегда развертываются дольше, чем ожидается; что характеризует инфраструктуру, готовую к промышленному использованию; как создавать модули Terraform для промышленного применения; небольшие модули; сборные модули; тестируемые модули; готовые к выпуску модули; реестр Terraform; проверка переменных; управление версиями Terraform, провайдеров Terraform, модулей Terraform и Terragrunt; «аварийные люки» в Terraform.

Глава 9 «Как тестировать код Terraform». Ручное тестирование кода Terraform; создание и удаление тестового окружения; автоматизированное тестирование кода Terraform; Terrarest; модульные тесты; интеграционные тесты; сквозные тесты; внедрение зависимостей; параллельное выполнение тестов; этапы тестирования; попытки; пирамида тестирования; статический анализ; план тестирования; тестирование сервера.

Глава 10 «Как использовать Terraform в команде». Как внедрить Terraform в командную работу; как убедить начальство; рабочий процесс развертывания прикладного кода; рабочий процесс развертывания инфраструктурного кода; управление версиями; золотое правило Terraform; рецензирование кода; рекомендации по оформлению кода; принятый в Terraform стиль; CI/CD для Terraform; процесс развертывания.

Эту книгу можно читать последовательно или сразу переходить к тем главам, которые вас больше всего интересуют. Имейте в виду, что все примеры последующих глав основаны на коде из предыдущих. Если вы листаете туда-сюда, используйте в качестве ориентира архив исходного кода (как описано в разделе «Примеры с открытым исходным кодом» далее). В приложении вы найдете список книг и статей о Terraform, системном администрировании, IaC и DevOps.
Что нового в третьем издании
Первое издание этой книги вышло в 2017 году, второе вышло в 2019 году, и хотя мне сложно в это поверить, но сейчас я работаю уже над третьим изданием. Время летит. Удивительно, как много изменилось за эти годы!

Если вы читали второе издание книги и хотите узнать, что нового появилось в этом издании, или вам просто интересно увидеть, как Terraform развивался между 2019 и 2022 годами, то ниже перечислены некоторые основные различия между вторым и третьим изданиями.

— Сотни страниц обновленного содержания. Третье издание книги примерно на 100 страниц объемнее второго. Также, по моим оценкам, примерно от трети до половины страниц второго издания были обновлены. Почему так много новой информации? Потому что с момента выхода второго издания вышло шесть новых версий Terraform со значительными изменениями: 0.13, 0.14, 0.15, 1.0, 1.1 и 1.2. Более того, многие провайдеры Terraform провели собственные серьезные обновления, в том числе AWS, который на момент выхода второго издания был представлен версией 2, а к моменту выхода третьего — версией 4. Кроме того, за последние несколько лет значительно выросло сообщество Terraform, что привело к появлению множества новых приемов, инструментов и модулей. Я постарался отразить как можно больше этих изменений, добавив две новые главы и внеся существенные обновления в остальные, как описано ниже.

— Новые функциональные возможности провайдеров. В Terraform значительно улучшена поддержка провайдеров. В третье издание я добавил совершенно новую главу (главу 7), где рассказывается, как работать с несколькими провайдерами: например, как организовать развертывание в нескольких регионах, нескольких учетных записях и нескольких облаках. Кроме того, по многочисленным просьбам в эту главу включен совершенно новый набор примеров, показывающих, как использовать Terraform, Kubernetes, Docker, AWS и EKS для запуска контейнерных приложений. Наконец, я обновил все остальные главы, чтобы осветить новые функциональные возможности провайдеров, появившиеся в последних нескольких версиях, включая блок require_providers, добавленный в Terraform 0.13 (предлагает лучший способ установки и версионирования официальных и неофициальных провайдеров Terraform, а также управления ими); файлы блокировки, внедренные в Terraform 0.14 (помогает гарантировать использование одних и тех же версий провайдеров всеми членами вашей команды) и параметр configuration_aliases, появившийся в Terraform 0.15 (улучшает управление провайдерами внутри модулей).

— Улучшенное управление секретами. При использовании кода Terraform часто приходится иметь дело со многими видами секретов: паролями баз данных, ключами API, учетными данными облачного провайдера, сертификатами TLS и т. д. Поэтому я добавил в третье издание новую главу (главу 6), посвященную этой теме, где сравниваются распространенные инструменты управления секретами и представлены примеры кода, иллюстрирующие разные методы безопасного использования секретов в Terraform, включая переменные окружения, зашифрованные файлы, централизованные хранилища секретов, роли IAM, OIDC и многое другое.

— Новые функциональные возможности модулей. В Terraform 0.13 появилась возможность использовать count, for_each и depend_on в блоках module, что делает модули более мощными, гибкими и пригодными для повторного применения. Примеры использования этих новых возможностей вы найдете в главах 5 и 7.

— Новые возможности проверки. В главе 8 я добавил примеры использования функции validation, появившейся в Terraform 0.13 и выполняющей простые проверки переменных (например, превышение минимальных или максимальных значений), а также функций precondition и postcondition, появившихся в Terraform 1.2 и выполняющих простые проверки ресурсов и источников данных перед запуском apply (например, для принудительного использования AMI при выборе пользователем архитектуры x86_64) либо после запуска apply (например, проверка успешности шифрования тома EBS). В главе 6 я покажу, как использовать параметр sensitive (добавлен в Terraform 0.14 и 0.15), не позволяющий выводить секреты при запуске plan или apply.

— Новые возможности рефакторинга. В Terraform 1.1 появился блок moved, обеспечивающий лучший способ выполнения таких операций рефакторинга, как переименование ресурса. Раньше для этого пользователь должен был вручную запускать операции terraform state mv, а теперь, как показано в новом примере в главе 5, этот процесс можно полностью автоматизировать, что делает обновление модулей более безопасным и удобным.

— Дополнительные возможности тестирования. Инструменты автоматического тестирования кода Terraform продолжают совершенствоваться. В главе 9 я добавил пример кода и привел сравнение инструментов статического анализа, включая tfsec, tflint, terrascan и validate, инструментов тестирования plan, включая Terratest, OPA и Sentinel, а также инструментов тестирования серверов, включая inspec, serverspec и goss. Я также добавил сравнение существующих подходов к тестированию, чтобы вы могли выбрать наиболее подходящий для вас.

— Улучшенная стабильность. Выпуск версии Terraform 1.0 стал важной вехой для Terraform, означающей не только достижение определенного уровня зрелости, но и гарантии совместимости — все выпуски 1.x будут обратно совместимы, поэтому обновление между выпусками v1.x больше не должно требовать изменений кода, рабочих процессов или файлов состояния. Файлы состояния Terraform теперь перекрестно совместимы с Terraform 0.14, 0.15 и всеми выпусками 1.x, а источники данных удаленного состояния Terraform перекрестно совместимы с Terraform 0.12.30, 0.13.6, 0.14.0, 0.15.0 и всеми версиями 1.x. Я также обновил главу 8, добавив примеры, показывающие как лучше управлять версиями Terraform (включая использование tfenv), Terragrunt (включая использование tgswitch) и провайдеров Terraform (включая использование файла блокировки).

— Возросшая зрелость. Terraform был загружен более 100 миллионов раз, насчитывает более 1500 участников и используется почти в 79 % компаний из списка Fortune 5001, поэтому можно с уверенностью сказать, что экосистема значительно выросла за последние несколько лет и стала более зрелой. Сейчас проект Terraform насчитывает больше разработчиков, провайдеров, повторно используемых модулей, инструментов, плагинов и классов. Появилось больше книг и учебных пособий. Кроме того, HashiCorp, компания, создавшая Terraform, провела IPO (первичное публичное размещение акций) в 2021 году, поэтому теперь Terraform больше не является небольшим стартапом — это крупная и устойчивая компания, продающая свои акции, для которой Terraform является крупнейшим предложением.

— Множество других изменений. Кроме перечисленного выше, произошло множество других изменений, включая запуск Terraform Cloud (веб-интерфейс для использования Terraform); возросшую зрелость инструментов, популярных в сообществе, таких как Terragrunt, Terratest и tfenv; добавление множества новых возможностей провайдеров (в том числе новых способов развертывания с нулевым временем простоя, таких как обновление экземпляра, о чем рассказывается в главе 5); появление новых функций (в частности, в главе 5 я добавил примеры использования функции one и в главе 7 — функции try); прекращение поддержки многих старых возможностей (например, источника данных template_file, многих параметров aws_s3_bucket, list и map, внешних ссылок в destroy) и многое другое.
Чего нет в этой книге
Книга не задумывалась как исчерпывающее руководство по Terraform. Она не охватывает все облачные провайдеры, все ресурсы, которые поддерживаются каждым из них, или каждую команду, доступную в этой системе. За этими подробностями я отсылаю вас к документации по адресу www.terraform.io/docs/index.html.

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

Чтобы это продемонстрировать, я включил в книгу ряд примеров кода. Я пытался сделать так, чтобы вам было просто работать с ними в домашних условиях. Для этого минимизировал количество сторонних зависимостей. Именно поэтому везде используется лишь один облачный провайдер, AWS. Таким образом, вам нужно будет зарегистрироваться только в одном стороннем сервисе (к тому же AWS предлагает хороший бесплатный тариф, и вам не придется ничего платить за выполнение примеров). Именно поэтому примеры кода не охватывают и не требуют платных услуг HashiCorp, Terraform Cloud или Terraform Enterprise. И именно поэтому я опубликовал все примеры с открытым исходным кодом.

Отрывок из книги. Интеграционные тесты


Подготовив несколько модульных тестов, мы можем переходить к интеграционному тестированию. Чтобы сформировать общее понимание, которое позже можно будет применить к коду Terraform, лучше начать с примера веб-сервера, написанного на Ruby. Для интеграционного тестирования такого сервера нужно выполнить следующее.
  1. Запустить веб-сервер на локальном компьютере так, чтобы он прослушивал порт.
  2. Отправить ему HTTP-запросы.
  3. Убедиться в получении получили ожидаемых ответов.

Создадим в файле web-server-test.rb метод, который выполняет эти шаги:

def do_integration_test(path, check_response)
   port = 8000
   server = WEBrick::HTTPServer.new :Port => port
   server.mount '/', WebServer

   begin
      # Запускаем веб-сервер в отдельном потоке, чтобы он не блокировал тест
      thread = Thread.new do
         server.start
      end

      # Отправляем HTTP-запрос по определенному пути веб-сервера
      uri = URI("http://localhost:#{port}#{path}")
      response = Net::HTTP.get_response(uri)

      # Используем для проверки ответа заданную лямбда-функцию check_response
      check_response.call(response)
  ensure

      # В конце теста останавливаем сервер и поток
      server.shutdown
      thread.join
  end
end

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

def test_integration_hello
   do_integration_test('/', lambda { |response|
      assert_equal(200, response.code.to_i)
      assert_equal('text/plain', response['Content-Type'])
      assert_equal('Hello, World', response.body)
   })
end

Этот код вызывает метод do_integration_test с путем / и передает ему лямбда-выражение (вложенную функцию), которая проверяет, равны ли код и тело ответа 200 OK и соответственно Hello, World. Интеграционные тесты для других конечных точек выглядят аналогично. Давайте запустим их все:

$ ruby web-server-test.rb

(...)

Finished in 0.221561 seconds.
--------------------------------------------
8 tests, 24 assertions, 0 failures, 0 errors
100% passed
--------------------------------------------

Обратите внимание, что при модульном тестировании на выполнение всех тестов уходило 0,000572 секунды. Теперь же это время выросло примерно в 387 раз, до 0,221561 секунды. Конечно, выполнение по-прежнему происходит молниеносно, но это связано лишь с тем, что код веб-сервера на Ruby мало что умеет. Этот пример специально сделан как можно более компактным. Здесь важны не абсолютные показатели, а относительная тенденция: интеграционные тесты обычно работают медленней, чем модульные. Позже мы еще к этому вернемся.

Теперь сосредоточимся на интеграционном тестировании кода Terraform. Если раньше мы тестировали отдельные модули, то теперь нужно проверить, как они работают вместе. Для этого мы должны развернуть их одновременно и убедиться в том, что они ведут себя корректно. В предыдущем разделе вы развернули демонстрационное приложение Hello, World с фиктивными данными вместо настоящей БД MySQL. Теперь развернем реальный модуль MySQL и посмотрим, как с ним интегрируется приложение Hello, World. У вас уже должен быть подходящий код в папках live/stage/data-stores/mysql и live/stage/services/hello-world-app. Таким образом, вы можете создать интеграционный тест для своей тестовой среды (точнее, для ее части).

Конечно, как уже упоминалось в этой главе, все автоматические тесты должны выполняться в изолированной учетной записи AWS. Поэтому при проверке кода для тестовой среды все тесты следует запускать от имени изолированного тестового пользователя. Если в ваших модулях есть код, который специально прописан для тестовой среды, самое время сделать его конфигурируемым, чтобы вы могли внедрять тестовые значения. В частности, добавьте в файл live/stage/data-stores/mysql/variables.tf новую входную переменную db_name для передачи имени базы данных:

variable "db_name" {
   description = "The name to use for the database"
   type = string
   default = "example_database_stage"
}

Передайте это значение модулю mysql в файле live/stage/data-stores/mysql/main.tf:

module "mysql" {
     source = "../../../../modules/data-stores/mysql"

     db_name = var.db_name
     db_username = var.db_username
     db_password = var.db_password
}

Теперь создадим в файле test/hello_world_integration_test.go каркас интеграционного теста, а детали реализации оставим на потом:

// Подставьте сюда подходящие пути к вашим модулям
const dbDirStage = "../live/stage/data-stores/mysql"
const appDirStage = "../live/stage/services/hello-world-app"

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

            // Развертываем БД MySQL
            dbOpts := createDbOpts(t, dbDirStage)
            defer terraform.Destroy(t, dbOpts)
            terraform.InitAndApply(t, dbOpts)

            // Развертываем hello-world-app
            helloOpts := createHelloOpts(dbOpts, appDirStage)
            defer terraform.Destroy(t, helloOpts)
            terraform.InitAndApply(t, helloOpts)

            // Проверяем, работает ли hello-world-app
            validateHelloApp(t, helloOpts)
}

Тест выполняет следующие действия: развертывает mysql и hello-world-app, проверяет приложение, удаляет hello-world-app (выполняется в конце благодаря defer) и в завершение удаляет mysql (выполняется в конце благодаря defer). Методы createDbOpts, createHelloOpts и validateHelloApp пока не существуют, поэтому реализуем их по очереди. Начнем с метода createDbOpts:

func createDbOpts(t *testing.T, terraformDir string) *terraform.Options {
           uniqueId := random.UniqueId()

           return &terraform.Options{
                      TerraformDir: terraformDir,
                      Vars: map[string]interface{}{
                                   "db_name": fmt.Sprintf("test%s", uniqueId),
                                   "db_username": "admin",
                                   "db_password": "password",
                      },
           }
}

Пока ничего нового: код передает методу terraform.Options заданную папку и устанавливает переменные db_name, db_username и db_password.

Дальше нужно разобраться с тем, где модуль mysql будет хранить свое состояние. До сих пор конфигурация backend содержала значения, прописанные вручную:

backend "s3" {
   # Подставьте имя своей корзины!
   bucket               = "terraform-up-and-running-state"
   key                    = "stage/data-stores/mysql/terraform.tfstate"
   region                = "us-east-2"

   # Подставьте имя своей таблицы DynamoDB!
   dynamodb_table = "terraform-up-and-running-locks"
   encrypt              = true
}

Эти значения создают большую проблему, потому что, если оставить их как есть, будет перезаписан реальный файл состояния тестовой среды! Одно из обходных решений — использовать рабочие области (как обсуждалось в подразделе «Изоляция через рабочие области» в главе 3), но для этого все равно нужен доступ к корзине S3, принадлежащей учетной записи тестовой среды, тогда как все ваши тесты должны выполняться от имени совершенно отдельного пользователя AWS. Вместо этого лучше использовать частичную конфигурацию, как было описано в разделе «Ограничения хранилищ Terraform» главы 3. Вынесите всю конфигурацию backend во внешний файл, такой как backend.hcl:

bucket               = "terraform-up-and-running-state"
key                    = "stage/data-stores/mysql/terraform.tfstate"
region                = "us-east-2"
dynamodb_table = "terraform-up-and-running-locks"
encrypt              = true

Таким образом, конфигурация backend в файле live/stage/data-stores/mysql/main.tf остается пустой:

backend "s3" {
}

При развертывании модуля mysql в настоящей тестовой среде нужно указать аргумент -backend-config, чтобы Terraform использовал конфигурацию backend из файла backend.hcl:

$ terraform init -backend-config=backend.hcl

При выполнении тестов для модуля mysql можно заставить Terratest передать тестовые значения, используя параметр BackendConfig для terraform.Options:

func createDbOpts(t *testing.T, terraformDir string) *terraform.Options {
           uniqueId := random.UniqueId()

           bucketForTesting := "YOUR_S3_BUCKET_FOR_TESTING"
           bucketRegionForTesting := "YOUR_S3_BUCKET_FOR_TESTING"
           dbStateKey := fmt.Sprintf("%s/%s/terraform.tfstate", t.Name(), uniqueId)

           return &terraform.Options{
                       TerraformDir: terraformDir,

                       Vars: map[string]interface{}{
                                    "db_name": fmt.Sprintf("test%s", uniqueId),
                                    "db_username": "admin",
                                    "db_password": "password",
                       },

                       BackendConfig: map[string]interface{}{
                                    "bucket": bucketForTesting,
                                    "region": bucketRegionForTesting,
                                    "key": dbStateKey,
                                    "encrypt": true,
                       },
           }
}

Вы должны указать собственные значения для переменных bucketForTesting и bucketRegionForTesting. В качестве хранилища в тестовой учетной записи AWS можно создать одну корзину S3, так как параметр key (путь внутри корзины) содержит идентификатор uniqueId, который должен быть достаточно уникальным, чтобы все тесты имели разные значения.

Далее следует внести некоторые изменения в модуль hello-world-app в тестовой среде. Откройте файл live/stage/services/hello-world-app/variables.tf и сделайте доступными переменные db_remote_state_bucket, db_remote_state_key и environment:

variable "db_remote_state_bucket" {
   description = "The name of the S3 bucket for the database's remote state"
   type          = string
}

variable "db_remote_state_key" {
   description = "The path for the database's remote state in S3"
   type          = string
}

variable "environment" {
   description = "The name of the environment we're deploying to"
   type          = string
   default      = "stage"
}

Передайте эти значения модулю hello-world-app в файле live/stage/services/hello-world-app/main.tf:

module "hello_world_app" {
     source = "../../../../modules/services/hello-world-app"

     server_text                    = "Hello, World"

     environment                   = var.environment
     db_remote_state_bucket = var.db_remote_state_bucket
     db_remote_state_key      = var.db_remote_state_key

     instance_type        = "t2.micro"
     min_size               = 2
     max_size               = 2
     enable_autoscaling = false
     ami                       = data.aws_ami.ubuntu.id
}

Теперь вы можете реализовать метод createHelloOpts:

func createHelloOpts(
      dbOpts *terraform.Options,
      terraformDir string) *terraform.Options {

      return &terraform.Options{
            TerraformDir: terraformDir,

            Vars: map[string]interface{}{
                         "db_remote_state_bucket": dbOpts.BackendConfig["bucket"],
                         "db_remote_state_key": dbOpts.BackendConfig["key"],
                         "environment": dbOpts.Vars["db_name"],
            },
      }
}

Обратите внимание, что переменным db_remote_state_bucket и db_remote_state_key присвоены значения из BackendConfig для модуля mysql. Благодаря этому модуль hello-world-app читает именно то состояние, которое только что было записано модулем mysql. Переменная environment равна db_name, чтобы все ресурсы распределялись по пространствам имен одним и тем же образом.

Наконец вы можете реализовать метод validateHelloApp:

func validateHelloApp(t *testing.T, helloOpts *terraform.Options) {
           albDnsName := terraform.OutputRequired(t, helloOpts, "alb_dns_name")
           url := fmt.Sprintf("http://%s", albDnsName)

           maxRetries := 10
           timeBetweenRetries := 10 * time.Second

           http_helper.HttpGetWithRetryWithCustomValidation(
                       t,
                       url,
                       nil,
                       maxRetries,
                       timeBetweenRetries,
                       func(status int, body string) bool {
                                   return status == 200 &&
                                         strings.Contains(body, "Hello, World")
                       },
           )
}

Как и наши модульные тесты, этот метод использует пакет http_helper, только на этот раз мы вызываем из него http_helper.HttpGetWithRetryWithCustomValidation, что позволяет нам указать наши собственные правила проверки кода состояния и тела HTTP-ответа. Это необходимо для проверки наличия в HTTP-ответе строки Hello, World, а не для точного сопоставления строк, так как ответ, который возвращает скрипт пользовательских данных внутри модуля hello-world-app, содержит и другой текст.

Теперь запустите интеграционный тест и проверьте, работает ли он:

$ go test -v -timeout 30m -v "TestHelloWorldAppStage"

(...)

PASS
ok terraform-up-and-running 795.63s

Превосходно! Теперь у вас есть интеграционный тест, с помощью которого можно убедиться в корректной совместной работе нескольких ваших модулей. Он получился более сложным, чем модульный тест, и его выполнение длится вдвое дольше (10–15 минут вместо 4–5). Сделать его быстрее будет непросто, потому что основное время занимает развертывание RDS, ASG, ALB и так далее в AWS. Но в определенных обстоятельствах работу теста можно сократить с помощью стадий тестирования.

Стадии тестирования


Если взглянуть на код вашего интеграционного теста, можно заметить, что он состоит из нескольких отдельных «стадий».
  1. Запустить terraform apply для модуля mysql.
  2. Запустить terraform apply для модуля hello-world-app.
  3. Выполнить проверку и убедиться в том, что все работает.
  4. Выполнить terraform destroy для модуля hello-world-app.
  5. Выполнить terraform destroy для модуля mysql.

Если вы запускаете эти тесты в среде CI, нужно выполнить каждую стадию от начала до конца. Но если вы используете их в локальной среде для разработки и вместе с этим шаг за шагом вносите изменения в свой код, выполнять все стадии не обязательно. Например, если вы редактируете только модуль hello-world-app, повторный запуск всего теста после каждой фиксации влечет за собой развертывание и удаление модуля mysql, хотя ваши изменения его никак не касаются. Это добавляет к времени работы теста 5–10 минут без какой-либо необходимости.

В идеале рабочий процесс должен выглядеть определенным образом.

1. Запустить terraform apply для модуля mysql.

2. Запустить terraform apply для модуля hello-world-app.

3. Переход к пошаговой разработке:

а) внести изменение в модуль hello-world-app;
б) повторно выполнить terraform apply для модуля hello-world-app, чтобы развернуть ваши обновления;
в) проверить и убедиться в том, что все работает;
г) если все работает, перейти к следующему шагу, если нет — вернуться к шагу 3а.

4. Выполнить terraform destroy для модуля hello-world-app.

5. Выполнить terraform destroy для модуля mysql.

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

Terratest имеет встроенную поддержку этой стратегии в виде пакета test_structure. Суть в том, что каждая стадия вашего теста заворачивается в функцию с именем; затем вы сможете заставить Terratest пропустить некоторые из этих имен, используя переменные среды. Каждая стадия тестирования сохраняет тестовые данные на диск, чтобы их можно было прочитать во время последующих выполнений. Попробуем применить эту стратегию к примеру test/hello_world_integration_test.go. Сначала набросаем каркас теста, а затем наполним его внутренние методы:

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

      // Сохраняем функцию в переменную с коротким именем, просто чтобы примеры
      // с кодом было легче уместить на страницах этой книги.
      stage := test_structure.RunTestStage

      // Развертываем БД MySQL
      defer stage(t, "teardown_db", func() { teardownDb(t, dbDirStage) })
      stage(t, "deploy_db", func() { deployDb(t, dbDirStage) })

      // Развертываем приложение hello-world-app
      defer stage(t, "teardown_app", func() { teardownApp(t, appDirStage) })
      stage(t, "deploy_app", func() { deployApp(t, dbDirStage, appDirStage) })

      // Проверяем, работает ли hello-world-app
      stage(t, "validate_app", func() { validateApp(t, appDirStage) })
}

Структура та же, что и прежде: развернуть mysql и hello-world-app, проверить приложение, удалить hello-world-app (выполняется в конце благодаря defer) и mysql (выполняется в конце с помощью defer). Разница лишь в том, что теперь каждая стадия обернута в test_structure.RunTestStage. Метод RunTestStage принимает три аргумента.
  • t. Первым аргументом выступает значение t, которое Go передает всем автоматическим тестам. С его помощью можно управлять состоянием теста. Например, вы можете его провалить, вызвав t.Fail().
  • Имя стадии. Второй аргумент позволяет задать имя этой стадии тестирования. Вскоре вы увидите пример того, как с помощью этого имени можно пропускать отдельные стадии.
  • Код для выполнения. Третий аргумент — это код, который нужно выполнить на данной стадии тестирования. Это может быть любая функция.

Теперь реализуем функции для каждой стадии тестирования. Начнем с deployDb:

func deployDb(t *testing.T, dbDir string) {
            dbOpts := createDbOpts(t, dbDir)
            // Сохраняем данные на диск, чтобы в процессе других стадий теста,
            // запущенных позже, можно было их прочитать
            test_structure.SaveTerraformOptions(t, dbDir, dbOpts)

            terraform.InitAndApply(t, dbOpts)
}

Как и прежде, чтобы развернуть mysql, код вызывает createDbOpts и terraform.InitAndApply. Единственное изменение лишь в том, что теперь между этими двумя шагами находится вызов test_structure.SaveTerraformOptions, который записывает содержимое dbOpts на диск, чтобы позже его могли прочитать другие стадии тестирования. Например, вот реализация функции teardownDb:

func teardownDb(t *testing.T, dbDir string) {
           dbOpts := test_structure.LoadTerraformOptions(t, dbDir)
           defer terraform.Destroy(t, dbOpts)
}

Эта функция вызывает test_structure.LoadTerraformOptions, чтобы загрузить с диска содержимое dbOps, которое было записано ранее функцией deployDb. Причина, по которой эти данные передаются через диск, а не в оперативной памяти, связана с тем, что каждая стадия может запускаться самостоятельно — то есть в отдельном процессе. Как вы увидите позже в этой главе, при первых нескольких запусках go test вам захочется выполнить deployDb, но пропустить teardownDb, а затем, при последующих запусках, сделать наоборот. Чтобы во время всех этих запусков использовалась одна и та же база данных, информацию о ней следует хранить на диске.

Теперь реализуем функцию deployHelloApp:

func deployApp(t *testing.T, dbDir string, helloAppDir string) {
            dbOpts := test_structure.LoadTerraformOptions(t, dbDir)
            helloOpts := createHelloOpts(dbOpts, helloAppDir)

            // Сохраняем данные на диск, чтобы в процессе других стадий теста,
            // запущенных позже, можно было их прочитать
            test_structure.SaveTerraformOptions(t, helloAppDir, helloOpts)

            terraform.InitAndApply(t, helloOpts)
}

Этот код повторно использует ранее определенную функцию createHelloOpts и вызывает для нее terraform.InitAndApply. И снова все новое поведение заключается в вызове методов test_structure.SaveTerraformOptions и test_structure.SaveTerraformOptions для загрузки dbOpts с диска и соответственно сохранения helloOpts на диск. Вы уже догадываетесь, как будет выглядеть реализация метода teardownApp:

func teardownApp(t *testing.T, helloAppDir string) {
           helloOpts := test_structure.LoadTerraformOptions(t, helloAppDir)
           defer terraform.Destroy(t, helloOpts)
}

А вот реализация метода validateApp:

func validateApp(t *testing.T, helloAppDir string) {
           helloOpts := test_structure.LoadTerraformOptions(t, helloAppDir)
           validateHelloApp(t, helloOpts)
}

Таким образом, данный код идентичен оригинальному интеграционному тесту, только теперь каждая стадия завернута в вызов test_structure.RunTestStage, и еще вам нужно приложить немного усилий для сохранения и чтения данных с диска. Эти простые изменения открывают перед вами важную возможность: заставить Terratest пропустить любую стадию с именем foo, установив переменную среды SKIP_foo=true. Разберем типичный процесс написания кода, чтобы увидеть, как это работает.

Для начала нужно запустить тест, пропустив при этом обе стадии очистки, чтобы по окончании тестирования модули mysql и hello-world-app оставались развернутыми. Поскольку эти стадии называются teardown_db и teardown_app, нужно установить переменные среды SKIP_teardown_db и соответственно SKIP_teardown_app. Так Terratest будет знать, что их нужно пропустить:

$ SKIP_teardown_db=true \
   SKIP_teardown_app=true \
   go test -timeout 30m -run 'TestHelloWorldAppStageWithStages'

(...)

The 'SKIP_deploy_db' environment variable is not set, so executing stage 'deploy_db'.

(...)

The 'deploy_app' environment variable is not set, so executing stage 'deploy_db'.

(...)

The 'validate_app' environment variable is not set,
so executing stage 'deploy_db'.

(...)

The 'teardown_app' environment variable is set, so skipping stage 'deploy_db'.

(...)

The 'teardown_db' environment variable is set, so skipping stage 'deploy_db'.

(...)

PASS
ok terraform-up-and-running 423.650s

Теперь вы можете приступить к последовательному редактированию модуля hello-world-app, повторно запуская тесты при каждом изменении. Но на этот раз сделайте так, чтобы, помимо очистки, пропускалась также стадия развертывания модуля mysql (так как mysql по-прежнему выполняется). Таким образом будет выполнена только команда terraform apply и проведена проверка модуля hello-world-app:

$ SKIP_teardown_db=true \
   SKIP_teardown_app=true \
   SKIP_deploy_db=true \
   go test -timeout 30m -run 'TestHelloWorldAppStageWithStages'

(...)

The 'SKIP_deploy_db' environment variable is set, so skipping stage 'deploy_db'.

(...)

The 'deploy_app' environment variable is not set, so executing stage 'deploy_db'.

(...)

The 'validate_app' environment variable is not set, so executing stage 'deploy_db'.

(...)

The 'teardown_app' environment variable is set, so skipping stage 'deploy_db'.

(...)

The 'teardown_db' environment variable is set, so skipping stage 'deploy_db'.

(...)

PASS
ok terraform-up-and-running 13.824s

Обратите внимание, как быстро теперь работает каждый из этих тестов: вместо 10–15 минут тестирование каждого нового изменения занимает 10–60 секунд (в зависимости от того, что поменялось). Учитывая, что в процессе разработки эти стадии, скорее всего, будут выполняться десятки или даже сотни раз, вы можете сэкономить уйму времени.

Когда после всех изменений модуль hello-world-app начнет работать так, как вы того ожидали, самое время очистить все ресурсы. Запустите тесты еще раз, но теперь пропустите стадии развертывания и проверки, чтобы выполнялась только очистка:

$ SKIP_deploy_db=true \
   SKIP_deploy_app=true \
   SKIP_validate_app=true \
   go test -timeout 30m -run 'TestHelloWorldAppStageWithStages'

(...)

The 'SKIP_deploy_db' environment variable is set, so skipping stage 'deploy_db'.

(...)

The 'SKIP_deploy_app' environment variable is set, so skipping stage 'deploy_app'.

(...)

The 'SKIP_validate_app' environment variable is set, so skipping stage 'validate_app'.

(...)

The 'SKIP_teardown_app' environment variable is not set, so executing stage
'teardown_app'.

(...)

The 'SKIP_teardown_db' environment variable is not set, so executing stage
'teardown_db'.

(...)

PASS
ok terraform-up-and-running 340.02s

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

Повторение попыток


Начав регулярно выполнять автоматические тесты для своего инфраструктурного кода, вы столкнетесь с их непредсказуемостью. Иногда они будут проваливаться по причинам временного характера: например, сервер EC2 может не запуститься, в Terraform может проявиться ошибка отложенной согласованности или вам не удастся установить TLS-соединение с S3. Мир инфраструктуры полон беспорядка, поэтому вы должны быть готовы к периодическим сбоям в своих тестах и обрабатывать их соответствующим образом.

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

* error loading the remote state: RequestError: send request failed
Post https://xxx.amazonaws.com/: dial tcp xx.xx.xx.xx:443:
connect: connection refused

Чтобы ваши тесты лучше справлялись с подобными ошибками, можно включить в Terratest повторные попытки, используя аргументы MaxRetries, TimeBetweenRetries и RetryableTerraformErrors метода terraform.Options:

func createHelloOpts(
      dbOpts *terraform.Options,
      terraformDir string) *terraform.Options {

      return &terraform.Options{
            TerraformDir: terraformDir,

            Vars: map[string]interface{}{
                  "db_remote_state_bucket": dbOpts.BackendConfig["bucket"],
                  "db_remote_state_key": dbOpts.BackendConfig["key"],
                  "environment": dbOpts.Vars["db_name"],
            },

            // Повторяем не более трех раз с интервалом 5 секунд
            // между попытками, для известных ошибок
            MaxRetries: 3,
            TimeBetweenRetries: 5 * time.Second,
            RetryableTerraformErrors: map[string]string{
                  "RequestError: send request failed": "Throttling issue?",
            },
      }
}

Аргументу RetryableTerraformErrors можно передать ассоциативный массив с известными ошибками, требующими повторения попытки. В качестве ключей выступают сообщения об ошибках, которые нужно искать в журнальных записях (здесь можно использовать регулярные выражения), а значениями служат дополнительные сведения, которые записываются в журнал, когда Terratest находит одну из ошибок и инициирует повторную попытку. Теперь, когда код теста сталкивается с указанной вами ошибкой, в журнале должно появляться сообщение, и по прошествии TimeBetweenRetries ваша команда выполнится еще раз:

$ go test -v -timeout 30m

(...)

Running command terraform with args [apply -input=false -lock=false -auto-approve]

(...)

* error loading the remote state: RequestError: send request failed
Post https://s3.amazonaws.com/: dial tcp 11.22.33.44:443:
connect: connection refused

(...)

'terraform [apply]' failed with the error 'exit status code 1'
but this error was expected and warrants a retry. Further details:
Intermittent error, possibly due to throttling?

(...)

Running command terraform with args [apply -input=false -lock=false -auto-approve]

Об авторе


Евгений (Джим) Брикман обожает программировать, писать, выступать, путешествовать и заниматься тяжелой атлетикой. Он соучредитель компании Gruntwork, которая предоставляет DevOps как услугу, и автор еще одной книги, опубликованной издательством O’Reilly Media под названием Hello, Startup: A Programmer’s Guide to Building Products, Technologies, and Teams. Ранее он работал программистом в LinkedIn, TripAdvisor, Cisco Systems и Thomson Financial. Свои степени бакалавра и магистра наук он получил в Корнеллском университете. Больше о нем можно узнать по адресу www.ybrikman.com.

Более подробно с книгой можно ознакомиться на сайте издательства:

» Оглавление
» Отрывок

По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Для Хаброжителей скидка 25% по купону — Terraform
Теги:
Хабы:
Всего голосов 7: ↑6 и ↓1+9
Комментарии3

Публикации

Информация

Сайт
piter.com
Дата регистрации
Дата основания
Численность
201–500 человек
Местоположение
Россия