Creating your entire WordPress Stack using AWS Cloudformation – Part 1 – The Network/VPC

By | January 29, 2019

I am starting a new series on this blog where I will take you through creating an entire WordPress cluster that uses its own secure VPC that meets PCI compliance standards. It will contain its own security groups, S3 bucket (or EFS which will be discussed later), Aurora Serverless RDS DB instance, Application Load Balancer, ElasticCache (again – optional and will be discussed later) for Read performance enhancements, the Web servers with CI/CD, Route53 for DNS, and CloudFront for CDN caching. In this particular post, I will begin by covering the Network portion. The VPC we will create contains several subnet segments and is multi AZ. It contains Public subnets for the Load Balancer front end and the Bastion host (AKA Jump Box). It will have Web subnets for the web servers, only accessible from the Bastion host. It will also have Data subnets for the Database servers. You will be able to choose from 2 to 3 AZ’s depending on your needs. All of the subnets will be fully IPv4 and IPv6 capable and will only have public IPs assigned to devices in the Public subnets. Everything else will use NAT Gateways or Egress Only Interfaces for internet access. Lets get started.

The first part of the template simply collects the parameters you specify. Most of this is self explanatory but I will give a quick run through regardless. See the code below.

WSTemplateFormatVersion: 2010-09-09

Metadata:

  AWS::CloudFormation::Interface:

    ParameterGroups:
    - Label:
        default: Amazon VPC Parameters
      Parameters:
      - NumberOfAZs
      - AvailabilityZones
      - VpcCidr
      - VpcTenancy
      - PublicSubnet0Cidr
      - PublicSubnet0Ipv6Cidr
      - PublicSubnet1Cidr
      - PublicSubnet1Ipv6Cidr
      - PublicSubnet2Cidr
      - PublicSubnet2Ipv6Cidr
      - WebSubnet0Cidr
      - WebSubnet0Ipv6Cidr
      - WebSubnet1Cidr
      - WebSubnet1Ipv6Cidr
      - WebSubnet2Cidr
      - WebSubnet2Ipv6Cidr
      - DataSubnet0Cidr
      - DataSubnet0Ipv6Cidr
      - DataSubnet1Cidr
      - DataSubnet1Ipv6Cidr
      - DataSubnet2Cidr
      - DataSubnet2Ipv6Cidr
    ParameterLabels:
      AvailabilityZones:
        default: Availability Zones
      NumberOfAZs:
        default: Number of Availability Zones
      VpcCidr:
        default: VpcCidr
      VpcTenancy:
        default: VpcTenancy
      PublicSubnet0Cidr:
        default: Public Subnet 0
      PublicSubnet0Ipv6Cidr:
        default: Public Subnet 0 IPv6
      PublicSubnet1Cidr:
        default: Public Subnet 1
      PublicSubnet1Ipv6Cidr:
        default: Public Subnet 1 IPv6
      PublicSubnet2Cidr:
        default: Public Subnet 2
      PublicSubnet2Ipv6Cidr:
        default: Public Subnet 2 IPv6
      WebSubnet0Cidr:
        default: Web Subnet 0
      WebSubnet0Ipv6Cidr:
        default: Web Subnet 0 IPv6
      WebSubnet1Cidr:
        default: Web Subnet 1
      WebSubnet1Ipv6Cidr:
        default: Web Subnet 1 IPv6
      WebSubnet2Cidr:
        default: Web Subnet 2
      WebSubnet2Ipv6Cidr:
        default: Web Subnet 2 IPv6
      DataSubnet0Cidr:
        default: Data Subnet 0
      DataSubnet0Ipv6Cidr:
        default: Data Subnet 0 IPv6
      DataSubnet1Cidr:
        default: Data Subnet 1
      DataSubnet1Ipv6Cidr:
        default: Data Subnet 1 IPv6
      DataSubnet2Cidr:
        default: Data Subnet 2
      DataSubnet2Ipv6Cidr:
        default: Data Subnet 2 IPv6

