Вот ровно про это и урок. Разберём, как Terraform работает с несколькими экземплярами провайдера, что такое алиасы, зачем модулю нужен configuration_aliases и почему наивный подход "просто скопирую провайдер внутрь модуля" приведёт тебя в ад при первом же terraform destroy. Это глава из серии про инфраструктуру как код, и тема скорее продвинутая, но без неё мульти-региональные и мульти-аккаунтные сетапы не собрать.
Алиасы провайдера: две копии одного и того же
Базовая механика простая. Один и тот же провайдер можно объявить несколько раз, и чтобы Terraform их различал, второму и последующим экземплярам дают alias. Первый блок без алиаса считается провайдером по умолчанию (default), его получают все ресурсы, которые явно не попросили другой.
Код: Выделить всё
provider "yandex" {
# default-конфигурация, зона A
zone = "ru-central1-a"
folder_id = var.folder_id
}
provider "yandex" {
alias = "zone_b"
zone = "ru-central1-b"
folder_id = var.folder_id
}
Код: Выделить всё
resource "yandex_compute_disk" "data_a" {
# без provider - значит default, зона A
name = "data-a"
zone = "ru-central1-a"
size = 20
}
resource "yandex_compute_disk" "data_b" {
provider = yandex.zone_b
name = "data-b"
zone = "ru-central1-b"
size = 20
}
Алиасы для аккаунтов и облаков работают точно так же, меняются только аргументы. Из доки провайдера Yandex Cloud видно, что у второй копии можно задать совсем другой сервисный аккаунт и каталог:
Код: Выделить всё
provider "yandex" {
alias = "client_two"
service_account_key_file = "key-client-two.json"
cloud_id = var.cloud_id_two
folder_id = var.folder_id_two
zone = "ru-central1-d"
}

