Backup, wait a minute...

Backup, wait a minute…

 13.08.2018, last updated 05.03.2021 -  Jeremy T. Bouse -  ~7 Minutes

So in my earlier Getting started with Amazon Web Services post, I had laid out my 2-tier VPC solution that setup a Public and Private tier of subnets across three Availability Zones. I’d also mentioned that the subnets could make use of the Fn::Cidr  function to simplify the CIDR block assignments within your CloudFormation template and that I’d discuss that in another post. Well, welcome to that post and a whole lot more!

As I went back to re-work my template to use the Fn::Cidr function I realized that I had only partially handled setting up the VPC to be dual stacked to support both IPv4 and IPv6. I had also noticed that I’d done a rather poor job with keeping their subnets for the respective tiers contiguous to make ACLs simpler by the way I’d ordered the subnets in my template. I also wanted to be able to have the flexibility to make use of 2, 3 or 4 AZs without having to maintain a whole lot of templates that I would then need to keep in sync when I’d make any changes. I had also mentioned setting up a 3-tier VPC solution that included a Data tier of subnets but I didn’t actually have a sample template written and available.

So I set out to correct the shortcomings I found and put together a more complete solution. The unfortunate side effect was that my existing CloudFormation stack for my VPC was not able to be updated due to the CIDR block changes which necessitated deleting the stack and recreating it with the new template. The good news was that I’m using templates that I already had so the process was quick and fairly painless with limited downtime.

