Вложенные стеки (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