Parameters:

  AvailabilityZones:
    Description: 'List of Availability Zones to use for the subnets in the VPC. Note:
      The logical order is preserved.'
    Type: List<AWS::EC2::AvailabilityZone::Name>
  NumberOfAZs:
    AllowedValues:
    - 2
    - 3
    Default: 3
    Description: Number of Availability Zones to use in the VPC. This must match your
      selections in the list of Availability Zones parameter.
    Type: Number
  VpcCidr:
    AllowedPattern: "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\\/(1[6-9]|2[0-8]))$"
    ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/16-28
    Default: 10.30.0.0/16
    Description: CIDR block for the VPC
    Type: String
  VpcTenancy:
    AllowedValues:
    - default
    - dedicated
    Default: default
    Description: The allowed tenancy of instances launched into the VPC
    Type: String
  DataSubnet0Cidr:
    AllowedPattern: "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\\/(1[6-9]|2[0-8]))$"
    ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/16-28
    Default: 10.30.100.0/24
    Description: CIDR block for data subnet 0 located in Availability Zone 0
    Type: String
  DataSubnet0Ipv6Cidr:
    ConstraintDescription: CIDR block parameter must be in the form of XX::/64 anywhere from 00 to FF (Hex)
    Default: a0::/64
    Description: CIDR block for Data subnet 0 located in Availability Zone 0
    Type: String
  DataSubnet1Cidr:
    AllowedPattern: "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\\/(1[6-9]|2[0-8]))$"
    ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/16-28
    Default: 10.30.101.0/24
    Description: CIDR block for data subnet 1 located in Availability Zone 1
    Type: String
  DataSubnet1Ipv6Cidr:
    ConstraintDescription: CIDR block parameter must be in the form of XX::/64 anywhere from 00 to FF (Hex)
    Default: a1::/64
    Description: CIDR block for Data subnet 1 located in Availability Zone 1
    Type: String
  DataSubnet2Cidr:
    AllowedPattern: "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\\/(1[6-9]|2[0-8]))$"
    ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/16-28
    Default: 10.30.102.0/24
    Description: CIDR block for data subnet 2 located in Availability Zone 2
    Type: String
  DataSubnet2Ipv6Cidr:
    ConstraintDescription: CIDR block parameter must be in the form of XX::/64 anywhere from 00 to FF (Hex)
    Default: a2::/64
    Description: CIDR block for Data subnet 2 located in Availability Zone 2
    Type: String
  PublicSubnet0Cidr:
    AllowedPattern: "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\\/(1[6-9]|2[0-8]))$"
    ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/16-28
    Default: 10.30.200.0/24
    Description: CIDR block for Public subnet 0 located in Availability Zone 0
    Type: String
  PublicSubnet0Ipv6Cidr:
    ConstraintDescription: CIDR block parameter must be in the form of XX::/64 anywhere from 00 to FF (Hex)
    Default: b0::/64
    Description: CIDR block for Public subnet 0 located in Availability Zone 0
    Type: String
  PublicSubnet1Cidr:
    AllowedPattern: "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\\/(1[6-9]|2[0-8]))$"
    ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/16-28
    Default: 10.30.201.0/24
    Description: CIDR block for Public subnet 1 located in Availability Zone 1
    Type: String
  PublicSubnet1Ipv6Cidr:
    ConstraintDescription: CIDR block parameter must be in the form of XX::/64 anywhere from 00 to FF (Hex)
    Default: b1::/64
    Description: CIDR block for Public subnet 1 located in Availability Zone 1
    Type: String
  PublicSubnet2Cidr:
    AllowedPattern: "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\\/(1[6-9]|2[0-8]))$"
    ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/16-28
    Default: 10.30.202.0/24
    Description: CIDR block for Public subnet 2 located in Availability Zone 2
    Type: String
  PublicSubnet2Ipv6Cidr:
    ConstraintDescription: CIDR block parameter must be in the form of XX::/64 anywhere from 00 to FF (Hex)
    Default: b2::/64
    Description: CIDR block for Public subnet 2 located in Availability Zone 2
    Type: String
  WebSubnet0Cidr:
    AllowedPattern: "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\\/(1[6-9]|2[0-8]))$"
    ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/16-28
    Default: 10.30.0.0/24
    Description: CIDR block for Web subnet 0 located in Availability Zone 0
    Type: String
  WebSubnet0Ipv6Cidr:
    ConstraintDescription: CIDR block parameter must be in the form of XX::/64 anywhere from 00 to FF (Hex)
    Default: 00::/64
    Description: CIDR block for Web subnet 0 located in Availability Zone 0
    Type: String
  WebSubnet1Cidr:
    AllowedPattern: "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\\/(1[6-9]|2[0-8]))$"
    ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/16-28
    Default: 10.30.1.0/24
    Description: CIDR block for Web subnet 1 located in Availability Zone 1
    Type: String
  WebSubnet1Ipv6Cidr:
    ConstraintDescription: CIDR block parameter must be in the form of XX::/64 anywhere from 00 to FF (Hex)
    Default: 01::/64
    Description: CIDR block for Web subnet 1 located in Availability Zone 1
    Type: String
  WebSubnet2Cidr:
    AllowedPattern: "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\\/(1[6-9]|2[0-8]))$"
    ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/16-28
    Default: 10.30.2.0/24
    Description: CIDR block for Web subnet 2 located in Availability Zone 2
    Type: String
  WebSubnet2Ipv6Cidr:
    ConstraintDescription: CIDR block parameter must be in the form of XX::/64 anywhere from 00 to FF (Hex)
    Default: 02::/64
    Description: CIDR block for Web subnet 2 located in Availability Zone 2
    Type: String