---
AWSTemplateFormatVersion: '2010-09-09'
Description: 'VPC: Public and Private subnets'
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
- Label:
default: 'VPC Parameters'
Parameters:
- ClassB
- DualStack
- NumberAZs
Parameters:
ClassB:
Description: 'Class B of VPC (10.XXX.0.0/16)'
Type: Number
Default: 0
ConstraintDescription: 'Must be in the range [0-255]'
MinValue: 0
MaxValue: 255
DualStack:
Description: 'Request IPv6 CIDR block'
Type: String
Default: 'false'
AllowedValues: ['true', 'false']
NumberAZs:
Description: 'Number of Availability Zones'
Type: Number
Default: 2
AllowedValues: [2, 3, 4]
Conditions:
Have4AZs: !Equals [!Ref NumberAZs, 4]
Have3AZs: !Or [!Condition Have4AZs, !Equals [!Ref NumberAZs, 3]]
DualStacked: !Equals [!Ref DualStack, 'true']
DualStacked3AZs: !And [!Condition DualStacked, !Condition Have3AZs]
DualStacked4AZs: !And [!Condition DualStacked, !Condition Have4AZs]
Resources:
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Sub "10.${ClassB}.0.0/16"
EnableDnsSupport: true
EnableDnsHostnames: true
InstanceTenancy: default
Tags:
- Key: Name
Value: !Ref 'AWS::StackName'
VPCv6CIDR:
Condition: DualStacked
Type: AWS::EC2::VPCCidrBlock
Properties:
AmazonProvidedIpv6CidrBlock: !Ref DualStack
VpcId: !Ref VPC
InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: !Sub "10.${ClassB}.0.0/16"
EgressOnlyInternetGateway:
Condition: DualStacked
Type: AWS::EC2::EgressOnlyInternetGateway
Properties:
VpcId: !Ref VPC
VPCGatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref VPC
InternetGatewayId: !Ref InternetGateway
# Setup Subnet for each tier
# - Cut off a /20 IPv4 CIDR block for each Subnet from the /16 VPC CIDR
# - If DualStack, cut off a /64 IPv6 CIDR block for each Subnet from /56 VPCv6CIDR
# - Each tier can be referenced by a single /18 IPv4 CIDR block
# - If DualStack, each tier can be referenced by a single /66 IPv6 CIDR block
SubnetAPublic:
Type: AWS::EC2::Subnet
Properties:
AssignIpv6AddressOnCreation: !If [DualStacked, !Ref DualStack, !Ref 'AWS::NoValue']
AvailabilityZone: !Select [0, {'Fn::GetAZs': !Ref 'AWS::Region'}]
CidrBlock: !Select [0, !Cidr [!GetAtt VPC.CidrBlock, 12, 12]]
Ipv6CidrBlock: !If [DualStacked, !Select [0, !Cidr [!Select [0, !GetAtt VPC.Ipv6CidrBlocks], 12, 64]], !Ref 'AWS::NoValue']
VpcId: !Ref VPC
Tags:
- Key: Name
Value: A public
- Key: Reach
Value: public
SubnetAPrivate:
Type: AWS::EC2::Subnet
Properties:
AssignIpv6AddressOnCreation: !If [DualStacked, !Ref DualStack, !Ref 'AWS::NoValue']
AvailabilityZone: !Select [0, {'Fn::GetAZs': !Ref 'AWS::Region'}]
CidrBlock: !Select [4, !Cidr [!GetAtt VPC.CidrBlock, 12, 12]]
Ipv6CidrBlock: !If [DualStacked, !Select [4, !Cidr [!Select [0, !GetAtt VPC.Ipv6CidrBlocks], 12, 64]], !Ref 'AWS::NoValue']
VpcId: !Ref VPC
Tags:
- Key: Name
Value: A private
- Key: Reach
Value: private
SubnetBPublic:
Type: AWS::EC2::Subnet
Properties:
AssignIpv6AddressOnCreation: !If [DualStacked, !Ref DualStack, !Ref 'AWS::NoValue']
AvailabilityZone: !Select [1, {'Fn::GetAZs': !Ref 'AWS::Region'}]
CidrBlock: !Select [1, !Cidr [!GetAtt VPC.CidrBlock, 12, 12]]
Ipv6CidrBlock: !If [DualStacked, !Select [1, !Cidr [!Select [0, !GetAtt VPC.Ipv6CidrBlocks], 12, 64]], !Ref 'AWS::NoValue']
VpcId: !Ref VPC
Tags:
- Key: Name
Value: B public
- Key: Reach
Value: public
SubnetBPrivate:
Type: AWS::EC2::Subnet
Properties:
AssignIpv6AddressOnCreation: !If [DualStacked, !Ref DualStack, !Ref 'AWS::NoValue']
AvailabilityZone: !Select [1, {'Fn::GetAZs': !Ref 'AWS::Region'}]
CidrBlock: !Select [5, !Cidr [!GetAtt VPC.CidrBlock, 12, 12]]
Ipv6CidrBlock: !If [DualStacked, !Select [5, !Cidr [!Select [0, !GetAtt VPC.Ipv6CidrBlocks], 12, 64]], !Ref 'AWS::NoValue']
VpcId: !Ref VPC
Tags:
- Key: Name
Value: B private
- Key: Reach
Value: private
SubnetCPublic:
Condition: Have3AZs
Type: AWS::EC2::Subnet
Properties:
AssignIpv6AddressOnCreation: !If [DualStacked, !Ref DualStack, !Ref 'AWS::NoValue']
AvailabilityZone: !Select [2, {'Fn::GetAZs': !Ref 'AWS::Region'}]
CidrBlock: !Select [2, !Cidr [!GetAtt VPC.CidrBlock, 12, 12]]
Ipv6CidrBlock: !If [DualStacked, !Select [2, !Cidr [!Select [0, !GetAtt VPC.Ipv6CidrBlocks], 12, 64]], !Ref 'AWS::NoValue']
VpcId: !Ref VPC
Tags:
- Key: Name
Value: C public
- Key: Reach
Value: public
SubnetCPrivate:
Condition: Have3AZs
Type: AWS::EC2::Subnet
Properties:
AssignIpv6AddressOnCreation: !If [DualStacked, !Ref DualStack, !Ref 'AWS::NoValue']
AvailabilityZone: !Select [2, {'Fn::GetAZs': !Ref 'AWS::Region'}]
CidrBlock: !Select [6, !Cidr [!GetAtt VPC.CidrBlock, 12, 12]]
Ipv6CidrBlock: !If [DualStacked, !Select [6, !Cidr [!Select [0, !GetAtt VPC.Ipv6CidrBlocks], 12, 64]], !Ref 'AWS::NoValue']
VpcId: !Ref VPC
Tags:
- Key: Name
Value: C private
- Key: Reach
Value: private
SubnetDPublic:
Condition: Have4AZs
Type: AWS::EC2::Subnet
Properties:
AssignIpv6AddressOnCreation: !If [DualStacked, !Ref DualStack, !Ref 'AWS::NoValue']
AvailabilityZone: !Select [3, {'Fn::GetAZs': !Ref 'AWS::Region'}]
CidrBlock: !Select [3, !Cidr [!GetAtt VPC.CidrBlock, 12, 12]]
Ipv6CidrBlock: !If [DualStacked, !Select [3, !Cidr [!Select [0, !GetAtt VPC.Ipv6CidrBlocks], 12, 64]], !Ref 'AWS::NoValue']
VpcId: !Ref VPC
Tags:
- Key: Name
Value: D public
- Key: Reach
Value: public
SubnetDPrivate:
Condition: Have4AZs
Type: AWS::EC2::Subnet
Properties:
AssignIpv6AddressOnCreation: !If [DualStacked, !Ref DualStack, !Ref 'AWS::NoValue']
AvailabilityZone: !Select [3, {'Fn::GetAZs': !Ref 'AWS::Region'}]
CidrBlock: !Select [7, !Cidr [!GetAtt VPC.CidrBlock, 12, 12]]
Ipv6CidrBlock: !If [DualStacked, !Select [7, !Cidr [!Select [0, !GetAtt VPC.Ipv6CidrBlocks], 12, 64]], !Ref 'AWS::NoValue']
VpcId: !Ref VPC
Tags:
- Key: Name
Value: D private
- Key: Reach
Value: private
# Setup a RouteTable for each Subnet
RouteTableAPublic:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: A Public
RouteTableAPrivate:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: A Private
RouteTableBPublic:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: B Public
RouteTableBPrivate:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: B Private
RouteTableCPublic:
Condition: Have3AZs
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: C Public
RouteTableCPrivate:
Condition: Have3AZs
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: C Private
RouteTableDPublic:
Condition: Have4AZs
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: D Public
RouteTableDPrivate:
Condition: Have4AZs
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: D Private
# Associate each Subnet with the correct RouteTable
RouteTableAssociationAPublic:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref SubnetAPublic
RouteTableId: !Ref RouteTableAPublic
RouteTableAssociationAPrivate:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref SubnetAPrivate
RouteTableId: !Ref RouteTableAPrivate
RouteTableAssociationBPublic:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref SubnetBPublic
RouteTableId: !Ref RouteTableBPublic
RouteTableAssociationBPrivate:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref SubnetBPrivate
RouteTableId: !Ref RouteTableBPrivate
RouteTableAssociationCPublic:
Condition: Have3AZs
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref SubnetCPublic
RouteTableId: !Ref RouteTableCPublic
RouteTableAssociationCPrivate:
Condition: Have3AZs
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref SubnetCPrivate
RouteTableId: !Ref RouteTableCPrivate
RouteTableAssociationDPublic:
Condition: Have4AZs
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref SubnetDPublic
RouteTableId: !Ref RouteTableDPublic
RouteTableAssociationDPrivate:
Condition: Have4AZs
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref SubnetDPrivate
RouteTableId: !Ref RouteTableDPrivate
# Setup default routes for each RouteTable
# - InternetGateway for Public Subnets
# - EgressOnlyInternetGateway for Private Subnets if DualStack
RouteTablePublicAInternetRoute:
Type: AWS::EC2::Route
DependsOn: VPCGatewayAttachment
Properties:
RouteTableId: !Ref RouteTableAPublic
DestinationCidrBlock: '0.0.0.0/0'
GatewayId: !Ref InternetGateway
RouteTablePublicAInternetRouteIpv6:
Condition: DualStacked
Type: AWS::EC2::Route
DependsOn: VPCGatewayAttachment
Properties:
RouteTableId: !Ref RouteTableAPublic
DestinationIpv6CidrBlock: '::/0'
GatewayId: !Ref InternetGateway
RouteTablePrivateAInternetRouteIpv6:
Condition: DualStacked
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref RouteTableAPrivate
DestinationIpv6CidrBlock: '::/0'
EgressOnlyInternetGatewayId: !Ref EgressOnlyInternetGateway
RouteTablePublicBInternetRoute:
Type: AWS::EC2::Route
DependsOn: VPCGatewayAttachment
Properties:
RouteTableId: !Ref RouteTableBPublic
DestinationCidrBlock: '0.0.0.0/0'
GatewayId: !Ref InternetGateway
RouteTablePublicBInternetRouteIpv6:
Condition: DualStacked
Type: AWS::EC2::Route
DependsOn: VPCGatewayAttachment
Properties:
RouteTableId: !Ref RouteTableBPublic
DestinationIpv6CidrBlock: '::/0'
GatewayId: !Ref InternetGateway
RouteTablePrivateBInternetRouteIpv6:
Condition: DualStacked
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref RouteTableBPrivate
DestinationIpv6CidrBlock: '::/0'
EgressOnlyInternetGatewayId: !Ref EgressOnlyInternetGateway
RouteTablePublicCInternetRoute:
Condition: Have3AZs
Type: AWS::EC2::Route
DependsOn: VPCGatewayAttachment
Properties:
RouteTableId: !Ref RouteTableCPublic
DestinationCidrBlock: '0.0.0.0/0'
GatewayId: !Ref InternetGateway
RouteTablePublicCInternetRouteIpv6:
Condition: DualStacked3AZs
Type: AWS::EC2::Route
DependsOn: VPCGatewayAttachment
Properties:
RouteTableId: !Ref RouteTableCPublic
DestinationIpv6CidrBlock: '::/0'
GatewayId: !Ref InternetGateway
RouteTablePrivateCInternetRouteIpv6:
Condition: DualStacked3AZs
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref RouteTableCPrivate
DestinationIpv6CidrBlock: '::/0'
EgressOnlyInternetGatewayId: !Ref EgressOnlyInternetGateway
RouteTablePublicDInternetRoute:
Condition: Have4AZs
Type: AWS::EC2::Route
DependsOn: VPCGatewayAttachment
Properties:
RouteTableId: !Ref RouteTableDPublic
DestinationCidrBlock: '0.0.0.0/0'
GatewayId: !Ref InternetGateway
RouteTablePublicDInternetRouteIpv6:
Condition: DualStacked4AZs
Type: AWS::EC2::Route
DependsOn: VPCGatewayAttachment
Properties:
RouteTableId: !Ref RouteTableDPublic
DestinationIpv6CidrBlock: '::/0'
GatewayId: !Ref InternetGateway
RouteTablePrivateDInternetRouteIpv6:
Condition: DualStacked4AZs
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref RouteTableDPrivate
DestinationIpv6CidrBlock: '::/0'
EgressOnlyInternetGatewayId: !Ref EgressOnlyInternetGateway
# Setup our VPC Gateway Endpoints
EndpointS3:
Type: AWS::EC2::VPCEndpoint
Properties:
RouteTableIds:
- !Ref RouteTableAPrivate
- !Ref RouteTableBPrivate
- !If [Have3AZs, !Ref RouteTableCPrivate, !Ref 'AWS::NoValue']
- !If [Have4AZs, !Ref RouteTableDPrivate, !Ref 'AWS::NoValue']
ServiceName: !Sub "com.amazonaws.${AWS::Region}.s3"
VpcId: !Ref VPC
EndpointDynamoDB:
Type: AWS::EC2::VPCEndpoint
Properties:
RouteTableIds:
- !Ref RouteTableAPrivate
- !Ref RouteTableBPrivate
- !If [Have3AZs, !Ref RouteTableCPrivate, !Ref 'AWS::NoValue']
- !If [Have4AZs, !Ref RouteTableDPrivate, !Ref 'AWS::NoValue']
ServiceName: !Sub "com.amazonaws.${AWS::Region}.dynamodb"
VpcId: !Ref VPC
# Setup NetworkAcl for each tier
NetworkAclPublic:
Type: AWS::EC2::NetworkAcl
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: Public
NetworkAclPrivate:
Type: AWS::EC2::NetworkAcl
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: Private
# Associate each Subnet with the respective tier NetworkAcl
SubnetNetworkAclAssociationAPublic:
Type: AWS::EC2::SubnetNetworkAclAssociation
Properties:
SubnetId: !Ref SubnetAPublic
NetworkAclId: !Ref NetworkAclPublic
SubnetNetworkAclAssociationAPrivate:
Type: AWS::EC2::SubnetNetworkAclAssociation
Properties:
SubnetId: !Ref SubnetAPrivate
NetworkAclId: !Ref NetworkAclPrivate
SubnetNetworkAclAssociationBPublic:
Type: AWS::EC2::SubnetNetworkAclAssociation
Properties:
SubnetId: !Ref SubnetBPublic
NetworkAclId: !Ref NetworkAclPublic
SubnetNetworkAclAssociationBPrivate:
Type: AWS::EC2::SubnetNetworkAclAssociation
Properties:
SubnetId: !Ref SubnetBPrivate
NetworkAclId: !Ref NetworkAclPrivate
SubnetNetworkAclAssociationCPublic:
Condition: Have3AZs
Type: AWS::EC2::SubnetNetworkAclAssociation
Properties:
SubnetId: !Ref SubnetCPublic
NetworkAclId: !Ref NetworkAclPublic
SubnetNetworkAclAssociationCPrivate:
Condition: Have3AZs
Type: AWS::EC2::SubnetNetworkAclAssociation
Properties:
SubnetId: !Ref SubnetCPrivate
NetworkAclId: !Ref NetworkAclPrivate
SubnetNetworkAclAssociationDPublic:
Condition: Have4AZs
Type: AWS::EC2::SubnetNetworkAclAssociation
Properties:
SubnetId: !Ref SubnetDPublic
NetworkAclId: !Ref NetworkAclPublic
SubnetNetworkAclAssociationDPrivate:
Condition: Have4AZs
Type: AWS::EC2::SubnetNetworkAclAssociation
Properties:
SubnetId: !Ref SubnetDPrivate
NetworkAclId: !Ref NetworkAclPrivate
# Setup NetworkAclEntry for each tier
# - Allow all traffic inbound & outbound for Public & Private tier
NetworkAclEntryInPublicAllowAll:
Type: AWS::EC2::NetworkAclEntry
Properties:
NetworkAclId: !Ref NetworkAclPublic
RuleNumber: 100
Protocol: -1
RuleAction: allow
Egress: false
CidrBlock: '0.0.0.0/0'
NetworkAclEntryOutPublicAllowAll:
Type: AWS::EC2::NetworkAclEntry
Properties:
NetworkAclId: !Ref NetworkAclPublic
RuleNumber: 100
Protocol: -1
RuleAction: allow
Egress: true
CidrBlock: '0.0.0.0/0'
NetworkAclEntryInPublicAllowAllIpv6:
Condition: DualStacked
Type: AWS::EC2::NetworkAclEntry
Properties:
NetworkAclId: !Ref NetworkAclPublic
RuleNumber: 101
Protocol: -1
RuleAction: allow
Egress: false
Ipv6CidrBlock: '::/0'
NetworkAclEntryOutPublicAllowAllIpv6:
Condition: DualStacked
Type: AWS::EC2::NetworkAclEntry
Properties:
NetworkAclId: !Ref NetworkAclPublic
RuleNumber: 101
Protocol: -1
RuleAction: allow
Egress: true
Ipv6CidrBlock: '::/0'
NetworkAclEntryInPrivateAllowAll:
Type: AWS::EC2::NetworkAclEntry
Properties:
NetworkAclId: !Ref NetworkAclPrivate
RuleNumber: 100
Protocol: -1
RuleAction: allow
Egress: false
CidrBlock: '0.0.0.0/0'
NetworkAclEntryOutPrivateAllowAll:
Type: AWS::EC2::NetworkAclEntry
Properties:
NetworkAclId: !Ref NetworkAclPrivate
RuleNumber: 100
Protocol: -1
RuleAction: allow
Egress: true
CidrBlock: '0.0.0.0/0'
NetworkAclEntryInPrivateAllowAllIpv6:
Condition: DualStacked
Type: AWS::EC2::NetworkAclEntry
Properties:
NetworkAclId: !Ref NetworkAclPrivate
RuleNumber: 101
Protocol: -1
RuleAction: allow
Egress: false
Ipv6CidrBlock: '::/0'
NetworkAclEntryOutPrivateAllowAllIpv6:
Condition: DualStacked
Type: AWS::EC2::NetworkAclEntry
Properties:
NetworkAclId: !Ref NetworkAclPrivate
RuleNumber: 101
Protocol: -1
RuleAction: allow
Egress: true
Ipv6CidrBlock: '::/0'
Outputs:
StackName:
Description: 'Stack name'
Value: !Ref 'AWS::StackName'
AZs:
Description: 'AZs'
Value: !Ref NumberAZs
Export:
Name: !Sub "${AWS::StackName}-AZs"
AZA:
Description: 'AZ of A'
Value: !GetAtt SubnetAPublic.AvailabilityZone
Export:
Name: !Sub "${AWS::StackName}-AZA"
AZB:
Description: 'AZ of B'
Value: !GetAtt SubnetBPublic.AvailabilityZone
Export:
Name: !Sub "${AWS::StackName}-AZB"
AZC:
Condition: Have3AZs
Description: 'AZ of C'
Value: !GetAtt SubnetCPublic.AvailabilityZone
Export:
Name: !Sub "${AWS::StackName}-AZC"
AZD:
Condition: Have4AZs
Description: 'AZ of D'
Value: !GetAtt SubnetDPublic.AvailabilityZone
Export:
Name: !Sub "${AWS::StackName}-AZD"
ClassB:
Description: 'Class B.'
Value: !Ref ClassB
Export:
Name: !Sub "${AWS::StackName}-ClassB"
VPC:
Description: 'VPC.'
Value: !Ref VPC
Export:
Name: !Sub "${AWS::StackName}-VPC"
SubnetsPublic:
Description: 'Subnets public.'
Value:
Fn::Join:
- ','
- - !Ref SubnetAPublic
- !Ref SubnetBPublic
- !If [Have3AZs, !Ref SubnetCPublic, !Ref 'AWS::NoValue']
- !If [Have4AZs, !Ref SubnetDPublic, !Ref 'AWS::NoValue']
Export:
Name: !Sub "${AWS::StackName}-SubnetsPublic"
SubnetsPrivate:
Description: 'Subnets private.'
Value:
Fn::Join:
- ','
- - !Ref SubnetAPrivate
- !Ref SubnetBPrivate
- !If [Have3AZs, !Ref SubnetCPrivate, !Ref 'AWS::NoValue']
- !If [Have4AZs, !Ref SubnetDPrivate, !Ref 'AWS::NoValue']
Export:
Name: !Sub "${AWS::StackName}-SubnetsPrivate"
RouteTablesPublic:
Description: 'Route tables public.'
Value:
Fn::Join:
- ','
- - !Ref RouteTableAPublic
- !Ref RouteTableBPublic
- !If [Have3AZs, !Ref RouteTableCPublic, !Ref 'AWS::NoValue']
- !If [Have4AZs, !Ref RouteTableDPublic, !Ref 'AWS::NoValue']
Export:
Name: !Sub "${AWS::StackName}-RouteTablesPublic"
RouteTablesPrivate:
Description: 'Route tables private.'
Value:
Fn::Join:
- ','
- - !Ref RouteTableAPrivate
- !Ref RouteTableBPrivate
- !If [Have3AZs, !Ref RouteTableCPrivate, !Ref 'AWS::NoValue']
- !If [Have4AZs, !Ref RouteTableDPrivate, !Ref 'AWS::NoValue']
Export:
Name: !Sub "${AWS::StackName}-RouteTablesPrivate"
SubnetAPublic:
Description: 'Subnet A public.'
Value: !Ref SubnetAPublic
Export:
Name: !Sub "${AWS::StackName}-SubnetAPublic"
RouteTableAPublic:
Description: 'Route table A public.'
Value: !Ref RouteTableAPublic
Export:
Name: !Sub "${AWS::StackName}-RouteTableAPublic"
SubnetAPrivate:
Description: 'Subnet A private.'
Value: !Ref SubnetAPrivate
Export:
Name: !Sub "${AWS::StackName}-SubnetAPrivate"
RouteTableAPrivate:
Description: 'Route table A private.'
Value: !Ref RouteTableAPrivate
Export:
Name: !Sub "${AWS::StackName}-RouteTableAPrivate"
SubnetBPublic:
Description: 'Subnet B public.'
Value: !Ref SubnetBPublic
Export:
Name: !Sub "${AWS::StackName}-SubnetBPublic"
RouteTableBPublic:
Description: 'Route table B public.'
Value: !Ref RouteTableBPublic
Export:
Name: !Sub "${AWS::StackName}-RouteTableBPublic"
SubnetBPrivate:
Description: 'Subnet B private.'
Value: !Ref SubnetBPrivate
Export:
Name: !Sub "${AWS::StackName}-SubnetBPrivate"
RouteTableBPrivate:
Description: 'Route table B private.'
Value: !Ref RouteTableBPrivate
Export:
Name: !Sub "${AWS::StackName}-RouteTableBPrivate"
SubnetCPublic:
Condition: Have3AZs
Description: 'Subnet C public.'
Value: !Ref SubnetCPublic
Export:
Name: !Sub "${AWS::StackName}-SubnetCPublic"
RouteTableCPublic:
Condition: Have3AZs
Description: 'Route table C public.'
Value: !Ref RouteTableCPublic
Export:
Name: !Sub "${AWS::StackName}-RouteTableCPublic"
SubnetCPrivate:
Condition: Have3AZs
Description: 'Subnet C private.'
Value: !Ref SubnetCPrivate
Export:
Name: !Sub "${AWS::StackName}-SubnetCPrivate"
RouteTableCPrivate:
Condition: Have3AZs
Description: 'Route table C private.'
Value: !Ref RouteTableCPrivate
Export:
Name: !Sub "${AWS::StackName}-RouteTableCPrivate"
SubnetDPublic:
Condition: Have4AZs
Description: 'Subnet D public.'
Value: !Ref SubnetDPublic
Export:
Name: !Sub "${AWS::StackName}-SubnetDPublic"
RouteTableDPublic:
Condition: Have4AZs
Description: 'Route table D public.'
Value: !Ref RouteTableDPublic
Export:
Name: !Sub "${AWS::StackName}-RouteTableDPublic"
SubnetDPrivate:
Condition: Have4AZs
Description: 'Subnet D private.'
Value: !Ref SubnetDPrivate
Export:
Name: !Sub "${AWS::StackName}-SubnetDPrivate"
RouteTableDPrivate:
Condition: Have4AZs
Description: 'Route table D private.'
Value: !Ref RouteTableDPrivate
Export:
Name: !Sub "${AWS::StackName}-RouteTableDPrivate"
EndpointS3:
Description: 'The VPC endpoint to S3.'
Value: !Ref EndpointS3
Export:
Name: !Sub "${AWS::StackName}-EndpointS3"
EndpointDynamoDB:
Description: 'The VPC endpoint to DynamoDB.'
Value: !Ref EndpointDynamoDB
Export:
Name: !Sub "${AWS::StackName}-EndpointDynamoDB"
view raw vpc-2tier.yaml hosted with ❤ by GitHub

