Когда вы впервые описываете инфраструктуру кодом и запускаете apply на своей машине — всё работает, и это вдохновляет. Но между «работает у меня в одиночку» и «работает в команде на продакшене» лежит несколько важных вещей, о которых учебники часто молчат. State, секреты, разделение окружений и автоматическая доставка — вот что превращает учебный пример в настоящий проект.

Эта статья — обзорная карта этих тем для тех, кто уже попробовал инфраструктуру как кода (Infrastructure as Code, IaC — описание серверов, сетей и сервисов в виде файлов конфигурации) и хочет понять, чего не хватает до «по-взрослому». Если базовые понятия пока в тумане, начните с раздела Основы IaC, а потом возвращайтесь.

pull request изменение в коде plan разница в комментарии ревью человек читает план merge одобрено apply из конвейера, а не с ноутбука

Инфраструктура едет в прод тем же путём, что и код: план считается на каждый запрос и ложится в ревью, а применяет его конвейер под временными правами, а не человек со своего ноутбука.

Обязательно

Где хранить state и зачем его блокировать

State (состояние) — это файл, в котором Terraform запоминает, что он уже создал в облаке: какие ресурсы, с какими ID и настройками. Без него Terraform не знает, что менять, а что оставить. Думайте о нём как о бухгалтерской книге: в реальности — серверы в AWS, в книге — записи о них, и они должны сходиться.

Когда вы учитесь, state лежит файлом terraform.tfstate рядом с кодом. Для одного человека это нормально. Но как только появляется второй разработчик — начинается беда: у каждого своя копия книги, и они расходятся.

Решение — remote state (удалённое состояние): хранить файл не локально, а в общем месте, куда смотрят все. На AWS это обычно бакет Amazon S3. Тогда книга одна на всех.

terraform {
  backend "s3" {
    bucket  = "myteam-tfstate"
    key     = "prod/network/terraform.tfstate"
    region  = "eu-central-1"
    encrypt = true
  }
}

Но общий файл рождает вторую проблему: что если два человека запустят apply одновременно? Они затрут правки друг друга и могут испортить state. Чтобы этого не случилось, нужна блокировка (locking) — пока один apply идёт, остальные ждут, как очередь в одну кабинку.

Классический способ — таблица в DynamoDB: Terraform создаёт там запись-замок на время операции.

terraform {
  backend "s3" {
    bucket         = "myteam-tfstate"
    key            = "prod/network/terraform.tfstate"
    region         = "eu-central-1"
    dynamodb_table = "tfstate-lock"
  }
}

Начиная с Terraform 1.10 появился более простой вариант — нативная блокировка средствами самого S3, отдельная таблица не нужна; вариант через DynamoDB при этом считается устаревшим:

terraform {
  backend "s3" {
    bucket       = "myteam-tfstate"
    key          = "prod/network/terraform.tfstate"
    region       = "eu-central-1"
    use_lockfile = true
  }
}

Полезная привычка: включить версионирование бакета S3 — тогда испорченный state можно откатить к предыдущей версии.

Важная развилка инструментов. У Terraform state — ваша забота. А вот CloudFormation и CDK (надстройка над CloudFormation, где инфраструктура описывается на обычном языке программирования) состояние ведут сами внутри AWS: отдельного файла нет, заводить бакет под state не нужно, блокировка тоже на стороне AWS. Оговорка про CDK: бакет всё-таки появится, просто не под состояние — команда cdk bootstrap заводит в аккаунте служебный бакет под артефакты развёртывания (и репозиторий под образы), и без него cdk deploy не пройдёт. Подробнее об инструментах — в статьях про Terraform, CloudFormation и CDK.

Операции над состоянием: когда книга разошлась с реальностью

Рано или поздно состояние и облако разъезжаются так, что обычный план не помогает: ресурс переименовали в коде, ресурс удалили руками, ресурс создали руками, модуль переложили в другое место. Для этого есть отдельный набор операций — их немного, и знать их надо все, потому что без них остаётся только удалять и создавать заново.

Переименовать или переместить ресурс в состоянии. Самая частая операция. Переименовали ресурс в коде (или вынесли его в модуль) — для инструмента это «удалить старый, создать новый», а для базы данных это катастрофа. Лечится перемещением записи:

