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

CloudFormation бесплатен сам по себе (платите только за созданные им ресурсы) и встроен в AWS — отдельно ставить ничего не нужно. Если вы только знакомитесь с самой идеей описывать инфраструктуру текстом, начните с обзорной статьи Инфраструктура как код, а базовые понятия облака — в основах AWS.

Что такое CloudFormation

CloudFormation — это сервис, который читает ваш шаблон (текстовый файл с описанием ресурсов) и приводит реальную инфраструктуру в соответствие с ним. Создать недостающее, изменить отличающееся, удалить лишнее — всё по тому, что написано в файле.

Главная идея — декларативность. Вы не пишете пошаговую инструкцию «сначала создай это, потом то». Вы описываете результат — какие ресурсы должны существовать, — а CloudFormation сам вычисляет порядок: видит, что база зависит от сети, и создаёт сеть первой. Это как список покупок против рецепта: вы говорите «что должно быть», а не «что делать по шагам».

Один шаблон — сколько угодно одинаковых стеков: dev, staging и prod поднимаются из одного файла с разными параметрами.

Из чего состоит шаблон

Шаблон пишется на YAML или JSON (YAML читается приятнее, поэтому новичкам советуют его). Файл разбит на именованные секции. Вот те, что нужны с первого дня:

  • AWSTemplateFormatVersion — версия формата. Значение почти всегда одно: 2010-09-09. Необязательная, но её принято указывать.
  • Parameters — входные значения, которые подставляются при запуске. Например, имя окружения или тип сервера. Благодаря им один шаблон годится и для теста, и для рабочей среды.
  • Mappings — таблицы соответствий «ключ → значение». Классический пример — разные идентификаторы образов в разных регионах.
  • Resources — единственная обязательная секция. Здесь перечислены сами ресурсы: хранилища, серверы, базы, сети.
  • Outputs — что вернуть наружу после развёртывания: адрес хранилища, идентификатор сети. Эти значения видны в консоли и могут передаваться другим стекам.

Список этим не заканчивается — в шаблоне бывают ещё Description, Metadata, Rules и две секции, о которые вы споткнётесь довольно скоро. Conditions — условия: именно ими один шаблон разводят на тестовую и рабочую среду («реплику базы создавать только в проде»), одних Parameters для этого не хватит. Transform — превращение шаблона: строка Transform: AWS::Serverless-2016-10-31 включает упрощённый синтаксис SAM, и почти любой шаблон с бессерверными функциями начинается именно с неё.

Минимальный рабочий шаблон с одним хранилищем S3:

AWSTemplateFormatVersion: "2010-09-09"
Resources:
  MyBucket:
    Type: AWS::S3::Bucket

Каждый ресурс описывается одинаково: вы даёте ему логическое имя (MyBucket — как вы зовёте его внутри шаблона), указываете Type (что это — AWS::S3::Bucket значит хранилище S3) и Properties (настройки конкретно этого ресурса). Имя типа всегда вида AWS::Сервис::Тип.

Тот же ресурс, но с настройками:

Resources:
  MyBucket:
    Type: AWS::S3::Bucket
    Properties:
      VersioningConfiguration:
        Status: Enabled

А вот шаблон побогаче — с параметром на входе и значением на выходе:

AWSTemplateFormatVersion: "2010-09-09"
Parameters:
  BucketName:
    Type: String
Resources:
  MyBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref BucketName
Outputs:
  BucketArn:
    Value: !GetAtt MyBucket.Arn

Что такое стек

Стек (stack) — это группа ресурсов, развёрнутых из одного шаблона и управляемых как единое целое. Запустили шаблон — получили стек. Все ресурсы внутри создаются, обновляются и удаляются вместе.

Это ключевое удобство: удалили стек — и CloudFormation сам убрал всё, что в него входило, в правильном порядке. Никаких забытых хранилищ, за которые потом приходит счёт. Стек — это «корзина», из которой ресурсы не теряются.

Создать стек из локального шаблона через командную строку (AWS CLI работает одинаково на любой операционной системе):

aws cloudformation create-stack \
  --stack-name my-first-stack \
  --template-body file://template.yaml

Как только в шаблоне появится роль или политика доступа — а это почти любой боевой шаблон, — та же команда упадёт с сообщением Requires capabilities: [CAPABILITY_IAM]. Это не ошибка, а предохранитель: AWS требует, чтобы вы явно подтвердили, что шаблон раздаёт права. Подтверждение — флаг --capabilities CAPABILITY_IAM (а если роль ещё и с собственным именем — CAPABILITY_NAMED_IAM):

