Terraform: реализация технологии “инфраструктура как код”

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

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

Terraform  —  язык для определения инфраструктуры. Это предметно-ориентированный язык. Другими словами, Terraform и его функции специально разработаны для создания инфраструктуры.

Эта статья посвящена краткому обзору его основных возможностей для создания инфраструктурных компонентов. Примеры вдохновлены туториалом по Docker, опубликованном на официальном сайте Terraform.

Простая схема ниже дает краткое представление о том, как работает Terraform. Terraform позволяет объявить инфраструктуру в высокоуровневом скрипте terraform. Движок интерпретирует скрипт и разрешает зависимости, затем создает ресурсы по провайдеру.

Terraform не зависит от инфраструктуры, то есть позволяет вам предоставлять вычислительные ресурсы для AWS, Azure, GCP и т. д., если доступен соответствующий провайдер. Например, провайдер AWS взаимодействует с API AWS для создания инстансов EC2, VPC, DynamoDB и т. д.

Terraform

Экспресс-демонстрация

Ниже приведен типичный пример создания Docker-контейнера NGINX для экспресс-демонстрации. Используя Docker-провайдер, Terraform может создать образ (image), сеть (network), контейнер (container) и другие Docker-ресурсы.

Этот Terraform-скрипт состоит из 3 блоков.

  • Terraform: определение необходимой версии провайдера для Docker.
  • Provider (провайдер): указание подключения к Docker-сервису.
  • Resource (ресурсы): Docker-компоненты, которые необходимо создать.

В начале Terraform-скрипта указываем версию провайдера и его конфигурацию. Например, задаем информацию о подключении для демона локального Docker-сервиса с указанием /var/run/docker.sock.

terraform {
required_providers {
docker = {
source = "kreuzwerker/docker"
version = "~> 3.0.2"
}
}
}

provider "docker" {
host = "unix:///var/run/docker.sock"
}

Затем определяем ресурсы: docker image, docker network и docker container.

Определение ресурсов соответствует приведенному ниже синтаксису.

  • Resource type (тип ресурса)  —  уникальный идентификатор, представляющий тип ресурса. Для Docker-провайдера типами ресурсов являются docker_image, docker_container и т. д. Полный список см. в официальной документации.
  • Resource name (имя ресурса)  —  уникальный идентификатор ресурса.
resource <resource type> <resource name> {
<attribute name> = <value>


}

В примере создается Docker-образ nginx:latest и Docker-сеть под названием “my_network”. Затем запускается контейнер, использующий этот образ, и экспортируется порт 80 как 8080. Terraform-скрипт определен в файле main.tf.

Заметьте: docker image и docker container используют одно и то же имя ресурса. Никакого конфликта не возникает, если разные типы ресурсов имеют одинаковые имена.

Один ресурс может ссылаться на атрибут другого ресурса с помощью синтаксиса <resource type>, <resource name>, <attribute name>. Например, docker_container ссылается на образ, определенный docker_image:

image = docker_image.nginx.image_id

Аналогичным образом идентификатор сети private_network устанавливается для контейнера:

networks_advanced {
name = docker_network.private_network.id
}

Предположим, что требуется в первую очередь создать Docker-образ и Docker-сеть перед запуском контейнера. Не беспокойтесь, Terraform анализирует скрипты и выясняет зависимости, чтобы ресурсы создавались в правильной последовательности.

resource "docker_image" "nginx" {
name = "nginx:latest"
keep_locally = false
}

resource "docker_network" "private_network" {
name = "my_network"
}

resource "docker_container" "nginx" {
image = docker_image.nginx.image_id
name = “web-server”
ports {
internal = 80
external = 8080
}
networks_advanced {
name = docker_network.private_network.id
}
}

Настройка и создание инфраструктуры

Для выполнения Terraform-скрипта CLI на локальном компьютере должен быть установлен terraform CLI. Информацию об установке см. на официальном сайте.

