AWS: CloudFormation — вложенные стеки и Import/Export параметров

Автор: | 02/28/2020
 

Вложенные стеки (Nested Stacks) в CloudFormation — это стеки, которые создаются из другого, «родительского», стека используя AWS::CloudFormation::Stack.

Основная идея использования вложенных стеков — избежать необходимости писать новый шаблон для ресурса, который используется в нескольких стеках.

Вместо этого — шаблон создаётся один раз, хранится в AWS S3 корзине, и при создании стеков — вы просто ссылаетесь на уже имеющийся шаблон. Например, вы можете использовать один шаблон для создания двух Load Balancer с разными параметрами и listeners, используя Conditions.

Документация тут>>>., и хороший пост тут>>>.

В этом посте:

  1. создадим рутовый стек: будет описывать используемые стеки, просто наш «скелет»
  2. стек с VPC
  3. стек к SecurityGroup

При этом стеки должны поддерживать передачу параметров, что бы можно было использовать шаблон/ы для Dev/Stage/Production окружений.

И в конце отдельно рассмотрим механизм импорта/экспорта парамметров между независимыми стеками.

Получившиеся в результате шаблоны можно посмотреть в Github.

Pitfalls

  1. не удаляйте вложенные стеки вручную
  2. используйте версинирование шаблонов в 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-корзину.

Создаём её:

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/"
}

Запоминаем «Location»: «http://eks-cloudformation.s3.amazonaws.com/» — сейчас пригодится.

Для такой корзины будет крайне полезно включить версинирование, что бы хранить копии предыдущих шаблонов на случай проблем.

Добавляем:

aws --region eu-west-3 --profile arseniy s3api put-bucket-versioning --bucket bttrm-eks-cloudformation --versioning-configuration Status=Enabled

Загружаем файл network-stack.yml в S3:

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

Возвращемся к 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"
      }
    }
  }
}

Создаём стек:

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"
}

Проверяем:

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:

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>

Тут:

  1. CLI загружает все шаблоны (артефакты), в том числе найденные в описании ресурсов шаблона root-stack.json в AWS S3
  2. обновляет в них TemplateURL, указывая вместо локальных путей URL к S3 корзине и файлу
  3. возвращает сгенерированный шаблон, который потом можно применить с 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:

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

deploy

После выполнения package — CLI вывел нам подсказку по следующему шагу:

...
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>

Применяем его к уже созданному стеку:

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

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:

!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

Деплоим:

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

Проверяем Parameters вложенного стека:

VPCCIDRBlock добавлен.

Nested stack Outputs

Кроме прочего, вложенные стеки позврляют использование Outputs других стеков, используя Fn::GetAtt.

Добавим стек SecurityGroupStack, в котором опишем SecurityGroup, которая будет получать VPC ID, используя Outputs стека, создаваемого из network-stack.yml.

При передаче Outputs между стеками учитывайте, что их можно передавать только «вверх» по дереву вложенности стека.

Т.е. из стека VPCStack нельзя передать параметр прямо в стек SecurityGroupStack, но можно передать его «вверх» в рутовый стек, а потом использовать как параметр для другого дочернего стека.

Для этого:

  1. в стеке VPCStack (network-stack.yml) добавляем Outputs, который выводит ID создаваемой VPC
  2. в корневом стеке root-stack.json описываем создание нового стека с именем SecurityGroupStack, которому в Parameters передаём значение из Outputs стека VPCStack (network-stack.yml)
  3. создаём новый стек 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:

Готово.

Ссылки по теме