«1. Обзор

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

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

2. Краткая история

Согласно GitHub, дата первой фиксации Terraform была 21 мая 2014 года. Автором был Митчелл Хашимото, один из основателей Hashicorp, и он содержит только файл README, который описывает то, что мы можем назвать его «заявление о миссии»:

Terraform is a tool for building and changing infrastructure safetly [sic] and efficiently.

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

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

Огромное количество поддерживаемых ресурсов делает Terraform предпочтительным инструментом для многих инженеров DevOps. Кроме того, использование одного инструмента для управления несколькими поставщиками является большим преимуществом.

3. Привет, Terraform

Прежде чем углубляться в подробности внутренней работы, давайте начнем с основных вещей: начальной настройки и быстрого проекта в стиле «Hello, World».

3.1. Загрузка и установка

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

Выполнив этот шаг, мы можем проверить правильность его работы с помощью простой команды:

$ terraform -v
Terraform v0.12.24

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

$ terraform
Usage: terraform [-version] [-help] <command> [args]
... help content omitted

3.2. Создание нашего первого проекта

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

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

$ cd $HOME
$ mkdir hello-terraform
$ cd hello-terraform
$ cat > main.tf <<EOF
provider "local" {
  version = "~> 1.4"
}
resource "local_file" "hello" {
  content = "Hello, Terraform"
  filename = "hello.txt"
}
EOF

Файл main.tf содержит два блока: объявление провайдера и определение ресурса. В объявлении провайдера указано, что мы будем использовать локальный провайдер версии 1.4 или совместимый.

Далее у нас есть определение ресурса с именем hello типа local_file. Этот тип ресурса, как следует из названия, представляет собой просто файл в локальной файловой системе с заданным содержимым.

3.3. запустите, спланируйте и примените

Теперь давайте продолжим и запустим Terraform в этом проекте. Поскольку мы запускаем этот проект впервые, нам нужно инициализировать его с помощью команды init:

$ terraform init

Initializing the backend...

Initializing provider plugins...
- Checking for available provider plugins...
- Downloading plugin for provider "local" (hashicorp/local) 1.4.0...

Terraform has been successfully initialized!
... more messages omitted

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

Затем мы используем команду plan, чтобы проверить, какие действия будет выполнять Terraform для создания наших ресурсов. Этот шаг работает в значительной степени как функция «пробного запуска», доступная в других системах сборки, таких как инструмент GNU make:

