CloudFormation allows provisioning and managing AWS resources with simple configuration files, which let us spend less time managing those resources and have more time to focus on our applications that run on AWS instead.
We can simply write a configuration template (YAML/JSON file) that describes the resources we need in our application (like EC2 instances, Dynamo DB tables, or having the entire app monitoring automated in CloudWatch). We do not need to manually create and configure individual AWS resources and figure out what is dependent on what, and more importantly, it is scalable so we can re-use the same template, with a bunch of parameters, and have the entire infrastructure replicated in different stages/environments.
Another important aspect of CloudFormation is that we have our infrastructure as code, which can be version controlled, reviewed and easily maintained.
As our infrastructure grows, common patterns can emerge, which can be separated into dedicated templates, and re-used later in other templates. A good example is load balancers and VPC network. There is another reason, that may look unimportant, but CloudFormation stacks have a limit, which is 200 resources per stack, which can be easily reached as our application grows. That is why nested stacks can be really useful.
A nested stack is a simple stack resource of type
AWS::CloudFormation::Stack. Nested stacks can have themselves contain other nested stacks, resulting in a hierarchy of stacks, as shown in the diagram on the right-hand side. There must be only one root stack, which is called parent.
Passing parameters to the nested stacks
One of the biggest challenges when having nested stacks is parameters exchange between stacks. Without parameters, it would be impossible to have robust and dynamic stacks, that are scalable and flexible.
The simplest example would be deploying the same CloudFormation stack to multiple stages, like beta, gamma and prod (dev, pre-prod, prod, or any other naming convention you prefer).
Depending on which stage you deploy your application, you may want to set different properties to certain resources. For example, in the development stage, you will not have the same traffic as prod, therefore you can fine-grain the resources for your needs, and prevent spending extra money for unused resources.
Another example is when an application is deployed to various regions, that have different traffic consumption and time spikes. For instance, an application may have 1 million users in Europe, but only 100 000 in Asia. Using stack parameters, allows you to reduce the resources you use in the latter region, which can significantly impact your finances.
Below is a code snippet, showing a simple use case where a DynamoDB table is created in a nested stack, that receives the stage parameter from the parent stack. Depending on which stage, at deploy time, we set different read and write capacity to our table resource.
In the parent stack, we define Stage parameter under the Properties section. We later pass the parameters to the nested stack, which is created from a template child_stack.yml, stored in an S3 bucket.
--- AWSTemplateFormatVersion: '2010-09-09' Description: Root stack Parameters: Stage: Type: String Default: beta AllowedValues: - beta - gamma - prod TestRegion: Type: String Resources: DynamoDBTablesStack: Type: AWS::CloudFormation::Stack Properties: TemplateURL: https://n47-cloudformation.s3.eu-central-1.amazonaws.com/child_stack.yml Parameters: Stage: Ref: Stage
In the nested stack, we define the Stage parameter, just like we did in the parent. If we do not define it here either, the creation will fail because the passed parameter (from the parent) is not recognized. Whatever parameters we pass to the nested stack, have to be defined in its template parameters.
--- AWSTemplateFormatVersion: '2010-09-09' Description: Nested stack Parameters: Stage: Type: String Default: beta AllowedValues: - beta - gamma - prod Mappings: UsersDDBReadWriteCapacityPerStage: beta: ReadCapacityUnits: 10 WriteCapacityUnits: 10 gamma: ReadCapacityUnits: 50 WriteCapacityUnits: 50 prod: ReadCapacityUnits: 500 WriteCapacityUnits: 1000 Resources: UserTable: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: user_id AttributeType: 'S' KeySchema: - AttributeName: user_id KeyType: HASH ProvisionedThroughput: ReadCapacityUnits: !FindInMap [UsersDDBReadWriteCapacityPerStage, !Ref Stage, ReadCapacityUnits] WriteCapacityUnits: !FindInMap [UsersDDBReadWriteCapacityPerStage, !Ref Stage, WriteCapacityUnits] TableName: Users
The Mappings section used in the child template is used for fetching the corresponding Read/Write capacity value at deploy time when the actual value for Stage parameter is available. More about Mappings can be found in the official documentation.
Output resources from nested stacks
Having many nested stacks usually implies cross-stack communication. This encourages more template code reuse.
We will do a simple illustration by extracting the DynamoDB table name we created in the nested stack before, and pass it as a parameter to a second nested stack, and also by exporting its value.
In order to expose resources from a stack, we need to define them in the Outputs section of the template. We start by adding an output resource, in the child stack, with logical id
UsersDDBTableName, and an export named
Outputs: UsersDDBTableName: # extract the table name from the arn Value: !Select [1, !Split ['/', !GetAtt UserTable.Arn]] Export: Name: UsersDDBTableExport
Note: For each AWS account,
Export names must be unique within a region.
Then we create a second nested stack, which will contain two DynamoDB tables, one named
UsersWithParameter and the second one
UsersWithImportValue. The former is created by passing the table name from the first child stack as a parameter, and the latter by importing the value that has been exported
(Note, that this is just an example to showcase the two options to access resources between stacks, and is no real-world scenario)
For that, we added this stack definition in the root’s stack resources:
SecondChild: Type: AWS::CloudFormation::Stack Properties: TemplateURL: https://n47-cloudformation.s3.eu-central-1.amazonaws.com/child_stack_2.yml Parameters: TableName: Fn::GetAtt: - DynamoDBTablesStack - Outputs.UsersDDBTableName
Below is the entire content of the second child stack:
--- AWSTemplateFormatVersion: '2010-09-09' Description: Nested stack Parameters: TableName: Type: String Resources: UserTableWithParameter: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: customer_id AttributeType: 'S' KeySchema: - AttributeName: customer_id KeyType: HASH ProvisionedThroughput: ReadCapacityUnits: 1 WriteCapacityUnits: 1 TableName: !Join ['', [!Ref TableName, 'WithParameter'] ] UserTableWithImportValue: Type: AWS::DynamoDB::Table Properties: AttributeDefinitions: - AttributeName: customer_id AttributeType: 'S' KeySchema: - AttributeName: customer_id KeyType: HASH ProvisionedThroughput: ReadCapacityUnits: 1 WriteCapacityUnits: 1 TableName: !Join ['', [!ImportValue UsersDDBTableExport, 'WithImportValue'] ]
Even though we achieved the same thing by using nested stacks outputs, and exporting values, there is a difference between them. When you do an export, the exporting value is accessible to external stacks, within the same region, on the other hand, nested stacks outputs can be only passed, as a parameter to the other nested stacks within the same parent.
- Cross-stack references across regions cannot be created. You can use the intrinsic function
Fn::ImportValueto import only values that have been exported within the same region
- You cannot delete a stack if another stack references one of its outputs
- You cannot modify or remove an output value that is referenced by another stack
Below are some screenshots from the AWS console, illustrating the created stacks, from the code snippets shared above:
Figure 1: root stack containing two nested stacks
Figure 2: first nested stack containing Users DynamoDB table
Figure 3: second nested stack containing UsersWithImportValue and UsersWithParameter DynamoDB tables
You can download the source templates here.
If you have any questions or feedback, feel free to comment here.