terraform state mv aws_db_instance.orders module.database.aws_db_instance.main

В современных версиях то же делается прямо в коде блоком moved, и это лучше: перемещение попадает в ревью и применяется у всех автоматически.

moved {
  from = aws_db_instance.orders
  to   = module.database.aws_db_instance.main
}

Убрать ресурс из состояния, не удаляя в облаке. Нужно, когда ресурс передают другой команде или перестают им управлять:

terraform state rm aws_s3_bucket.reports

После этого инструмент про бакет забывает, а бакет продолжает жить. Обратная операция — импорт, о котором статья про основы.

Посмотреть, что вообще лежит в состоянии. Полезно перед любой операцией:

terraform state list                      # все адреса
terraform state show aws_db_instance.orders   # все атрибуты одного ресурса

Обновить состояние по реальности. Команда terraform refresh устарела; то же делает terraform plan -refresh-only — она показывает, чем реальность отличается от записанного, и предлагает обновить состояние без изменения инфраструктуры. Это правильный первый шаг при разборе расхождений.

Разблокировать состояние. Если применение прервали (упал исполнитель, сеть, нажали отмену), блокировка может остаться. Инструмент при следующем запуске сообщит об этом и назовёт идентификатор блокировки:

terraform force-unlock <lock-id>

Делают это только убедившись, что применение действительно не идёт — иначе два одновременных применения испортят состояние по-настоящему.

Когда состояние разошлось необратимо. Бывает: состояние потеряли, или в нём остались ресурсы, которых нет, или наоборот. Порядок действий такой.

  1. Сначала копия. У удалённого состояния обычно включено версионирование — восстановить предыдущую версию быстрее, чем что-либо чинить. Это первое, что проверяют.
  2. Записи о несуществующих ресурсах убирают (state rm), существующие, но незаписанные — импортируют.
  3. План должен сойтись в ноль изменений — это критерий, что порядок восстановлен.
  4. Если состояние потеряно целиком, вариантов два: восстановить из версии хранилища или пересобрать импортом (долго, но реально — по одному ресурсу). Именно поэтому версионирование бакета с состоянием не «хорошая практика», а обязательная настройка.

И правило, которое предотвращает большинство этих историй: все операции над состоянием делаются по одному, с копией под рукой и на не боевом контуре первым делом. Состояние — единственное, чего нельзя восстановить из кода.

Секреты: чего нельзя делать

Секрет — это любое значение, утечка которого опасна: пароль к базе, ключ API, токен. С секретами в IaC два железных правила.

Первое: не коммитить секреты в git. Файл с паролем в репозитории — это пароль, который видят все с доступом к коду, и который останется в истории навсегда, даже если вы потом его удалите.

Второе, менее очевидное: всё, к чему прикоснулся Terraform, оседает в state открытым текстом. Впишете пароль прямо в конфигурацию — он окажется в state-файле. Прочитаете его из внешнего хранилища — он всё равно окажется в state-файле: результат любого обращения Terraform запоминает у себя. Отдельной «защищённой» дорожки мимо state в Terraform нет, и рассчитывать на неё не стоит.

Отсюда два вывода, и второй важнее первого.

Первый: секреты держат в специальном сервисе, а не в коде.

  • AWS Secrets Manager — хранилище с шифрованием и автоматической ротацией (сменой) паролей, удобно для учётных данных к базам.
  • SSM Parameter Store — параметры приложения; тип SecureString шифрует значение ключом KMS.

Второй: раз секрет в state всё равно попадёт, state защищают как сам секрет — шифруют в бакете (encrypt = true), включают версионирование и закрывают доступ так, чтобы читать его могли единицы. Управление доступом — тема статьи про IAM.

А лучше всего — вовсе не тащить значение секрета через Terraform. Пусть Terraform передаст приложению только ссылку (имя или ARN секрета), а само значение приложение прочитает уже на ходу, своими правами:

data "aws_secretsmanager_secret" "db" {
  name = "prod/db/password"
}

Здесь Terraform узнаёт, где лежит пароль, но не сам пароль, — и в state попадает безобидная ссылка. Стоит взять за правило: если можно обойтись ссылкой, обходитесь ссылкой.