aws cloudformation create-stack \
  --stack-name my-first-stack \
  --template-body file://template.yaml \
  --capabilities CAPABILITY_IAM

Обновить — тот же стек, изменённый шаблон:

aws cloudformation update-stack \
  --stack-name my-first-stack \
  --template-body file://template.yaml

Одна неожиданность здесь есть: если шаблон не изменился, update-stack не промолчит, а завершится ошибкой No updates are to be performed и ненулевым кодом возврата. В Terraform повторный запуск на том же коде спокойно пишет «0 changed», а тут — падение, на котором спотыкается конвейер сборки. Такой случай в конвейере обрабатывают отдельно: сверяют текст ошибки и считают её успехом.

Удалить стек целиком вместе со всеми ресурсами:

aws cloudformation delete-stack --stack-name my-first-stack

Если в процессе что-то ломается, CloudFormation по умолчанию откатывает сделанное — частично сломанной инфраструктуры не остаётся. Но откат при обновлении и откат при создании — вещи разные, и вторая новичка обычно застаёт врасплох. При обновлении стек возвращается к прошлому рабочему состоянию и живёт дальше. А при первом создании возвращаться некуда: созданное удаляется, а сам стек остаётся пустой оболочкой в состоянии ROLLBACK_COMPLETE. Обновить такой стек нельзя — его удаляют и создают заново под тем же именем. Это первая стена, о которую бьются все, и выглядит она страшнее, чем есть.

Change sets: посмотреть перед применением

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

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

Работа идёт в три шага. Сначала создаём набор изменений:

aws cloudformation create-change-set \
  --stack-name my-first-stack \
  --change-set-name my-changes \
  --template-body file://template.yaml

Затем смотрим, что внутри:

aws cloudformation describe-change-set \
  --stack-name my-first-stack \
  --change-set-name my-changes

И, если всё устраивает, применяем:

aws cloudformation execute-change-set \
  --stack-name my-first-stack \
  --change-set-name my-changes

Привычка «сначала change set, потом execute» — один из главных признаков аккуратной работы с CloudFormation на рабочих средах.

Одна команда вместо трёх: deploy

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

aws cloudformation deploy \
  --stack-name my-first-stack \
  --template-file template.yaml \
  --parameter-overrides Environment=prod BucketName=acme-reports-prod \
  --capabilities CAPABILITY_NAMED_IAM \
  --no-fail-on-empty-changeset

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

Два флага здесь не украшение. --no-fail-on-empty-changeset обязателен в конвейере: без него повторный запуск без изменений завершается ошибкой, и конвейер краснеет там, где всё в порядке. --capabilities нужен, когда шаблон создаёт роли и политики: CloudFormation требует явного подтверждения, что вы знаете о создании прав (CAPABILITY_IAM, а если имена ролей заданы явно — CAPABILITY_NAMED_IAM).

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

Развёртывание упало: как читать события

Половина работы с CloudFormation — разбор неудачного развёртывания, и он делается одинаково всегда.

Когда создание или обновление не удаётся, стек сам откатывается в предыдущее состояние (ROLLBACK_IN_PROGRESS, затем ROLLBACK_COMPLETE или UPDATE_ROLLBACK_COMPLETE). Это хорошая новость с неприятным следствием: к моменту, когда вы посмотрели в консоль, сломанного ресурса уже нет, и что именно не получилось — видно только в событиях.

aws cloudformation describe-stack-events --stack-name my-first-stack \
  --query 'StackEvents[?ResourceStatus==`CREATE_FAILED` || ResourceStatus==`UPDATE_FAILED`].[Timestamp,LogicalResourceId,ResourceStatusReason]' \
  --output table

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

Три частые ситуации, которые стоит узнавать по виду:

ROLLBACK_COMPLETE у нового стека. Стек создался неудачно и остался в этом состоянии. Обновить его нельзя — только удалить и создать заново. Это нормально и часто сбивает: «исправил шаблон, а обновление отказывается идти».

UPDATE_ROLLBACK_FAILED. Откат тоже не удался (например, ресурс уже изменили руками). Стек застревает, и лечится это продолжением откатки с пропуском проблемных ресурсов (continue-update-rollback --resources-to-skip) — после чего расхождение приводят в порядок вручную.

