Прекратите смешивать окружения: почему состояния dev и prod никогда не должны пересекаться

У вас есть три каталога: dev, staging и prod. В каждом лежат конфигурационные файлы, записи состояния и описания ресурсов. Когда нужно обновить правило файрвола, вы открываете все три каталога и вносите одно и то же изменение трижды. В один из дней вы забываете обновить staging. Через две недели деплой в staging падает, потому что файрвол блокирует соединение, которое работает везде. Никто не замечает этого, пока релиз не оказывается заблокирован.

Эта ситуация знакома многим. Команды начинают с одного окружения, затем добавляют второе, потом третье. Разделение кажется очевидным, потому что папки называются по-разному. Но это разделение лишь косметическое. Настоящая проблема в том, что один и тот же человек, один и тот же инструмент и один и тот же шаблон доступа могут взаимодействовать с любым окружением без каких-либо защитных барьеров.

Настоящая проблема разделения окружений

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

Когда вы используете один и тот же файл состояния для всех окружений, вы создаете единую точку отказа. Ошибка в разработке может удалить продакшн-ресурс. Неправильно настроенный CI-пайплайн может применить значения из staging к продакшн-инфраструктуре. Это не теоретические риски. Это происходит, когда команды делят файлы состояния, бэкенды или учетные данные между окружениями.

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

Три подхода к разделению окружений

1. Отдельные каталоги

Следующая диаграмма отображает каждый подход к его структуре и ключевым характеристикам:

flowchart TD subgraph Approach1["1. Отдельные каталоги"] A1[Код + Конфиг + Состояние для каждого окружения] --> D1[dev] A1 --> S1[staging] A1 --> P1[prod] end subgraph Approach2["2. Общая структура + конфигурационные файлы"] A2[Общий код] --> C2[Конфигурационные файлы] C2 --> D2[dev] C2 --> S2[staging] C2 --> P2[prod] A2 -.->|один бэкенд состояния| D2 A2 -.->|один бэкенд состояния| S2 A2 -.->|один бэкенд состояния| P2 end subgraph Approach3["3. Отдельные бэкенды состояния"] A3[Общий код + Конфиг] --> D3[dev] A3 --> S3[staging] A3 --> P3[prod] D3 --> B1[Бэкенд dev] S3 --> B2[Бэкенд staging] P3 --> B3[Бэкенд prod] end Approach1 -.->|высокая дупликация| L1[Просто, но дрейфует] Approach2 -.->|средняя изоляция| L2[Меньше дублирования, общий риск] Approach3 -.->|полная изоляция| L3[Лучше для масштаба и безопасности]

Это самый простой подход. Вы создаете папку для каждого окружения: dev/, staging/, prod/. Каждая папка содержит собственные конфигурационные файлы и собственный файл состояния. Когда вы запускаете команду, вы указываете, какую папку использовать.

Это хорошо работает для небольших команд с малым количеством окружений. Вы можете увидеть различия между окружениями, сравнивая файлы рядом. Структура легко понимается. Новые члены команды могут найти то, что им нужно, без лишних вопросов.

Проблема возникает, когда окружения разделяют большую часть конфигурации. Разработка и staging могут отличаться только размером сервера и именем базы данных. Когда нужно изменить настройку безопасности, вы должны обновить три файла. Забыв об одном, вы создаете невидимый дрейф. Со временем окружения расходятся, пока staging больше не отражает продакшн.

2. Общая структура с отдельными конфигурационными файлами

Вместо дублирования всей конфигурации вы храните один набор определений ресурсов и отдельные файлы для значений, специфичных для окружения. Основные файлы определяют логику и структуру. Конфигурационные файлы содержат такие значения, как имена серверов, размеры инстансов и строки подключения.

Этот подход уменьшает дублирование. Вы пишете определения ресурсов один раз. Когда вы меняете правило файрвола, вы меняете его в одном месте. Файлы, специфичные для окружения, содержат только те значения, которые действительно различаются.

Плата за это — сложность. Вам нужно понимать, как конфигурационные файлы объединяются с основными определениями. Нужно убедиться, что каждое окружение имеет правильный конфигурационный файл. Отсутствие значения может привести к сбою деплоя или, что еще хуже, к использованию значения по умолчанию, которое не подходит для этого окружения.

3. Отдельные бэкенды состояния

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

Вот конкретный пример настройки отдельных бэкендов состояния в Terraform с использованием S3:

# конфигурация бэкенда для окружения production
terraform {
  backend "s3" {
    bucket = "my-company-tfstate"
    key    = "prod/terraform.tfstate"
    region = "us-east-1"
  }
}

Для окружения staging вы используете тот же bucket, но другой ключ:

terraform {
  backend "s3" {
    bucket = "my-company-tfstate"
    key    = "staging/terraform.tfstate"
    region = "us-east-1"
  }
}

И для разработки:

terraform {
  backend "s3" {
    bucket = "my-company-tfstate"
    key    = "dev/terraform.tfstate"
    region = "us-east-1"
  }
}

Файл состояния каждого окружения хранится под отдельным префиксом в одном и том же S3 bucket. Затем вы можете применять политики доступа на уровне bucket или префикса, гарантируя, что разработчики могут читать и писать только в префикс dev/, в то время как доступ к продакшну ограничен вашим CI/CD пайплайном.

Вы можете применять политики доступа на уровне бэкенда. Члены команды разработки имеют доступ только к бэкенду разработки. Операционные или SRE-команды имеют доступ к бэкенду продакшна. Разработчик, запускающий команду со своего ноутбука, не может затронуть продакшн-ресурсы, потому что его учетные данные не имеют доступа к бэкенду продакшна.

Это разделение критически важно, потому что файлы состояния содержат чувствительную информацию. Состояние продакшна обычно записывает IP-адреса серверов, подключения к базам данных и конфигурации безопасности. Если состояния продакшна и разработки хранятся в одном месте, утечка или случайное изменение затронут оба окружения. С отдельными бэкендами вы можете применять разные политики безопасности к каждому окружению.

Когда использовать каждый подход

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

Выбирайте общую структуру с отдельными конфигурационными файлами, когда ваши окружения имеют одинаковую структуру, но разные значения. Это хорошо работает для команд, которые стандартизировали свою инфраструктуру и нуждаются только в изменении таких параметров, как размер, регион или именование.

Выбирайте отдельные бэкенды состояния, когда ваша команда большая, продакшн должен быть строго защищен или вам нужен аудиторский след для каждого изменения. Это подход, который масштабируется вместе со сложностью организации.

Политическая сторона разделения окружений

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

Разделение окружений требует политики. Кто может читать состояние продакшна? Кто может его изменять? Кто может утверждать изменения в продакшн-инфраструктуре? На эти вопросы нужно ответить и обеспечить их соблюдение с помощью контроля доступа, а не устных договоренностей.

Некоторые команды используют отдельный CI/CD пайплайн для деплоя в продакшн. Пайплайн продакшна запускается только из определенной ветки, требует утверждений и использует учетные данные, недоступные разработчикам. Это добавляет дополнительный уровень разделения помимо самого бэкенда.

Практический чек-лист

  • Каждое окружение использует отдельный бэкенд состояния
  • Бэкенд продакшна доступен только из CI/CD пайплайна, а не с локальных машин
  • Бэкенды разработки и staging доступны разработчикам
  • Файлы состояния зашифрованы в покое
  • Доступ к состоянию продакшна требует утверждения и логируется
  • Значения конфигурации проверяются перед тем, как попасть в продакшн

Конкретный вывод

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