Terraform
Terraform за 16 минут: IaC, провайдеры, ресурсы, переменные, output, data, locals, count/for_each, модули и команды на примерах HCL.
Terraform — инструмент Infrastructure as Code (IaC) от HashiCorp. Вы описываете нужную инфраструктуру на языке HCL, а Terraform сам приводит реальный мир к этому описанию. Этот тур — почти весь Terraform в комментариях: читайте код сверху вниз.
Что такое IaC и Terraform
Декларативность: вы пишете что должно быть, а не как это создать.
# Это однострочный комментарий (# или //).
/* А это
многострочный комментарий. */
# Идея Terraform:
# 1. Вы описываете желаемое состояние (ресурсы) в файлах .tf.
# 2. providers — плагины, которые умеют общаться с API (AWS, GCP, Docker...).
# 3. state (terraform.tfstate) — слепок того, что Terraform уже создал.
# 4. terraform сравнивает: желаемое состояние ⇄ state ⇄ реальный мир
# и строит план изменений (создать / изменить / удалить).
#
# Terraform ДЕКЛАРАТИВЕН: порядок ресурсов в файле не важен —
# зависимости вычисляются автоматически по ссылкам.
Провайдеры и terraform-блок
Провайдер — это плагин для конкретной платформы. Версии фиксируются в блоке terraform.
# Блок terraform: настройки самого Terraform и требуемые провайдеры.
terraform {
required_version = ">= 1.5.0" # минимальная версия CLI
required_providers {
aws = {
source = "hashicorp/aws" # откуда качать плагин (registry)
version = "~> 5.0" # ~> 5.0 == любая 5.x, но не 6.0
}
docker = {
source = "kreuzwerker/docker"
version = "~> 3.0"
}
}
}
# Конфигурация конкретного провайдера.
provider "aws" {
region = "eu-central-1" # регион AWS (Франкфурт)
}
# Можно завести несколько копий провайдера через alias.
provider "aws" {
alias = "us"
region = "us-east-1"
}
Ресурсы
Ресурс — главный объект Terraform: кусок инфраструктуры, которым он управляет.
# resource "<ТИП>" "<ИМЯ>" { ... }
# ТИП — задаётся провайдером (aws_instance, docker_container...).
# ИМЯ — локальное имя для ссылок внутри конфигурации.
resource "aws_instance" "web" {
# Аргументы — то, что вы ЗАДАЁТЕ (вход):
ami = "ami-0abcd1234efgh5678"
instance_type = "t3.micro"
tags = {
Name = "web-server"
Env = "prod"
}
}
# Атрибуты — то, что Terraform ВЫЧИСЛЯЕТ после создания (выход):
# aws_instance.web.id
# aws_instance.web.public_ip
# aws_instance.web.private_ip
# Ссылка на ресурс: <ТИП>.<ИМЯ>.<АТРИБУТ>
Переменные (input variables)
Параметризуют конфигурацию: значения приходят снаружи.
# Объявление переменной.
variable "instance_type" {
description = "Тип EC2-инстанса"
type = string
default = "t3.micro" # без default — переменная обязательна
}
variable "instance_count" {
type = number
default = 2
}
variable "enable_monitoring" {
type = bool
default = false
}
# Использование: var.<ИМЯ>
resource "aws_instance" "app" {
instance_type = var.instance_type
monitoring = var.enable_monitoring
}
# Передать значение можно так:
# terraform apply -var="instance_type=t3.large"
# в файле terraform.tfvars: instance_type = "t3.large"
# через переменную окружения: export TF_VAR_instance_type=t3.large
Выходные значения (output)
Output показывает важные данные после apply и отдаёт их родительским модулям.
# Выводит значение в консоль после terraform apply.
output "instance_ip" {
description = "Публичный IP сервера"
value = aws_instance.web.public_ip
}
output "instance_id" {
value = aws_instance.web.id
}
# Чувствительные данные можно скрыть из вывода:
output "db_password" {
value = aws_db_instance.main.password
sensitive = true # покажет (sensitive value) вместо текста
}
# Посмотреть значения: terraform output
# terraform output instance_ip
Типы и выражения
HCL поддерживает скаляры и коллекции; внутри строк работает интерполяция.
# Скалярные типы:
local_string = "привет"
local_number = 42
local_bool = true
# Коллекции:
local_list = ["a", "b", "c"] # список (порядок важен)
local_map = { env = "prod", tier = "web" } # ассоциативный массив
# Доступ к элементам:
# local_list[0] -> "a"
# local_map["env"] -> "prod"
# Интерполяция ${...} — подстановка выражений в строку:
name = "server-${var.instance_type}-${local_number}"
# Внутри ${ } можно вызывать функции:
upper_name = "${upper(var.instance_type)}"
# Если строка == одно выражение, ${ } не нужны: name = var.instance_type
Data sources
Data source читает данные о существующих объектах, не создавая их.
# data "<ТИП>" "<ИМЯ>" { ... } — запрос к провайдеру (только чтение).
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"] # Canonical
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
}
# Ссылка на data: data.<ТИП>.<ИМЯ>.<АТРИБУТ>
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id # подставится найденный AMI
instance_type = "t3.micro"
}
Локальные значения (locals)
Locals — именованные выражения, чтобы не повторять код (DRY).
# Блок locals: вычисляемые значения, видимые во всём модуле.
locals {
project = "shop"
env = "prod"
# Можно строить новые значения из переменных и других locals:
name_prefix = "${local.project}-${local.env}"
common_tags = {
Project = local.project
ManagedBy = "terraform"
}
}
# Использование: local.<ИМЯ> (единственное число!)
resource "aws_instance" "web" {
tags = merge(local.common_tags, { Name = "${local.name_prefix}-web" })
}
Зависимости
Обычно зависимости неявные — через ссылки. Изредка нужны явные.
# Неявная зависимость: ссылка на атрибут => Terraform сам поймёт порядок.
resource "aws_security_group" "web" {
name = "web-sg"
}
resource "aws_instance" "web" {
ami = "ami-123"
instance_type = "t3.micro"
# ссылка ниже => instance создастся ПОСЛЕ security_group:
vpc_security_group_ids = [aws_security_group.web.id]
}
# Явная зависимость depends_on — когда связи по данным нет,
# но порядок всё равно важен (например, права/политики).
resource "aws_instance" "app" {
ami = "ami-123"
instance_type = "t3.micro"
depends_on = [aws_security_group.web]
}
Циклы и условия
Множественные ресурсы создаются через count или for_each.
# count — создать N одинаковых ресурсов (доступен count.index).
resource "aws_instance" "web" {
count = 3
ami = "ami-123"
instance_type = "t3.micro"
tags = { Name = "web-${count.index}" } # web-0, web-1, web-2
}
# Ссылка: aws_instance.web[0].id или aws_instance.web[*].id (все)
# for_each — по множеству/карте (доступны each.key и each.value).
resource "aws_instance" "srv" {
for_each = { api = "t3.small", db = "t3.medium" }
ami = "ami-123"
instance_type = each.value
tags = { Name = each.key } # srv["api"], srv["db"]
}
# Тернарный оператор: условие ? если_да : если_нет
instance_type = var.is_prod ? "t3.large" : "t3.micro"
# for-выражение — трансформация коллекций:
upper_names = [for n in var.names : upper(n)] # список
name_map = { for n in var.names : n => length(n) } # карта
Модули
Модуль — переиспользуемый набор ресурсов. Любая папка с .tf — это модуль.
# Вызов модуля: source указывает, ОТКУДА брать код.
module "network" {
source = "./modules/network" # локальная папка
# Входы модуля = его variable-блоки:
vpc_cidr = "10.0.0.0/16"
env = "prod"
}
# Модуль из публичного реестра:
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0"
name = "main-vpc"
}
# Использование выходов модуля: module.<ИМЯ>.<OUTPUT>
resource "aws_instance" "web" {
subnet_id = module.network.public_subnet_id
}
# После добавления модуля нужно: terraform init (скачать его).
State и команды
State — это память Terraform. Вокруг него крутится весь рабочий цикл.
# Рабочий цикл (в терминале, не HCL):
#
# terraform init — инициализация: скачать провайдеры и модули.
# terraform fmt — отформатировать .tf-файлы.
# terraform validate — проверить синтаксис и корректность.
# terraform plan — показать план изменений (ничего не меняет).
# terraform apply — применить изменения (создать/изменить/удалить).
# terraform destroy — удалить всё, что описано в конфигурации.
#
# State-файл terraform.tfstate хранит соответствие
# "ресурс в коде" ⇄ "объект в облаке".
#
# Команды для state:
# terraform state list — список ресурсов в state.
# terraform state show aws_... — детали ресурса.
# terraform import aws_... <id> — взять существующий объект под управление.
#
# На команде: НЕ редактируйте tfstate руками и храните его в
# remote backend (S3 + блокировка), а не в git.
Удалённый backend
Backend задаёт, где хранится state. Для команды — общий и заблокированный.
# Блок backend внутри terraform { }.
terraform {
backend "s3" {
bucket = "my-tf-state"
key = "prod/terraform.tfstate" # путь к файлу в бакете
region = "eu-central-1"
dynamodb_table = "tf-locks" # блокировка от параллельных apply
encrypt = true
}
}
# Сменили backend -> снова terraform init (он предложит перенести state).
Полезные функции
Встроенные функции вызываются как имя(аргументы); своих функций в HCL нет.
# Строки и числа:
upper("abc") # "ABC"
length(["a", "b"]) # 2
join("-", ["a", "b"]) # "a-b"
format("web-%d", 3) # "web-3"
# Коллекции:
merge({ a = 1 }, { b = 2 }) # { a = 1, b = 2 }
lookup({ a = 1 }, "a", 0) # 1 (значение по умолчанию 0)
concat([1, 2], [3]) # [1, 2, 3]
contains(["a", "b"], "a") # true
# Шаблоны из файла: templatefile подставляет переменные в файл.
# Файл init.sh.tpl: echo "port=${port}"
user_data = templatefile("${path.module}/init.sh.tpl", {
port = 8080
})
# Прочитать файл целиком: file("${path.module}/key.pub")
Типичная конфигурация целиком
Соберём изученное в один маленький, но полноценный проект.
# versions.tf — версии
terraform {
required_version = ">= 1.5"
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.0" }
}
}
provider "aws" {
region = var.region
}
# variables.tf — входы
variable "region" {
type = string
default = "eu-central-1"
}
variable "instance_count" {
type = number
default = 2
}
# main.tf — ресурсы
locals {
tags = { Project = "demo", ManagedBy = "terraform" }
}
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"]
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
}
resource "aws_instance" "web" {
count = var.instance_count
ami = data.aws_ami.ubuntu.id
instance_type = "t3.micro"
tags = merge(local.tags, { Name = "web-${count.index}" })
}
# outputs.tf — выходы
output "web_ips" {
value = aws_instance.web[*].public_ip # список всех IP
}
# Запуск: terraform init && terraform plan && terraform apply