Since we are going to introduce a Master template at the end of this series that combines all of these, we have laid out our parameters using three distinct blocks – Parameter Groups, Parameter Labels, and Parameters. Parameter Groups define how the parameters are grouped together and will make more sense when we view the Master template. Parameter Labels give labels that show up in CloudFormation to indicate what the parameter is used for. Lastly, Parameters is where we define default values and prompt the person running the script for the values. Each of these parameters contain descriptions so I won’t dig into them here. However, you’ll notice you can choose between 2 or 3 AZs, the overall /16 block CIDR range used for the VPC (make sure this doesn’t overlap with any of your on site resources so you can create a VPN tunnel or VPC Peering at a later date), the tenancy to determine if we want shared or dedicated hypervisors, and lastly, we define each of the subnets within the initial /16 CIDR range we chose along with two hex characters for the IPv6 subnets. We can’t define the entire IPv6 subnet like we can with IPv4 because we will be using Global addresses and the IPv6 CIDR is provided by Amazon. So instead, we will be using the template to modify the bits that Amazon allows us to modify in order to create distinct /64 subnets like we do with /24 subnets for IPv4. Now that we have all this info, we need to establish some conditions before we proceed.

Conditions:

  NumberOfAZs1:
      !Equals [ '1', !Ref NumberOfAZs ]
  NumberOfAZs2:
      !Equals [ '2', !Ref NumberOfAZs ]
  NumberOfAZs3:
      !Equals [ '3', !Ref NumberOfAZs ]
  AZ0: !Or
    - !Condition NumberOfAZs1
    - !Condition NumberOfAZs2
    - !Condition NumberOfAZs3
  AZ1: !Or
    - !Condition NumberOfAZs2
    - !Condition NumberOfAZs3
  AZ2: !Condition NumberOfAZs3

We create the above conditions that will be referenced later in the template for provisioning resources. Depending on the number of AZs chosen, we use the Fn::Equals and Fn::Condition functions to create additional parameters (AZ0, AZ1, and AZ2) that will be referenced later, if they exist. Now, onto the actual resource provisioning.

