Один код — два окружения: отличаются только файлы dev.tfvars и prod.tfvars
locals и пользоваться функциямиsensitiveВ модуле 03 имя контейнера и порт были записаны прямо в коде. Для второго окружения пришлось бы копировать каталог и править значения в десяти местах — и через месяц копии разойдутся. Правильно — один код и разные входные значения: dev и prod отличаются файлами с переменными, а не кодом. Учебный сервис теперь из двух контейнеров — nginx и PostgreSQL.
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
}
type — тип значения: string, number, bool, коллекции list(string), set(string), map(number) и структуры object({…}). Terraform проверит тип до всякого плана и при необходимости преобразует: "8080" в number подойдёт, "восемь" — нет.description — для людей и для документации модуля. Пишут всегда.default — значение по умолчанию. Переменная без него обязательна.validation — условие на значение и понятное сообщение. Проверка срабатывает до обращения к API.sensitive = true — не показывать значение в планах и выводе (ниже — чего это не делает).Обращение к переменной в коде — var.ИМЯ, к полю объекта — var.versions.nginx. Значение переменной не меняется во время работы: это вход, а не переменная в смысле языков программирования.
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 об этом не скажет, а линтеры скажут).
Запуск без значений. -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.
╵
Значения для окружения — в файле .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.Если значение задано в нескольких местах, побеждает последнее в этом списке:
| № | Источник | Когда использовать |
|---|---|---|
| 1 | default в блоке variable | Разумное значение для всех |
| 2 | Переменные окружения TF_VAR_ИМЯ | Секреты, значения из CI |
| 3 | terraform.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; если оно нужно надолго — его место в файле окружения.
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.В 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 она обязательнаTF_VAR_ → terraform.tfvars → *.auto.tfvars → -var-file/-var по порядкуTF_VAR_ из CI или менеджера паролей, не в tfvars в Gitlocals — внутренние договорённости, variable — то, что меняет запускающийoutput — результат для людей, скриптов (-raw, -json) и других проектовsensitive прячет значение в выводе, но в state оно открытым текстомterraform console — проверить выражение и функцию до того, как писать их в код