Несколько окружений (dev, prod)

Почти у любого проекта есть как минимум два окружения: dev — где экспериментируют и не страшно сломать, и prod — где живут реальные пользователи и ломать нельзя. Главная цель — чтобы случайная правка в dev физически не могла задеть prod.

В Terraform есть два пути. Workspaces (рабочие пространства) разделяют state внутри одного бакета и одной конфигурации — это удобно для мелочей, но опасно для prod: легко перепутать активное пространство и применить изменения не туда. Для надёжной изоляции продакшена лучше отдельные директории и отдельный state на каждое окружение: у dev своя папка и свой ключ state, у prod — свои.

environments/
  dev/   -> key = "dev/terraform.tfstate"
  prod/  -> key = "prod/terraform.tfstate"

Чтобы две директории не превратились в две копии одного кода, саму конфигурацию выносят в модуль, а в envs/dev и envs/prod остаются только вызов модуля с параметрами окружения и свой backend: prod и dev применяют один и тот же код с разными значениями.

Разные файлы state — разные «бухгалтерские книги», и одно окружение не видит ресурсы другого. В мире CloudFormation/CDK тот же принцип достигается отдельными стеками (stack — именованный набор ресурсов): myapp-dev и myapp-prod существуют независимо.

Как слои обмениваются значениями

Из «отдельное состояние на каждое окружение» сразу следует вопрос: а как приложение узнает идентификатор сети, если сеть лежит в другом состоянии? Разделение на слои (сеть, данные, приложение) полезно — сеть меняется раз в год, приложение каждую неделю, — и без механизма обмена оно рассыпается.

Способов три, и выбор между ними не вкусовой.

Источник данных по устойчивому признаку — самый развязанный вариант: слой приложения находит сеть по тегу, ничего не зная о том, чем она создана.

data "aws_vpc" "main" {
  tags = { Name = "prod-main" }
}

data "aws_subnets" "private" {
  filter {
    name   = "vpc-id"
    values = [data.aws_vpc.main.id]
  }
  tags = { Tier = "private" }
}

Плюс: слои не связаны вовсе, можно менять инструмент в одном из них. Минус: связь держится на договорённости об именах и тегах, и её надо соблюдать (тег переименовали — план сломался).

Чтение состояния соседнего слоя — прямая связь: слой сети объявляет выходные значения, слой приложения читает его состояние.

# в слое сети
output "private_subnet_ids" { value = aws_subnet.private[*].id }

# в слое приложения
data "terraform_remote_state" "network" {
  backend = "s3"
  config = {
    bucket = "myteam-tfstate"
    key    = "prod/network/terraform.tfstate"
    region = "eu-central-1"
  }
}

resource "aws_instance" "app" {
  subnet_id = data.terraform_remote_state.network.outputs.private_subnet_ids[0]
}

Плюс: явная связь, видно, откуда что берётся. Минусы серьёзнее, чем кажется: слою приложения нужны права на чтение состояния сети — а в состоянии, как мы знаем, лежат все пароли; и выходные значения становятся контрактом, который нельзя менять, не сломав соседей.

Через хранилище параметров — компромисс, который часто оказывается лучшим: слой сети записывает нужные значения в параметры (или в теги), слой приложения читает их оттуда.

# слой сети публикует
resource "aws_ssm_parameter" "private_subnets" {
  name  = "/prod/network/private_subnet_ids"
  type  = "StringList"
  value = join(",", aws_subnet.private[*].id)
}

# слой приложения читает
data "aws_ssm_parameter" "private_subnets" {
  name = "/prod/network/private_subnet_ids"
}

Плюс: никаких прав на чужое состояние, контракт виден как список параметров, и читать его может кто угодно — в том числе конвейер приложения и код на другом инструменте. Минус: значения надо не забывать публиковать.

Что выбирают. Внутри одной команды и одного инструмента — источники данных по тегам (проще всего). Между командами — хранилище параметров как явный контракт. Чтение чужого состояния — только когда первые два не подходят, и тогда с ограниченными правами только на нужный ключ.