Resources:

  WebSubnet0:
    Condition: AZ0
    Type: AWS::EC2::Subnet
    DependsOn: Ipv6CidrBlocks
    Properties:
      AvailabilityZone: !Select [ 0, !Ref AvailabilityZones ]
      CidrBlock: !Ref WebSubnet0Cidr
      MapPublicIpOnLaunch: true
      Ipv6CidrBlock:
        Fn::Sub:
          - "${VpcPart}${SubnetPart}"
          - SubnetPart: !Ref WebSubnet0Ipv6Cidr
            VpcPart: !Select [ 0, !Split [ '00::/56', !Select [ 0, !GetAtt Vpc.Ipv6CidrBlocks ]]]
      Tags:
        - Key: Name
          Value: !Join [ '', [ 'WebSubnet0 / ', !Ref 'AWS::StackName' ] ]
        - Key: SubnetType
          Value: Private
      VpcId: !Ref Vpc
  WebSubnet1:
    Condition: AZ1
    Type: AWS::EC2::Subnet
    DependsOn: Ipv6CidrBlocks
    Properties:
      AvailabilityZone: !Select [ 1, !Ref AvailabilityZones ]
      CidrBlock: !Ref WebSubnet1Cidr
      MapPublicIpOnLaunch: true
      Ipv6CidrBlock:
        Fn::Sub:
          - "${VpcPart}${SubnetPart}"
          - SubnetPart: !Ref WebSubnet1Ipv6Cidr
            VpcPart: !Select [ 0, !Split [ '00::/56', !Select [ 0, !GetAtt Vpc.Ipv6CidrBlocks ]]]
      Tags:
        - Key: Name
          Value: !Join [ '', [ 'WebSubnet1 / ', !Ref 'AWS::StackName' ] ]
        - Key: SubnetType
          Value: Private
      VpcId: !Ref Vpc
  WebSubnet2:
    Condition: AZ2
    Type: AWS::EC2::Subnet
    DependsOn: Ipv6CidrBlocks
    Properties:
      AvailabilityZone: !Select [ 2, !Ref AvailabilityZones ]
      CidrBlock: !Ref WebSubnet2Cidr
      MapPublicIpOnLaunch: true
      Ipv6CidrBlock:
        Fn::Sub:
          - "${VpcPart}${SubnetPart}"
          - SubnetPart: !Ref WebSubnet2Ipv6Cidr
            VpcPart: !Select [ 0, !Split [ '00::/56', !Select [ 0, !GetAtt Vpc.Ipv6CidrBlocks ]]]
      Tags:
        - Key: Name
          Value: !Join [ '', [ 'WebSubnet2 / ', !Ref 'AWS::StackName' ] ]
        - Key: SubnetType
          Value: Private
      VpcId: !Ref Vpc

  WebSubnetRouteTableAssociation0:
    Condition: AZ0
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref NatRouteTable0
      SubnetId: !Ref WebSubnet0
  WebSubnetRouteTableAssociation1:
    Condition: AZ1
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref NatRouteTable1
      SubnetId: !Ref WebSubnet1
  WebSubnetRouteTableAssociation2:
    Condition: AZ2
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref NatRouteTable2
      SubnetId: !Ref WebSubnet2

  DataSubnet0:
    Condition: AZ0
    Type: AWS::EC2::Subnet
    DependsOn: Ipv6CidrBlocks
    Properties:
      AvailabilityZone: !Select [ 0, !Ref AvailabilityZones ]
      CidrBlock: !Ref DataSubnet0Cidr
      AssignIpv6AddressOnCreation: true
      Ipv6CidrBlock:
        Fn::Sub:
          - "${VpcPart}${SubnetPart}"
          - SubnetPart: !Ref DataSubnet0Ipv6Cidr
            VpcPart: !Select [ 0, !Split [ '00::/56', !Select [ 0, !GetAtt Vpc.Ipv6CidrBlocks ]]]
      Tags:
        - Key: Name
          Value: !Join [ '', [ 'DataSubnet0 / ', !Ref 'AWS::StackName' ] ]
        - Key: SubnetType
          Value: Private
      VpcId: !Ref Vpc
  DataSubnet1:
    Condition: AZ1
    Type: AWS::EC2::Subnet
    DependsOn: Ipv6CidrBlocks
    Properties:
      AvailabilityZone: !Select [ 1, !Ref AvailabilityZones ]
      CidrBlock: !Ref DataSubnet1Cidr
      AssignIpv6AddressOnCreation: true
      Ipv6CidrBlock:
        Fn::Sub:
          - "${VpcPart}${SubnetPart}"
          - SubnetPart: !Ref DataSubnet1Ipv6Cidr
            VpcPart: !Select [ 0, !Split [ '00::/56', !Select [ 0, !GetAtt Vpc.Ipv6CidrBlocks ]]]
      Tags:
        - Key: Name
          Value: !Join [ '', [ 'DataSubnet1 / ', !Ref 'AWS::StackName' ] ]
        - Key: SubnetType
          Value: Private
      VpcId: !Ref Vpc
  DataSubnet2:
    Condition: AZ2
    Type: AWS::EC2::Subnet
    DependsOn: Ipv6CidrBlocks
    Properties:
      AvailabilityZone: !Select [ 2, !Ref AvailabilityZones ]
      CidrBlock: !Ref DataSubnet2Cidr
      AssignIpv6AddressOnCreation: true
      Ipv6CidrBlock:
        Fn::Sub:
          - "${VpcPart}${SubnetPart}"
          - SubnetPart: !Ref DataSubnet2Ipv6Cidr
            VpcPart: !Select [ 0, !Split [ '00::/56', !Select [ 0, !GetAtt Vpc.Ipv6CidrBlocks ]]]
      Tags:
        - Key: Name
          Value: !Join [ '', [ 'DataSubnet2 / ', !Ref 'AWS::StackName' ] ]
        - Key: SubnetType
          Value: Private
      VpcId: !Ref Vpc

  DataSubnetRouteTableAssociation0:
    Condition: AZ0
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref NatRouteTable0
      SubnetId: !Ref DataSubnet0
  DataSubnetRouteTableAssociation1:
    Condition: AZ1
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref NatRouteTable1
      SubnetId: !Ref DataSubnet1
  DataSubnetRouteTableAssociation2:
    Condition: AZ2
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref NatRouteTable2
      SubnetId: !Ref DataSubnet2

  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Join [ '', [ 'InternetGateway / ', !Ref 'AWS::StackName' ] ]
  AttachInternetGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId: !Ref InternetGateway
      VpcId: !Ref Vpc
  EgressOnlyInternetGateway:
    Type: AWS::EC2::EgressOnlyInternetGateway
    Properties: 
      VpcId: !Ref Vpc

  NatEIP0:
    Condition: AZ0
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
  NatGateway0:
    Condition: AZ0
    Type: AWS::EC2::NatGateway
    DependsOn: AttachInternetGateway
    Properties:
      AllocationId: !GetAtt NatEIP0.AllocationId
      SubnetId: !Ref PublicSubnet0
  NatRoute0:
    Condition: AZ0
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref NatRouteTable0
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NatGateway0
  NatIpv6Route0:
    Type: AWS::EC2::Route
    Properties:
      DestinationIpv6CidrBlock: ::/0
      RouteTableId: !Ref NatRouteTable0
      EgressOnlyInternetGatewayId: !Ref EgressOnlyInternetGateway
  NatRouteTable0:
    Condition: AZ0
    Type: AWS::EC2::RouteTable
    Properties:
      Tags:
        - Key: Name
          Value: !Join [ '', ['NatRouteTable0 / ', !Ref 'AWS::StackName' ] ]
        - Key: Network
          Value: Public
      VpcId: !Ref Vpc

  NatEIP1:
    Condition: AZ1
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
  NatGateway1:
    Condition: AZ1
    Type: AWS::EC2::NatGateway
    DependsOn: AttachInternetGateway
    Properties:
      AllocationId: !GetAtt NatEIP1.AllocationId
      SubnetId: !Ref PublicSubnet1
  NatRoute1:
    Condition: AZ1
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref NatRouteTable1
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NatGateway1
  NatIpv6Route1:
    Type: AWS::EC2::Route
    Properties:
      DestinationIpv6CidrBlock: ::/0
      RouteTableId: !Ref NatRouteTable1
      EgressOnlyInternetGatewayId: !Ref EgressOnlyInternetGateway
  NatRouteTable1:
    Condition: AZ1
    Type: AWS::EC2::RouteTable
    Properties:
      Tags:
        - Key: Name
          Value: !Join [ '', [ 'NatRouteTable1 / ', !Ref 'AWS::StackName' ] ]
        - Key: Network
          Value: Public
      VpcId: !Ref Vpc

  NatEIP2:
    Condition: AZ2
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
  NatGateway2:
    Condition: AZ2
    Type: AWS::EC2::NatGateway
    DependsOn: AttachInternetGateway
    Properties:
      AllocationId: !GetAtt NatEIP2.AllocationId
      SubnetId: !Ref PublicSubnet2
  NatRoute2:
    Condition: AZ2
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref NatRouteTable2
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NatGateway2
  NatIpv6Route2:
    Type: AWS::EC2::Route
    Condition: AZ2
    Properties:
      DestinationIpv6CidrBlock: ::/0
      RouteTableId: !Ref NatRouteTable2
      EgressOnlyInternetGatewayId: !Ref EgressOnlyInternetGateway
  NatRouteTable2:
    Condition: AZ2
    Type: AWS::EC2::RouteTable
    Properties:
      Tags:
        - Key: Name
          Value: !Join [ '', [ 'NatRouteTable2 / ', !Ref 'AWS::StackName' ] ]
        - Key: Network
          Value: Public
      VpcId: !Ref Vpc

  PublicRoute:
    Type: AWS::EC2::Route
    DependsOn: AttachInternetGateway
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway
  PublicSubnetDefaultIpv6Route:
    Type: AWS::EC2::Route
    Properties:
      DestinationIpv6CidrBlock: ::/0
      RouteTableId: !Ref PublicRouteTable
      GatewayId: !Ref InternetGateway
  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      Tags:
        - Key: Name
          Value: !Join [ '', [ 'PublicRouteTable / ', !Ref 'AWS::StackName' ] ]
        - Key: Network
          Value: Public
      VpcId: !Ref Vpc
  PublicRouteTableAssociation0:
    Condition: AZ0
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet0
      RouteTableId: !Ref PublicRouteTable
  PublicRouteTableAssociation1:
    Condition: AZ1
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet1
      RouteTableId: !Ref PublicRouteTable
  PublicRouteTableAssociation2:
    Condition: AZ2
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet2
      RouteTableId: !Ref PublicRouteTable
       
  PublicSubnet0:
    Condition: AZ0
    Type: AWS::EC2::Subnet
    DependsOn: Ipv6CidrBlocks
    Properties:
      AvailabilityZone: !Select [ 0, !Ref AvailabilityZones ]
      CidrBlock: !Ref PublicSubnet0Cidr
      MapPublicIpOnLaunch: true
      Ipv6CidrBlock:
        Fn::Sub:
          - "${VpcPart}${SubnetPart}"
          - SubnetPart: !Ref PublicSubnet0Ipv6Cidr
            VpcPart: !Select [ 0, !Split [ '00::/56', !Select [ 0, !GetAtt Vpc.Ipv6CidrBlocks ]]]
      Tags:
        - Key: Name
          Value: !Join [ '', [ 'PublicSubnet0 / ', !Ref 'AWS::StackName' ] ]
        - Key: SubnetType
          Value: Public
      VpcId: !Ref Vpc
  PublicSubnet1:
    Condition: AZ1
    Type: AWS::EC2::Subnet
    DependsOn: Ipv6CidrBlocks
    Properties:
      AvailabilityZone: !Select [ 1, !Ref AvailabilityZones ]
      CidrBlock: !Ref PublicSubnet1Cidr
      MapPublicIpOnLaunch: true
      Ipv6CidrBlock:
        Fn::Sub:
          - "${VpcPart}${SubnetPart}"
          - SubnetPart: !Ref PublicSubnet1Ipv6Cidr
            VpcPart: !Select [ 0, !Split [ '00::/56', !Select [ 0, !GetAtt Vpc.Ipv6CidrBlocks ]]]
      Tags:
        - Key: Name
          Value: !Join [ '', [ 'PublicSubnet1 / ', !Ref 'AWS::StackName' ] ]
        - Key: SubnetType
          Value: Public
      VpcId: !Ref Vpc
  PublicSubnet2:
    Condition: AZ2
    Type: AWS::EC2::Subnet
    DependsOn: Ipv6CidrBlocks
    Properties:
      AvailabilityZone: !Select [ 2, !Ref AvailabilityZones ]
      CidrBlock: !Ref PublicSubnet2Cidr
      MapPublicIpOnLaunch: true
      Ipv6CidrBlock:
        Fn::Sub:
          - "${VpcPart}${SubnetPart}"
          - SubnetPart: !Ref PublicSubnet2Ipv6Cidr
            VpcPart: !Select [ 0, !Split [ '00::/56', !Select [ 0, !GetAtt Vpc.Ipv6CidrBlocks ]]]
      Tags:
        - Key: Name
          Value: !Join [ '', [ 'PublicSubnet2 / ', !Ref 'AWS::StackName' ] ]
        - Key: SubnetType
          Value: Public
      VpcId: !Ref Vpc

  S3Endpoint:
    Type: AWS::EC2::VPCEndpoint
    DependsOn: NatRouteTable1
    Properties:
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal: '*'
            Action: '*'
            Resource: '*'
      RouteTableIds:
        !If
          [ NumberOfAZs3,
            [ !Ref PublicRouteTable, !Ref NatRouteTable0 , !Ref NatRouteTable1, !Ref NatRouteTable2 ] ,
            [ !Ref PublicRouteTable, !Ref NatRouteTable0 , !Ref NatRouteTable1] 
          ]
      ServiceName: !Join 
        - ''
        - - com.amazonaws.
          - !Ref 'AWS::Region'
          - .s3
      VpcId: !Ref Vpc

  Vpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VpcCidr
      EnableDnsHostnames: true
      EnableDnsSupport: true
      Tags:
        - Key: Name
          Value: !Join [ '', [ 'Vpc / ', !Ref 'AWS::StackName' ] ]
  Ipv6CidrBlocks:
    Type: AWS::EC2::VPCCidrBlock
    Properties:
      VpcId: !Ref Vpc
      AmazonProvidedIpv6CidrBlock: true
  VpcFlowLog:
    Type: AWS::EC2::FlowLog
    Properties:
      DeliverLogsPermissionArn: !GetAtt VpcFlowLogsRole.Arn
      LogGroupName: !Join [ '', [ !Ref 'AWS::StackName', '-FlowLog' ] ]
      ResourceId: !Ref Vpc
      ResourceType: VPC
      TrafficType: ALL
  VpcFlowLogsLogGroup:
    Type: AWS::Logs::LogGroup
  VpcFlowLogsRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Action:
              - sts:AssumeRole
            Effect: Allow
            Principal:
              Service:
                - vpc-flow-logs.amazonaws.com
      Path: '/'
      Policies:
        - PolicyName: root
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:DescribeLogGroups
                  - logs:DescribeLogStreams
                  - logs:PutLogEvents
                Effect: Allow
                Resource: '*'

