Провайдеры: настройка и версии
Провайдер — это плагин-мост между Terraform и реальной системой (AWS, GitHub, Cloudflare). Его настраивают блоком provider, а версии фиксируют в required_providers и lock-файле.
Версия провайдера так же важна, как версия зависимости в коде. Незакреплённый провайдер однажды обновится сам, изменит поведение ресурса — и ваш безупречный plan вдруг захочет пересоздать прод.
Каждый провайдер нужно объявить (где взять и какой версии) и настроить (регион, аутентификация). Объявление живёт в блоке terraform { required_providers { ... } }, а настройка — в блоке provider "имя" { ... }. При init Terraform скачивает плагин и записывает точную версию в lock-файл.
Объявление и настройка
terraform {
required_version = ">= 1.6"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.40"
}
}
}
provider "aws" {
region = var.region
}
# алиас: два экземпляра одного провайдера (мульти-регион)
provider "aws" {
alias = "eu"
region = "eu-west-1"
}
Аутентификацию (ключи доступа) почти никогда не пишут в коде — её берут из переменных окружения, профилей или ролей. Алиасы позволяют держать несколько настроек одного провайдера: например, ресурсы в двух регионах через provider = aws.eu.
Как работает под капотом
Lock-файл .terraform.lock.hcl фиксирует точную версию и контрольную сумму каждого провайдера, чтобы у всей команды и в CI был одинаковый плагин. Terraform проверяет: подходит ли залоченная версия под ограничение в коде. Смоделируем это:
constraint = (5, 40) # ~> 5.40 => >=5.40, <6.0
locked = "5.42.0"
available = ["5.40.1", "5.42.0", "5.99.0", "6.0.0"]
def parse(v): return tuple(int(x) for x in v.split("."))
def satisfies(v, base):
p = parse(v)
return p >= base and p[0] < base[0] + 1
print("Залочена:", locked, "-> подходит?", satisfies(locked, constraint))
print("Совместимые версии:",
[v for v in available if satisfies(v, constraint)])
print("6.0.0 запрещён мажором:", not satisfies("6.0.0", constraint))
«Попробуй сам ▶» — пока залоченная версия укладывается в ограничение, все на одной странице. init -upgrade поднимет lock до свежей совместимой.
Частые ошибки
- Ключи доступа в коде. Хардкод credentials в
provider— утечка в git. Используйте окружение/роли. - Не коммитить lock-файл. Без
.terraform.lock.hclв git у всех могут оказаться разные версии провайдера. - Незакреплённая версия провайдера. Без
versionTerraform возьмёт свежайшую — и поведение «уедет».
Best practices
- Всегда указывайте
versionдля провайдеров и коммитьте lock-файл в git. - Аутентификация — через переменные окружения, профили или IAM-роли, не через код.
- Фиксируйте и
required_versionядра, чтобы команда не разошлась по версиям Terraform.
Разбор глубже
Стоит разобраться, как провайдер вообще находит учётные данные. У большинства провайдеров есть цепочка источников аутентификации, которую он проверяет по порядку: явные аргументы в блоке provider (так делать не надо), затем переменные окружения (AWS_ACCESS_KEY_ID и подобные), затем общие конфиг-файлы (~/.aws/credentials), и наконец — роли среды выполнения (IAM-роль инстанса или OIDC-федерация в CI). Идеал — самый последний вариант: пайплайн получает временные права через роль, и в системе нет ни одного долгоживущего ключа, который можно украсть. Поэтому хардкод ключей в коде — это не просто плохой стиль, а прямая дыра в безопасности.
Версии провайдеров пинят по тем же причинам, что и модули, но с дополнительным нюансом: провайдер напрямую общается с API облака, и его новая мажорная версия может изменить поведение ресурсов или вовсе переименовать аргументы. Поэтому ограничение вида ~> 5.40 в продакшене — норма, а не перестраховка. Lock-файл .terraform.lock.hcl при этом фиксирует не просто номер версии, а ещё и контрольные суммы для каждой платформы (Linux, macOS, разные архитектуры), чтобы у разработчика на ноутбуке и в CI-контейнере оказался байт-в-байт один и тот же плагин. Обновляют lock осознанно командой terraform init -upgrade, после чего изменения в нём проходят через ревью.
Стоит также знать про механизм наследования провайдеров в модули. По умолчанию дочерний модуль получает конфигурацию провайдера от родителя автоматически, и явно прокидывать её не нужно. Но если модуль работает с несколькими экземплярами провайдера (например, два региона через алиасы), их передают явно через блок providers = { aws = aws, aws.replica = aws.eu } в вызове модуля. Это разделение «по умолчанию наследуется, при необходимости передаётся явно» держит простые модули простыми, а сложные мульти-региональные сценарии — управляемыми, не размывая при этом границу инкапсуляции между родителем и дочерним модулем.
Итог: провайдеры объявляют в required_providers, настраивают в provider, версии пинят через version и lock-файл. Секреты — мимо кода. Дальше — как организовать проект под несколько окружений.