OpenTofu 1.7: нативное шифрование state и provider-defined functions
OpenTofu 1.7 выходит с шифрованием state прямо в backend-конфиге и provider-defined functions. Переходим со стороннего wrapper-а на нативное решение без остановки пайплайна.
OpenTofu 1.7 вышел с нативным шифрованием state-файлов и поддержкой provider-defined functions - двумя функциями, которых никогда не было в Terraform
OpenTofu 1.7 вышел в мае, и мы несколько недель держались на 1.6, наблюдая как другие ходят по граблям первых дней после релиза. Грабли оказались мелкими, и в июле мы наконец занялись тем, ради чего собственно обновлялись: убрали сторонний скрипт-обёртку вокруг шифрования state и перешли на нативный механизм.
Почему вообще был wrapper
Когда в конце 2023-го мы переводили первые проекты с Terraform на OpenTofu, вопрос шифрования state-файлов стоял остро. State хранится в S3, и хотя бакет с серверным шифрованием - это базовая гигиена, нас беспокоило другое: кто угодно с доступом к бакету читает state в открытом виде. Для инфраструктуры с секретами в outputs это неприятно.
Костыльное решение, которое тогда поставили: небольшой Python-скрипт между CI и tofu apply. Скрипт скачивал зашифрованный state из S3 (age-шифрование), расшифровывал во временный файл, передавал в tofu через -state, после применения шифровал обратно и загружал. Работало. Но было хрупко: скрипт нужно было поддерживать, ключи передавались через переменные окружения по своей логике, ротация ключа означала отдельную ручную процедуру. В общем - типичный interim, который живёт дольше, чем планировалось.
OpenTofu 1.7 закрыл этот вопрос нативно.
Как устроено шифрование в 1.7
Конфигурация полностью в HCL, живёт в блоке terraform вместе с backend:
terraform {
backend "s3" {
bucket = "my-infra-state"
key = "prod/terraform.tfstate"
region = "ru-central1"
}
encryption {
key_provider "pbkdf2" "main" {
passphrase = var.state_passphrase
}
method "aes_gcm" "default" {
keys = key_provider.pbkdf2.main
}
state {
method = method.aes_gcm.default
}
plan {
method = method.aes_gcm.default
}
}
}
Блок plan - отдельная радость: план-файлы тоже шифруются. Это важно, потому что tofu plan -out=plan.tfplan в CI нередко сохраняется как артефакт, и там тоже есть чувствительные данные.
Помимо PBKDF2 из passphrase поддерживаются AWS KMS и OpenPGP-ключи. Для проектов с нормальным KMS это чище: ключ в KMS, IAM-роль пайплайна имеет право расшифровывать, никаких паролей в переменных окружения.
Миграция: что сломалось и что нет
Сама переконфигурация прошла штатно. Единственное, что потребовало внимания - первый tofu init после добавления блока encryption. OpenTofu предупреждает, что существующий незашифрованный state будет прочитан, но записываться уже будет зашифрованным. Это одноразовая конверсия, откатить назад командой нельзя - нужно либо расшифровывать вручную, либо держать backup до конверсии.
Мы сделали резервную копию state перед init, запустили, убедились что tofu plan видит инфраструктуру корректно и показывает пустой план. Весь процесс занял минут пятнадцать на проект.
Скрипт-обёртку удалили. Пайплайн упростился ровно на один шаг плюс исчезло хранение ключа шифрования в двух местах одновременно.
Ротация ключей
Это было нашим главным вопросом перед миграцией: как менять ключ без остановки пайплайна.
В OpenTofu 1.7 ротация решена через блок fallback. Конфигурация выглядит так:
encryption {
key_provider "pbkdf2" "new_key" {
passphrase = var.state_passphrase_new
}
key_provider "pbkdf2" "old_key" {
passphrase = var.state_passphrase_old
}
method "aes_gcm" "default" {
keys = key_provider.pbkdf2.new_key
}
method "aes_gcm" "old" {
keys = key_provider.pbkdf2.old_key
}
state {
method = method.aes_gcm.default
fallback {
method = method.aes_gcm.old
}
enforced = false
}
}
Логика: при чтении OpenTofu пробует расшифровать новым ключом, при неудаче пробует старым (fallback). При записи всегда использует новый ключ. Ставим enforced = false на период ротации, чтобы не заблокировать себя если что-то пошло не так. После того как все state-файлы перезаписаны новым ключом - убираем fallback и ставим enforced = true.
На практике ротация выглядит как два tofu apply на каждый проект с промежуточной конфигурацией. Пайплайн не останавливается, параллельные запуски между двумя apply корректно обрабатываются через state locking в S3.
Provider-defined functions
Вторая крупная фича 1.7 - возможность для провайдеров экспортировать собственные функции, вызываемые прямо из HCL. До сих пор функции в Terraform/OpenTofu были только встроенными: tolist(), jsonencode() и так далее.
Пример из провайдера AWS (когда он поддержку добавит): можно будет вызвать функцию парсинга ARN прямо в коде вместо того чтобы городить regex или split. Это чище.
На июль 2024-го большинство популярных провайдеров support ещё не добавили - функциональность есть в runtime, но провайдеры пока не спешат. Yandex Cloud провайдер тоже без изменений. Так что смотрим на это как на инфраструктурную возможность, которую провайдеры будут использовать по мере готовности.
Итог
Шифрование state - это та функция, отсутствие которой в Terraform/OpenTofu было системной дырой с тех пор, как state начали хранить в удалённых бэкендах. Workaround-ы существовали, но создавали отдельный слой сложности для поддержки.
В 1.7 это наконец решено на уровне самого инструмента, конфигурируется декларативно в HCL, ротация ключей работает без простоев. Для нас это означает списание технического долга, который копился с момента первого перехода на удалённый state. Скрипт-обёртка ушла, пайплайн стал на два шага короче, ключ живёт в одном месте.
Provider-defined functions пока смотрим со стороны - потенциально интересная вещь, но реальная польза придёт когда провайдеры начнут её активно использовать.
- OpenTofu 1.6 GA: переводим первый production-проект с Terraform · 11 декабря 2023