Here you can see the new 2-tier VPC template in all it’s grandeur, but seeing as how we covered much of it’s design aspects in the previous post I’ll talk more about the 3-tier VPC template and the additions to it from the 2-tier. So let’s have a look at the new 3-tier template to start with.

---
AWSTemplateFormatVersion: '2010-09-09'
Description: 'VPC: Public, Private and Data subnets'
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
- Label:
default: 'VPC Parameters'
Parameters:
- ClassB
- DualStack
- NumberAZs
Parameters:
ClassB:
Description: 'Class B of VPC (10.XXX.0.0/16)'
Type: Number
Default: 0
ConstraintDescription: 'Must be in the range [0-255]'
MinValue: 0
MaxValue: 255
DualStack:
Description: 'Request IPv6 CIDR block'
Type: String
Default: 'false'
AllowedValues: ['true', 'false']
NumberAZs:
Description: 'Number of Availability Zones'
Type: Number
Default: 2
AllowedValues: [2, 3, 4]
Conditions:
Have4AZs: !Equals [!Ref NumberAZs, 4]
Have3AZs: !Or [!Condition Have4AZs, !Equals [!Ref NumberAZs, 3]]
DualStacked: !Equals [!Ref DualStack, 'true']
DualStacked3AZs: !And [!Condition DualStacked, !Condition Have3AZs]
DualStacked4AZs: !And [!Condition DualStacked, !Condition Have4AZs]
Resources:
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Sub "10.${ClassB}.0.0/16"
EnableDnsSupport: true
EnableDnsHostnames: true
InstanceTenancy: default
Tags:
- Key: Name
Value: !Ref 'AWS::StackName'
VPCv6CIDR:
Condition: DualStacked
Type: AWS::EC2::VPCCidrBlock
Properties:
AmazonProvidedIpv6CidrBlock: !Ref DualStack
VpcId: !Ref VPC
InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: !Sub "10.${ClassB}.0.0/16"
EgressOnlyInternetGateway:
Condition: DualStacked
Type: AWS::EC2::EgressOnlyInternetGateway
Properties:
VpcId: !Ref VPC
VPCGatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref VPC
InternetGatewayId: !Ref InternetGateway
# Setup Subnet for each tier
# - Cut off a /20 IPv4 CIDR block for each Subnet from the /16 VPC CIDR
# - If DualStack, cut off a /64 IPv6 CIDR block for each Subnet from /56 VPCv6CIDR
# - Each tier can be referenced by a single /18 IPv4 CIDR block
# - If DualStack, each tier can be referenced by a single /66 IPv6 CIDR block
SubnetAPublic:
Type: AWS::EC2::Subnet
Properties:
AssignIpv6AddressOnCreation: !If [DualStacked, !Ref DualStack, !Ref 'AWS::NoValue']
AvailabilityZone: !Select [0, {'Fn::GetAZs': !Ref 'AWS::Region'}]
CidrBlock: !Select [0, !Cidr [!GetAtt VPC.CidrBlock, 12, 12]]
Ipv6CidrBlock: !If [DualStacked, !Select [0, !Cidr [!Select [0, !GetAtt VPC.Ipv6CidrBlocks], 12, 64]], !Ref 'AWS::NoValue']
VpcId: !Ref VPC
Tags:
- Key: Name
Value: A public
- Key: Reach
Value: public
SubnetAPrivate:
Type: AWS::EC2::Subnet
Properties:
AssignIpv6AddressOnCreation: !If [DualStacked, !Ref DualStack, !Ref 'AWS::NoValue']
AvailabilityZone: !Select [0, {'Fn::GetAZs': !Ref 'AWS::Region'}]
CidrBlock: !Select [4, !Cidr [!GetAtt VPC.CidrBlock, 12, 12]]
Ipv6CidrBlock: !If [DualStacked, !Select [4, !Cidr [!Select [0, !GetAtt VPC.Ipv6CidrBlocks], 12, 64]], !Ref 'AWS::NoValue']
VpcId: !Ref VPC
Tags:
- Key: Name
Value: A private
- Key: Reach
Value: private
SubnetAData:
Type: AWS::EC2::Subnet
Properties:
AssignIpv6AddressOnCreation: !If [DualStacked, !Ref DualStack, !Ref 'AWS::NoValue']
AvailabilityZone: !Select [0, {'Fn::GetAZs': !Ref 'AWS::Region'}]
CidrBlock: !Select [8, !Cidr [!GetAtt VPC.CidrBlock, 12, 12]]
Ipv6CidrBlock: !If [DualStacked, !Select [8, !Cidr [!Select [0, !GetAtt VPC.Ipv6CidrBlocks], 12, 64]], !Ref 'AWS::NoValue']
VpcId: !Ref VPC
Tags:
- Key: Name
Value: A data
- Key: Reach
Value: data
SubnetBPublic:
Type: AWS::EC2::Subnet
Properties:
AssignIpv6AddressOnCreation: !If [DualStacked, !Ref DualStack, !Ref 'AWS::NoValue']
AvailabilityZone: !Select [1, {'Fn::GetAZs': !Ref 'AWS::Region'}]
CidrBlock: !Select [1, !Cidr [!GetAtt VPC.CidrBlock, 12, 12]]
Ipv6CidrBlock: !If [DualStacked, !Select [1, !Cidr [!Select [0, !GetAtt VPC.Ipv6CidrBlocks], 12, 64]], !Ref 'AWS::NoValue']
VpcId: !Ref VPC
Tags:
- Key: Name
Value: B public
- Key: Reach
Value: public
SubnetBPrivate:
Type: AWS::EC2::Subnet
Properties:
AssignIpv6AddressOnCreation: !If [DualStacked, !Ref DualStack, !Ref 'AWS::NoValue']
AvailabilityZone: !Select [1, {'Fn::GetAZs': !Ref 'AWS::Region'}]
CidrBlock: !Select [5, !Cidr [!GetAtt VPC.CidrBlock, 12, 12]]
Ipv6CidrBlock: !If [DualStacked, !Select [5, !Cidr [!Select [0, !GetAtt VPC.Ipv6CidrBlocks], 12, 64]], !Ref 'AWS::NoValue']
VpcId: !Ref VPC
Tags:
- Key: Name
Value: B private
- Key: Reach
Value: private
SubnetBData:
Type: AWS::EC2::Subnet
Properties:
AssignIpv6AddressOnCreation: !If [DualStacked, !Ref DualStack, !Ref 'AWS::NoValue']
AvailabilityZone: !Select [1, {'Fn::GetAZs': !Ref 'AWS::Region'}]
CidrBlock: !Select [9, !Cidr [!GetAtt VPC.CidrBlock, 12, 12]]
Ipv6CidrBlock: !If [DualStacked, !Select [9, !Cidr [!Select [0, !GetAtt VPC.Ipv6CidrBlocks], 12, 64]], !Ref 'AWS::NoValue']
VpcId: !Ref VPC
Tags:
- Key: Name
Value: B data
- Key: Reach
Value: data
SubnetCPublic:
Condition: Have3AZs
Type: AWS::EC2::Subnet
Properties:
AssignIpv6AddressOnCreation: !If [DualStacked, !Ref DualStack, !Ref 'AWS::NoValue']
AvailabilityZone: !Select [2, {'Fn::GetAZs': !Ref 'AWS::Region'}]
CidrBlock: !Select [2, !Cidr [!GetAtt VPC.CidrBlock, 12, 12]]
Ipv6CidrBlock: !If [DualStacked, !Select [2, !Cidr [!Select [0, !GetAtt VPC.Ipv6CidrBlocks], 12, 64]], !Ref 'AWS::NoValue']
VpcId: !Ref VPC
Tags:
- Key: Name
Value: C public
- Key: Reach
Value: public
SubnetCPrivate:
Condition: Have3AZs
Type: AWS::EC2::Subnet
Properties:
AssignIpv6AddressOnCreation: !If [DualStacked, !Ref DualStack, !Ref 'AWS::NoValue']
AvailabilityZone: !Select [2, {'Fn::GetAZs': !Ref 'AWS::Region'}]
CidrBlock: !Select [6, !Cidr [!GetAtt VPC.CidrBlock, 12, 12]]
Ipv6CidrBlock: !If [DualStacked, !Select [6, !Cidr [!Select [0, !GetAtt VPC.Ipv6CidrBlocks], 12, 64]], !Ref 'AWS::NoValue']
VpcId: !Ref VPC
Tags:
- Key: Name
Value: C private
- Key: Reach
Value: private
SubnetCData:
Condition: Have3AZs
Type: AWS::EC2::Subnet
Properties:
AssignIpv6AddressOnCreation: !If [DualStacked, !Ref DualStack, !Ref 'AWS::NoValue']
AvailabilityZone: !Select [2, {'Fn::GetAZs': !Ref 'AWS::Region'}]
CidrBlock: !Select [10, !Cidr [!GetAtt VPC.CidrBlock, 12, 12]]
Ipv6CidrBlock: !If [DualStacked, !Select [10, !Cidr [!Select [0, !GetAtt VPC.Ipv6CidrBlocks], 12, 64]], !Ref 'AWS::NoValue']
VpcId: !Ref VPC
Tags:
- Key: Name
Value: C data
- Key: Reach
Value: data
SubnetDPublic:
Condition: Have4AZs
Type: AWS::EC2::Subnet
Properties:
AssignIpv6AddressOnCreation: !If [DualStacked, !Ref DualStack, !Ref 'AWS::NoValue']
AvailabilityZone: !Select [3, {'Fn::GetAZs': !Ref 'AWS::Region'}]
CidrBlock: !Select [3, !Cidr [!GetAtt VPC.CidrBlock, 12, 12]]
Ipv6CidrBlock: !If [DualStacked, !Select [3, !Cidr [!Select [0, !GetAtt VPC.Ipv6CidrBlocks], 12, 64]], !Ref 'AWS::NoValue']
VpcId: !Ref VPC
Tags:
- Key: Name
Value: D public
- Key: Reach
Value: public
SubnetDPrivate:
Condition: Have4AZs
Type: AWS::EC2::Subnet
Properties:
AssignIpv6AddressOnCreation: !If [DualStacked, !Ref DualStack, !Ref 'AWS::NoValue']
AvailabilityZone: !Select [3, {'Fn::GetAZs': !Ref 'AWS::Region'}]
CidrBlock: !Select [7, !Cidr [!GetAtt VPC.CidrBlock, 12, 12]]
Ipv6CidrBlock: !If [DualStacked, !Select [7, !Cidr [!Select [0, !GetAtt VPC.Ipv6CidrBlocks], 12, 64]], !Ref 'AWS::NoValue']
VpcId: !Ref VPC
Tags:
- Key: Name
Value: D private
- Key: Reach
Value: private
SubnetDData:
Condition: Have4AZs
Type: AWS::EC2::Subnet
Properties:
AssignIpv6AddressOnCreation: !If [DualStacked, !Ref DualStack, !Ref 'AWS::NoValue']
AvailabilityZone: !Select [3, {'Fn::GetAZs': !Ref 'AWS::Region'}]
CidrBlock: !Select [11, !Cidr [!GetAtt VPC.CidrBlock, 12, 12]]
Ipv6CidrBlock: !If [DualStacked, !Select [11, !Cidr [!Select [0, !GetAtt VPC.Ipv6CidrBlocks], 12, 64]], !Ref 'AWS::NoValue']
VpcId: !Ref VPC
Tags:
- Key: Name
Value: D data
- Key: Reach
Value: data
# Setup a RouteTable for each Subnet
RouteTableAPublic:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: A Public
RouteTableAPrivate:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: A Private
RouteTableAData:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: A Data
RouteTableBPublic:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: B Public
RouteTableBPrivate:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: B Private
RouteTableBData:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: B Data
RouteTableCPublic:
Condition: Have3AZs
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: C Public
RouteTableCPrivate:
Condition: Have3AZs
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: C Private
RouteTableCData:
Condition: Have3AZs
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: C Data
RouteTableDPublic:
Condition: Have4AZs
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: D Public
RouteTableDPrivate:
Condition: Have4AZs
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: D Private
RouteTableDData:
Condition: Have4AZs
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: D Data
# Associate each Subnet with the correct RouteTable
RouteTableAssociationAPublic:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref SubnetAPublic
RouteTableId: !Ref RouteTableAPublic
RouteTableAssociationAPrivate:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref SubnetAPrivate
RouteTableId: !Ref RouteTableAPrivate
RouteTableAssociationAData:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref SubnetAData
RouteTableId: !Ref RouteTableAData
RouteTableAssociationBPublic:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref SubnetBPublic
RouteTableId: !Ref RouteTableBPublic
RouteTableAssociationBPrivate:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref SubnetBPrivate
RouteTableId: !Ref RouteTableBPrivate
RouteTableAssociationBData:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref SubnetBData
RouteTableId: !Ref RouteTableBData
RouteTableAssociationCPublic:
Condition: Have3AZs
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref SubnetCPublic
RouteTableId: !Ref RouteTableCPublic
RouteTableAssociationCPrivate:
Condition: Have3AZs
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref SubnetCPrivate
RouteTableId: !Ref RouteTableCPrivate
RouteTableAssociationCData:
Condition: Have3AZs
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref SubnetCData
RouteTableId: !Ref RouteTableCData
RouteTableAssociationDPublic:
Condition: Have4AZs
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref SubnetDPublic
RouteTableId: !Ref RouteTableDPublic
RouteTableAssociationDPrivate:
Condition: Have4AZs
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref SubnetDPrivate
RouteTableId: !Ref RouteTableDPrivate
RouteTableAssociationDData:
Condition: Have4AZs
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref SubnetDData
RouteTableId: !Ref RouteTableDData
# Setup default routes for each RouteTable
# - InternetGateway for Public Subnets
# - EgressOnlyInternetGateway for Private & Data Subnets if DualStack
RouteTablePublicAInternetRoute:
Type: AWS::EC2::Route
DependsOn: VPCGatewayAttachment
Properties:
RouteTableId: !Ref RouteTableAPublic
DestinationCidrBlock: '0.0.0.0/0'
GatewayId: !Ref InternetGateway
RouteTablePublicAInternetRouteIpv6:
Condition: DualStacked
Type: AWS::EC2::Route
DependsOn: VPCGatewayAttachment
Properties:
RouteTableId: !Ref RouteTableAPublic
DestinationIpv6CidrBlock: '::/0'
GatewayId: !Ref InternetGateway
RouteTablePrivateAInternetRouteIpv6:
Condition: DualStacked
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref RouteTableAPrivate
DestinationIpv6CidrBlock: '::/0'
EgressOnlyInternetGatewayId: !Ref EgressOnlyInternetGateway
RouteTableDataAInternetRouteIpv6:
Condition: DualStacked
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref RouteTableAData
DestinationIpv6CidrBlock: '::/0'
EgressOnlyInternetGatewayId: !Ref EgressOnlyInternetGateway
RouteTablePublicBInternetRoute:
Type: AWS::EC2::Route
DependsOn: VPCGatewayAttachment
Properties:
RouteTableId: !Ref RouteTableBPublic
DestinationCidrBlock: '0.0.0.0/0'
GatewayId: !Ref InternetGateway
RouteTablePublicBInternetRouteIpv6:
Condition: DualStacked
Type: AWS::EC2::Route
DependsOn: VPCGatewayAttachment
Properties:
RouteTableId: !Ref RouteTableBPublic
DestinationIpv6CidrBlock: '::/0'
GatewayId: !Ref InternetGateway
RouteTablePrivateBInternetRouteIpv6:
Condition: DualStacked
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref RouteTableBPrivate
DestinationIpv6CidrBlock: '::/0'
EgressOnlyInternetGatewayId: !Ref EgressOnlyInternetGateway
RouteTableDataBInternetRouteIpv6:
Condition: DualStacked
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref RouteTableBData
DestinationIpv6CidrBlock: '::/0'
EgressOnlyInternetGatewayId: !Ref EgressOnlyInternetGateway
RouteTablePublicCInternetRoute:
Condition: Have3AZs
Type: AWS::EC2::Route
DependsOn: VPCGatewayAttachment
Properties:
RouteTableId: !Ref RouteTableCPublic
DestinationCidrBlock: '0.0.0.0/0'
GatewayId: !Ref InternetGateway
RouteTablePublicCInternetRouteIpv6:
Condition: DualStacked3AZs
Type: AWS::EC2::Route
DependsOn: VPCGatewayAttachment
Properties:
RouteTableId: !Ref RouteTableCPublic
DestinationIpv6CidrBlock: '::/0'
GatewayId: !Ref InternetGateway
RouteTablePrivateCInternetRouteIpv6:
Condition: DualStacked3AZs
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref RouteTableCPrivate
DestinationIpv6CidrBlock: '::/0'
EgressOnlyInternetGatewayId: !Ref EgressOnlyInternetGateway
RouteTableDataCInternetRouteIpv6:
Condition: DualStacked3AZs
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref RouteTableCData
DestinationIpv6CidrBlock: '::/0'
EgressOnlyInternetGatewayId: !Ref EgressOnlyInternetGateway
RouteTablePublicDInternetRoute:
Condition: Have4AZs
Type: AWS::EC2::Route
DependsOn: VPCGatewayAttachment
Properties:
RouteTableId: !Ref RouteTableDPublic
DestinationCidrBlock: '0.0.0.0/0'
GatewayId: !Ref InternetGateway
RouteTablePublicDInternetRouteIpv6:
Condition: DualStacked4AZs
Type: AWS::EC2::Route
DependsOn: VPCGatewayAttachment
Properties:
RouteTableId: !Ref RouteTableDPublic
DestinationIpv6CidrBlock: '::/0'
GatewayId: !Ref InternetGateway
RouteTablePrivateDInternetRouteIpv6:
Condition: DualStacked4AZs
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref RouteTableDPrivate
DestinationIpv6CidrBlock: '::/0'
EgressOnlyInternetGatewayId: !Ref EgressOnlyInternetGateway
RouteTableDataDInternetRouteIpv6:
Condition: DualStacked4AZs
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref RouteTableDData
DestinationIpv6CidrBlock: '::/0'
EgressOnlyInternetGatewayId: !Ref EgressOnlyInternetGateway
# Setup our VPC Gateway Endpoints
EndpointS3:
Type: AWS::EC2::VPCEndpoint
Properties:
RouteTableIds:
- !Ref RouteTableAPrivate
- !Ref RouteTableBPrivate
- !If [Have3AZs, !Ref RouteTableCPrivate, !Ref 'AWS::NoValue']
- !If [Have4AZs, !Ref RouteTableDPrivate, !Ref 'AWS::NoValue']
- !Ref RouteTableAData
- !Ref RouteTableBData
- !If [Have3AZs, !Ref RouteTableCData, !Ref 'AWS::NoValue']
- !If [Have4AZs, !Ref RouteTableDData, !Ref 'AWS::NoValue']
ServiceName: !Sub "com.amazonaws.${AWS::Region}.s3"
VpcId: !Ref VPC
EndpointDynamoDB:
Type: AWS::EC2::VPCEndpoint
Properties:
RouteTableIds:
- !Ref RouteTableAPrivate
- !Ref RouteTableBPrivate
- !If [Have3AZs, !Ref RouteTableCPrivate, !Ref 'AWS::NoValue']
- !If [Have4AZs, !Ref RouteTableDPrivate, !Ref 'AWS::NoValue']
- !Ref RouteTableAData
- !Ref RouteTableBData
- !If [Have3AZs, !Ref RouteTableCData, !Ref 'AWS::NoValue']
- !If [Have4AZs, !Ref RouteTableDData, !Ref 'AWS::NoValue']
ServiceName: !Sub "com.amazonaws.${AWS::Region}.dynamodb"
VpcId: !Ref VPC
# Setup NetworkAcl for each tier
NetworkAclPublic:
Type: AWS::EC2::NetworkAcl
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: Public
NetworkAclPrivate:
Type: AWS::EC2::NetworkAcl
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: Private
NetworkAclData:
Type: AWS::EC2::NetworkAcl
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: Data
# Associate each Subnet with the respective tier NetworkAcl
SubnetNetworkAclAssociationAPublic:
Type: AWS::EC2::SubnetNetworkAclAssociation
Properties:
SubnetId: !Ref SubnetAPublic
NetworkAclId: !Ref NetworkAclPublic
SubnetNetworkAclAssociationAPrivate:
Type: AWS::EC2::SubnetNetworkAclAssociation
Properties:
SubnetId: !Ref SubnetAPrivate
NetworkAclId: !Ref NetworkAclPrivate
SubnetNetworkAclAssociationAData:
Type: AWS::EC2::SubnetNetworkAclAssociation
Properties:
SubnetId: !Ref SubnetAData
NetworkAclId: !Ref NetworkAclData
SubnetNetworkAclAssociationBPublic:
Type: AWS::EC2::SubnetNetworkAclAssociation
Properties:
SubnetId: !Ref SubnetBPublic
NetworkAclId: !Ref NetworkAclPublic
SubnetNetworkAclAssociationBPrivate:
Type: AWS::EC2::SubnetNetworkAclAssociation
Properties:
SubnetId: !Ref SubnetBPrivate
NetworkAclId: !Ref NetworkAclPrivate
SubnetNetworkAclAssociationBData:
Type: AWS::EC2::SubnetNetworkAclAssociation
Properties:
SubnetId: !Ref SubnetBData
NetworkAclId: !Ref NetworkAclData
SubnetNetworkAclAssociationCPublic:
Condition: Have3AZs
Type: AWS::EC2::SubnetNetworkAclAssociation
Properties:
SubnetId: !Ref SubnetCPublic
NetworkAclId: !Ref NetworkAclPublic
SubnetNetworkAclAssociationCPrivate:
Condition: Have3AZs
Type: AWS::EC2::SubnetNetworkAclAssociation
Properties:
SubnetId: !Ref SubnetCPrivate
NetworkAclId: !Ref NetworkAclPrivate
SubnetNetworkAclAssociationCData:
Condition: Have3AZs
Type: AWS::EC2::SubnetNetworkAclAssociation
Properties:
SubnetId: !Ref SubnetCData
NetworkAclId: !Ref NetworkAclData
SubnetNetworkAclAssociationDPublic:
Condition: Have4AZs
Type: AWS::EC2::SubnetNetworkAclAssociation
Properties:
SubnetId: !Ref SubnetDPublic
NetworkAclId: !Ref NetworkAclPublic
SubnetNetworkAclAssociationDPrivate:
Condition: Have4AZs
Type: AWS::EC2::SubnetNetworkAclAssociation
Properties:
SubnetId: !Ref SubnetDPrivate
NetworkAclId: !Ref NetworkAclPrivate
SubnetNetworkAclAssociationDData:
Condition: Have4AZs
Type: AWS::EC2::SubnetNetworkAclAssociation
Properties:
SubnetId: !Ref SubnetDData
NetworkAclId: !Ref NetworkAclData
# Setup NetworkAclEntry for each tier
# - Allow all traffic inbound & outbound for Public & Private tier
# - Deny all traffic to/from Public tier to Data tier
# - Allow all other traffic inbound & outbound for Data Tier
NetworkAclEntryInPublicAllowAll:
Type: AWS::EC2::NetworkAclEntry
Properties:
NetworkAclId: !Ref NetworkAclPublic
RuleNumber: 100
Protocol: -1
RuleAction: allow
Egress: false
CidrBlock: '0.0.0.0/0'
NetworkAclEntryOutPublicAllowAll:
Type: AWS::EC2::NetworkAclEntry
Properties:
NetworkAclId: !Ref NetworkAclPublic
RuleNumber: 100
Protocol: -1
RuleAction: allow
Egress: true
CidrBlock: '0.0.0.0/0'
NetworkAclEntryInPublicAllowAllIpv6:
Condition: DualStacked
Type: AWS::EC2::NetworkAclEntry
Properties:
NetworkAclId: !Ref NetworkAclPublic
RuleNumber: 101
Protocol: -1
RuleAction: allow
Egress: false
Ipv6CidrBlock: '::/0'
NetworkAclEntryOutPublicAllowAllIpv6:
Condition: DualStacked
Type: AWS::EC2::NetworkAclEntry
Properties:
NetworkAclId: !Ref NetworkAclPublic
RuleNumber: 101
Protocol: -1
RuleAction: allow
Egress: true
Ipv6CidrBlock: '::/0'
NetworkAclEntryInPrivateAllowAll:
Type: AWS::EC2::NetworkAclEntry
Properties:
NetworkAclId: !Ref NetworkAclPrivate
RuleNumber: 100
Protocol: -1
RuleAction: allow
Egress: false
CidrBlock: '0.0.0.0/0'
NetworkAclEntryOutPrivateAllowAll:
Type: AWS::EC2::NetworkAclEntry
Properties:
NetworkAclId: !Ref NetworkAclPrivate
RuleNumber: 100
Protocol: -1
RuleAction: allow
Egress: true
CidrBlock: '0.0.0.0/0'
NetworkAclEntryInPrivateAllowAllIpv6:
Condition: DualStacked
Type: AWS::EC2::NetworkAclEntry
Properties:
NetworkAclId: !Ref NetworkAclPrivate
RuleNumber: 101
Protocol: -1
RuleAction: allow
Egress: false
Ipv6CidrBlock: '::/0'
NetworkAclEntryOutPrivateAllowAllIpv6:
Condition: DualStacked
Type: AWS::EC2::NetworkAclEntry
Properties:
NetworkAclId: !Ref NetworkAclPrivate
RuleNumber: 101
Protocol: -1
RuleAction: allow
Egress: true
Ipv6CidrBlock: '::/0'
NetworkAclEntryInDataDenyPublic:
Type: AWS::EC2::NetworkAclEntry
Properties:
NetworkAclId: !Ref NetworkAclData
RuleNumber: 100
Protocol: -1
RuleAction: deny
Egress: false
CidrBlock: !Select [0, !Cidr [!GetAtt VPC.CidrBlock, 4, 14]]
NetworkAclEntryOutDataDenyPublic:
Type: AWS::EC2::NetworkAclEntry
Properties:
NetworkAclId: !Ref NetworkAclData
RuleNumber: 100
Protocol: -1
RuleAction: deny
Egress: true
CidrBlock: !Select [0, !Cidr [!GetAtt VPC.CidrBlock, 4, 14]]
NetworkAclEntryInDataAllowAll:
Type: AWS::EC2::NetworkAclEntry
Properties:
NetworkAclId: !Ref NetworkAclData
RuleNumber: 200
Protocol: -1
RuleAction: allow
Egress: false
CidrBlock: '0.0.0.0/0'
NetworkAclEntryOutDataAllowAll:
Type: AWS::EC2::NetworkAclEntry
Properties:
NetworkAclId: !Ref NetworkAclData
RuleNumber: 200
Protocol: -1
RuleAction: allow
Egress: true
CidrBlock: '0.0.0.0/0'
NetworkAclEntryInDataDenyPublicIpv6:
Condition: DualStacked
Type: AWS::EC2::NetworkAclEntry
Properties:
NetworkAclId: !Ref NetworkAclData
RuleNumber: 101
Protocol: -1
RuleAction: deny
Egress: false
Ipv6CidrBlock: !Select [1, !Cidr [!Select [0, !GetAtt VPC.Ipv6CidrBlocks], 4, 66]]
NetworkAclEntryOutDataDenyPublicIpv6:
Condition: DualStacked
Type: AWS::EC2::NetworkAclEntry
Properties:
NetworkAclId: !Ref NetworkAclData
RuleNumber: 101
Protocol: -1
RuleAction: deny
Egress: true
Ipv6CidrBlock: !Select [1, !Cidr [!Select [0, !GetAtt VPC.Ipv6CidrBlocks], 4, 66]]
NetworkAclEntryInDataAllowAllIpv6:
Condition: DualStacked
Type: AWS::EC2::NetworkAclEntry
Properties:
NetworkAclId: !Ref NetworkAclData
RuleNumber: 201
Protocol: -1
RuleAction: allow
Egress: false
Ipv6CidrBlock: '::/0'
NetworkAclEntryOutDataAllowAllIpv6:
Condition: DualStacked
Type: AWS::EC2::NetworkAclEntry
Properties:
NetworkAclId: !Ref NetworkAclData
RuleNumber: 201
Protocol: -1
RuleAction: allow
Egress: true
Ipv6CidrBlock: '::/0'
Outputs:
StackName:
Description: 'Stack name'
Value: !Ref 'AWS::StackName'
AZs:
Description: 'AZs'
Value: !Ref NumberAZs
Export:
Name: !Sub "${AWS::StackName}-AZs"
AZA:
Description: 'AZ of A'
Value: !GetAtt SubnetAPublic.AvailabilityZone
Export:
Name: !Sub "${AWS::StackName}-AZA"
AZB:
Description: 'AZ of B'
Value: !GetAtt SubnetBPublic.AvailabilityZone
Export:
Name: !Sub "${AWS::StackName}-AZB"
AZC:
Condition: Have3AZs
Description: 'AZ of C'
Value: !GetAtt SubnetCPublic.AvailabilityZone
Export:
Name: !Sub "${AWS::StackName}-AZC"
AZD:
Condition: Have4AZs
Description: 'AZ of D'
Value: !GetAtt SubnetDPublic.AvailabilityZone
Export:
Name: !Sub "${AWS::StackName}-AZD"
ClassB:
Description: 'Class B.'
Value: !Ref ClassB
Export:
Name: !Sub "${AWS::StackName}-ClassB"
VPC:
Description: 'VPC.'
Value: !Ref VPC
Export:
Name: !Sub "${AWS::StackName}-VPC"
SubnetsPublic:
Description: 'Subnets public.'
Value:
Fn::Join:
- ','
- - !Ref SubnetAPublic
- !Ref SubnetBPublic
- !If [Have3AZs, !Ref SubnetCPublic, !Ref 'AWS::NoValue']
- !If [Have4AZs, !Ref SubnetDPublic, !Ref 'AWS::NoValue']
Export:
Name: !Sub "${AWS::StackName}-SubnetsPublic"
SubnetsPrivate:
Description: 'Subnets private.'
Value:
Fn::Join:
- ','
- - !Ref SubnetAPrivate
- !Ref SubnetBPrivate
- !If [Have3AZs, !Ref SubnetCPrivate, !Ref 'AWS::NoValue']
- !If [Have4AZs, !Ref SubnetDPrivate, !Ref 'AWS::NoValue']
Export:
Name: !Sub "${AWS::StackName}-SubnetsPrivate"
SubnetsData:
Description: 'Subnets data.'
Value:
Fn::Join:
- ','
- - !Ref SubnetAData
- !Ref SubnetBData
- !If [Have3AZs, !Ref SubnetCData, !Ref 'AWS::NoValue']
- !If [Have4AZs, !Ref SubnetDData, !Ref 'AWS::NoValue']
Export:
Name: !Sub "${AWS::StackName}-SubnetsData"
RouteTablesPublic:
Description: 'Route tables public.'
Value:
Fn::Join:
- ','
- - !Ref RouteTableAPublic
- !Ref RouteTableBPublic
- !If [Have3AZs, !Ref RouteTableCPublic, !Ref 'AWS::NoValue']
- !If [Have4AZs, !Ref RouteTableDPublic, !Ref 'AWS::NoValue']
Export:
Name: !Sub "${AWS::StackName}-RouteTablesPublic"
RouteTablesPrivate:
Description: 'Route tables private.'
Value:
Fn::Join:
- ','
- - !Ref RouteTableAPrivate
- !Ref RouteTableBPrivate
- !If [Have3AZs, !Ref RouteTableCPrivate, !Ref 'AWS::NoValue']
- !If [Have4AZs, !Ref RouteTableDPrivate, !Ref 'AWS::NoValue']
Export:
Name: !Sub "${AWS::StackName}-RouteTablesPrivate"
RouteTablesData:
Description: 'Route tables data.'
Value:
Fn::Join:
- ','
- - !Ref RouteTableAData
- !Ref RouteTableBData
- !If [Have3AZs, !Ref RouteTableCData, !Ref 'AWS::NoValue']
- !If [Have4AZs, !Ref RouteTableDData, !Ref 'AWS::NoValue']
Export:
Name: !Sub "${AWS::StackName}-RouteTablesData"
SubnetAPublic:
Description: 'Subnet A public.'
Value: !Ref SubnetAPublic
Export:
Name: !Sub "${AWS::StackName}-SubnetAPublic"
RouteTableAPublic:
Description: 'Route table A public.'
Value: !Ref RouteTableAPublic
Export:
Name: !Sub "${AWS::StackName}-RouteTableAPublic"
SubnetAPrivate:
Description: 'Subnet A private.'
Value: !Ref SubnetAPrivate
Export:
Name: !Sub "${AWS::StackName}-SubnetAPrivate"
RouteTableAPrivate:
Description: 'Route table A private.'
Value: !Ref RouteTableAPrivate
Export:
Name: !Sub "${AWS::StackName}-RouteTableAPrivate"
SubnetAData:
Description: 'Subnet A data.'
Value: !Ref SubnetAData
Export:
Name: !Sub "${AWS::StackName}-SubnetAData"
RouteTableAData:
Description: 'Route table A data.'
Value: !Ref RouteTableAData
Export:
Name: !Sub "${AWS::StackName}-RouteTableAData"
SubnetBPublic:
Description: 'Subnet B public.'
Value: !Ref SubnetBPublic
Export:
Name: !Sub "${AWS::StackName}-SubnetBPublic"
RouteTableBPublic:
Description: 'Route table B public.'
Value: !Ref RouteTableBPublic
Export:
Name: !Sub "${AWS::StackName}-RouteTableBPublic"
SubnetBPrivate:
Description: 'Subnet B private.'
Value: !Ref SubnetBPrivate
Export:
Name: !Sub "${AWS::StackName}-SubnetBPrivate"
RouteTableBPrivate:
Description: 'Route table B private.'
Value: !Ref RouteTableBPrivate
Export:
Name: !Sub "${AWS::StackName}-RouteTableBPrivate"
SubnetBData:
Description: 'Subnet B data.'
Value: !Ref SubnetBData
Export:
Name: !Sub "${AWS::StackName}-SubnetBData"
RouteTableBData:
Description: 'Route table B data.'
Value: !Ref RouteTableBData
Export:
Name: !Sub "${AWS::StackName}-RouteTableBData"
SubnetCPublic:
Condition: Have3AZs
Description: 'Subnet C public.'
Value: !Ref SubnetCPublic
Export:
Name: !Sub "${AWS::StackName}-SubnetCPublic"
RouteTableCPublic:
Condition: Have3AZs
Description: 'Route table C public.'
Value: !Ref RouteTableCPublic
Export:
Name: !Sub "${AWS::StackName}-RouteTableCPublic"
SubnetCPrivate:
Condition: Have3AZs
Description: 'Subnet C private.'
Value: !Ref SubnetCPrivate
Export:
Name: !Sub "${AWS::StackName}-SubnetCPrivate"
RouteTableCPrivate:
Condition: Have3AZs
Description: 'Route table C private.'
Value: !Ref RouteTableCPrivate
Export:
Name: !Sub "${AWS::StackName}-RouteTableCPrivate"
SubnetCData:
Condition: Have3AZs
Description: 'Subnet C data.'
Value: !Ref SubnetCData
Export:
Name: !Sub "${AWS::StackName}-SubnetCData"
RouteTableCData:
Condition: Have3AZs
Description: 'Route table C data.'
Value: !Ref RouteTableCData
Export:
Name: !Sub "${AWS::StackName}-RouteTableCData"
SubnetDPublic:
Condition: Have4AZs
Description: 'Subnet D public.'
Value: !Ref SubnetDPublic
Export:
Name: !Sub "${AWS::StackName}-SubnetDPublic"
RouteTableDPublic:
Condition: Have4AZs
Description: 'Route table D public.'
Value: !Ref RouteTableDPublic
Export:
Name: !Sub "${AWS::StackName}-RouteTableDPublic"
SubnetDPrivate:
Condition: Have4AZs
Description: 'Subnet D private.'
Value: !Ref SubnetDPrivate
Export:
Name: !Sub "${AWS::StackName}-SubnetDPrivate"
RouteTableDPrivate:
Condition: Have4AZs
Description: 'Route table D private.'
Value: !Ref RouteTableDPrivate
Export:
Name: !Sub "${AWS::StackName}-RouteTableDPrivate"
SubnetDData:
Condition: Have4AZs
Description: 'Subnet D data.'
Value: !Ref SubnetDData
Export:
Name: !Sub "${AWS::StackName}-SubnetDData"
RouteTableDData:
Condition: Have4AZs
Description: 'Route table D data.'
Value: !Ref RouteTableDData
Export:
Name: !Sub "${AWS::StackName}-RouteTableDData"
EndpointS3:
Description: 'The VPC endpoint to S3.'
Value: !Ref EndpointS3
Export:
Name: !Sub "${AWS::StackName}-EndpointS3"
EndpointDynamoDB:
Description: 'The VPC endpoint to DynamoDB.'
Value: !Ref EndpointDynamoDB
Export:
Name: !Sub "${AWS::StackName}-EndpointDynamoDB"
view raw vpc-3tier.yaml hosted with ❤ by GitHub