Перейдите в каталог, где находится Terraform-скрипт, и выполните эту команду:

terraform apply

Docker-образ, Docker-сеть и Docker-контейнер будут созданы соответствующим образом:

> docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
6ead75e16a8d c613f16b6642 "/docker-entrypoint.…" 15 seconds ago Up 14 seconds 0.0.0.0:8080->80/tcp web-server

Вывод

После завершения настройки ресурсов Terraform сообщит, сколько ресурсов было добавлено, изменено и удалено. Однако он не сообщает идентификаторы ресурсов. Чтобы узнать идентификатор вновь созданного Docker-контейнера, потребуется выполнить команду docker для проверки только что созданных контейнеров.

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

output <output name> {
description = “...”
value = …
}

В приведенном ниже примере выводятся идентификаторы контейнера, образа и сети. Атрибут value должен быть ссылкой на целевые элементы. Все определения вывода указываются в файле output.tf в соответствии с соглашением.

output "container_id" {
description = "ID of the Docker container"
value = docker_container.nginx.id
}

output "image_id" {
description = "ID of the Docker image"
value = docker_image.nginx.id
}

output "network_id" {
description = "ID of the Docker network"
value = docker_network.private_network.id
}

Снова применив Terraform, получим следующий результат:

Outputs:

container_id = "6ead75e16a8d69b0e2d4ab951672c1a0d67d390f63c24b1cece1f38295edb6a8"
image_id = "sha256:c613f16b664244b150d1c3644cbc387ec1fe8376377f9419992280eb4a82ff3bnginx:latest"
network_id = "7d61349d28239ece2098f21619ded13b5dde2e193e494d68cf128aad6d7ef9ab"

Переменные

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

Определяем переменную в другом файле под названием variable.tf. На самом деле имя файла дается в соответствии с соглашением об именовании. Это может быть любое другое имя файла с суффиксом .tf, а переменные могут даже определяться в файле main.tf. Для Terraform это не имеет значения, потому что все файлы *.tf загружаются для подготовки к работе.

Вот синтаксис:

variable <variable name> {
description = “....”
type = …
default = “...”
}

Определяем новую переменную под названием “container_name” со значением по умолчанию “web-server”:

variable "container_name" {
description = "Value of the name for the Docker container"
type = string
default = "web-server"
}

Теперь обратимся к переменной для имени Docker-контейнера, используя следующий синтаксис: var.<variable name>:

resource "docker_container" "nginx" {
image = docker_image.nginx.image_id
name = var.container_name
ports {
internal = 80
external = 8080
}
networks_advanced {
name = docker_network.private_network.id
}
}

Кроме того, значения переменных по умолчанию можно переопределить в командной строке при применении Terraform-скрипта:

terraform apply -var "container_name=a-different-web-server"

Предоставление нескольких экземпляров ресурсов

Параллельная работа нескольких экземпляров сервисов  —  распространенная стратегия, связанная с обеспечением устойчивости и пропускной способности.

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

Скажем, требуется развернуть 3 Docker-контейнера nginx. Вместо того чтобы дублировать определение ресурса, добавляем “count = 3” в определение ресурса.

Terraform запустит цикл for для создания ресурса. Можно ссылаться на count.index для получения текущего индекса в каждой итерации. Чтобы создавать контейнеры с уникальными именами и номерами портов, в приведенном ниже примере для создания имени и порта контейнера используется current.index.

resource "docker_container" "nginx" {
count = 3
image = docker_image.nginx.image_id
name = format("%s-%d", var.container_name, count.index)
ports {
internal = 80
external = "808${count.index}"
}
networks_advanced {
name = docker_network.private_network.id
}
}

Взгляните на созданные Docker-контейнеры. Обратите внимание, что count.index начинается с нуля при запуске 3 контейнеров: web-server-0, web-server-1, web-server-2.

> docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
3bc102a302d9 c613f16b6642 "/docker-entrypoint.…" 7 seconds ago Up 6 seconds 0.0.0.0:8080->80/tcp web-server-0
fa79fe548b88 c613f16b6642 "/docker-entrypoint.…" 7 seconds ago Up 6 seconds 0.0.0.0:8082->80/tcp web-server-2
5b9b27e3df05 c613f16b6642 "/docker-entrypoint.…" 7 seconds ago Up 6 seconds 0.0.0.0:8081->80/tcp web-server-1

Блок вывода  —  пример ссылки на ресурсы, созданные атрибутом count:

output "container_id" {
description = "ID of the Docker container"
value = docker_container.nginx.*.id
}

Вот примерный результат вышеприведенного определения вывода. Выводится список идентификаторов контейнеров:

Outputs:

container_id = [
"c4dcae40ba6831d310bd16d6dc02018371d96110c90697ef5a2ba907d4f0bc4c",
"be806cfedf400524e1d4a6fafb1a9a1d6ba1068bbb414c67e0d75e82d50f363d",
"22b2b3c9bb835548d2bf9b44c7c6fd59c0bbaaf1ae71e708d320e8919f0e8dfc",
]

Чтобы сосредоточиться на конкретном экземпляре ресурса, можно обратиться к нему, используя формат индекса массива. В примере ниже выводится первый идентификатор контейнера с индексом 0.

output "container_id" {
description = "ID of the first Docker container"
value = docker_container.nginx[0].id
}

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

count.index  —  это просто порядковый номер. Он может создавать только ресурсы с номерами портов с порядковыми номерами, такими как 8080, 8081, 8082 и т. д. А что, если потребуется открыть порты 9091 и 8071? 

В то время как атрибут count представляет собой простой цикл for с индексом, другой атрибут for_each предлагает гораздо более гибкий способ создания нескольких экземпляров на основе переменной data map.

Для начала определим переменную data map. Каждая запись map содержит 3 атрибута: name (имя), port (порт) и environment (среда). Используя приведенные ниже 2 записи map, подготовим 2 экземпляра сервера.

variable "nginx_containers" {
description = "Nginx container configuration"
type = map(any)
default = {
dev-external-proxy = {
name = "eproxy-server"
port = "9091"
environment = "dev"
},
test-internal-proxy = {
name = "iproxy-server"
port = "8071"
environment = "test"
},
}

В блоке resource установим переменную data map в значение атрибута for_each. При этом each.key будет ссылаться на ключ map, а each.value.<attribute name>  —  на конкретный атрибут в записи map. В приведенном ниже примере на основе данных записи map устанавливается имя контейнера, переменная среды и номер порта: 

resource "docker_container" "nginx" {
for_each = var.nginx_containers
image = docker_image.nginx.image_id
name = format("%s-%s", each.key, each.value.name)
env = ["environment=${each.value.environment}"]
ports {
internal = 80
external = each.value.port
}
networks_advanced {
name = docker_network.private_network.id
}
}

Блок output содержит список идентификаторов Docker-контейнеров, полученный в результате выполнения цикла for для переменной data map:

output "container_id" {
description = "IDs of the Docker container"
value = { for p in sort(keys(var.nginx_containers)) : p => docker_container.nginx[p].id }
}

Заключение

Надеюсь, этот простой и краткий обзор ключевых функций Terraform вооружит вас базовыми знаниями для дальнейшего изучения. Хотя демонстрация была основана на Docker-контейнерах, Terraform в основном используется для предоставления компонентов на облачных платформах, таких как AWS, Azure, GCP и т. д. Доступны дополнительные возможности, такие как создание многократно используемых модулей, запросы к ресурсам и множество встроенных функций, поддерживающих инфраструктуру как код.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Gavin F.: A brief introduction of infrastructure as a code — Terraform

Предыдущая статьяНовая эра Angular: беззоновое обнаружение изменений
Следующая статьяЗабудьте про pip  —  используйте uv