Build a VPC with IaC and Maybe TLC

Since we deleted our VPC last week, let's recreate it but this time using CloudFormation.

Prerequisites

  • Have a workload account where you can build the VPC.

The Lesson

In our last couple labs we built a VPC with two public and two private subnets using the console. Then we… deleted it, wiping out all our hard work. Why? Because those pesky NAT Gateway charges add up quickly.

This week I won’t introduce any new big concepts. Instead we will reinforce our learning by building out the exact same VPC using CloudFormation. And I’ll provide the entire template so this is an opportunity to read through each piece, learn how a VPC is wired, and learn some Infrastructure as Code (IaC) in the process.

This is important because, as I mentioned before, one of the best ways to manage costs in training environments is to create and destroy our infrastructure on demand so it isn’t sitting there, idly drawing on our credit card like a freeloading child. Seriously kids, do you really need to eat that much?!?

YAML vs. JSON

There are a variety of IaC ‘languages’ out there, but they mostly use some form of JSON or YAML (or YML) for their syntax. The most popular for AWS are Amazon’s CloudFormation and Hashicorp’s Terraform. As mentioned, we use CloudFormation (for now) because it’s built into AWS. To use Terraform we would need to run it from our computer or some other execution environment. There are also overlay options where you write your IaC in a programming language and behind the scenes it translates back to the foundational IaC (like AWS CDK).

JSON stands for JavaScript Object Notation, and YAML for Yet Another Markdown Language. Based on sense of humor, at least, YAML wins. Also YAML is a superset of JSON, so YAML parsers can understand JSON embedded in YAML documents.

Although we use JSON for all our policies, I prefer YAML for IaC. Here are two equivalent statements (first JSON, then YAML):

    "CloudSLAWInternetGateway": {
      "Type": "AWS::EC2::InternetGateway",
      "Properties": {
        "Tags": [
          {
            "Key": "Name",
            "Value": "CloudSLAW-IGW"
          }
        ]
      }
    },
  # Internet Gateway
  CloudSLAWInternetGateway:
    Type: 'AWS::EC2::InternetGateway'
    Properties: 
      Tags:
        - Key: Name
          Value: CloudSLAW-IGW 

Our VPC Piece by Piece

Instead of boring you with more background, let’s jump in and walk through each major piece of the template we’ll build in the lab. This will help you understand VPC basics, as you see all the relationships and required elements.

AWSTemplateFormatVersion: '2010-09-09'
Description: CloudSLAW CloudFormation template to create a VPC with public and private subnets, and a NAT gateway

The template format version is a standard required element for all templates. The Description is optional but always recommended.

Resources:
  # VPC
  CloudSLAWVPC:
    Type: 'AWS::EC2::VPC'
    Properties: 
      CidrBlock: '10.0.0.0/16'
      EnableDnsSupport: 'true'
      EnableDnsHostnames: 'true'
      Tags:
        - Key: Name
          Value: CloudSLAW

Resources tells CloudFormation that it’s time to get to work! It’s the list of things to be built. In more complex templates we might take in external parameters or set some variables or something.

Anything with a # is a comment, like in most programming languages. That one is just so we know what we are building. Then CloudSLAWVPC is the unique name we can reference from other places. Then we hit two big sections:

  • Type is “this is what I want you to build”. In this case it’s a VPC. These are all defined in the documentation.

  • Properties are resource-specific settings, which also come from the documentation.

In this case you see we define the CIDR block like we did in the console. Then we turn on DNS support and hostnames. The console did this for us automatically, but with IaC we must define everything ourselves.

You should also notice that we include a tag for the name. Most names in AWS are just tags, and the console knows how to display them. Okay, we have our VPC… now we need some core components to make it work:

  # Internet Gateway
  CloudSLAWInternetGateway:
    Type: 'AWS::EC2::InternetGateway'
    Properties: 
      Tags:
        - Key: Name
          Value: CloudSLAW-IGW