#  NetworkAclInboundEntry1:
#    Type: AWS::EC2::NetworkAclEntry
#    Properties:
#      NetworkAclId: !GetAtt Vpc.DefaultNetworkAcl
#      RuleNumber: 1
#      Protocol: -1
#      RuleAction: deny
#      Egress: false
#      CidrBlock: 0.0.0.0/8
#  NetworkAclInboundEntry2:
#    Type: AWS::EC2::NetworkAclEntry
#    Properties:
#      NetworkAclId: !GetAtt Vpc.DefaultNetworkAcl
#      RuleNumber: 2
#      Protocol: -1
#      RuleAction: deny
#      Egress: false
#      CidrBlock: 100.64.0.0/10
#  NetworkAclInboundEntry3:
#    Type: AWS::EC2::NetworkAclEntry
#    Properties:
#      NetworkAclId: !GetAtt Vpc.DefaultNetworkAcl
#      RuleNumber: 3
#      Protocol: -1
#      RuleAction: deny
#      Egress: false
#      CidrBlock: 127.0.0.0/8
#  NetworkAclInboundEntry5:
#    Type: AWS::EC2::NetworkAclEntry
#    Properties:
#      NetworkAclId: !GetAtt Vpc.DefaultNetworkAcl
#      RuleNumber: 5
#      Protocol: -1
#      RuleAction: deny
#      Egress: false
#      CidrBlock: 169.254.0.0/16
#  NetworkAclInboundEntry7:
#    Type: AWS::EC2::NetworkAclEntry
#    Properties:
#      NetworkAclId: !GetAtt Vpc.DefaultNetworkAcl
#      RuleNumber: 7
#      Protocol: -1
#      RuleAction: deny
#      Egress: false
#      CidrBlock: 192.0.0.0/24
#  NetworkAclInboundEntry8:
#    Type: AWS::EC2::NetworkAclEntry
#    Properties:
#      NetworkAclId: !GetAtt Vpc.DefaultNetworkAcl
#      RuleNumber: 8
#      Protocol: -1
#      RuleAction: deny
#      Egress: false
#      CidrBlock: 192.0.2.0/24
#  NetworkAclInboundEntry15:
#    Type: AWS::EC2::NetworkAclEntry
#    Properties:
#      NetworkAclId: !GetAtt Vpc.DefaultNetworkAcl
#      RuleNumber: 15
#      Protocol: -1
#      RuleAction: deny
#      Egress: false
#      Ipv6CidrBlock: 4000::/2
  NetworkAclInboundEntry101:
    Type: AWS::EC2::NetworkAclEntry
    Properties:
      NetworkAclId: !GetAtt Vpc.DefaultNetworkAcl
      RuleNumber: 101
      Protocol: -1
      RuleAction: allow
      Egress: false
      Ipv6CidrBlock: ::/0