Для CloudFormation тот же вопрос решается штатно: стек экспортирует значение, другой импортирует его. Оборотная сторона — жёсткость: пока значение импортировано, экспортирующий стек нельзя изменить в этой части, и удалить его тоже нельзя.

IaC в конвейере доставки

Запускать apply руками со своего ноутбука — нормально, пока вы один и проект учебный. В команде это превращается в конвейер доставки (CI/CD — автоматический процесс сборки и выкатки изменений), и инфраструктура едет по тем же рельсам, что и код.

Базовый рабочий процесс простой и пошаговый:

  1. Вы открываете pull request (запрос на слияние ветки) с правкой инфраструктуры.
  2. Конвейер автоматически запускает terraform plan — это сухой прогон, который показывает, что изменится, ничего не трогая. План попадает в обсуждение pull request, и коллеги его смотрят.
  3. После одобрения и слияния (merge) конвейер запускает terraform apply — изменения применяются. В CloudFormation/CDK ту же роль играют cloudformation deploy и cdk deploy.

Одна деталь во втором и третьем шагах решает всё. План на ревью и план, который в итоге применится, должны быть одним и тем же планом — иначе между «посмотрели» и «применили» проходит время, облако успевает измениться, и применится не то, что одобрили. Поэтому план сохраняют в файл и применяют именно его:

terraform plan -out=tfplan
terraform apply tfplan

apply с готовым файлом плана заодно не спрашивает подтверждения — в конвейере это как раз то, что нужно.

Так ни одно изменение продакшена не происходит «втихую»: сначала видимый план на ревью, потом применение ровно его.

Остаётся вопрос: как конвейеру войти в AWS? Соблазн — положить вечный ключ доступа (access key) в настройки CI. Это плохая идея: такой ключ долгоживущий, и если он утечёт, им можно пользоваться сколько угодно. Правильный способ — OIDC-федерация (OpenID Connect): CI на лету получает короткоживущий токен, действующий ровно на одну задачу, и AWS выдаёт временные права по доверию, а не по сохранённому паролю.

permissions:
  id-token: write
  contents: read
- uses: aws-actions/configure-aws-credentials@v4
  with:
    role-to-assume: arn:aws:iam::111122223333:role/ci-terraform
    aws-region: eu-central-1

Этих двух кусков недостаточно, и вот та часть, без которой схема не работает — а хуже того, работает неправильно. В AWS нужно завести две вещи.

Поставщик удостоверений (это делается один раз на аккаунт): AWS должна знать, что токенам от вашей системы сборки можно доверять.

Роль с условием доверия, ограниченным конкретным репозиторием и ветвью. Вот это условие и есть главное:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "Federated": "arn:aws:iam::111122223333:oidc-provider/token.actions.githubusercontent.com" },
    "Action": "sts:AssumeRoleWithWebIdentity",
    "Condition": {
      "StringEquals": {
        "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
        "token.actions.githubusercontent.com:sub": "repo:acme/infra:ref:refs/heads/main"
      }
    }
  }]
}

Почему это критично. Поставщик удостоверений выдаёт токены всем репозиториям этой платформы, а не только вашим. Если в условии доверия нет ограничения по репозиторию (или стоит *), роль сможет принять любой чужой репозиторий в интернете — и получить ваши права в аккаунте. Это не теоретическая уязвимость, а самая частая ошибка настройки: схема при этом выглядит работающей, потому что ваш собственный конвейер, конечно же, работает.

Три правила, которые закрывают вопрос:

  • Условие по sub обязательно и указывает конкретный репозиторий; ветвь — там, где это уместно (ref:refs/heads/main для применения, отдельная роль с правами только на чтение — для планирования из запросов на изменение).
  • Сравнение строгое (StringEquals), а не по шаблону. Если шаблон всё-таки нужен (например, все ветви одного репозитория), то он обязан начинаться с имени репозитория: repo:acme/infra:*, и никогда не быть просто *.
  • Проверка aud — вторая половина условия, её тоже не пропускают.

И полезная привычка: после настройки проверить обратное — попробовать принять роль из другого репозитория (или посмотреть глазами, что условие не пускает). Настройка, которую проверили только положительным случаем, здесь особенно опасна.