Pretty basic. We don’t need to worry about assigning anything. Technically we don’t even need a name. Must a VPC have an Internet Gateway? Not necessarily — you can build a fully private VPC without Internet access. In our case we need it to handle our public subnets and for our NAT Gateway.

  CloudSLAWAttachGateway:
    Type: 'AWS::EC2::VPCGatewayAttachment'
    Properties: 
      VpcId: !Ref CloudSLAWVPC
      InternetGatewayId: !Ref CloudSLAWInternetGateway

Did I fail to mention you actually need to attach the gateway? Take a look at its definition and notice that it doesn’t specify the VPC. It’s just an Internet Gateway, floating in the ether. We need to create a gateway attachment to link it to the VPC.

Oh, and notice that !Ref? Like, what’s that all about? This is a core capability of any IaC. The magic of IaC is that we define what we want built, and the IaC does the building for us. Think about making that attachment — as you’ve seen when making other things in AWS, when you create something it gets a unique ID. When we build things that use other things we need those IDs. As in “Attach the gateway with igw-123 to VPC-456”. But we can’t do that in our template if we don’t know the IDs. Those references allow us to refer to other parts of our template, and the IaC platform uses them to decide what order to build in and to understand dependencies and links.

  # DHCP Options Set
  CloudSLAWDHCPOptionSet:
    Type: 'AWS::EC2::DHCPOptions'
    Properties: 
      DomainName: 'ec2.internal'
      DomainNameServers:
        - 'AmazonProvidedDNS'
      Tags:
        - Key: Name
          Value: CloudSLAW-DHCP

  CloudSLAWVPCDHCPOptionsAssociation:
    Type: 'AWS::EC2::VPCDHCPOptionsAssociation'
    Properties: 
      DhcpOptionsId: !Ref CloudSLAWDHCPOptionSet
      VpcId: !Ref CloudSLAWVPC

I mentioned it in our other labs, but a DHCP Option Set is a required component of a VPC, which manages some common networking elements which other systems expect to have available. It defines internal domain names, and can also define additional options such as which DNS or Network Time Protocol (NTP) servers to use. We keep ours to the basics, and the undefined options just fall back to Amazon’s defaults.

There are other core components we need for a functional VPC, like route tables, but we need subnets before we get there.

# Subnets
  SlawPublicSubnet1:
    Type: 'AWS::EC2::Subnet'
    Properties: 
      VpcId: !Ref CloudSLAWVPC
      CidrBlock: '10.0.1.0/24'
      AvailabilityZone: !Select [ 0, !GetAZs '' ]
      MapPublicIpOnLaunch: 'true'
      Tags:
        - Key: Name
          Value: slaw-public-1

  SlawPublicSubnet2:
    Type: 'AWS::EC2::Subnet'
    Properties: 
      VpcId: !Ref CloudSLAWVPC
      CidrBlock: '10.0.2.0/24'
      AvailabilityZone: !Select [ 1, !GetAZs '' ]
      MapPublicIpOnLaunch: 'true'
      Tags:
        - Key: Name
          Value: slaw-public-2

Look familiar? It’s exactly what we built last week. The name and type are pretty obvious, but there’s some new weirdness in the properties in Availability Zones?