While we had the VPCv6CIDR resource (lines 56-61 in both templates) previously that requested a /56 IPv6 CIDR block from Amazon, we never really actually did anything with this CIDR block as we only defined the CidrBlock on each subnet with the IPv4 CIDR block without including an Ipv6CidrBlock property and assigning it a /64 from our assigned /56 CIDR. Unlike IPv4 CIDR blocks for your subnet in AWS, the IPv6 CIDR block has to be a /64 assignment. If you take a look at the SubnetAPublic subnet resource (lines 87-99) you can see the CidrBlock and Ipv6CidrBlock properties on lines 92-93. Unlike the previous template iteration, instead of just defining the CIDR block statically this time I’ve used the Fn::Cidr function to break up my VPC CIDR blocks for me. Not previously using this function was a big reason why I wasn’t including the Ipv6CidrBlock as you didn’t know what your IPv6 assignment would be when the template was being created but it is available as a return value from the VPC resource which allows us to feed it to Fn::Cidr and request the subnets of the size we want.

The other major change now that we have IPv6 subnets being assigned comes down between lines 446 and 578 where we setup the default routes. We had the default route (0.0.0.0/0) for the Public RouteTables before but now we add the default route for IPv6 (::/0) to those RouteTables as well. As well I set the default route for IPv6 for the Private and Data tier RouteTables to go through the EgressOnlyInternetGateway which allows them to route traffic outbound over IPv6 but traffic can not route back inbound. While this means other IPv6 sites are reachable without setting up a NAT Gateway or a VPN Connection to route IPv4 traffic over, there are still some AWS resources that can’t be reached and one important one is CloudFormation itself.

