1. Обзор
Инфраструктура как код (IaC) — это практика, которая стала популярной благодаря растущей популярности поставщиков общедоступных облачных сервисов, таких как AWS, Google и Microsoft. В двух словах, он состоит в управлении набором ресурсов (вычислений, сети, хранилища и т. д.) с использованием того же подхода, который разработчики используют для управления кодом приложения .
В этом руководстве мы кратко рассмотрим Terraform, один из самых популярных инструментов, используемых командами DevOps для автоматизации задач инфраструктуры. Основная привлекательность Terraform заключается в том, что мы просто объявляем , как должна выглядеть
наша инфраструктура, а инструмент решает, какие действия необходимо предпринять для «материализации» этой инфраструктуры.
2. Краткая история
Согласно GitHub, дата первой фиксации Terraform была 21 мая 2014 года. Автором был Митчелл Хашимото, один из основателей Hashicorp, и он содержит только файл README , в котором описывается то, что мы можем назвать «заявлением о миссии»:
Terraform — это инструмент для безопасного и эффективного построения и изменения инфраструктуры.
Эта фраза довольно хорошо описывает его намерения. С тех пор инструмент неуклонно расширял свои возможности с точки зрения поддерживаемых им поставщиков инфраструктуры.
На момент написания этой статьи Terraform официально поддерживает около 130 провайдеров . На странице поставщиков, поддерживаемых сообществом, перечислены еще 160. Некоторые из этих поставщиков предоставляют всего несколько ресурсов, но другие, такие как AWS или Azure, имеют их сотни.
Огромное количество поддерживаемых ресурсов делает Terraform предпочтительным инструментом для многих инженеров DevOps. Кроме того, использование одного инструмента для управления несколькими поставщиками является большим преимуществом.
3. Привет, Терраформ
Прежде чем вдаваться в подробности его внутренней работы, давайте начнем с основных вещей: начальной настройки и быстрого проекта в стиле «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
Все хорошо! Теперь давайте посмотрим, что произойдет, если мы повторно запустим команду apply
, на этот раз с флагом -auto-approve,
чтобы Terraform работал сразу же, не запрашивая подтверждения:
$ terraform apply -auto-approve
local_file.hello: Refreshing state... [id=392b5481eae4ab2178340f62b752297f72695d57]
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
На этот раз Terraform ничего не делал, потому что файл уже существовал. Но это еще не все. Иногда ресурс существует, но кто-то может изменить один из его атрибутов, что обычно называют «дрейфом конфигурации» .
Посмотрим, как поведет себя 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 обнаружил изменение в содержимом файла hello.txt
и разработал план его восстановления. Поскольку у локального
провайдера отсутствует поддержка модификации на месте, мы видим, что план состоит из одного шага — пересоздания файла.
Теперь мы можем снова запустить apply
, и в результате он восстановит содержимое файла до его предполагаемого содержания:
$ terraform apply -auto-approve
... messages omitted
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
$ cat hello.txt
Hello, Terraform
4. Основные понятия
Теперь, когда мы рассмотрели основы, давайте перейдем к основным концепциям Terraform.
4.1. Провайдеры
Провайдер работает в значительной степени как
драйвер
устройства операционной системы. Он предоставляет набор типов ресурсов
, используя общую абстракцию, тем самым скрывая детали того, как создавать, изменять и уничтожать ресурс, в значительной степени прозрачные для пользователей .
Terraform автоматически загружает провайдеров из общедоступного реестра по мере необходимости на основе ресурсов данного проекта. Он также может использовать пользовательские плагины, которые должны быть установлены пользователем вручную. Наконец, некоторые встроенные провайдеры являются частью основного бинарного файла и всегда доступны.
За некоторыми исключениями, использование провайдера требует его настройки с некоторыми параметрами . Они сильно различаются от провайдера к провайдеру, но в целом нам нужно предоставить учетные данные, чтобы он мог получить доступ к своему API и отправлять запросы.
Хотя это и не является строго необходимым, считается хорошей практикой явно указывать, какой провайдер мы будем использовать в нашем проекте Terraform, и сообщать его версию. Для этого мы используем атрибут версии , доступный для любого объявления
провайдера
:
provider "kubernetes" {
version = "~> 1.10"
}
Здесь, поскольку мы не предоставляем никаких дополнительных параметров, 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. После этого идет определяемое пользователем имя ресурса, которое должно быть уникальным для этого типа ресурса в том же модуле — подробнее о модулях позже.
Наконец, у нас есть блок, содержащий ряд аргументов, используемых в качестве спецификации ресурса. Ключевым моментом в отношении ресурсов является то, что после их создания мы можем использовать выражения для запроса их атрибутов. Кроме того, что не менее важно, мы можем использовать эти атрибуты в качестве аргументов для других ресурсов .
Чтобы проиллюстрировать, как это работает, давайте расширим предыдущий пример, создав наш экземпляр EC2 в нестандартном VPC (виртуальном частном облаке):
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"
}
Здесь мы используем атрибут id
из нашего ресурса VPC в качестве значения для аргумента vpc_id внешнего интерфейса.
Затем его параметр id
становится аргументом экземпляра EC2. Обратите внимание, что для этого конкретного синтаксиса требуется Terraform версии 0.12 или выше . В предыдущих версиях использовался более громоздкий синтаксис «${expression}»
, который все еще доступен, но считается устаревшим.
Этот пример также показывает одну из сильных сторон Terraform: независимо от порядка, в котором мы объявляем ресурсы в нашем проекте, он определит правильный порядок, в котором он должен создавать или обновлять их, на основе графа зависимостей, который он строит при их разборе.
4.3. мета -аргументы count
и for_each
Мета - аргументы count
и for_each
позволяют нам создавать несколько экземпляров любого ресурса. Основное различие между ними заключается в том, что count
ожидает неотрицательное число, тогда как for_each
принимает список или карту значений.
Например, давайте воспользуемся count
для создания нескольких экземпляров EC2 на AWS:
resource "aws_instance" "server" {
count = var.server_count
ami = "ami-xxxxxxx"
instance_type = "t2.micro"
tags = {
Name = "WebServer - ${count.index}"
}
}
Внутри ресурса, использующего count
, мы можем использовать объект count
в выражениях . Этот объект имеет только одно свойство: index
, которое содержит индекс (отсчитываемый от нуля) каждого экземпляра.
Точно так же мы можем использовать мета- аргумент for_each
для создания этих экземпляров на основе карты:
variable "instances" {
type = map(string)
}
resource "aws_instance" "server" {
for_each = var.instances
ami = each.value
instance_type = "t2.micro"
tags = {
Name = each.key
}
}
На этот раз мы использовали сопоставление меток с именами AMI (Amazon Machine Image) для создания наших серверов. Внутри нашего ресурса мы можем использовать объект each
, который дает нам доступ к текущему ключу
и значению
для конкретного экземпляра.
Ключевым моментом в отношении count
и for_each
является то, что, хотя мы можем назначать им выражения, Terraform должен иметь возможность разрешать их значения перед выполнением каких-либо действий с ресурсами . В результате мы не можем использовать выражение, которое зависит от атрибутов вывода из других ресурсов.
4.4. Источники данных
Источники данных в значительной степени работают как ресурсы «только для чтения» в том смысле, что мы можем получать информацию о существующих, но не можем их создавать или изменять. Обычно они используются для получения параметров, необходимых для создания других ресурсов.
Типичным примером является источник данных aws_ami
, доступный в провайдере AWS, который мы используем для восстановления атрибутов из существующего AMI:
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
}
В этом примере определяется источник данных
под названием «ubuntu», который запрашивает реестр AMI и возвращает несколько атрибутов, связанных с найденным образом. Затем мы можем использовать эти атрибуты в других определениях ресурсов, добавляя префикс данных
к имени атрибута:
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = "t2.micro"
}
4.5. Состояние
Состояние проекта Terraform — это файл, в котором хранятся все сведения о ресурсах, созданных в контексте данного проекта . Например, если мы объявим ресурс azure_resourcegroup
в нашем проекте и запустим Terraform, файл состояния сохранит его идентификатор.
Основная цель файла состояния — предоставить информацию об уже существующих ресурсах, поэтому, когда мы изменяем определения наших ресурсов, Terraform может понять, что ему нужно делать.
Важным моментом в файлах состояния является то, что они могут содержать конфиденциальную информацию . Примеры включают начальные пароли, используемые для создания базы данных, закрытые ключи и т. д.
Terraform использует концепцию серверной
части для хранения и извлечения файлов состояния. Серверной частью по умолчанию является локальная
серверная часть, которая использует файл в корневой папке проекта в качестве места хранения. Мы также можем настроить альтернативный удаленный
сервер, объявив его в блоке terraform
в одном из файлов .tf
проекта :
terraform {
backend "s3" {
bucket = "some-bucket"
key = "some-storage-key"
region = "us-east-1"
}
}
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 выдаст ошибку, если мы попытаемся использовать модуль, которому требуется значение для входной переменной, но мы не сможем его предоставить.
После определения мы можем использовать переменные в выражениях, используя префикс var :
resource "xxx_type" "some_name" {
arg = var.myvar
}
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 — очень мощный инструмент, который помогает нам внедрять в наши проекты практику «инфраструктура как код». Эта сила, однако, сопряжена со своими проблемами. В этой статье мы представили краткий обзор этого инструмента, чтобы лучше понять его возможности и основные концепции.
Как обычно, весь код доступен на GitHub .