configuration_aliases: как протащить алиас в модуль
Внутри корневого конфига всё просто. Боль начинается с модулями. По умолчанию модуль наследует только default-провайдеров родителя. Если модуль хочет работать с ДВУМЯ копиями yandex одновременно, он должен явно заявить: "мне нужны две конфигурации этого провайдера". Делается это через configuration_aliases в блоке required_providers (фича стабильна начиная с Terraform 0.15, в 2026 это давно норма и в Terraform, и в OpenTofu).
Вот объявление внутри модуля, файл modules/two-zones/versions.tf:
Код: Выделить всё
terraform {
required_providers {
yandex = {
source = "yandex-cloud/yandex"
version = ">= 0.100"
configuration_aliases = [yandex.primary, yandex.secondary]
}
}
}
Внутри модуля ресурсы ссылаются на эти алиасы как на локальные имена:
Код: Выделить всё
resource "yandex_vpc_network" "net" {
provider = yandex.primary
name = "${var.prefix}-net"
}
resource "yandex_vpc_subnet" "primary" {
provider = yandex.primary
name = "${var.prefix}-subnet-primary"
zone = var.zone_primary
network_id = yandex_vpc_network.net.id
v4_cidr_blocks = ["10.10.1.0/24"]
}
resource "yandex_vpc_subnet" "secondary" {
provider = yandex.secondary
name = "${var.prefix}-subnet-secondary"
zone = var.zone_secondary
network_id = yandex_vpc_network.net.id
v4_cidr_blocks = ["10.10.2.0/24"]
}
Код: Выделить всё
module "two_zones" {
source = "./modules/two-zones"
prefix = "app"
zone_primary = "ru-central1-a"
zone_secondary = "ru-central1-b"
providers = {
yandex.primary = yandex
yandex.secondary = yandex.zone_b
}
}
Полный рабочий пример: ВМ в двух зонах
Соберём всё вместе. Модуль разворачивает по виртуалке в каждой зоне поверх общей сети. Файл modules/two-zones/main.tf (versions.tf с configuration_aliases у нас уже выше):
Код: Выделить всё
data "yandex_compute_image" "ubuntu" {
provider = yandex.primary
family = "ubuntu-2404-lts"
}
resource "yandex_compute_instance" "primary" {
provider = yandex.primary
name = "${var.prefix}-vm-a"
platform_id = "standard-v3"
zone = var.zone_primary
resources {
cores = 2
memory = 2
}
boot_disk {
initialize_params {
image_id = data.yandex_compute_image.ubuntu.id
size = 20
}
}
network_interface {
subnet_id = yandex_vpc_subnet.primary.id
nat = true
}
}
resource "yandex_compute_instance" "secondary" {
provider = yandex.secondary
name = "${var.prefix}-vm-b"
platform_id = "standard-v3"
zone = var.zone_secondary
resources {
cores = 2
memory = 2
}
boot_disk {
initialize_params {
image_id = data.yandex_compute_image.ubuntu.id
size = 20
}
}
network_interface {
subnet_id = yandex_vpc_subnet.secondary.id
nat = true
}
}
Типичные грабли
- Кавычки вокруг алиаса. provider = "yandex.zone_b" - неправильно. Это ссылка, кавычек быть не должно.
- Конфигурация провайдера внутри переиспользуемого модуля. Технически можно положить блок provider {} в модуль, но Terraform будет тебя ругать предупреждениями, а главное - такой модуль нельзя нормально удалить через terraform destroy, если провайдер настроен через выражения, которые при удалении уже недоступны. Правило: модули конфигурируют ресурсы, а провайдеры настраивает корень.
- Забыл блок providers при вызове. Если модуль объявил configuration_aliases, но ты не передал providers = { ... }, Terraform упадёт ещё на init с жалобой на необъявленный провайдер. Алиасы (не default) НЕ наследуются автоматически.
- Перепутал стороны в providers. Слева - имя внутри модуля, справа - провайдер родителя. yandex.secondary = yandex.zone_b, а не наоборот.
- Один общий стейт там, где нужны разные. Главная стратегическая ошибка. Несколько провайдеров - это всё ещё ОДИН terraform state. Об этом ниже.
Несколько копий провайдера в одном конфиге - мощно, но не бесплатно. Всё это живёт в едином terraform state и катится одним apply. Это плюс, когда ресурсы действительно связаны: сеть в зоне A и сеть в зоне B, между которыми ходит трафик; реплики одной базы; балансировщик и его бэкенды в разных зонах. Тут атомарность и общий граф зависимостей - именно то, что нужно.
Но если ты пихаешь в один стейт прод и дев только потому, что "так меньше папок", - ты роешь себе яму. Любая ошибка в дев-части блокирует apply прода. Радиус поражения растёт, а terraform plan становится простынёй на сто ресурсов. Для слабо связанных окружений правильнее РАЗДЕЛЬНЫЕ стейты (разные backend-ключи или воркспейсы) и, по возможности, отдельные прогоны. Грубое правило: один state - один радиус отказа. Если падение одной части не должно ронять другую, разноси по стейтам, а не по алиасам.
Мини-лаба
- Объяви в корне два провайдера yandex: default в зоне ru-central1-a и алиас zone_b в ru-central1-b.
- Создай в корне два yandex_compute_disk - один в default, другой через provider = yandex.zone_b. Сделай terraform plan и убедись, что диски уезжают в разные зоны.
- Вынеси логику в модуль с configuration_aliases = [yandex.primary, yandex.secondary]. Пробрось providers при вызове.
- Запусти terraform validate, затем plan. Намеренно убери блок providers и посмотри текст ошибки - запомни его, ты его ещё встретишь.
- Чем default-провайдер отличается от провайдера с alias и какой из них наследуется модулем автоматически?
- Зачем модулю нужен configuration_aliases, если внутри он всё равно не настраивает провайдер?
- Как в блоке providers = { ... } соотносятся левая и правая части? Что есть что?
- По какому признаку решать: тащить два окружения в один стейт через алиасы или разнести по отдельным стейтам?
Один провайдер можно объявить много раз через alias, а ресурс выбирает копию мета-аргументом provider (без кавычек). Чтобы модуль работал с несколькими копиями, он декларирует их через configuration_aliases в required_providers, сам провайдеры не настраивает, а получает их извне через providers = { ... } при вызове. Слева в этом блоке - имена модуля, справа - реальные провайдеры корня. И помни: несколько алиасов - это по-прежнему один terraform state, поэтому слабо связанные окружения лучше разносить по разным стейтам, а не сваливать в кучу ради экономии папок. Всё то же самое работает и в OpenTofu - синтаксис идентичен.