Принципы самого конвейера — в разделах Принципы pipeline и Стратегии релизов.

Проверки в конвейере: не только план

Конвейер из плана и применения — это ровно то, что делают руками, только автоматически. Ценность появляется, когда между ними встают проверки, которые человек не сделает.

Порядок шагов, который сложился как рабочий:

  1. Форматирование (terraform fmt -check -recursive). Секунды, и ревью перестаёт состоять из пробелов.
  2. Проверка корректности (terraform validate). Синтаксис, типы, ссылки — без обращения в облако.
  3. Линтер (tflint). Ловит то, чего не видит проверка: несуществующий тип машины, устаревшие аргументы, нарушение соглашений об именах.
  4. Сканер политик — самый полезный шаг. Он читает код и ищет небезопасные конфигурации: открытая наружу группа безопасности, бакет без шифрования, база без резервных копий, права «всё со всем». Для Terraform это tfsec, checkov или trivy config, для CloudFormation — cfn-nag и тот же checkov, для своих правил на любом формате — conftest с политиками на языке Rego.
  5. План с сохранением в файл.
  6. Оценка стоимости — о ней ниже.
  7. Применение ровно сохранённого плана.
- run: terraform fmt -check -recursive
- run: terraform init -backend=false && terraform validate
- run: tflint --recursive
- run: checkov -d . --framework terraform --compact --quiet
- run: terraform init && terraform plan -out=tfplan

Про сканеры политик стоит сказать то же, что про сканеры образов: нужен порог и список исключений. Первый прогон на живом проекте выдаёт сотню находок, и если падать на всех, шаг отключат в тот же день. Рабочая схема: падать на высокой критичности, остальное в отчёт; у каждого исключения — комментарий с причиной и датой пересмотра (в checkov это специальный комментарий прямо в коде рядом с ресурсом).

Отдельный вид проверок, который добавляют позже: тесты инфраструктурного кода. Для модулей, которые отдают другим командам, пишут проверки в духе «модуль с такими параметрами создаёт ровно такие ресурсы» (terraform test во встроенном виде или terratest с реальным развёртыванием в тестовом аккаунте). Для конфигурации одного проекта это обычно избыточно, для общего модуля — обязательно.

Сколько это будет стоить и кто нажимает применение

Два шага между планом и применением на боевом контуре, которые в зрелом конвейере есть почти всегда.

Оценка стоимости изменений. План говорит, что создастся, но не говорит, во что это обойдётся. Инструмент оценки (Infracost) читает план и выдаёт разницу в месячной стоимости — и пишет её комментарием прямо в запрос на изменение:

- run: infracost breakdown --path tfplan.json --format json --out-file infracost.json
- run: infracost comment github --path infracost.json --repo $GITHUB_REPOSITORY --pull-request $PR_NUMBER

Почему это работает лучше любых призывов к экономии: строка «+ 420 долларов в месяц» в обсуждении изменения заставляет задать вопрос до применения, а не через месяц при разборе счёта. Оценка приблизительная (она не знает вашего трафика и обязательств), и этого достаточно, чтобы заметить порядок.

Ручное подтверждение для боевого контура. Применение в проде не должно случаться от самого факта слияния — между ними ставят остановку, на которой конкретный человек нажимает «продолжить». В системах сборки это оформляется правилами защиты среды: у среды production указывают, кто может подтверждать, и конвейер ждёт.

jobs:
  apply-prod:
    environment: production     # среда с обязательным подтверждением
    needs: plan

Что это даёт, кроме формальности: подтверждающий видит и план, и оценку стоимости, и результаты сканеров — то есть решение принимается по данным. И остаётся след: кто и когда одобрил изменение прода.

Здесь же обычно живут два правила поспокойнее: применение в прод только из основной ветви и только последовательно (одновременные применения одной конфигурации запрещены на уровне конвейера, чтобы не полагаться только на блокировку состояния).

Как ловить drift

Drift (дрейф) — это расхождение между тем, что описано в коде, и тем, что реально в облаке. Появляется он, когда кто-то правит ресурс вручную через консоль: код говорит одно, реальность — другое, и при следующем apply бывают сюрпризы.

