In my previous post, I have talked about deploying a CloudFormation nested stack. Here I will discuss how I made that nested stack work and the differences between a nested stack and cross-stack references.
Multi-Stack Architecture
Most simple projects will use a single CloudFormation stack. This has all the resources together in the same stack. All of the resources are isolated from other stacks and are created, updated and deleted together. None of the resources are shared between any other stacks.
There is nothing wrong with this single template approach for smaller projects. There is the potential to hit the 500 resource limit per stack, and if creating multiple similar stacks, each one would need to have the same resource duplicated per stack. Such as creating a VPC for each stack. That VPC resource would need to be copied into each stack, duplicating code.
The multi-stack architecture enables resources to be split up per stack, it also allows for resources to be shared between stacks. Multi-stack architecture can even allow for base resources to be created in one stack and shared with other stacks. Such as a VPC where other stacks will share the same VPC and build their resources onto that VPC.
Multi-Stack Architecture – Nested Stacks
Each nested stack will have a root and parent stack. The root and parent stack shown in the diagram as the top stack. The root stack is created manually, by either a human or software process. This stack spawns the VPC and SG stacks. It is important to be aware that the child stacks such as VPCStack or SGStack can also be parent stacks and spawn other child stacks. This will give a hierarchy with the root stack sat at the top.
Inside the VPCStack there are two resources, one for each child stack for the VPC and SG. These templates are stored in S3 and can be reused many times by other nested stacks. The nested stacks will create brand-new resources each time they are used. The VPC or SG will not be shared between stacks. Only the code to create the resource is shared, meaning less duplication in writing, but not in actual resources created in the AWS account.
There are two parameters referenced by the SGStack. These are outputs from the VPCStack, one for VPCId and the other for PublicSubnetA. Those output parameters are then imported into the SGStack. Outputs between child stacks are passed via the root or parent stack.
The stack forms a single solution and is lifecycle linked, meaning that all the resources are created, updated and deleted together. To share resources between stacks that are not lifecycle linked this would be cross-stack references.
Multi-Stack Architecture – Cross-Stack References
Cross-stack references are used when there is a requirement for resources to be shared between stacks. For example, creating a single shared VPC for multiple environments.
Stacks are isolated by default, however there is a way to reference parameters between stacks, although there are limitations to be aware of.
Values are exported to an exports list in the region the stack has been created in. The exports list is isolated per region and per account. Each value must be uniquely named and may then be imported into other stacks using Fn::ImportValaue
Below is an example of a cross-stack reference. This is the same as the nested stack, where there is a VPC created in one stack and a security group created in another stack. The VPC and a public subnet is passed from the VPC stack to the security group stack.
The VPC is being shared out to any other stacks that import the VPC ID parameter and use it.
The important part of the VPC Stack is the outputs, the Export
key
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 |
AWSTemplateFormatVersion: "2010-09-09" Description: "VPC and related resources" Parameters: Environment: Type: "String" Default: "dev" CloudFormation: Type: "String" Default: "true" Resources: EC2VPC: Type: "AWS::EC2::VPC" Properties: CidrBlock: "10.0.0.0/16" EnableDnsSupport: true EnableDnsHostnames: true InstanceTenancy: "default" Tags: - Key: "Environment" Value: !Ref Environment - Key: "Name" Value: "Pipeline" - Key: "CloudFormation" Value: !Ref CloudFormation EC2Subnet: Type: "AWS::EC2::Subnet" Properties: AvailabilityZone: !GetAtt EC2Subnet4.AvailabilityZone CidrBlock: "10.0.50.0/24" VpcId: !Ref EC2VPC MapPublicIpOnLaunch: false Tags: - Key: "CloudFormation" Value: !Ref CloudFormation - Key: "Environment" Value: !Ref Environment - Key: "Name" Value: "public_subnet" EC2Subnet2: Type: "AWS::EC2::Subnet" Properties: AvailabilityZone: !GetAtt EC2Subnet6.AvailabilityZone CidrBlock: "10.0.16.0/20" VpcId: !Ref EC2VPC MapPublicIpOnLaunch: false Tags: - Key: "CloudFormation" Value: !Ref CloudFormation - Key: "Environment" Value: !Ref Environment - Key: "Name" Value: !Sub "Pipeline-private-${EC2Subnet6.AvailabilityZone}" EC2Subnet3: Type: "AWS::EC2::Subnet" Properties: AvailabilityZone: !GetAtt EC2Subnet5.AvailabilityZone CidrBlock: "10.0.48.0/24" VpcId: !Ref EC2VPC MapPublicIpOnLaunch: false Tags: - Key: "CloudFormation" Value: !Ref CloudFormation - Key: "Environment" Value: !Ref Environment - Key: "Name" Value: "public_subnet" EC2Subnet4: Type: "AWS::EC2::Subnet" Properties: AvailabilityZone: !Sub "${AWS::Region}c" CidrBlock: "10.0.32.0/20" VpcId: !Ref EC2VPC MapPublicIpOnLaunch: false Tags: - Key: "CloudFormation" Value: !Ref CloudFormation - Key: "Environment" Value: !Ref Environment - Key: "Name" Value: !Sub "Pipeline-private-${AWS::Region}c" EC2Subnet5: Type: "AWS::EC2::Subnet" Properties: AvailabilityZone: !Sub "${AWS::Region}a" CidrBlock: "10.0.0.0/20" VpcId: !Ref EC2VPC MapPublicIpOnLaunch: false Tags: - Key: "CloudFormation" Value: !Ref CloudFormation - Key: "Environment" Value: !Ref Environment - Key: "Name" Value: !Sub "Pipeline-private-${AWS::Region}a" EC2Subnet6: Type: "AWS::EC2::Subnet" Properties: AvailabilityZone: !Sub "${AWS::Region}b" CidrBlock: "10.0.49.0/24" VpcId: !Ref EC2VPC MapPublicIpOnLaunch: false Tags: - Key: "CloudFormation" Value: !Ref CloudFormation - Key: "Environment" Value: !Ref Environment - Key: "Name" Value: "public_subnet" EC2InternetGateway: Type: "AWS::EC2::InternetGateway" Properties: Tags: - Key: "Environment" Value: !Ref Environment - Key: "Name" Value: "Pipeline" - Key: "CloudFormation" Value: !Ref CloudFormation EC2VPCGatewayAttachment: Type: "AWS::EC2::VPCGatewayAttachment" Properties: InternetGatewayId: !Ref EC2InternetGateway VpcId: !Ref EC2VPC EC2EIP: Type: "AWS::EC2::EIP" Properties: Domain: "vpc" Tags: - Key: "CloudFormation" Value: !Ref CloudFormation - Key: "Name" Value: !Sub "Pipeline-${EC2Subnet3.AvailabilityZone}" - Key: "Environment" Value: !Ref Environment EC2NatGateway: Type: "AWS::EC2::NatGateway" Properties: SubnetId: !Ref EC2Subnet3 Tags: - Key: "CloudFormation" Value: !Ref CloudFormation - Key: "Environment" Value: !Ref Environment - Key: "Name" Value: !Sub "Pipeline-${EC2Subnet3.AvailabilityZone}" AllocationId: !GetAtt EC2EIP.AllocationId EC2RouteTable: Type: "AWS::EC2::RouteTable" Properties: VpcId: !Ref EC2VPC Tags: - Key: "Name" Value: "Pipeline-private" - Key: "Environment" Value: !Ref Environment - Key: "CloudFormation" Value: !Ref CloudFormation EC2RouteTable3: Type: "AWS::EC2::RouteTable" Properties: VpcId: !Ref EC2VPC Tags: - Key: "CloudFormation" Value: !Ref CloudFormation - Key: "Name" Value: "Pipeline-public" - Key: "Environment" Value: !Ref Environment EC2Route: Type: "AWS::EC2::Route" Properties: DestinationCidrBlock: "0.0.0.0/0" NatGatewayId: !Ref EC2NatGateway RouteTableId: !Ref EC2RouteTable EC2Route2: Type: "AWS::EC2::Route" Properties: DestinationCidrBlock: "0.0.0.0/0" GatewayId: !Ref EC2InternetGateway RouteTableId: !Ref EC2RouteTable3 EC2SubnetRouteTableAssociation: Type: "AWS::EC2::SubnetRouteTableAssociation" Properties: RouteTableId: !Ref EC2RouteTable SubnetId: !Ref EC2Subnet4 EC2SubnetRouteTableAssociation2: Type: "AWS::EC2::SubnetRouteTableAssociation" Properties: RouteTableId: !Ref EC2RouteTable SubnetId: !Ref EC2Subnet5 EC2SubnetRouteTableAssociation3: Type: "AWS::EC2::SubnetRouteTableAssociation" Properties: RouteTableId: !Ref EC2RouteTable SubnetId: !Ref EC2Subnet2 EC2SubnetRouteTableAssociation4: Type: "AWS::EC2::SubnetRouteTableAssociation" Properties: RouteTableId: !Ref EC2RouteTable3 SubnetId: !Ref EC2Subnet3 EC2SubnetRouteTableAssociation5: Type: "AWS::EC2::SubnetRouteTableAssociation" Properties: RouteTableId: !Ref EC2RouteTable3 SubnetId: !Ref EC2Subnet6 EC2SubnetRouteTableAssociation6: Type: "AWS::EC2::SubnetRouteTableAssociation" Properties: RouteTableId: !Ref EC2RouteTable3 SubnetId: !Ref EC2Subnet Outputs: VPCId: Value: !Ref EC2VPC Export: Name: VPCId PrivateSubnetA: Value: !Ref EC2Subnet5 Export: Name: PrivateSubnetA PrivateSubnetB: Value: !Ref EC2Subnet2 Export: Name: PrivateSubnetB PrivateSubnetC: Value: !Ref EC2Subnet4 Export: Name: PrivateSubnetC PublicSubnetA: Value: !Ref EC2Subnet3 Export: Name: PublicSubnetA PublicSubnetB: Value: !Ref EC2Subnet6 Export: Name: PublicSubnetB PublicSubnetC: Value: !Ref EC2Subnet Export: Name: PublicSubnetC |
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
AWSTemplateFormatVersion: "2010-09-09" Description: "Security Groups" Parameters: Environment: Type: "String" Default: "dev" CloudFormation: Type: "String" Default: "true" VPCId: # Add this parameter Type: "String" Description: "The VPC ID for the Security Groups" PublicSubnetA: # Add this parameter Type: "String" Description: "The Public Subnet A for InstanceConnectEndpoint" Resources: EC2SecurityGroup: Type: "AWS::EC2::SecurityGroup" Properties: GroupDescription: "Security Group to allow SSH ports to instances" GroupName: !Sub "SSH Ingress-${VPCId}" Tags: - Key: "Environment" Value: !Ref Environment - Key: "CloudFormation" Value: !Ref CloudFormation - Key: "Name" Value: "SSH Ingress" VpcId: !ImportValue VPCId SecurityGroupIngress: - CidrIp: "2.120.215.110/32" Description: "SSH" FromPort: 22 IpProtocol: "tcp" ToPort: 22 - CidrIp: "3.16.146.0/29" Description: "SSH" FromPort: 22 IpProtocol: "tcp" ToPort: 22 - CidrIp: "10.0.0.0/8" IpProtocol: "-1" EC2SecurityGroup3: Type: "AWS::EC2::SecurityGroup" Properties: GroupDescription: "Security Group to allow web ports to instances" GroupName: !Sub "Web Ingress-${VPCId}" Tags: - Key: "CloudFormation" Value: !Ref CloudFormation - Key: "Name" Value: "Web Ingress" - Key: "Environment" Value: !Ref Environment VpcId: !ImportValue VPCId SecurityGroupEgress: - CidrIp: "0.0.0.0/0" Description: "All protocols" IpProtocol: "-1" - CidrIpv6: "::/0" Description: "All protocols" IpProtocol: "-1" EC2SecurityGroup2: Type: "AWS::EC2::SecurityGroup" Properties: GroupDescription: "Security group to allow port 80 from ALB" GroupName: !Sub "Web Ingress ALB-${VPCId}" Tags: - Key: "CloudFormation" Value: !Ref CloudFormation - Key: "Name" Value: "alb-allow-port-80" - Key: "Environment" Value: !Ref Environment VpcId: !ImportValue VPCId SecurityGroupIngress: - SourceSecurityGroupId: !Ref EC2SecurityGroup3 Description: "Allow port 80 from ALB" FromPort: 80 IpProtocol: "tcp" ToPort: 80 SecurityGroupEgress: - CidrIp: "0.0.0.0/0" Description: "All protocols" IpProtocol: "-1" - CidrIpv6: "::/0" Description: "All protocols" IpProtocol: "-1" EC2SecurityGroupIC: Type: "AWS::EC2::SecurityGroup" Properties: GroupDescription: "VPC" GroupName: "VPC" VpcId: !ImportValue VPCId SecurityGroupIngress: - CidrIp: "52.94.36.0/25" Description: "pl-fca24795 - com.amazonaws.firewall.regional-corp-only-eu-west-2" IpProtocol: "-1" - CidrIp: "10.0.0.0/8" IpProtocol: "-1" SecurityGroupEgress: - CidrIp: "0.0.0.0/0" IpProtocol: "-1" VPCendpointICA: Type: AWS::EC2::InstanceConnectEndpoint Properties: PreserveClientIp: false SecurityGroupIds: - !Ref EC2SecurityGroupIC SubnetId: !ImportValue PublicSubnetA |