Another new change to the template starts down on line 803 where we setup the Network ACLs for the Data tier. In this case we want to keep clear separation between the Public and Data tiers so it has deny rules setup for the Public tier CIDR blocks for both IPv4 and IPv6. When using a 3 tier approach you should only have traffic between Public and Private allowed and traffic between Private and Data allowed. Your front-end load balancers typically being placed in the Public tier which would need to talk with your application servers typically being placed in the Private tier. The your application servers in the Private tier would talk with your persistent storage or databases placed in the Data tier.

So the final detail to cover is the Fn::Cidr function itself. This helpful function takes 3 parameters and returns back a list of CIDR blocks which is why I use the Fn::Select to pick which block to select. The first parameter is the CIDR block to split up, so for our use case the VPC CIDR. The second parameter is a count of how many CIDR blocks we want returned, for this I wanted 12 as that would be the max needed for 3 tiers with 4 AZs. The last parameter is the CIDR bits for subnets subtracting the number of bits you want from either 32 for IPv4 or 128 for IPv6, so we’re using a /16 IPv4 CIDR for the VPC and we want the subnets to be /20 we needed 12 CIDR bits and we needed /64 subnets for IPv6 we needed 64 CIDR bits for those. We then use the first 4 (0-3) blocks returned for the Public  subnets which fall under a contiguous /18 subnet themselves for the tier. The next 4 (3-7) blocks get assigned to the Private subnets and the last 4 (8-11) block get assigned to the Data subnets. Demo VPC Design 

