LEARN X · ЗА 16 МИН

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
Поддержать проект