If you recall, an AZ is basically a separate datacenter for resiliency, and when we create subnets we assign them to an AZ. !GatAZs is a built-in function of CloudFomation which says “gimme a list of the current AZs”. !Select [0 means “gimme the first thing in the list”. Since we want our subnets in different AZs, we also select the next one from the list for public subnet #2.

MapPublicIPOnLaunch means we assign an AWS controlled Internet-accessible IP by default to anything launched in that subnet. You can totally turn this off, and probably want to in most production environments.

Okay, that’s our two public subnets — anything different for the privates?

  SlawPrivateSubnet1:
    Type: 'AWS::EC2::Subnet'
    Properties: 
      VpcId: !Ref CloudSLAWVPC
      CidrBlock: '10.0.3.0/24'
      AvailabilityZone: !Select [ 0, !GetAZs '' ]
      Tags:
        - Key: Name
          Value: slaw-private-1

  SlawPrivateSubnet2:
    Type: 'AWS::EC2::Subnet'
    Properties: 
      VpcId: !Ref CloudSLAWVPC
      CidrBlock: '10.0.4.0/24'
      AvailabilityZone: !Select [ 1, !GetAZs '' ]
      Tags:
        - Key: Name
          Value: slaw-private-2

No real difference yet, except we skip the MapPublicIP thing, which isn’t required. Notice we stick with the same two AZs. This is important for when we start mapping things like load balancers, where we need the public and private sides to share the same Availability Zones.

# Route Tables
  PublicRouteTable:
    Type: 'AWS::EC2::RouteTable'
    Properties: 
      VpcId: !Ref CloudSLAWVPC
      Tags:
        - Key: Name
          Value: CloudSLAW-Public-RT

  PrivateRouteTable:
    Type: 'AWS::EC2::RouteTable'
    Properties: 
      VpcId: !Ref CloudSLAWVPC
      Tags:
        - Key: Name
          Value: CloudSLAW-Private-RT

Now we get to route tables. Like what we built in the console, we have two — one for the public subnet and one for the private. But aside from the names, notice something missing? We haven’t attached them to the subnets — they just exist in the VPC.

# Subnet Route Table Associations
  PublicSubnet1RouteTableAssociation:
    Type: 'AWS::EC2::SubnetRouteTableAssociation'
    Properties: 
      SubnetId: !Ref SlawPublicSubnet1
      RouteTableId: !Ref PublicRouteTable

  PublicSubnet2RouteTableAssociation:
    Type: 'AWS::EC2::SubnetRouteTableAssociation'
    Properties: 
      SubnetId: !Ref SlawPublicSubnet2
      RouteTableId: !Ref PublicRouteTable

  PrivateSubnet1RouteTableAssociation:
    Type: 'AWS::EC2::SubnetRouteTableAssociation'
    Properties: 
      SubnetId: !Ref SlawPrivateSubnet1
      RouteTableId: !Ref PrivateRouteTable

  PrivateSubnet2RouteTableAssociation:
    Type: 'AWS::EC2::SubnetRouteTableAssociation'
    Properties: 
      SubnetId: !Ref SlawPrivateSubnet2
      RouteTableId: !Ref PrivateRouteTable

Cool, that was keeping me up at night. Okay, our route tables are wired into our subnets. But… they don’t route any traffic yet. Let’s fix that.

  PublicRoute:
    Type: 'AWS::EC2::Route'
    DependsOn: CloudSLAWAttachGateway
    Properties: 
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: '0.0.0.0/0'
      GatewayId: !Ref CloudSLAWInternetGateway

That’s a route. The first !Ref tag assigns it to the public route table. Then we define our destination (what the resource is trying to reach) which in this case, is 0.0.0.0/0 (the Internet). Then the second !Ref says to send any traffic to 0.0.0.0/0 to the Internet Gateway, exactly like last week.

But what about our private subnets? First we need a NAT Gateway.

 # NAT Gateway
  CloudSLAWElasticIP:
    Type: 'AWS::EC2::EIP'
    Properties: 
      Domain: vpc

  CloudSLAWNATGateway:
    Type: 'AWS::EC2::NatGateway'
    Properties: 
      AllocationId: !GetAtt CloudSLAWElasticIP.AllocationId
      SubnetId: !Ref SlawPublicSubnet1
      Tags:
        - Key: Name
          Value: CloudSLAW-NAT

Okay, the point here is notice that we first ask for an Elastic IP address. This is a static Internet IP address we rent from AWS. NAT Gateways need their own public IP address, and AWS makes us pay for one instead of just assigning it.

This little block first requests the IP. Then it requests a NAT Gateway with an “allocation” of that IP, and puts it on one of our public subnets (again, we did all this in the console last week). A NAT Gateway need three things: a public IP, a public subnet to sit in (since it uses the Internet Gateway), and the NAT Gateway itself.

With that set up, let’s build our private subnet routes.

 PrivateRoute:
    Type: 'AWS::EC2::Route'
    Properties: 
      RouteTableId: !Ref PrivateRouteTable
      DestinationCidrBlock: '0.0.0.0/0'
      NatGatewayId: !Ref CloudSLAWNATGateway

Pretty simple. Route anything asking for the Internet to the NAT Gateway.

“Now wait a darn minute!” you are asking yourself, “what about the local route we saw last week?” That route looked like 10.0.0.0/16 local. AWS just adds that by default to all route tables — we don’t need to explicitly set it up.

At this point we have our core VPC, our Internet Gateway, our NAT Gateway, and our subnets and route tables. What’s missing? My least favorite required VPC component: the venerable Network Access Control List (NACL). A NACL controls what traffic can go where, but it’s not stateful, which is a topic for another day. I very very rarely use them, but those of you in enterprises and pushing old-school firewalls around are more likely to encounter them. I don’t want to talk about them yet, but we need them, so let’s plop ‘em down at the end.

 # Default NACL Entries
  InboundPublicNetworkAclEntry:
    Type: 'AWS::EC2::NetworkAclEntry'
    Properties: 
      NetworkAclId: !Ref PublicNetworkAcl
      RuleNumber: 100
      Protocol: -1
      RuleAction: allow
      Egress: false
      CidrBlock: '0.0.0.0/0'

  OutboundPublicNetworkAclEntry:
    Type: 'AWS::EC2::NetworkAclEntry'
    Properties: 
      NetworkAclId: !Ref PublicNetworkAcl
      RuleNumber: 100
      Protocol: -1
      RuleAction: allow
      Egress: true
      CidrBlock: '0.0.0.0/0'

  InboundPrivateNetworkAclEntry:
    Type: 'AWS::EC2::NetworkAclEntry'
    Properties: 
      NetworkAclId: !Ref PrivateNetworkAcl
      RuleNumber: 100
      Protocol: -1
      RuleAction: allow
      Egress: false
      CidrBlock: '0.0.0.0/0'

  OutboundPrivateNetworkAclEntry:
    Type: 'AWS::EC2::NetworkAclEntry'
    Properties: 
      NetworkAclId: !Ref PrivateNetworkAcl
      RuleNumber: 100
      Protocol: -1
      RuleAction: allow
      Egress: true
      CidrBlock: '0.0.0.0/0'

  # Associate Network ACLs with Subnets
  PublicSubnet1NetworkAclAssociation:
    Type: 'AWS::EC2::SubnetNetworkAclAssociation'
    Properties: 
      SubnetId: !Ref SlawPublicSubnet1
      NetworkAclId: !Ref PublicNetworkAcl

  PublicSubnet2NetworkAclAssociation:
    Type: 'AWS::EC2::SubnetNetworkAclAssociation'
    Properties: 
      SubnetId: !Ref SlawPublicSubnet2
      NetworkAclId: !Ref PublicNetworkAcl

  PrivateSubnet1NetworkAclAssociation:
    Type: 'AWS::EC2::SubnetNetworkAclAssociation'
    Properties: 
      SubnetId: !Ref SlawPrivateSubnet1
      NetworkAclId: !Ref PrivateNetworkAcl

  PrivateSubnet2NetworkAclAssociation:
    Type: 'AWS::EC2::SubnetNetworkAclAssociation'
    Properties: 
      SubnetId: !Ref SlawPrivateSubnet2
      NetworkAclId: !Ref PrivateNetworkAcl

That’s our entire template to build out the exact same VPC we built by hand in the last lab. For our hands-on lab this week we’ll deploy it, and I’m giving you two different options on how to build it.

One quick side note: Creating a VPC also creates a default route table. We didn’t specify it, and when you look in the console later you’ll see it isn’t even attached to anything, since we created our own by hand.

Lesson Key Points

  • Infrastructure as Code enables us to define what we want built, and IaC tools use the template to build out the defined infrastructure.

  • IaC is usually a version of JSON or YAML. Pick your favorite.

  • When defining a resource it will have various properties, some required and some optional. This will be in the documentation.

  • !Ref is how we tell CloudFormation to pull the ID of something it already built.

    • IaC typically doesn’t need things to be in the right order — the tool figures out the best order to create things for you — but correct sequencing makes things much easier to read.

  • Most IaC tools include fancy functions for things, like getting a list of Availability Zones, which would be hard or impossible otherwise.

  • We covered the minimum requirements for building a VPC with 2 public and 2 private subnets with a NAT Gateway. This is one structure for a network, but far from the only way.

The Lab

Today’s lab is a choose your own adventure!

  • You can copy and paste the IaC elements from this blog post, and build your final template piece by piece.

  • You can just paste the entire thing in all at once. It isn’t like I’m in your home to stop you. Or am I?!?!?!

We have two educational goals:

  • Learn the required components and connections to create a VPC.

  • Learn the fundamentals of CloudFormation so we can read them, not just copy and paste.

Let’s roll!

Video Walkthrough

Step-by-Step

Sign in and go to TestAccount1 > AdministratorAccess:

Make sure you are in Oregon, and then go to CloudFormation > Create stack:

You may recall that previously when we used CloudFormation, I provided the URL of a template stored in Amazon S3, and you pasted that URL in. Today we’ll build our template in a builder, so you can visualize everything as we go. Here you have a choice. If you are short on time (or lazy) you can copy the entire template at once and use that. Here it is:

AWSTemplateFormatVersion: '2010-09-09'
Description: CloudSLAW CloudFormation template to create a VPC with public and private subnets, and a NAT gateway

Resources:
  # VPC
  CloudSLAWVPC:
    Type: 'AWS::EC2::VPC'
    Properties: 
      CidrBlock: '10.0.0.0/16'
      EnableDnsSupport: 'true'
      EnableDnsHostnames: 'true'
      Tags:
        - Key: Name
          Value: CloudSLAW

  # Internet Gateway
  CloudSLAWInternetGateway:
    Type: 'AWS::EC2::InternetGateway'
    Properties: 
      Tags:
        - Key: Name
          Value: CloudSLAW-IGW

  CloudSLAWAttachGateway:
    Type: 'AWS::EC2::VPCGatewayAttachment'
    Properties: 
      VpcId: !Ref CloudSLAWVPC
      InternetGatewayId: !Ref CloudSLAWInternetGateway

  # DHCP Options Set
  CloudSLAWDHCPOptionSet:
    Type: 'AWS::EC2::DHCPOptions'
    Properties: 
      DomainName: 'ec2.internal'
      DomainNameServers:
        - 'AmazonProvidedDNS'
      Tags:
        - Key: Name
          Value: CloudSLAW-DHCP

  CloudSLAWVPCDHCPOptionsAssociation:
    Type: 'AWS::EC2::VPCDHCPOptionsAssociation'
    Properties: 
      DhcpOptionsId: !Ref CloudSLAWDHCPOptionSet
      VpcId: !Ref CloudSLAWVPC

  # Subnets
  SlawPublicSubnet1:
    Type: 'AWS::EC2::Subnet'
    Properties: 
      VpcId: !Ref CloudSLAWVPC
      CidrBlock: '10.0.1.0/24'
      AvailabilityZone: !Select [ 0, !GetAZs '' ]
      MapPublicIpOnLaunch: 'true'
      Tags:
        - Key: Name
          Value: slaw-public-1

  SlawPublicSubnet2:
    Type: 'AWS::EC2::Subnet'
    Properties: 
      VpcId: !Ref CloudSLAWVPC
      CidrBlock: '10.0.2.0/24'
      AvailabilityZone: !Select [ 1, !GetAZs '' ]
      MapPublicIpOnLaunch: 'true'
      Tags:
        - Key: Name
          Value: slaw-public-2

  SlawPrivateSubnet1:
    Type: 'AWS::EC2::Subnet'
    Properties: 
      VpcId: !Ref CloudSLAWVPC
      CidrBlock: '10.0.3.0/24'
      AvailabilityZone: !Select [ 0, !GetAZs '' ]
      Tags:
        - Key: Name
          Value: slaw-private-1

  SlawPrivateSubnet2:
    Type: 'AWS::EC2::Subnet'
    Properties: 
      VpcId: !Ref CloudSLAWVPC
      CidrBlock: '10.0.4.0/24'
      AvailabilityZone: !Select [ 1, !GetAZs '' ]
      Tags:
        - Key: Name
          Value: slaw-private-2

  # Route Tables
  PublicRouteTable:
    Type: 'AWS::EC2::RouteTable'
    Properties: 
      VpcId: !Ref CloudSLAWVPC
      Tags:
        - Key: Name
          Value: CloudSLAW-Public-RT

  PrivateRouteTable:
    Type: 'AWS::EC2::RouteTable'
    Properties: 
      VpcId: !Ref CloudSLAWVPC
      Tags:
        - Key: Name
          Value: CloudSLAW-Private-RT

  # Routes
  PublicRoute:
    Type: 'AWS::EC2::Route'
    DependsOn: CloudSLAWAttachGateway
    Properties: 
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: '0.0.0.0/0'
      GatewayId: !Ref CloudSLAWInternetGateway

  # Subnet Route Table Associations
  PublicSubnet1RouteTableAssociation:
    Type: 'AWS::EC2::SubnetRouteTableAssociation'
    Properties: 
      SubnetId: !Ref SlawPublicSubnet1
      RouteTableId: !Ref PublicRouteTable

  PublicSubnet2RouteTableAssociation:
    Type: 'AWS::EC2::SubnetRouteTableAssociation'
    Properties: 
      SubnetId: !Ref SlawPublicSubnet2
      RouteTableId: !Ref PublicRouteTable

  PrivateSubnet1RouteTableAssociation:
    Type: 'AWS::EC2::SubnetRouteTableAssociation'
    Properties: 
      SubnetId: !Ref SlawPrivateSubnet1
      RouteTableId: !Ref PrivateRouteTable

  PrivateSubnet2RouteTableAssociation:
    Type: 'AWS::EC2::SubnetRouteTableAssociation'
    Properties: 
      SubnetId: !Ref SlawPrivateSubnet2
      RouteTableId: !Ref PrivateRouteTable

  # NAT Gateway
  CloudSLAWElasticIP:
    Type: 'AWS::EC2::EIP'
    Properties: 
      Domain: vpc

  CloudSLAWNATGateway:
    Type: 'AWS::EC2::NatGateway'
    Properties: 
      AllocationId: !GetAtt CloudSLAWElasticIP.AllocationId
      SubnetId: !Ref SlawPublicSubnet1
      Tags:
        - Key: Name
          Value: CloudSLAW-NAT

  PrivateRoute:
    Type: 'AWS::EC2::Route'
    Properties: 
      RouteTableId: !Ref PrivateRouteTable
      DestinationCidrBlock: '0.0.0.0/0'
      NatGatewayId: !Ref CloudSLAWNATGateway

  # Network ACLs
  PublicNetworkAcl:
    Type: 'AWS::EC2::NetworkAcl'
    Properties: 
      VpcId: !Ref CloudSLAWVPC
      Tags:
        - Key: Name
          Value: CloudSLAW-Public-NACL

  PrivateNetworkAcl:
    Type: 'AWS::EC2::NetworkAcl'
    Properties: 
      VpcId: !Ref CloudSLAWVPC
      Tags:
        - Key: Name
          Value: CloudSLAW-Private-NACL

  # Default NACL Entries
  InboundPublicNetworkAclEntry:
    Type: 'AWS::EC2::NetworkAclEntry'
    Properties: 
      NetworkAclId: !Ref PublicNetworkAcl
      RuleNumber: 100
      Protocol: -1
      RuleAction: allow
      Egress: false
      CidrBlock: '0.0.0.0/0'

  OutboundPublicNetworkAclEntry:
    Type: 'AWS::EC2::NetworkAclEntry'
    Properties: 
      NetworkAclId: !Ref PublicNetworkAcl
      RuleNumber: 100
      Protocol: -1
      RuleAction: allow
      Egress: true
      CidrBlock: '0.0.0.0/0'

  InboundPrivateNetworkAclEntry:
    Type: 'AWS::EC2::NetworkAclEntry'
    Properties: 
      NetworkAclId: !Ref PrivateNetworkAcl
      RuleNumber: 100
      Protocol: -1
      RuleAction: allow
      Egress: false
      CidrBlock: '0.0.0.0/0'

  OutboundPrivateNetworkAclEntry:
    Type: 'AWS::EC2::NetworkAclEntry'
    Properties: 
      NetworkAclId: !Ref PrivateNetworkAcl
      RuleNumber: 100
      Protocol: -1
      RuleAction: allow
      Egress: true
      CidrBlock: '0.0.0.0/0'

  # Associate Network ACLs with Subnets
  PublicSubnet1NetworkAclAssociation:
    Type: 'AWS::EC2::SubnetNetworkAclAssociation'
    Properties: 
      SubnetId: !Ref SlawPublicSubnet1
      NetworkAclId: !Ref PublicNetworkAcl

  PublicSubnet2NetworkAclAssociation:
    Type: 'AWS::EC2::SubnetNetworkAclAssociation'
    Properties: 
      SubnetId: !Ref SlawPublicSubnet2
      NetworkAclId: !Ref PublicNetworkAcl

  PrivateSubnet1NetworkAclAssociation:
    Type: 'AWS::EC2::SubnetNetworkAclAssociation'
    Properties: 
      SubnetId: !Ref SlawPrivateSubnet1
      NetworkAclId: !Ref PrivateNetworkAcl

  PrivateSubnet2NetworkAclAssociation:
    Type: 'AWS::EC2::SubnetNetworkAclAssociation'
    Properties: 
      SubnetId: !Ref SlawPrivateSubnet2
      NetworkAclId: !Ref PrivateNetworkAcl

Or you can iteratively paste in the segments as I have them in the Lesson section above. That’s what I’ll show in this step by step. To start, click the radio button for Build from Application Composer, and then click Create in Application Composer:

This can be helpful when you are getting started with CloudFormation. It’s a drag and drop visual system. I rarely use it because I’ve been building templates for so long, but I find it helpful for debugging. From here click Template, which allows us to paste in the code I already wrote.

It will default to YAML but make sure:

Now copy and paste from the template snippets above (or cheat, paste the entire thing in at once, and feel guilty until your dying day, for letting me down). Then click Validate to make sure you didn’t introduce any errors.

If you make a mistake it will tell you:

See how it tells me where my mistake is? In this case I can see that my resource isn’t lined up right — YAML cares about indentation (use spaces, not tabs, and I’m not going to argue).

Okay, to visualize everything I recommend you paste in block by block, based on how I have things broken out in the Lesson. After each block Validate and then click Canvas to see things more visually. Here’s what it looked like when I create the VPC and the Internet Gateway, and then what it looked like after I added the Internet Gateway Attachment:

The attachment really ties the room together

Okay — keep running through, and eventually it will look like this. Revel in the glory, and click Create template:

This will ask whether it should create a default S3 bucket to store the template. Anything you create with the builder is either stored in a new bucket, or you can pick an existing one. Follow the default, but I recommend you copy that URL down somewhere, to save time when we pull it up in a future lab.

This brings us back to the create stack page fills in the URL. Click Next, use CloudSLAW as the name of the stack, and click past the next screens until you reach Submit:

Click past a couple pages, then…

CloudFormation will do its thing. This will take a few minutes, but you might find the following three tabs interesting: Events is the stream of what it’s doing, Resources are all the resources created, and Template is the actual template. These are helpful if you ever need to do a security investigation of a rogue template. Ask me how I know.

Okay, when all that is done, go to VPC > CloudSLAW > Resource map. It should look like this, which should look just like what we built last lab, except with an extra route table. That’s because the VPC itself always wants a top-level route table with a local route. In our last lab we hijacked that for our public subnet, but this time we created a table for that subnet.

Pretty cool, eh?

NOW DESTROY IT ALL!!!

We definitely don’t want that NAT Gateway increasing our bill. Go back to CloudFormation > CloudSLAW > Delete:

You can now create and delete a fully functional VPC whenever you want. And hopefully this helped you understand a little more about how things work under the hood.

-Rich

Reply

or to participate.