Ловят дрейф проще всего регулярной проверкой. В Terraform это периодический terraform plan: если план не пустой, хотя вы ничего не меняли — значит, реальность ушла от кода.

terraform plan -detailed-exitcode

Флаг здесь главный: без него plan завершается успешно в любом случае, и ночная проверка «упало или нет» не заметит ничего. С флагом код возврата становится содержательным: 0 — изменений нет, всё сходится; 2 — изменения есть, то есть дрейф; 1 — сама команда сломалась. Различать 1 и 2 обязательно, иначе ошибка доступа будет выглядеть как найденный дрейф.

У CloudFormation для этого есть встроенное обнаружение дрейфа (drift detection):

aws cloudformation detect-stack-drift --stack-name myapp-prod

А CDK предоставляет команду, которая под капотом вызывает ту же проверку CloudFormation:

cdk drift

Команда появилась в CDK CLI версии 2.1017.0 — если в ответ прилетает «unknown command», значит, инструмент старее, и его надо обновить.

Хорошая привычка — гонять такую проверку по расписанию (например, ночью) и оповещать команду, если дрейф найден. Тогда ручные правки не накапливаются незаметно.

Дрейф нашли — что дальше

Проверка по расписанию отвечает на вопрос «есть ли расхождение», и дальше нужен ответ на вопрос «что с ним делать». Вариантов ровно три, и решение принимается осознанно, а не автоматически.

Вернуть реальность к коду. Обычное применение — и ручная правка исчезает. Правильный ответ, когда правка была случайной или временной («поправил, чтобы починить, и забыл»). Важное условие: перед этим надо понять, что именно правили и зачем, иначе вы повторно сломаете то, что кто-то ночью починил.

Принять изменение в код. Правку описывают в конфигурации, план сходится в ноль. Правильный ответ, когда изменение было осмысленным — просто сделано не тем путём. Заодно это разговор о том, почему пошли в консоль: обычно потому, что через код это сделать долго или неудобно, и вот это и стоит исправить.

Перестать управлять этим свойством. Если значение меняет другой механизм — автомасштабирование, корпоративная политика тегов, конвейер приложения, — правильный ответ не «бороться», а признать чужое владение: ignore_changes на это свойство. Иначе каждое применение будет гасить работу соседа, и расхождение появится снова через час.

Чего делать не надо: автоматически применять по расписанию. Соблазн большой («пусть само возвращает как в коде»), и последствия неприятные: ночное применение без человека однажды вернёт как в коде то, что руками правили для устранения аварии. Автоматическое исправление уместно только для узкого набора вещей, где это безопасно и осознанно записано.

Практическая схема, которая работает: проверка ночью, результат — в оповещение дежурному, разбор утром в течение дня (пока помнят, кто это сделал), решение из трёх вариантов выше и запись решения в запрос на изменение. Повторяющееся расхождение в одном и том же месте — сигнал не про дисциплину, а про то, что код не описывает реальность: например, свойством владеет другой механизм.

Где это применяется

Эти темы всплывают ровно в тот момент, когда инфраструктура из личного эксперимента становится командной. Remote state и блокировка — как только над проектом работает больше одного человека. Разделение окружений — как только появляется реальный продакшен, который нельзя ронять. Конвейер с планом на ревью — когда изменения инфраструктуры должны быть прозрачными и обратимыми, как обычный код.

Типичные ошибки новичков, и у каждой есть прямое лекарство из этой статьи:

  • оставить state локальным файлом и потом долго мирить расхождения;
  • вписать пароль прямо в конфигурацию и забыть, что он осел в state;
  • путать workspace и применить prod-правку в dev;
  • положить вечный ключ AWS в настройки CI.

Что учить дальше: устройство конкретных инструментов — Terraform, CloudFormation, CDK; базу облака, чтобы понимать, что именно вы описываете кодом — Основы AWS, сети, IAM; и принципы доставки в целом — ветки и релизы. Если инфраструктура едет в Kubernetes, пригодятся основы Kubernetes и деплой и конфигурация.

Дополнительно: при первом чтении можно пропустить

Глубже: GitOps для инфраструктуры: репозиторий как источник правды и агент, который подтягиваетрасширенное