I hope the above code is mostly self explanatory, but I will break down a lot of it here. You can see first that we begin by defining Subnet resources. Based on our conditions defined previously, we create an AWS::EC2::Subnet resource in an Availability Zone referenced previously along with the IPv6 CIDR blocks, which actually breaks down the AWS allocated CIDR and inserts the hex bits we defined earlier. It will also assign the IPv4 space and launch a Public IP if the subnet is defined as public. Unfortunately, there is a bug currently that prevents us from dual assigning IPv4 and IPv6 so we can only pick one initially. You can always go in later and set the IPv6 address to auto assign along with IPv4, it just can’t be done in CloudFormation at this point in time. You’ll also notice that all Web and Data subnets are private. Only components in the Public Subnet will accessible from the internet (Load Balancer and Bastion Host), however the Web and Data subnets do have outbound access to the internet.

The Route Table associations allow us to assign our private subnets to private route tables that route through NAT Gateways or IPv6 Egress Only Interfaces (one per AZ) and connect our Public Subnets to the public route table which routes using the Internet Gateway. We actually need 2-3 private route tables based on the number of AZs we have so it can be mapped to the appropriate NAT Gateway.

All NAT Gateways will need their own static “Elastic” IP address in order to perform this functionality. Keep in mind, you only get 5 of these per AWS account without contacting support to lift this restriction.

