Представьте, что вы хотите создать в облаке десяток ресурсов — хранилище, базу данных, сеть — и при этом ничего не кликать мышкой в консоли. Вы описываете желаемое в текстовом файле, отдаёте его 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. Чтобы понять, как любой из этих инструментов хранит представление о развёрнутом, полезна статья про состояние и доставку. А если ваши ресурсы — это хранилища объектов, загляните в раздел про объектное хранилище.