Конвейер выше толкает изменения в облако: слияние запускает apply. Есть обратная модель, и в мире Kubernetes она стала основной: репозиторий объявлен источником правды, а агент внутри среды сам сравнивает её с репозиторием и приводит к нему. Это GitOps, и у него есть форма и для инфраструктуры.

Для манифестов Kubernetes это Argo CD и Flux: агент в кластере следит за репозиторием, применяет изменения и, что важнее, откатывает ручные правки, потому что дрейф для него это отклонение, которое нужно исправить. Откат это возврат коммита, история изменений это история git, а доступ к кластеру у людей не нужен вовсе; статьи про Kubernetes и Argo CD разбирают это подробно.

Для Terraform та же модель собирается из инструментов, потому что у apply есть побочные эффекты и состояние. Atlantis живёт рядом с репозиторием: на pull request он сам запускает plan и пишет результат в обсуждение, по команде в комментарии делает apply, и никто не запускает Terraform с ноутбука. Управляемые платформы (Terraform Cloud, Spacelift, env0) делают то же с очередью запусков, политиками и хранением state. Контроллер Terraform для Flux и Crossplane идут дальше: описание инфраструктуры лежит в кластере как ресурс, и агент постоянно сверяет облако с ним, автоматически исправляя дрейф, как Argo CD исправляет манифесты.

Что меняется по сравнению с конвейером. Изменение попадает в облако не потому, что кто-то нажал кнопку, а потому, что оно слилось в ветку; ветка защищена ревью, и это единственная дверь. Дрейф не накапливается: агент либо исправляет его сам, либо показывает как расхождение постоянно, а не по ночному расписанию. Доступ к облаку есть у одного агента с узкой ролью, а не у каждого инженера и каждого раннера. Плата: ещё один компонент с правами администратора на инфраструктуру, который нужно защищать сильнее всего остального, и привычка «срочно поправлю руками», которая перестаёт работать, потому что через минуту агент вернёт всё как в репозитории.

Когда брать. Кластер Kubernetes есть: GitOps для его манифестов почти обязателен, и это первое место, где его заводят. Для Terraform его берут, когда инженеров инфраструктуры больше двух и apply с ноутбука уже приводил к инцидентам; до этого хватает конвейера с планом на ревью из раздела выше. Автоматическое исправление дрейфа включают осторожно: для сети и учётных записей да, для ресурсов, которые кто-то ещё меняет через API (автомасштабирование, операторы), нет, иначе агент и автоматика будут спорить.

Коротко

  • State держат в общем хранилище с блокировкой и версионированием; у CloudFormation и CDK его ведёт AWS.
  • Секреты не в коде и не в state: хранилище секретов и ссылка вместо значения; state защищают как секрет.
  • Окружения разделяют отдельными каталогами и файлами state с общим модулем, а не рабочими пространствами.
  • Конвейер: plan на ревью, apply ровно сохранённого плана, вход в облако по федерации OIDC без вечных ключей.
  • Дрейф ловят по расписанию plan -detailed-exitcode и проверкой дрейфа стека. У найденного расхождения три ответа: вернуть реальность к коду, принять изменение в код или признать чужое владение через ignore_changes; автоматически применять по расписанию не стоит.
  • GitOps: репозиторий источник правды, агент (Argo CD, Flux, Atlantis, контроллер Terraform) применяет и исправляет дрейф; берут для Kubernetes сразу, для Terraform когда инженеров больше двух.
  • Состояние правят отдельными операциями: moved или state mv при переименовании, state rm при передаче ресурса, plan -refresh-only для сверки, force-unlock только убедившись, что применение не идёт.
  • Слои обмениваются значениями тремя способами: источники данных по тегам (самый развязанный), хранилище параметров как явный контракт между командами и чтение чужого состояния (требует прав на все его пароли).
  • В условии доверия роли для конвейера обязательно ограничение по репозиторию и ветви: без него роль сможет принять любой чужой репозиторий платформы.
  • Между планом и применением стоят проверки: форматирование, корректность, линтер, сканер политик с порогом и исключениями, оценка стоимости изменений и ручное подтверждение для боевого контура.

Что почитать дальше