The subnetting is perhaps easier to see visually using a handy website I’ve found online. Looking at the image to the right you see the subnet layout for the 2-tier template using the default CIDR block which only has the Public and Private tiers so we only need to divide it into 8 /20 subnets. Demo VPC Design 

The image on the left then shows the subnet layout for the 12 /20 subnets needed for the 3-tier solution. Using a visual tool to assist with dividing out the subnets can be very handy, but as you could see from the template itself we don’t need to code the actual subnets themselves and instead let the Fn::Cidr function do the work for us. You could mix and match your subnet CIDR sizes if you wanted as they don’t all have to be the same size. You would then just need to modify the last two parameters to the Fn::Cidr function to reflect how many subnets you needed to be generated of that size and how many bits for subnetting. You might also then need to change the index for the Fn::Select to get the right block if you needed to skip some.

To better understand this let’s consider this scenario, you want a 3-tier VPC using the CIDR of 10.0.0.0/20  for a small sandbox environment with 4 AZs. You think you only need /24 subnets for Public and Data but you need /23 subnets for Private. In this case you would want to use 8 for the CIDR bits for the Public and Data subnets while using 9 for the Private subnets as the third parameter. The second parameter for the subnet count is a bit tricky given the different sizes. You still need to calculate the /24 subnets that make up the Private subnets between the Public and Private so you would need to set the count to 16 on Public and Data but set the count to 8 on Private.