Стек висит в CREATE_IN_PROGRESS полчаса. Обычно это ресурс, который ждёт чего-то внешнего: сертификат ждёт подтверждения домена, база создаётся долго по своей природе, пользовательский ресурс не отвечает. У ресурсов есть свои таймауты; у пользовательских ресурсов истечение таймаута — самая частая причина зависания стека.

И привычка, которая экономит время: при отладке шаблона отключают автоматический откат (--disable-rollback или --on-failure DO_NOTHING). Тогда сломанный ресурс остаётся на месте, и можно посмотреть, что с ним не так, вместо того чтобы читать про него в событиях.

Функции шаблона и как разбить большой шаблон

Чтобы шаблон не был набором жёстко зашитых строк, в CloudFormation есть intrinsic functions (встроенные функции) — они подставляют значения на лету. В YAML у каждой есть короткая запись через !.

  • !Ref — подставить значение параметра или сам ресурс (например, имя созданного хранилища).
  • !GetAtt — взять атрибут ресурса, которого нет в Ref: например, !GetAtt MyBucket.Arn — полный идентификатор хранилища.
  • !Sub — собрать строку с подстановками: !Sub "${AWS::StackName}-bucket" вставит имя стека.
  • !Join — склеить список строк через разделитель: !Join ["-", [prod, bucket]] даст prod-bucket.

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

  • Nested stacks (вложенные стеки) — шаблон ссылается на другой шаблон как на ресурс типа AWS::CloudFormation::Stack, указывая его адрес в TemplateURL. Так большой шаблон разбивается на переиспользуемые куски (отдельно сеть, отдельно база).
Resources:
  NetworkStack:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: https://s3.amazonaws.com/my-templates/network.yaml

Тут важная деталь, без которой пример остаётся нерабочим: вложенный шаблон обязан лежать в хранилище объектов. Локальный путь в TemplateURL не принимается — CloudFormation читает файл сам, со своей стороны, и локальной папки у него нет. Значит, перед каждым развёртыванием вложенные шаблоны надо выкладывать в бакет и подставлять их адреса.

Делать это руками неудобно, и для этого есть отдельная команда:

aws cloudformation package \
  --template-file main.yaml \
  --s3-bucket acme-cfn-templates \
  --output-template-file main.packaged.yaml

aws cloudformation deploy --template-file main.packaged.yaml --stack-name prod ...

package проходит по шаблону, находит локальные ссылки (вложенные шаблоны, архивы с кодом функций, тела политик), выкладывает их в указанный бакет под именами с контрольной суммой и подставляет в копию шаблона настоящие адреса. Дальше развёртывают уже эту копию. Отсюда и бакет для шаблонов, который в таких проектах создают один раз и держат отдельно.

  • Cross-stack references (ссылки между стеками) — один стек публикует значение через Export в секции Outputs, другой забирает его функцией Fn::ImportValue. Удобно, когда сеть живёт в одном стеке, а серверы — в другом.
Outputs:
  VpcId:
    Value: !Ref MyVPC
    Export:
      Name: !Sub "${AWS::StackName}-VPCID"

У этого удобства есть цена, и она кусается. Пока хоть один стек импортирует значение, стек-источник им как будто пришпилен: экспорт нельзя ни переименовать, ни изменить, ни удалить, а сам стек-источник — удалить целиком. Чтобы что-то поменять, сначала убирают Fn::ImportValue из всех потребителей и обновляют их, и только потом трогают источник. Это самая известная ловушка ссылок между стеками.

  • StackSets (наборы стеков) — разворачивают один шаблон сразу во многих аккаунтах и регионах одной командой. Незаменимо в крупных организациях.
  • Drift detection (обнаружение расхождений) — встроенная проверка: кто-то поправил ресурс вручную в консоли, и реальность разошлась с шаблоном. CloudFormation покажет эти расхождения, чтобы вы вернули порядок.

Чего CloudFormation не покрывает

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

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

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

Чем затыкают дыру. Два механизма, и оба стоит знать по имени.

Пользовательский ресурс (Custom Resource) — тип ресурса, за которым стоит ваша функция. CloudFormation вызывает её на создание, обновление и удаление, а функция делает что угодно через обычный SDK и возвращает результат. Так описывают то, чего в CloudFormation нет: настройку чужого сервиса, вызов внешнего API, вычисление значения. Есть и готовая обёртка (AWS::CloudFormation::CustomResource или более удобные конструкции в CDK).

  DnsRecord:
    Type: Custom::ExternalDns
    Properties:
      ServiceToken: !GetAtt DnsFunction.Arn
      Name: api.example.com
      Value: !GetAtt Alb.DNSName

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