We create an S3 VPC endpoint since we will be commonly leveraging S3 storage for content that is shared between our web cluster. This ensures that the traffic remains private and doesn’t traverse the internet when leaving our VPC and writing/reading to/from S3.

The VPC itself will be created with the IPv4 CIDR we assigned along with an AWS provided IPv6 CIDR. We also enable Flow Logs and DNS support within the VPC.

Lastly, I commented out a section used for Network ACLs in case you wanted to provision some by default. The only one I used is for outbound IPv6 connectivity. Since we use Security Groups to permit traffic we need between instances and we get very specific in the next post with the rules in the Security Groups, I highly recommend only using these for global IP blocking. The one use-case I can think of is blocking an IP that constantly attacks your Bastion host or Load Balancers. Nothing else will be accessible from the internet. Now lets review the Outputs section.

Outputs:
  
  Vpc:
    Value: !Ref Vpc
  VpcCidr:
    Value: !Ref VpcCidr
  Ipv6CidrBlock:
    Value: !Select [ 0, !GetAtt Vpc.Ipv6CidrBlocks ]
  PublicSubnet0:
    Condition: AZ0
    Value: !Ref PublicSubnet0
  PublicSubnet1:
    Condition: AZ1
    Value: !Ref PublicSubnet1
  PublicSubnet2:
    Condition: AZ2
    Value: !Ref PublicSubnet2
  WebSubnet0:
    Condition: AZ0
    Value: !Ref WebSubnet0
  WebSubnet1:
    Condition: AZ1
    Value: !Ref WebSubnet1
  WebSubnet2:
    Condition: AZ2
    Value: !Ref WebSubnet2
  DataSubnet0:
    Condition: AZ0
    Value: !Ref DataSubnet0
  DataSubnet1:
    Condition: AZ1
    Value: !Ref DataSubnet1
  DataSubnet2:
    Condition: AZ2
    Value: !Ref DataSubnet2
  DataSubnet:
    Value:
      !If
        [ NumberOfAZs1,
        !Ref DataSubnet0,
        !If
          [ NumberOfAZs2,
          !Join [ ',', [ !Ref DataSubnet0, !Ref DataSubnet1 ] ],
          !If
            [ NumberOfAZs3,
            !Join [ ',', [ !Ref DataSubnet0, !Ref DataSubnet1, !Ref DataSubnet2 ] ],
            !Join [ ',', [ !Ref DataSubnet0, !Ref DataSubnet1, !Ref DataSubnet2 ] ]
            ]
          ]
        ] 
  WebSubnet:
    Value:
      !If
        [ NumberOfAZs1,
        !Ref WebSubnet0,
        !If
          [ NumberOfAZs2,
          !Join [ ',', [ !Ref WebSubnet0, !Ref WebSubnet1 ] ],
          !If
            [ NumberOfAZs3,
            !Join [ ',', [ !Ref WebSubnet0, !Ref WebSubnet1, !Ref WebSubnet2 ] ],
            !Join [ ',', [ !Ref WebSubnet0, !Ref WebSubnet1, !Ref WebSubnet2 ] ]
            ]
          ]
        ] 
  PublicSubnet:
    Value:
      !If
        [ NumberOfAZs1,
        !Ref PublicSubnet0,
        !If
          [ NumberOfAZs2,
          !Join [ ',', [ !Ref PublicSubnet0, !Ref PublicSubnet1 ] ],
          !If
            [ NumberOfAZs3,
            !Join [ ',', [ !Ref PublicSubnet0, !Ref PublicSubnet1, !Ref PublicSubnet2 ] ],
            !Join [ ',', [ !Ref PublicSubnet0, !Ref PublicSubnet1, !Ref PublicSubnet2 ] ]
            ]
          ]
        ] 

The Outputs section above serves two purposes. One, it outputs the information for us to review once CloudFormation has completed successfully. Two, when we run our Master template to build the entire environment at once, these Output values will be used by other templates that are triggered via the Master template so the entire process is automated.

This sums it up for the first part of our Cloudformation series. As stated previously, this is the first of many templates that will be used to create the underlying VPC the rest of our infrastructure will run on. The end result will be a PCI compliant, highly scale-able WordPress cluster, complete with IPv6, Auto Scaling, and CI/CD by leveraging Code Pipeline. I hope you are looking forward to the rest of the series. The template I covered in this post can be downloaded below. I highly recommend deploying the Stack yourself to see what it builds and ensure you understand it completely. I spent a lot of time perfecting these templates and making them the best they can be. If you would like to contact me about them, please follow links to my Facebook and send me a message.