---
AWSTemplateFormatVersion: '2010-09-09'
Description: 'VPC: Test Case'
Resources:
S3Bucket:
Type: AWS::S3::Bucket
Properties: {}
Outputs:
SubnetAPublic:
Value: !Select [0, !Cidr ['10.0.0.0/20', 16, 8]]
SubnetBPublic:
Value: !Select [1, !Cidr ['10.0.0.0/20', 16, 8]]
SubnetCPublic:
Value: !Select [2, !Cidr ['10.0.0.0/20', 16, 8]]
SubnetDPublic:
Value: !Select [3, !Cidr ['10.0.0.0/20', 16, 8]]
SubnetAPrivate:
Value: !Select [2, !Cidr ['10.0.0.0/20', 8, 9]]
SubnetBPrivate:
Value: !Select [3, !Cidr ['10.0.0.0/20', 8, 9]]
SubnetCPrivate:
Value: !Select [4, !Cidr ['10.0.0.0/20', 8, 9]]
SubnetDPrivate:
Value: !Select [5, !Cidr ['10.0.0.0/20', 8, 9]]
SubnetAData:
Value: !Select [12, !Cidr ['10.0.0.0/20', 16, 8]]
SubnetBData:
Value: !Select [13, !Cidr ['10.0.0.0/20', 16, 8]]
SubnetCData:
Value: !Select [14, !Cidr ['10.0.0.0/20', 16, 8]]
SubnetDData:
Value: !Select [15, !Cidr ['10.0.0.0/20', 16, 8]]
PublicCidr:
Value: !Select [0, !Cidr ['10.0.0.0/20', 4, 10]]
VPCv4Cidr:
Value: '10.0.0.0/20'
view raw test-case.yaml hosted with ❤ by GitHub

Your Fn::Select indexes for Public would remain 0-3 as they are still the first 4 subnets out of the 16 and the Data indexes would become 12-15 as the last 4 subnets. In the case of the Private subnets you would then use the indexes 2-5. To define the Public CIDR covering all 4 subnets you could use the count of 4 and CIDR bits of 10 then take the first index. You can execute the simple test case template above which will only create a S3 bucket and produce the values for the subnets in the stack Outputs.

I do hope you find this informative and helps you in developing your AWS environment. As always feel free to check out the templates in my repository and adjust to your needs as they are a compilation of my findings and experiences.