Модуль 04 — Переменные, выходы, locals

Один код — два окружения: отличаются только файлы dev.tfvars и prod.tfvars

Прогресс курса Модуль 4 из 9

Что вы освоите в этом модуле

01

Зачем переменные

В модуле 03 имя контейнера и порт были записаны прямо в коде. Для второго окружения пришлось бы копировать каталог и править значения в десяти местах — и через месяц копии разойдутся. Правильно — один код и разные входные значения: dev и prod отличаются файлами с переменными, а не кодом. Учебный сервис теперь из двух контейнеров — nginx и PostgreSQL.

02

Блок variable

variable "env" {
  type        = string
  description = "Окружение: dev или prod"

  validation {
    condition     = contains(["dev", "prod"], var.env)
    error_message = "env — dev или prod."
  }
}

variable "web_port" {
  type        = number
  description = "Порт сервиса на 127.0.0.1"
  default     = 8080

  validation {
    condition     = var.web_port >= 1024 && var.web_port <= 65535
    error_message = "Порт — от 1024 до 65535: ниже нужны права root."
  }
}

variable "versions" {
  type = object({
    nginx    = string
    postgres = string
  })
  description = "Теги образов"
  default = {
    nginx    = "1.28-alpine"
    postgres = "17"
  }
}

variable "db_password" {
  type        = string
  description = "Пароль пользователя PostgreSQL"
  sensitive   = true
}

Обращение к переменной в коде — var.ИМЯ, к полю объекта — var.versions.nginx. Значение переменной не меняется во время работы: это вход, а не переменная в смысле языков программирования.

03

locals и строковые шаблоны

locals {
  prefix  = "price-${var.env}"
  db_name = "price"
}

resource "docker_image" "nginx" {
  name         = "nginx:${var.versions.nginx}"
  keep_locally = true
}

resource "docker_image" "postgres" {
  name         = "postgres:${var.versions.postgres}"
  keep_locally = true
}

resource "docker_container" "db" {
  name  = "${local.prefix}-db"
  image = docker_image.postgres.image_id
  env = [
    "POSTGRES_DB=${local.db_name}",
    "POSTGRES_PASSWORD=${var.db_password}",
  ]
}

resource "docker_container" "web" {
  name  = "${local.prefix}-web"
  image = docker_image.nginx.image_id

  ports {
    internal = 80
    external = var.web_port
    ip       = "127.0.0.1"
  }
}

locals — именованные выражения внутри проекта: local.prefix вычисляется один раз и используется везде, где нужно имя. Снаружи их не задать — в отличие от переменных. Правило выбора простое: если значение должен уметь менять тот, кто запускает код, — variable; если это внутренняя договорённость («все имена начинаются с price-окружение») — local.

"${…}" внутри строки — подстановка выражения. Если вся строка — одна подстановка, кавычки не нужны: external = var.web_port, а не "${var.web_port}" (иначе число превратится в строку, и terraform fmt об этом не скажет, а линтеры скажут).

04

Обязательные значения и проверки

Запуск без значений. -input=false запрещает Terraform спрашивать их интерактивно — так он ведёт себя в CI:

