Проект ЦИТадель
Облачные API позволяют создавать сети, виртуальные машины и управляемые сервисы программно, а Kubernetes показывает, как контроллер приводит фактическое состояние к желаемому. Инфраструктура как код (Infrastructure as Code, IaC) переносит на инфраструктуру инженерные практики разработки: описание изменений в файлах, контроль версий, ревью, автоматические проверки и воспроизводимый процесс применения. Вместо последовательности ручных действий команда хранит проверяемое описание и использует инструмент для вычисления и выполнения необходимых операций.
Код не становится единственным источником сведений об инфраструктуре. Для работы также нужны состояние инструмента, версии провайдеров и модулей, образы, секреты и данные внешних систем. Поэтому IaC даёт воспроизводимый процесс, но не обещает, что один и тот же файл через несколько лет создаст побитово одинаковый результат. Разберём границы этого обещания, два основных класса инструментов, состояние и дрейф, GitOps и способы ограничить риск автоматизации. Практикум выполняется локально, без облачного аккаунта.
Сервер, который годами изменяли вручную, часто превращается в снежинку: его точное состояние не воспроизводится по документации. Один пакет установили при миграции, строку конфигурации исправили во время аварии, а назначение символической ссылки помнит только бывший сотрудник. Расхождение между предполагаемым и фактическим состоянием называется конфигурационным дрейфом. Оно усложняет обновления, сравнение окружений и восстановление после отказа.
IaC переносит желаемую конфигурацию из памяти людей и пошаговых инструкций в версионируемое описание. Изменение можно обсудить до применения, связать с задачей и повторить на другой площадке. Однако история Git не является резервной копией инфраструктуры: возврат старой версии кода не восстанавливает удалённый диск, данные базы или прежнюю версию внешнего API. Для воспроизводимости нужны зафиксированные зависимости и артефакты, а для восстановления — отдельные резервные копии и проверенная процедура.
В IaC полезно различать два основных класса задач:
command, shell, сторонние модули и сам плейбук таких свойств автоматически не получают.Граница выражается вопросами «что существует?» и «как настроено содержимое?», но она не абсолютна: Ansible умеет создавать облачные ресурсы, а провайдеры OpenTofu и Terraform — управлять отдельными настройками сервисов. Выбор определяется жизненным циклом объекта и тем, какой инструмент будет его единственным владельцем. Сборка образов и декларации Kubernetes образуют соседние слои. Контейнеры и управляемые сервисы уменьшают объём настройки машин, но конфигурационное управление остаётся нужным для ОС, специализированных служб и наследуемых систем.
IaC не обязательно декларативен: программу, которая вызывает облачный SDK, тоже можно версионировать и проверять. Однако OpenTofu и Terraform в основном используют декларативную модель, а Ansible сочетает описания конечного состояния с явным порядком задач. Для безопасного применения важны три свойства:
Неизменяемая инфраструктура уменьшает дрейф ещё сильнее: образ и декларацию изменяют в конвейере, а работающие экземпляры заменяют новыми вместо настройки на месте. Замена требует стратегии раскатки, проверки готовности и отдельного жизненного цикла постоянных данных. Она сокращает пространство ручных изменений внутри машины, но не устраняет дрейф облачных настроек, секретов и внешних зависимостей.
Декларация говорит о ресурсе service.web, а облако — об объекте с конкретным идентификатором. Соответствие, последние известные атрибуты и служебные данные хранятся в состоянии (state). Отсюда следуют несколько правил:
sensitive скрывает значение в обычном выводе, но не обязательно удаляет его из этих файлов. Возможность вообще не сохранять секрет зависит от версии инструмента и схемы провайдера. OpenTofu умеет шифровать состояние и планы на стороне клиента; для Terraform защита удалённого состояния определяется выбранным хранилищем. Ключи шифрования тоже требуют резервирования и ротации.-refresh-only показывает, как фактическое состояние расходится с записанным, но не решает, следует ли вернуть объект к коду или принять аварийное изменение в код.Для аварийных ручных действий используют регламент аварийного доступа break-glass: ограниченный временный доступ, журналирование, последующее ревью и явное согласование кода с выбранным итоговым состоянием. Периодические планы помогают искать дрейф, но должны выполняться с минимальными правами и не применяться автоматически только потому, что нашли различие.
Повторяемые блоки выделяют в модули с небольшим интерфейсом и явными входами и выходами. Модуль не должен скрывать всю платформу за десятками переключателей: композиция нескольких понятных модулей обычно легче проверяется. Версии внешних модулей и провайдеров ограничивают, выбранные версии провайдеров и контрольные суммы фиксируют в .terraform.lock.hcl. Образы также задают неизменяемым хешем (digest) или иным контролируемым идентификатором; одной фиксации HCL недостаточно.
Dev, stage и prod могут использовать общие версионируемые модули, но обычно имеют отдельные корневые конфигурации, состояния, учётные данные и правила одобрения. Это позволяет продвигать проверенную версию модуля между окружениями, не связывая их одним применением. Различия параметров должны быть явными и оправданными, а не спрятанными в копиях файлов.
Секреты не записывают в код и tfvars: конфигурация ссылается на менеджер секретов либо получает краткоживущие учётные данные во время запуска. Если значение всё же проходит через инструмент, нужно проверить по документации конкретной версии и провайдера, попадёт ли оно в план и состояние; такие файлы и вывод конвейера нельзя публиковать. В конвейере выполняют форматирование, валидацию, тесты модулей и политик, затем строят план именно для целевого окружения. Ревью рассматривает и изменение кода, и свежий план; опубликованное представление плана должно безопасно скрывать чувствительные поля.
OpenGitOps формулирует четыре свойства: желаемое состояние выражено декларативно; его версии неизменяемы и сохраняют историю; агент автоматически получает декларации из источника; агент непрерывно наблюдает фактическое состояние и пытается его согласовать. Git — обычный, но не единственно возможный источник версий. Argo CD и Flux реализуют этот подход прежде всего для Kubernetes.
Слияние изменения означает только появление новой желаемой версии. Контроллер ещё должен получить её и успешно применить, а проверки — подтвердить работоспособность. В Argo CD автоматическая синхронизация, удаление исчезнувших из Git объектов (prune) и исправление ручного дрейфа (selfHeal) настраиваются отдельно. У каждого режима свой риск: например, ошибочное удаление файла при включённом prune становится удалением ресурса.
git revert создаёт новую декларацию с прежним содержимым, но не гарантирует обратимость операции. Схема базы, удалённые данные и несовместимый образ требуют собственной стратегии возврата или движения вперёд. GitOps также не отменяет административный доступ: контроллер сам обладает значительными правами, а людям нужен контролируемый аварийный путь. Поэтому защищают репозиторий, учётные данные и цепочку поставки деклараций.
Применение OpenTofu или Terraform конвейером после слияния использует те же практики версионирования и ревью, но обычно остаётся дискретным push-процессом, а не GitOps в строгом смысле с автоматическим получением и непрерывным согласованием. Эти модели можно сочетать: один слой создаёт платформу, другой управляет размещёнными на ней приложениями.
Автоматизация быстро и последовательно выполняет как правильные, так и ошибочные изменения. Защита строится несколькими независимыми слоями:
План и политика уменьшают вероятность ошибки, но не доказывают корректность результата: провайдер может завершиться с ошибкой после части операций, внешний API — изменить состояние параллельно, а синтаксически правильная конфигурация — нарушить пользовательский сценарий. После применения нужны проверки работоспособности и наблюдение, а конвейер должен уметь остановиться, не продолжая раскатку автоматически.
Для основной части понадобятся OpenTofu или Terraform и локальный Docker Engine. Ниже используются команды tofu; при работе с Terraform замените имя команды. Доступ к сокету Docker фактически даёт широкие полномочия на хосте, поэтому выполняйте опыт на отдельной машине или с rootless Docker и не подключайте сокет к недоверенным контейнерам.
Задание 1. Декларация, сохранённый план и применение. Создайте пустой каталог и файл main.tf:
terraform {
required_providers {
docker = {
source = "kreuzwerker/docker"
version = "4.5.0"
}
}
}
provider "docker" {}
variable "nginx_image" {
type = string
description = "Неизменяемое имя образа с хешем sha256"
}
locals {
instances = {
blue = 8080
green = 8081
}
}
resource "docker_image" "nginx" {
name = var.nginx_image
keep_locally = true
}
resource "docker_container" "web" {
for_each = local.instances
name = "citadel-iac-${each.key}"
image = docker_image.nginx.image_id
must_run = true
ports {
ip = "127.0.0.1"
internal = 80
external = each.value
}
}
Сначала загрузите выбранный образ nginx и получите неизменяемое имя из RepoDigests:
$ docker pull nginx:alpine
$ docker image inspect nginx:alpine \
--format '{{index .RepoDigests 0}}'
Запишите полученное имя в terraform.tfvars, например nginx_image = "nginx@sha256:...". Это отделяет декларацию от изменяемого тега. Затем выполните:
$ tofu init $ tofu fmt -check $ tofu validate $ tofu plan -out=lab.plan $ tofu show lab.plan $ tofu apply lab.plan $ curl http://127.0.0.1:8080/ $ curl http://127.0.0.1:8081/
Файл .terraform.lock.hcl фиксирует выбранный пакет провайдера и его контрольные суммы — его следует хранить вместе с конфигурацией. Каталог .terraform/, состояние и lab.plan в Git не добавляют. Даже в лаборатории план применяйте только после чтения.
Задание 2. Сходимость и дрейф. Повторный tofu plan -detailed-exitcode должен завершиться кодом 0 и сообщить об отсутствии изменений; код 2 означает непустой план, код 1 — ошибку. Остановите один контейнер командой docker stop citadel-iac-green. Новый план должен увидеть изменение must_run, а применение — снова запустить контейнер. Затем удалите его командой docker rm -f citadel-iac-green: план предложит создать отсутствующий объект. Проверьте восстановление и объясните, почему неизвестный состоянию посторонний контейнер этот план не обнаружит.
Задание 3. Состояние и безопасный рефакторинг. Выполните tofu state list и tofu state show 'docker_container.web["blue"]'; исходный JSON вручную не редактируйте. Переименуйте блок ресурса из web в frontend. План воспримет новые адреса как другие объекты и предложит замену — не применяйте её. Добавьте документированное перемещение:
moved {
from = docker_container.web
to = docker_container.frontend
}
Повторный план должен показать перемещение адресов без удаления контейнеров. Сохранение блока moved в репозитории делает рефакторинг воспроизводимым для других копий состояния, в отличие от локальной команды state mv.
Задание 4. Конфигурационное управление. На отдельной тестовой ВМ создайте плейбук Ansible, который устанавливает пакет, формирует конфигурацию модулем template и через обработчик (handler) перезапускает службу только при изменении файла. Сначала выполните ansible-playbook --syntax-check, затем --check --diff и обычный запуск. Второй обычный запуск должен дать changed=0. Измените управляемый файл вручную, снова запустите --check --diff и проверьте, что Ansible показывает ожидаемое исправление до применения. Не используйте для опыта рабочий сервер: установка пакета и перезапуск службы являются настоящими изменениями.
Задание 5*. GitOps. В локальном кластере kind или minikube установите Argo CD по официальной инструкции и создайте отдельный доступный ему репозиторий с небольшим Deployment. Для учебного приложения явно включите автоматическую синхронизацию, selfHeal: true и, только после проверки границ приложения, prune: true. Измените число реплик коммитом и дождитесь состояния Synced/Healthy. Затем выполните kubectl scale: при включённом selfHeal контроллер должен вернуть число реплик к декларации. Удалите объект из Git и отдельно зафиксируйте действие prune. Объясните, почему успешный merge ещё не доказывает успешную выкатку.
Задание 6*. Воспроизводимость и границы RTO. После сохранения наблюдений выполните tofu destroy, создайте новый план и восстановите два контейнера, измеряя время до успешных HTTP-проверок. Это время характеризует только восстановление описанных ресурсов. Оно не включает получение резервных данных, восстановление состояния из удалённого хранилища, DNS и внешние зависимости, поэтому не является RTO всей системы. В отчёте перечислите эти исключения и предложите, как включить их в следующее учение.
merge и revert сами по себе не гарантируют выкатку и возврат данных.