ADG Оставить заявку
Блог DevOps 5 мин чтения

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 пока смотрим со стороны - потенциально интересная вещь, но реальная польза придёт когда провайдеры начнут её активно использовать.

Контакт

Нужна такая же инженерная работа?

Опишите задачу и контекст. Ответим в течение рабочего дня, при необходимости подпишем NDA.