Вложенные стеки (Nested Stacks) в CloudFormation – это стеки, которые создаются из другого, “родительского”, стека используя AWS::CloudFormation::Stack
.
Основная идея использования вложенных стеков – избежать необходимости писать новый шаблон для ресурса, который используется в нескольких стеках.
Вместо этого – шаблон создаётся один раз, хранится в AWS S3 корзине, и при создании стеков – вы просто ссылаетесь на уже имеющийся шаблон. Например, вы можете использовать один шаблон для создания двух Load Balancer с разными параметрами и listeners, используя Conditions.
Документация тут>>>., и хороший пост тут>>>.
В этом посте:
- создадим рутовый стек: будет описывать используемые стеки, просто наш “скелет”
- стек с VPC
- стек к SecurityGroup
При этом стеки должны поддерживать передачу параметров, что бы можно было использовать шаблон/ы для Dev/Stage/Production окружений.
И в конце отдельно рассмотрим механизм импорта/экспорта парамметров между независимыми стеками.
Получившиеся в результате шаблоны можно посмотреть в Github.
Содержание
Pitfalls
- не удаляйте вложенные стеки вручную
- используйте версинирование шаблонов в S3
The Root stack
Начинаем с описания корневого стека, в котором будем описывать вложенные стеки, назовём его root-stack.json
:
{ "AWSTemplateFormatVersion" : "2010-09-09", "Description" : "AWS CloudFormation Root stack", "Resources" : { "VPCStack": { "Type": "AWS::CloudFormation::Stack", "Properties": { "TemplateURL": "network-stack.yml" } } } }
Тут мы описываем создание одного ресурса – AWS::CloudFormation::Stack
, которому в Properties
передаём путь к файлу шаблона для второго, дочернего, стека.
В TemplateURL
надо будет указать URL S3-корзины — сейчас обновим.
Создаём шаблон для вложенного стека – network-stack.yml
:
{ "AWSTemplateFormatVersion" : "2010-09-09", "Description" : "AWS CloudFormation Nested Network Stack", "Resources" : { "VPC" : { "Type" : "AWS::EC2::VPC", "Properties" : { "CidrBlock" : "11.0.0.0/16" } } } }
Шаблоны для вложенных стеков должны передаваться в виде ссылок на S3-корзину.
Создаём её:
[simterm]
$ aws s3api create-bucket --bucket eks-cloudformation --region eu-west-3 --create-bucket-configuration LocationConstraint=eu-west-3 --profile arseniy --region eu-west-3 { "Location": "http://eks-cloudformation.s3.amazonaws.com/" }
[/simterm]
Запоминаем «Location»: «http://eks-cloudformation.s3.amazonaws.com/» – сейчас пригодится.
Для такой корзины будет крайне полезно включить версинирование, что бы хранить копии предыдущих шаблонов на случай проблем.
Добавляем:
[simterm]
$ aws --region eu-west-3 --profile arseniy s3api put-bucket-versioning --bucket bttrm-eks-cloudformation --versioning-configuration Status=Enabled
[/simterm]
Загружаем файл network-stack.yml
в S3:
[simterm]
$ aws --profile arseniy --region eu-west-3 s3 cp network-stack.yml s3://bttrm-eks-cloudformationnn upload: ./network-stack.yml to s3://bttrm-eks-cloudformation/network-stack.yml
[/simterm]
Возвращемся к root-stack.json
, обновляем TemplateURL
:
{ "AWSTemplateFormatVersion" : "2010-09-09", "Description" : "AWS CloudFormation Root stack", "Resources" : { "VPCStack": { "Type": "AWS::CloudFormation::Stack", "Properties": { "TemplateURL": "https://bttrm-eks-cloudformation.s3.amazonaws.com/network-stack.yml" } } } }
Создаём стек:
[simterm]
$ aws cloudformation create-stack --stack-name nested-stacks-root --template-body file://root-stack.json --profile arseniy --region eu-west-3 { "StackId": "arn:aws:cloudformation:eu-west-3:534****385:stack/nested-stacks-root/6450c320-57ab-11ea-be30-0a9cc8c39c1c" }
[/simterm]
Проверяем:
CloudFormation создал стек nested-stacks-root, для которого создал дочерний стек с именем nested-stacks-root-VPCStack-1FTY8TI2PR2D2, в котором создал VPC:
AWS CloudFormation package && deploy
Что бы не загружать шаблон руками при каждом обновлении — используем AWS CLI CloudFormation package
и deploy
.
package
package
копирует указанные файлы шаблонов или каталог в AWS S3 корзину.
Обновляем файл root-stack.json
— меняем TemplateURL
для ресурса стека VPCStack
на локальный путь — относительный, или полный:
{ "AWSTemplateFormatVersion" : "2010-09-09", "Description" : "AWS CloudFormation Root stack", "Resources" : { "VPCStack": { "Type": "AWS::CloudFormation::Stack", "Properties": { "TemplateURL": "network-stack.yml" } } } }
Упаковываем шаблоны, и загружаем и в S3:
[simterm]
$ aws cloudformation package --template-file root-stack.json --output-template packed-nested-stacks.json --s3-bucket bttrm-eks-cloudformation --profile arseniy --region eu-west-3 --use-json Uploading to ce12898553365980827b9aa59a99426d.template 187 / 187.0 (100.00%) Successfully packaged artifacts and wrote output template to file packed-nested-stacks.json. Execute the following command to deploy the packaged template aws cloudformation deploy --template-file /home/setevoy/Work/devops/projects/EKS/roles/cloudformation/files/packed-nested-stacks.json --stack-name <YOUR STACK NAME>
[/simterm]
Тут:
- CLI загружает все шаблоны (артефакты), в том числе найденные в описании ресурсов шаблона
root-stack.json
в AWS S3 - обновляет в них
TemplateURL
, указывая вместо локальных путей URL к S3 корзине и файлу - возвращает сгенерированный шаблон, который потом можно применить с
deploy
Возвращаемый шаблон сохраняем локально в файл packed-nested-stacks.json
(указываем --use-json
, т.к. по дефолту будет использован YAML, см. пост What is: YAML — общий обзор, типы данных, YAML vs JSON и PyYAML).
Проверяем его содержимое:
{ "AWSTemplateFormatVersion": "2010-09-09", "Description": "AWS CloudFormation Root stack", "Resources": { "VPCStack": { "Type": "AWS::CloudFormation::Stack", "Properties": { "TemplateURL": "https://s3.eu-west-3.amazonaws.com/eks-cloudformation/ce12898553365980827b9aa59a99426d.template" } } } }
И файл https://s3.eu-west-3.amazonaws.com/eks-cloudformation/ce12898553365980827b9aa59a99426d.template:
[simterm]
$ aws --profile arseniy --region eu-west-3 s3 cp --quiet s3://eks-cloudformation/ce12898553365980827b9aa59a99426d.template /dev/stdout AWSTemplateFormatVersion: '2010-09-09' Description: AWS CloudFormation Nested Network Stack Resources: VPC: Type: AWS::EC2::VPC Properties: CidrBlock: 11.0.0.0/16
[/simterm]
deploy
После выполнения package — CLI вывел нам подсказку по следующему шагу:
[simterm]
... Execute the following command to deploy the packaged template aws cloudformation deploy --template-file /home/setevoy/Work/devops/projects/EKS/roles/cloudformation/files/packed-nested-stacks.json --stack-name <YOUR STACK NAME>
[/simterm]
Применяем его к уже созданному стеку:
[simterm]
$ aws --profile arseniy --region eu-west-3 cloudformation deploy --template-file packed-nested-stacks.json --stack-name nested-stacks-root Waiting for changeset to be created.. Waiting for stack create/update to complete Successfully created/updated stack - nested-stacks-root
[/simterm]
deploy
создал ChangeSet, и применил его к нашему рутовому стеку:
Nested stack – передача параметров
Сейчас в наших шаблонах параметров нет — исправляем.
Например, в рутовом стеке можем определить какие-то глобальные параметры.
Добавим передачу сети для создаваемой VPC.
В root-stack.json
добавляем Parameters
и дефолтное значение для VPCCIDRBlock
:
{ "AWSTemplateFormatVersion" : "2010-09-09", "Description" : "AWS CloudFormation Root stack", "Parameters": { "VPCCIDRBlock": { "Description": "VPC CidrBlock", "Type": "String", "Default": "11.0.0.0/16" } }, ...
А в Resources
для ресурса VPCStack
– добавляем Parameters
и параметр VPCID
, в который передаём значение из VPCCIDRBlock
, приводим шаблон к такому виду:
{ "AWSTemplateFormatVersion" : "2010-09-09", "Description" : "AWS CloudFormation Root stack", "Parameters": { "VPCCIDRBlock": { "Description": "VPC CidrBlock", "Type": "String", "Default": "11.0.0.0/16" } }, "Resources" : { "VPCStack": { "Type": "AWS::CloudFormation::Stack", "Properties": { "TemplateURL": "network-stack.yml", "Parameters": { "VPCCIDRBlock" : { "Ref": "VPCCIDRBlock" } } } } } }
В шаблоне network-stack.yml
добавляем использование этого параметра:
{ "AWSTemplateFormatVersion" : "2010-09-09", "Description" : "AWS CloudFormation Nested Network Stack", "Parameters": { "VPCCIDRBlock": { "Description": "VPC CidrBlock", "Type": "String" } }, "Resources" : { "VPC" : { "Type" : "AWS::EC2::VPC", "Properties" : { "CidrBlock" : { "Ref": "VPCCIDRBlock" } } } } }
Упаковываем в S3:
[simterm]
$ !518 aws cloudformation package --template-file root-stack.json --output-template packed-nested-stacks.json --s3-bucket bttrm-eks-cloudformation --profile arseniy --region eu-west-3 --use-json
[/simterm]
Деплоим:
[simterm]
$ aws --profile arseniy --region eu-west-3 cloudformation deploy --template-file packed-nested-stacks.json --stack-name nested-stacks-root --profile arseniy --region eu-west-3 --use-json
[/simterm]
Проверяем Parameters
вложенного стека:
VPCCIDRBlock
добавлен.
Nested stack Outputs
Кроме прочего, вложенные стеки позврляют использование Outputs
других стеков, используя Fn::GetAtt
.
Добавим стек SecurityGroupStack, в котором опишем SecurityGroup, которая будет получать VPC ID, используя Outputs
стека, создаваемого из network-stack.yml
.
При передаче Outputs
между стеками учитывайте, что их можно передавать только «вверх» по дереву вложенности стека.
Т.е. из стека VPCStack нельзя передать параметр прямо в стек SecurityGroupStack, но можно передать его “вверх” в рутовый стек, а потом использовать как параметр для другого дочернего стека.
Для этого:
- в стеке VPCStack (
network-stack.yml
) добавляемOutputs
, который выводит ID создаваемой VPC - в корневом стеке
root-stack.json
описываем создание нового стека с именем SecurityGroupStack, которому вParameters
передаём значение изOutputs
стека VPCStack (network-stack.yml
) - создаём новый стек SecurityGroupStack, в шаблоне которого используем
Parameters
>VPCID
network-stack.yml
Stack Outputs
Добавляем вывод ID создаваемой VPC:
... "Resources" : { "VPC" : { "Type" : "AWS::EC2::VPC", "Properties" : { "CidrBlock" : { "Ref": "VPCCIDRBlock" } } } }, "Outputs" : { "VPCID" : { "Description" : "EKS VPC ID", "Value" : { "Ref" : "VPC" } } } }
root-stack.json
Stack
В корневом шаблоне описываем создание второго вложенного стека с именем SecurityGroupStack, которому в Parameters
передаём значение для VPCID
из Outputs
стека VPCStack:
... "Resources" : { "VPCStack": { "Type": "AWS::CloudFormation::Stack", "Properties": { "TemplateURL": "network-stack.yml", "Parameters": { "VPCCIDRBlock" : { "Ref": "VPCCIDRBlock" } } } }, "SecurityGroupStack": { "Type": "AWS::CloudFormation::Stack", "Properties": { "TemplateURL": "sg-stack.yml", "Parameters": { "VPCID" : { "Fn::GetAtt": ["VPCStack", "Outputs.VPCID"] } } } } } ...
И создаём шаблон для SecurityGroups — sg-stack.yml
:
{ "AWSTemplateFormatVersion" : "2010-09-09", "Description" : "AWS CloudFormation SecurityGroups stack", "Parameters" : { "VPCID": { "Description": "Network Stack VPC ID", "Type": "String", } }, "Resources" : { "SecurityGroup": { "Type": "AWS::EC2::SecurityGroup", "Properties" : { "GroupDescription" : "Example SecurityGroup", "VpcId" : { "Ref": "VPCID" }, "SecurityGroupIngress" : [ { "Description": "Allow HTTP", "IpProtocol" : "tcp", "FromPort" : 80, "ToPort" : 80, "CidrIp" : "0.0.0.0/0" }, { "Description": "Allow HTTPS", "IpProtocol" : "tcp", "FromPort" : 443, "ToPort" : 443, "CidrIp" : "0.0.0.0/0" } ] } }, } }
Тут в "VpcId" : { "Ref": "VPCID" }
используем переданное значение, что бы SecuirtyGroup была подключена к создаваемой VPC.
Упаковываем, деплоим, проверяем стеки:
Новый стек создан.
Проверяем его параметры:
Всё на месте.
Template reuse
Как говорилось в начале, основная идея вложенных стеков – принцип модульности, когда мы можем использовать один и тот же файл шаблона для создания аналогичных ресурсов.
Предположим, нам требуется не одна, а две VPC с различными блоками адресов.
Используем CloudFormation Mappins
, в которой определим два блока адресов.
В шаблоне рутового стека root-stack.json
убираем "Parameters": "VPCCIDRBlock"
, и добавляем Mappings
:
{ "AWSTemplateFormatVersion" : "2010-09-09", "Description" : "AWS CloudFormation Root stack", "Mappings": { "VPCCIDRBlock": { "vpc1": { "cidr": "11.0.0.0/16" }, "vpc2": { "cidr": "12.0.0.0/16" } } }, ...
В Resources
добавляем создание ещё одного стека с VPC из того же шаблона, но в Parameters
используем Fn::FindInMap
для получения значений для Property VPCCIDRBlock
:
... "Resources" : { "VPCStack1": { "Type": "AWS::CloudFormation::Stack", "Properties": { "TemplateURL": "network-stack.yml", "Parameters": { "VPCCIDRBlock" : { "Fn::FindInMap" : [ "VPCCIDRBlock", "vpc1", "cidr" ] } } } }, "VPCStack2": { "Type": "AWS::CloudFormation::Stack", "Properties": { "TemplateURL": "network-stack.yml", "Parameters": { "VPCCIDRBlock" : { "Fn::FindInMap" : [ "VPCCIDRBlock", "vpc2", "cidr" ] } } } }, ...
И не забываем про ресурс SecurityGroup, который подключался к одной сети – добавляем создание второй SG используя тот же шаблон sg-stack.yml
, которую подключаем ко второй VPC:
... "SecurityGroupStack1": { "Type": "AWS::CloudFormation::Stack", "Properties": { "TemplateURL": "sg-stack.yml", "Parameters": { "VPCID" : { "Fn::GetAtt": ["VPCStack1", "Outputs.VPCID"] } } } }, "SecurityGroupStack2": { "Type": "AWS::CloudFormation::Stack", "Properties": { "TemplateURL": "sg-stack.yml", "Parameters": { "VPCID" : { "Fn::GetAtt": ["VPCStack2", "Outputs.VPCID"] } } } } ...
Деплоим, проверяем:
CloudFormation удалил стек VPCStack, и вместо него создал два новых – VPCStack1 и VPCStack2, аналогично – для SecurityGroup.
Import/export values vs Nested Stacks
Вариант с использованием Outputs
во вложенных стеках хорош, но он будет работать только для этого “дерева” стеков, и их нельзя использовать в других стеках этого же AWS-аккаунта.
Тут на помощь приходит другой функционал CloudFormation cross-stack reference – в одном стеке выполняется Export
данных, а в другом – их Import
.
Pitfalls
- експортированные данные доступны для импорта только в том же регионе
- нельзя удалить стек, который экспортирует данные, импортируемые другим стеком
Добавим экспорт, например – ID создаваемых SecurityGroups, что бы мы могли их потом использовать в других, независимых, стеках, а потмо создадим стек, который будет выводить эти данные в своём Outputs
.
Для этого в шаблоне стека с SecurityGroup sg-stack.yml
добавим вывод его ID в Outputs
:
... "ToPort" : 443, "CidrIp" : "0.0.0.0/0" } ] } }, }, "Outputs" : { "SecurityGroupID" : { "Description" : "The SecurityGroup ID", "Value" : { "Ref" : "SecurityGroup" } } } }
В рутовом шаблоне – в Outputs
добавляем Export
для обоих SecurityGroupStack-стеков:
... "Outputs" : { "SecurityGroup1" : { "Description" : "The SecurityGroup-1 ID", "Value" : { "Fn::GetAtt": [ "SecurityGroupStack1", "Outputs.SecurityGroupID" ] }, "Export": { "Name": { "Fn::Sub": "${AWS::StackName}-SecurityGroupStack1" } } }, "SecurityGroup2" : { "Description" : "The SecurityGroup-2 ID", "Value" : { "Fn::GetAtt": [ "SecurityGroupStack2", "Outputs.SecurityGroupID" ] }, "Export": { "Name": { "Fn::Sub": "${AWS::StackName}-SecurityGroupStack2" } } } } }
Тут:
- в
Value
получаем SecurityGroup ID изOutputs
стека - в
Export: Name
с помощьюFn::Sub
формируем екпортируемое имя, которое должно быть уникально во всём аккаунте.
Деплоим, проверяем Outputs
стека с SecurityGroup:
В Outputs
рутового стека – находим Exported:
И они же доступны в Exports всего CloudFormation:
Теперь можем использовать их при создании других стеков.
Для проверки создадим отдельный стек с одним ресурсом, и в его Outputs
через Fn::ImportValue
выведем SecurityGroups IDs из стека nested-stacks-root:
{ "AWSTemplateFormatVersion" : "2010-09-09", "Description" : "AWS CloudFormation Nested Network stack", "Parameters": { "VPCCIDRBlock": { "Description": "VPC CidrBlock", "Type": "String", "Default": "13.0.0.0/16" } }, "Resources" : { "VPC" : { "Type" : "AWS::EC2::VPC", "Properties" : { "CidrBlock" : { "Ref": "VPCCIDRBlock" } } } }, "Outputs" : { "SecurityGroup1ID" : { "Description" : "The SecurityGroup ID", "Value" : { "Fn::ImportValue" : "nested-stacks-root-SecurityGroupStack1" } }, "SecurityGroup2ID" : { "Description" : "The SecurityGroup ID", "Value" : { "Fn::ImportValue" : "nested-stacks-root-SecurityGroupStack2" } } } }
Деплоим стек (только надо было назвать его не nested-, а external-), и проверяем его Outputs
:
Готово.
Ссылки по теме
- Working with Nested Stacks
- AWS CloudFormation Best Practices
- CloudFormation Nested Stacks Primer
- Walkthrough with Nested CloudFormation Stacks
- Understanding Nested CloudFormation Stacks
- Use Nested Stacks to Create Reusable Templates and Support Role Specialization
- CloudFormation Best-Practices
- CloudFormation package & deploy
- 7 Awesome CloudFormation Hacks
- Using CloudFormation Cross-Stack References
- How can I reference a resource in another stack from an AWS CloudFormation template?
- How do I use multiple values for individual parameters in an AWS CloudFormation template?
- How do I pass CommaDelimitedList parameters to nested stacks in AWS CloudFormation?
- Two years with CloudFormation: lessons learned