Реестр ресурсов (CloudFormation Registry) — более современный способ: свой тип ресурса регистрируется в аккаунте и дальше используется как штатный. Здесь же живут сторонние поставщики типов (в том числе для сервисов вне AWS). Это аккуратнее пользовательских ресурсов и дороже по усилиям.

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

Развёртывание во многие аккаунты: StackSets

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

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

Что нужно для работы. Два варианта, и это первое, что выбирают.

Через самостоятельные права (self-managed). В аккаунте-администраторе нужна роль администратора наборов, а в каждом целевом аккаунте — роль исполнения, которая доверяет администратору. Без этих ролей операция не начнётся. Роли создаются шаблонами, которые AWS публикует, но развернуть их в целевых аккаунтах надо заранее — то есть у задачи есть первый шаг вручную.

Через интеграцию с организацией (service-managed). Если аккаунты собраны в организацию, роли создаются автоматически, а целью можно указать не список аккаунтов, а организационное подразделение: новый аккаунт, добавленный в него, получает стек сам (это называется автоматическим развёртыванием). Это правильный путь, и требует он одной галочки — доверенного доступа CloudFormation в организации.

aws cloudformation create-stack-set --stack-set-name baseline \
  --template-body file://baseline.yaml --permission-model SERVICE_MANAGED \
  --auto-deployment Enabled=true,RetainStacksOnAccountRemoval=false

aws cloudformation create-stack-instances --stack-set-name baseline \
  --deployment-targets OrganizationalUnitIds=ou-abc1-1234abcd \
  --regions eu-central-1 eu-west-1

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

Проверка расхождений: как это работает на самом деле

Обнаружение расхождений (drift detection) заслуживает больше одного предложения, потому что у него есть особенности, из-за которых на него нельзя полагаться слепо.

aws cloudformation detect-stack-drift --stack-name prod-network
# вернёт идентификатор операции — она асинхронная

aws cloudformation describe-stack-drift-detection-status --stack-drift-detection-id <id>
aws cloudformation describe-stack-resource-drifts --stack-name prod-network \
  --stack-resource-drift-status-filters MODIFIED DELETED

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

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

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

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

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

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

CloudFormation встречается почти везде, где инфраструктура AWS управляется кодом, а не кликами: развёртывание сетей (networking), серверов (compute), бессерверных функций (serverless), баз данных (managed data). Шаблон обычно лежит в репозитории рядом с приложением, а развёртывание происходит автоматически из конвейера сборки — change set создаётся и применяется как шаг выкладки, что хорошо ложится на стратегии релизов.

Типичные ошибки новичков:

  • Правка ресурсов руками в консоли. Стоит включить привычку менять только шаблон. Ручные изменения создают drift, и следующее обновление стека может их затереть.
  • Обновление рабочего стека без change set. Легко случайно пересоздать базу. Сначала create-change-set и describe-change-set, потом execute-change-set.
  • Один гигантский шаблон. Когда файл переваливает за пару сотен строк, его пора резать на nested stacks или связывать через cross-stack. Есть и жёсткая граница: шаблон, переданный прямо в команду через --template-body, не может быть больше 51 200 байт. Дальше его кладут в S3 и передают ссылкой через --template-url.
  • Удаление того, что нельзя терять. Для критичных ресурсов (база, хранилище с данными) задают политику сохранения. Политик две, и путать их дорого: DeletionPolicy: Retain спасает ресурс, когда удаляют весь стек, а UpdateReplacePolicy: Retain — когда обновление решает пересоздать ресурс (то самое replacement, которым пугает раздел про change sets). Поставили только первую — и потеряли базу на обычном обновлении.

Что учить дальше. CloudFormation — это родной для AWS способ; параллельно стоит посмотреть на облачно-независимый Terraform и на AWS CDK, где инфраструктуру описывают обычным языком программирования (TypeScript, Python и другими), а CDK уже сам генерирует шаблон CloudFormation. Чтобы понять, как любой из этих инструментов хранит представление о развёрнутом, полезна статья про состояние и доставку. А если ваши ресурсы — это хранилища объектов, загляните в раздел про объектное хранилище.