$ terraform plan -input=false
╷
│ Error: No value for required variable
│ 
│   on variables.tf line 1:
│    1: variable "env" {
│ 
│ The root module input variable "env" is not set, and has no default value.
│ Use a -var or -var-file command line argument to provide a value for this
│ variable.
╵
╷
│ Error: No value for required variable
│ 
│   on variables.tf line 34:
│   34: variable "db_password" {
│ 
│ The root module input variable "db_password" is not set, and has no default
│ value. Use a -var or -var-file command line argument to provide a value for
│ this variable.
╵

Без -input=false Terraform спросил бы значения в терминале. Удобно для первого знакомства, но значения, набранные руками, никто не повторит: их записывают в файл. Неправильное значение останавливает проверка validation:

$ terraform plan -var env=test -var db_password=x 2>&1 | sed -n "/Error/,\$p"
│ Error: Invalid value for variable
│ 
│   on variables.tf line 1:
│    1: variable "env" {
│     ├────────────────
│     │ var.env is "test"
│ 
│ env — dev или prod.
│ 
│ This was checked by the validation rule at variables.tf:5,3-13.
╵
05

Файлы tfvars и TF_VAR_

Значения для окружения — в файле .tfvars того же синтаксиса HCL, только без блоков:

env      = "prod"
web_port = 8090

versions = {
  nginx    = "1.28-alpine"
  postgres = "17"
}

Пароль в файл не пишем: файлы окружений лежат в Git. Секреты передают переменной окружения TF_VAR_ИМЯ — в CI её задают защищённой переменной проекта, на своей машине — из менеджера паролей:

$ cat dev.tfvars
env      = "dev"
web_port = 8080
$ export TF_VAR_db_password="s3cr3t-dev"
$ terraform plan -var-file=dev.tfvars -no-color | grep -E "^  # |[+] (name|env) |external|Plan:|^  [+] (url|containers|db_dsn)"
  # docker_container.db will be created
      + env                                         = (sensitive value)
      + name                                        = "price-dev-db"
  # docker_container.web will be created
      + env                                         = (known after apply)
      + name                                        = "price-dev-web"
          + external = 8080
  # docker_image.nginx will be created
      + name         = "nginx:1.28-alpine"
  # docker_image.postgres will be created
      + name         = "postgres:17"
Plan: 4 to add, 0 to change, 0 to destroy.
  + containers = [
  + db_dsn     = (sensitive value)
  + url        = "http://127.0.0.1:8080/"

Обратите внимание на env = (sensitive value) у контейнера базы: в список вошёл пароль, и весь список стал чувствительным. Выход db_dsn тоже скрыт.

export TF_VAR_db_password=… в интерактивной оболочке попадёт в ~/.bash_history. Начните команду с пробела (при HISTCONTROL=ignorespace, как в Debian по умолчанию) или прочитайте значение без эха: read -s TF_VAR_db_password; export TF_VAR_db_password.
06

Приоритет источников

Если значение задано в нескольких местах, побеждает последнее в этом списке:

№ИсточникКогда использовать
1default в блоке variableРазумное значение для всех
2Переменные окружения TF_VAR_ИМЯСекреты, значения из CI
3terraform.tfvars в каталоге проекта (читается сам)Единственное окружение
4*.auto.tfvars (читаются сами, по алфавиту)Общие для всех окружений значения
5-var-file=… и -var ИМЯ=… — в порядке на командной строкеВыбор окружения; разовая подмена

Порт из dev.tfvars перебит ключом -var, стоящим после него:

$ terraform plan -var-file=dev.tfvars -var web_port=8085 -no-color | grep -E "external|Plan:"
          ~ external = 8080 -> 8085 # forces replacement
Plan: 1 to add, 0 to change, 1 to destroy.

Ключ -var удобен для опыта, но такое значение не попадёт в Git; если оно нужно надолго — его место в файле окружения.

07

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

output "url" {
  description = "Адрес сервиса"
  value       = "http://127.0.0.1:${var.web_port}/"
}

output "containers" {
  value = [docker_container.web.name, docker_container.db.name]
}

output "db_dsn" {
  description = "Строка подключения к базе"
  value       = "postgresql://postgres:${var.db_password}@${docker_container.db.network_data[0].ip_address}/${local.db_name}"
  sensitive   = true
}

Выходы печатаются после apply и сохраняются в состоянии. Их читают люди («по какому адресу сервис»), скрипты и другие проекты Terraform (модули 06 и 08):

$ terraform apply -var-file=dev.tfvars -auto-approve -no-color | sed -n "/Apply complete/,\$p"
Apply complete! Resources: 4 added, 0 changed, 0 destroyed.

Outputs:

containers = [
  "price-dev-web",
  "price-dev-db",
]
db_dsn = <sensitive>
url = "http://127.0.0.1:8080/"

terraform output показывает их в любой момент; -raw — одно значение без кавычек, для подстановки в скрипт; -json — для jq и программ:

$ terraform output -raw url
http://127.0.0.1:8080/
$ terraform output -json containers
["price-dev-web","price-dev-db"]
$ terraform output -raw db_dsn
postgresql://postgres:s3cr3t-dev@172.17.0.2/price
$ grep -c "s3cr3t-dev" terraform.tfstate
2

Пароль в выводе -raw виден открытым текстом: sensitive скрывает значение только в обычном выводе, а получить его можно всегда — иначе им нельзя было бы пользоваться. И главное — в файле состояния:

sensitive — защита от подглядывания в терминал и логи CI, а не шифрование. Все значения, включая пароли, Terraform хранит в terraform.tfstate открытым текстом — последняя команда журнала выше нашла пароль там дважды (в контейнере и в выходе). Поэтому состояние хранят как секрет: модуль 05.
08

Функции и terraform console

В HCL нет своих функций, но есть около сотни встроенных: строки, коллекции, сети, кодирование, файлы. terraform console вычисляет выражения в контексте проекта — с его переменными и locals; удобно проверять выражение до того, как вставить его в код. Здесь выражения подаются через echo, в работе консоль запускают интерактивно:

$ echo "local.prefix" | terraform console -var-file=dev.tfvars
"price-dev"
$ echo "upper(var.env)" | terraform console -var-file=dev.tfvars
"DEV"
$ echo "format(\"%s-%03d\", local.prefix, 7)" | terraform console -var-file=dev.tfvars
"price-dev-007"
$ echo "join(\", \", [\"web\", \"db\"])" | terraform console -var-file=dev.tfvars
"web, db"
$ echo "cidrsubnet(\"10.10.0.0/16\", 8, 2)" | terraform console -var-file=dev.tfvars
"10.10.2.0/24"
$ echo "merge({a = 1, b = 2}, {b = 3})" | terraform console -var-file=dev.tfvars
{
  "a" = 1
  "b" = 3
}
$ echo "var.web_port == 8080 ? \"стандартный\" : \"другой\"" | terraform console -var-file=dev.tfvars
"стандартный"
$ echo "var.db_password" | terraform console -var-file=dev.tfvars
(sensitive value)
ГруппаФункции
Строкиformat, join, split, replace, lower, upper, trimspace
Коллекцииlength, merge, lookup, concat, contains, keys, values, flatten
Сетиcidrsubnet, cidrhost — нарезать подсети без калькулятора
Файлы и кодированиеfile, templatefile, jsonencode, yamlencode, base64encode
Проверкиcan, try, regex — в validation и для необязательных полей

Условное выражение условие ? если_да : если_нет — единственный «if» в HCL; с его помощью в модуле 07 ресурсы будут создаваться только в одном окружении. Чувствительная переменная и в консоли показана как (sensitive value).

Удаляем стенд — значения для destroy нужны те же, что для apply:

$ terraform destroy -var-file=dev.tfvars -auto-approve | grep "Destroy complete"
Destroy complete! Resources: 4 destroyed.

Ключевые выводы модуля

Запомните главное

  • Окружения отличаются файлами .tfvars, а не копиями кода
  • У переменной — type, description, при необходимости validation; без default она обязательна
  • Приоритет: default → TF_VAR_ → terraform.tfvars → *.auto.tfvars → -var-file/-var по порядку
  • Секреты — через TF_VAR_ из CI или менеджера паролей, не в tfvars в Git
  • locals — внутренние договорённости, variable — то, что меняет запускающий
  • output — результат для людей, скриптов (-raw, -json) и других проектов
  • sensitive прячет значение в выводе, но в state оно открытым текстом
  • terraform console — проверить выражение и функцию до того, как писать их в код
Практические задания — на учебном портале. Задания, критерии оценивания, сдача работ и оценки преподавателя — в курсе на portal.nevabit.ru. Учётную запись выдаёт преподаватель.
Модуль 03: plan, apply, destroy Все модули Модуль 05: Состояние