$ terraform plan
... messages omitted
Terraform will perform the following actions:

  # local_file.hello will be created
  + resource "local_file" "hello" {
      + content              = "Hello, Terraform"
      + directory_permission = "0777"
      + file_permission      = "0777"
      + filename             = "hello.txt"
      + id                   = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.
... messages omitted

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

Теперь мы можем перейти к фактическому созданию ресурса с помощью команды apply:

$ terraform apply

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # local_file.hello will be created
  + resource "local_file" "hello" {
      + content              = "Hello, Terraform"
      + directory_permission = "0777"
      + file_permission      = "0777"
      + filename             = "hello.txt"
      + id                   = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

local_file.hello: Creating...
local_file.hello: Creation complete after 0s [id=392b5481eae4ab2178340f62b752297f72695d57]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

«

$ cat hello.txt
Hello, Terraform

«Теперь мы можем убедиться, что файл был создан с указанным содержимым:

$ terraform apply -auto-approve
local_file.hello: Refreshing state... [id=392b5481eae4ab2178340f62b752297f72695d57]

Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

Все хорошо! Теперь давайте посмотрим, что произойдет, если мы повторно запустим команду apply, на этот раз с флагом -auto-approve, чтобы Terraform сразу ушел, не запрашивая подтверждения:

$ echo foo > hello.txt
$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.

local_file.hello: Refreshing state... [id=392b5481eae4ab2178340f62b752297f72695d57]

------------------------------------------------------------------------

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # local_file.hello will be created
  + resource "local_file" "hello" {
      + content              = "Hello, Terraform"
      + directory_permission = "0777"
      + file_permission      = "0777"
      + filename             = "hello.txt"
      + id                   = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.
... more messages omitted

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

Terraform обнаружил изменение в содержимом файла hello.txt и сгенерировал план его восстановления. Поскольку у локального провайдера отсутствует поддержка модификации на месте, мы видим, что план состоит из одного шага — воссоздания файла.

$ terraform apply -auto-approve
... messages omitted
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

$ cat hello.txt
Hello, Terraform

Теперь мы можем снова запустить команду apply, и в результате она восстановит содержимое файла до его предполагаемого содержания:

4. Основные понятия

Теперь, когда мы рассмотрели основы, давайте рассмотрим основные концепции Terraform.

4.1. Провайдеры

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

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

За некоторыми исключениями, использование провайдера требует его настройки с некоторыми параметрами. Они сильно различаются от провайдера к провайдеру, но в целом нам нужно предоставить учетные данные, чтобы он мог получить доступ к своему API и отправлять запросы.

provider "kubernetes" {
  version = "~> 1.10"
}

Хотя это и не является строго необходимым, считается хорошей практикой явно указывать, какой провайдер мы будем использовать в нашем проекте Terraform, и сообщать его версию. Для этой цели мы используем атрибут версии, доступный для любого объявления провайдера:

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

4.2. Ресурсы

В Terraform ресурс — это все, что может быть целью для операций CRUD в контексте данного провайдера. Некоторые примеры — это экземпляр EC2, Azure MariaDB или запись DNS.

resource "aws_instance" "web" {
  ami = "some-ami-id"
  instance_type = "t2.micro"
}

Давайте рассмотрим простое определение ресурса:

Во-первых, у нас всегда есть ключевое слово ресурса, с которого начинается определение. Затем у нас есть тип ресурса, который обычно следует соглашению provider_type. В приведенном выше примере aws_instance — это тип ресурса, определенный поставщиком AWS, используемый для определения экземпляра EC2. После этого идет определяемое пользователем имя ресурса, которое должно быть уникальным для этого типа ресурса в том же модуле — подробнее о модулях позже.

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

resource "aws_instance" "web" {
  ami = "some-ami-id"
  instance_type = "t2.micro"
  subnet_id = aws_subnet.frontend.id
}
resource "aws_subnet" "frontend" {
  vpc_id = aws_vpc.apps.id
  cidr_block = "10.0.1.0/24"
}
resource "aws_vpc" "apps" {
  cidr_block = "10.0.0.0/16"
}

Чтобы проиллюстрировать, как это работает, давайте расширим предыдущий пример, создав наш экземпляр EC2 в нестандартном VPC (виртуальном частном облаке):

Здесь мы используем атрибут id из нашего ресурса VPC в качестве значение для аргумента vpc_id внешнего интерфейса. Затем его параметр id становится аргументом экземпляра EC2. Обратите внимание, что для этого конкретного синтаксиса требуется Terraform версии 0.12 или более поздней. В предыдущих версиях использовался более громоздкий синтаксис «${выражение}», который все еще доступен, но считается устаревшим.

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

4.3. Мета-аргументы count и for_each

Мета-аргументы count и for_each позволяют нам создавать несколько экземпляров любого ресурса. Основное различие между ними заключается в том, что count ожидает неотрицательное число, тогда как for_each принимает список или карту значений.

resource "aws_instance" "server" {
  count = var.server_count 
  ami = "ami-xxxxxxx"
  instance_type = "t2.micro"
  tags = {
    Name = "WebServer - ${count.index}"
  }
}

Например, давайте воспользуемся count для создания нескольких экземпляров EC2 на AWS:

Внутри ресурса, который использует count, мы можем использовать объект count в выражениях. Этот объект имеет только одно свойство: index, который содержит индекс (отсчитываемый от нуля) каждого экземпляра.

variable "instances" {
  type = map(string)
}
resource "aws_instance" "server" {
  for_each = var.instances 
  ami = each.value
  instance_type = "t2.micro"
  tags = {
    Name = each.key
  }
}

Точно так же мы можем использовать мета-аргумент for_each для создания этих экземпляров на основе карты:

На этот раз мы использовали карту из меток в имена AMI (Amazon Machine Image) для создания наших серверов. . Внутри нашего ресурса мы можем использовать объект each, который дает нам доступ к текущему ключу и значению для конкретного экземпляра.

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

4.4. Источники данных

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

data "aws_ami" "ubuntu" {
  most_recent = true
  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-trusty-14.04-amd64-server-*"]
  }
  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
  owners = ["099720109477"] # Canonical
}

Типичным примером является источник данных aws_ami, доступный в провайдере AWS, который мы используем для восстановления атрибутов из существующего AMI:

resource "aws_instance" "web" {
  ami = data.aws_ami.ubuntu.id 
  instance_type = "t2.micro"
}

В этом примере определяется источник данных под названием «ubuntu», который запрашивает реестр AMI. и возвращает несколько атрибутов, связанных с найденным изображением. Затем мы можем использовать эти атрибуты в определениях других ресурсов, добавляя префикс данных к имени атрибута:

4.5. Состояние

Состояние проекта Terraform — это файл, в котором хранятся все сведения о ресурсах, созданных в контексте данного проекта. Например, если мы объявим ресурс azure_resourcegroup в нашем проекте и запустим Terraform, файл состояния сохранит его идентификатор.

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

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

terraform {
  backend "s3" {
    bucket = "some-bucket"
    key = "some-storage-key"
    region = "us-east-1"
  }
}

Terraform использует концепцию серверной части для хранения и извлечения файлов состояния. Серверной частью по умолчанию является локальная серверная часть, которая использует файл в корневой папке проекта в качестве места хранения. Мы также можем настроить альтернативный удаленный сервер, объявив его в блоке terraform в одном из файлов .tf проекта:

4.6. Модули

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

module "networking" {
  source = "./networking"
  create_public_ip = true
}

Модуль — это просто каталог, содержащий один или несколько файлов определения ресурсов. На самом деле, даже когда мы помещаем весь наш код в один файл/каталог, мы все равно используем модули — в данном случае только один. Важным моментом является то, что подкаталоги не являются частью модуля. Вместо этого родительский модуль должен явно включать их с помощью объявления модуля:

Здесь мы ссылаемся на модуль, расположенный в подкаталоге «networking», и передаем ему единственный параметр — логическое значение. в таком случае.

«Важно отметить, что в своей текущей версии Terraform не позволяет использовать count и for_each для создания нескольких экземпляров модуля.

4.7. Входные переменные

variable "myvar" {
  type = string
  default = "Some Value"
  description = "MyVar description"
}

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

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

-var параметр командной строки файлы .tfvar, используя параметры командной строки или сканируя известные файлы/местоположения Среда переменные, начинающиеся с TF_VAR_ Значение переменной по умолчанию, если оно присутствует

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

resource "xxx_type" "some_name" {
  arg = var.myvar
}

После определения мы можем использовать переменные в выражениях с префиксом var:

4.8. Выходные значения

output "web_addr" {
  value = aws_instance.web.private_ip
  description = "Web server's private IP address"
}

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

Здесь мы определяем выходное значение с именем «web_addr», содержащее IP-адрес экземпляра EC2, который наш модуль созданный. Теперь любой модуль, который ссылается на наш модуль, может использовать это значение в выражениях как module.module_name.web_addr, где module_name — это имя, которое мы использовали в соответствующем объявлении модуля.

4.9. Локальные переменные

locals {
  vpc_id = module.network.vpc_id
}
module "network" {
  source = "./network"
}
module "service1" {
  source = "./service1"
  vpc_id = local.vpc_id
}
module "service2" {
  source = "./service2"
  vpc_id = local.vpc_id
}

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

Здесь локальная переменная vpc_id получает значение выходной переменной из сетевого модуля. Позже мы передаем это значение в качестве аргумента модулям service1 и service2.

4.10. Рабочие пространства

Рабочие пространства Terraform позволяют нам хранить несколько файлов состояний для одного и того же проекта. Когда мы запускаем Terraform в первый раз в проекте, сгенерированный файл состояния переходит в рабочую область по умолчанию. Позже мы можем создать новую рабочую область с помощью команды terraform workspace new, опционально указав существующий файл состояния в качестве параметра.

Мы можем использовать рабочие пространства почти так же, как ветки в обычной системе контроля версий. Например, у нас может быть одно рабочее пространство для каждой целевой среды — DEV, QA, PROD — и, переключая рабочие пространства, мы можем терраформировать применение изменений по мере добавления новых ресурсов.

Учитывая то, как это работает, рабочие области — отличный выбор для управления несколькими версиями — или, если хотите, «воплощениями» — одного и того же набора конфигураций. Это отличная новость для всех, кто столкнулся с печально известной проблемой «работает в моей среде», поскольку она позволяет нам гарантировать, что все среды выглядят одинаково.

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

5. Заключение

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