Getting Started

This guide walks you through the creation of a Convection template that matches up with one of Amazon's VPC Wizard Scenarios, creating a VPC with public and private subnets.

By the end of this guide you'll have the following resources in your Amazon account

  • A CloudFormation template describing everything in this guide
  • A VPC with two subnets, one public, one private
  • A NAT router so EC2 instances in the private subnet can reach the internet
  • A security group for the NAT router so you can control access to it

To get started, create the following directory structure for your project. If your region is not us-east-1 then change that to your region. If you have multiple regions create multiple region folders each with their own cloud file.

my-convection-project/
├── clouds
│   └── us-east-1
└── templates

In the top level of your convection project create a file "Gemfile" with the following inside.

gem 'convection'

In the us-east-1 folder open a new file named "Cloudfile" and put the following Ruby

Dir.glob('./../../templates/**.rb') do |file|
  require_relative file
end

region 'us-east-1'
name 'convection-demo'

Cloudfiles are written using Convection's DSL. The "convection" gem defines methods like region and name. The region method tells Convection where AWS resources should be created. The name method provides a common identifier for grouping AWS resources.

The core of Convection's DSL consists of two parts, templates and stacks. Templates let you to describe AWS resources in a reusable fashion. Stacks are instances of a template in Amazon. We're going to start by creating a template and stack to define our VPC.

Update your Cloudfile to look like the one below.

Dir.glob('./../../templates/**.rb') do |file|
  require_relative file
end

region 'us-east-1'
name 'convection-demo'

stack 'vpc', Templates::VPC

In the templates directory create a vpc.rb file and include the following in it.

require 'convection'

module Templates
  VPC = Convection.template do
    description 'VPC with Public and Private Subnets (NAT)'

  end
end

The template method creates a Convection template. The description property in the template maps to the "Description" property in a CloudFormation template. Assigning the result of the template method to the VPC variable allows us to pass the template to the stack method. The stack method defines a CloudFormation stack in AWS.

This separation between the description of AWS resources (the template) and the instantiation of those resources (the stack) makes Convection templates flexible.

Now that we've got a Convection template, we can run Convection's diff command to compare what's in our template with what's in Amazon. This is a new template, so we should only see new resources being created.

$> convection diff

compare  Compare local state of stack vpc (convection-demo-vpc) with remote template
 create  .AWSTemplateFormatVersion: 2010-09-09
 create  .Description: VPC with Public and Private Subnets (NAT)

The first line of output tells us that Convection's comparing our "vpc" stack with the remote CloudFormation stack in Amazon. Since this is a new stack, Convection prints two more lines telling us it's going to create the stack and give it a description.

Now that we know what Convection's going to do, we can ask it to check that stack creation will succeed. To do that, we run Convection's validate command.

$> convection validate vpc

Template format error: At least one Resources member must be defined.

The validate command takes a single argument, the name of the stack to validate. We only have the "vpc" stack, so that's what we're validating. Convection tells us that our template's not valid. We're missing a resource.

Since we're building a VPC with two subnets, let's add the VPC itself as a resource. Our VPC will be of size /23 and use the CIDR block 10.10.10.0/23.

Update your vpc.rb template to look like the one below.

require 'convection'

module Templates
  VPC = Convection.template do
    description 'VPC with Public and Private Subnets (NAT)'

    ec2_vpc 'DemoVPC' do
      network '10.10.10.0/23'
    end
  end
end

The ec2_vpc method defines an AWS::EC2::VPC resource in Convection. VPC resources require a CIDR block. We set the network property in our template to the CIDR block we want our VPC to use. Now we can validate our template.

$> convection validate vpc

Template validated successfully

Our template's good, so we'll diff it again to see what's going to change.

$> convection diff

compare  Compare local state of stack vpc (convection-demo-vpc) with remote template
 create  .AWSTemplateFormatVersion: 2010-09-09
 create  .Description: VPC with Public and Private Subnets (NAT)
 create  .Resources.DemoVPC.Type: AWS::EC2::VPC
 create  .Resources.DemoVPC.Properties.CidrBlock: 10.10.10.0/23

That looks correct. Convection will create a VPC with a CIDR block of 10.10.10.0/23. We can run Convection's converge command to create the VPC in Amazon.

$> convection converge

converge  Stack vpc
create_in_progress  convection-demo-vpc: (AWS::CloudFormation::Stack) User Initiated
create_in_progress  DemoVPC: (AWS::EC2::VPC)
create_in_progress  DemoVPC: (AWS::EC2::VPC) Resource creation Initiated
create_complete  DemoVPC: (AWS::EC2::VPC)
create_complete  convection-demo-vpc: (AWS::CloudFormation::Stack)

If we look at the Amazon web console, you can see Convection's created two new things for us. There's a CloudFormation stack named "convection-demo-vpc" and there's the actual VPC resource, which is currently unnamed.

We can name the VPC resource by tagging it. Setting the tag attribute on the "DemoVPC" resource in Convection will add a tag to the resource. We'll use a combination of the cloud name and the stack name as the tag for the VPC.

Update your vpc.rb template to look like the one below.

require 'convection'

module Templates
  VPC = Convection.template do
    description 'VPC with Public and Private Subnets (NAT)'

    ec2_vpc 'DemoVPC' do
      network '10.10.10.0/23'
      tag 'Name', "#{stack.cloud}-#{stack.name}"
    end
  end
end

We're using a naming convection for resources that includes the cloud name and the stack name. That makes it easy to look through the Amazon web console and see which resources belong to which Convection managed clouds. Now we run the diff command to see the changes Convection will apply.

$> convection diff

compare  Compare local state of stack vpc (convection-demo-vpc) with remote template
 create  .Resources.DemoVPC.Properties.Tags.0.Key: Name
 create  .Resources.DemoVPC.Properties.Tags.0.Value: convection-demo-vpc

To see what the cloud formation template for your vpc template would look like you can run convection print_template vpc. This can help you verify that values referenced under the stack namespace are set correctly.

$> convection print-template vpc
{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Description": "VPC with Public and Private Subnets (NAT)",
  "Parameters": {
  },
  "Mappings": {
  },
  "Conditions": {
  },
  "Resources": {
    "DemoVPC": {
      "Type": "AWS::EC2::VPC",
      "Properties": {
        "CidrBlock": "10.10.10.0/23",
        "Tags": [
          {
            "Key": "Name",
            "Value": "convection-demo-vpc"
          }
        ]
      }
    }
  },
  "Outputs": {
  }
}

Convection's going to add the "Name" tag to the "DemoVPC" resource. That's what we want, so we can converge our template.

$> convection converge

converge  Stack vpc
update_in_progress  convection-demo-vpc: (AWS::CloudFormation::Stack) User Initiated
update_in_progress  DemoVPC: (AWS::EC2::VPC)
update_complete  DemoVPC: (AWS::EC2::VPC)
update_complete_cleanup_in_progress  convection-demo-vpc: (AWS::CloudFormation::Stack)
update_complete  convection-demo-vpc: (AWS::CloudFormation::Stack)

Looking at the Amazon web console, we can see that our VPC is now named "convection-demo-vpc".

Creating a private subnet

The CIDR block for our VPC is 10.10.10/23. We'll set up a public subnet with a CIDR block of 10.10.11.0/24 and a private subnet with a CIDR block of 10.10.10.0/24. Let's create the private subnet first.

Just like there's an ec2_vpc method in Convection for creating a VPC, there's also an ec2_subnet method for creating a subnet. And like our VPC, our subnet will have network and tag attributes.

Update your vpc.rb template to look like the one below.

require 'convection'

module Templates
  VPC = Convection.template do
    description 'VPC with Public and Private Subnets (NAT)'

    ec2_vpc 'DemoVPC' do
      network '10.10.10.0/23'
      tag 'Name', "#{stack.cloud}-#{stack.name}"
    end

    ec2_subnet 'PrivateSubnet' do
      network '10.10.10.0/24'
      tag 'Name', "#{stack.cloud}-#{stack.name}-private"
    end

  end
end

We can run the diff command to see what Convection's going to create.

$> convection diff

compare  Compare local state of stack vpc (convection-demo-vpc) with remote template
 create  .Resources.PrivateSubnet.Type: AWS::EC2::Subnet
 create  .Resources.PrivateSubnet.Properties.CidrBlock: 10.10.10.0/24
 create  .Resources.PrivateSubnet.Properties.Tags.0.Key: Name
 create  .Resources.PrivateSubnet.Properties.Tags.0.Value: convection-demo-vpc-private

There's our new private subnet with its CIDR block and "Name" tag. If we look at the documentation for the AWS::EC2::Subnet resource, we can see it has a required "VpcId" attribute. We can use the Convection's fn_ref method to get the logical ID of our VPC resource and pass it in to the subnet.

Update your vpc.rb template to look like the one below.

require 'convection'

module Templates
  VPC = Convection.template do
    description 'VPC with Public and Private Subnets (NAT)'

    ec2_vpc 'DemoVPC' do
      network '10.10.10.0/23'
      tag 'Name', "#{stack.cloud}-#{stack.name}"
    end

    ec2_subnet 'PrivateSubnet' do
      network '10.10.10.0/24'
      tag 'Name', "#{stack.cloud}-#{stack.name}-private"
      vpc fn_ref('DemoVPC')
    end

  end
end

Now we can diff the Cloudfile and see that the "VpcId" property's getting set on our subnet.

$> convection diff

compare  Compare local state of stack vpc (convection-demo-vpc) with remote template
 create  .Resources.PrivateSubnet.Type: AWS::EC2::Subnet
 create  .Resources.PrivateSubnet.Properties.VpcId.Ref: DemoVPC
 create  .Resources.PrivateSubnet.Properties.CidrBlock: 10.10.10.0/24
 create  .Resources.PrivateSubnet.Properties.Tags.0.Key: Name
 create  .Resources.PrivateSubnet.Properties.Tags.0.Value: convection-demo-vpc-private

Let's converge the Cloudfile, and create a new private subnet in Amazon.

$> convection converge

converge  Stack vpc
update_in_progress  convection-demo-vpc: (AWS::CloudFormation::Stack) User Initiated
create_in_progress  PrivateSubnet: (AWS::EC2::Subnet)
create_in_progress  PrivateSubnet: (AWS::EC2::Subnet) Resource creation Initiated
create_complete  PrivateSubnet: (AWS::EC2::Subnet)
update_complete_cleanup_in_progress  convection-demo-vpc: (AWS::CloudFormation::Stack)
update_complete  convection-demo-vpc: (AWS::CloudFormation::Stack)

Creating a public subnet

Creating a public subnet is exactly the same as creating a private subnet. Use the ec2_subnet method to create an AWS::EC2::Subnet resource and give it a CIDR block of 10.10.11.0/24. Insert the following convection code into your vpc.rb template to accomplish this.

ec2_subnet 'PublicSubnet' do
  network '10.10.11.0/24'
  tag 'Name', "#{stack.cloud}-#{stack.name}-public"
  vpc fn_ref('DemoVPC')
end

Looking at the documentation for the AWS::EC2::Subnet resource, the major difference between a public and private subnet is that Amazon assigns IP addresses to public subnets. Making a subnet public is a matter of setting the "MapPublicIpOnLaunch" property to true.

Add the below line to your PublicSubnet block

public_ips true

Your diff should contain your changes for the public subnet.

$> convection diff

compare  Compare local state of stack vpc (convection-demo-vpc) with remote template
 create  .Resources.PublicSubnet.Type: AWS::EC2::Subnet
 create  .Resources.PublicSubnet.Properties.VpcId.Ref: DemoVPC
 create  .Resources.PublicSubnet.Properties.CidrBlock: 10.10.11.0/24
 create  .Resources.PublicSubnet.Properties.MapPublicIpOnLaunch: true
 create  .Resources.PublicSubnet.Properties.Tags.0.Key: Name
 create  .Resources.PublicSubnet.Properties.Tags.0.Value: convection-demo-vpc-public

Convection says it will create our public subnet with the "MapPublicIpOnLaunch" property set to true. It also doesn't show any changes to our private subnet. That's what we expect, so we can converge our template.

$> convection converge

converge  Stack vpc
update_in_progress  convection-demo-vpc: (AWS::CloudFormation::Stack) User Initiated
create_in_progress  PublicSubnet: (AWS::EC2::Subnet)
create_in_progress  PublicSubnet: (AWS::EC2::Subnet) Resource creation Initiated
create_complete  PublicSubnet: (AWS::EC2::Subnet)
update_complete_cleanup_in_progress  convection-demo-vpc: (AWS::CloudFormation::Stack)
update_complete  convection-demo-vpc: (AWS::CloudFormation::Stack)

If we look at our subnets in the Amazon web console, we can see the public subnet has the "Auto-assign Public IP" attribute set to "yes" and the private subnet has that value set to "no". Any EC2 instances we create in the public subnet will automatically get public IP addresses.

Create a security group for the NAT router

Our NAT router is going to live in the public subnet. However, we want to restrict access so only EC2 instances in our private subnet can use it. We can do this with a security group.

Convection provides an ec2_security_group method for creating security groups. The AWS::EC2::SecurityGroup resource requires a description and a reference to our VPC. We can add the ec2_security_group method to our vpc template with a description and vpc attribute to create a default security group for our NAT router.

Add the below block to your vpc.rb template

ec2_security_group 'NATSecurityGroup' do
  description 'NAT access for private subnet'
  vpc fn_ref('DemoVPC')
  tag 'Name', "#{stack.cloud}-#{stack.name}-nat-security-group"
end

We'll follow the same pattern we've used before. Diff the Convection template to make sure it does what we expect, then converge it. This pattern of diffing then converging is useful when paired with code reviews. You can make changes to a template, diff it, then have the changes and diff reviewed before you converge.

$> convection diff

compare  Compare local state of stack vpc (convection-demo-vpc) with remote template
 create  .Resources.NATSecurityGroup.Type: AWS::EC2::SecurityGroup
 create  .Resources.NATSecurityGroup.Properties.GroupDescription: NAT access for private subnet
 create  .Resources.NATSecurityGroup.Properties.VpcId.Ref: DemoVPC
 create  .Resources.NATSecurityGroup.Properties.Tags.0.Key: Name
 create  .Resources.NATSecurityGroup.Properties.Tags.0.Value: convection-demo-vpc-nat-security-group
$> convection converge

converge  Stack vpc
update_in_progress  convection-demo-vpc: (AWS::CloudFormation::Stack) User Initiated
create_in_progress  NATSecurityGroup: (AWS::EC2::SecurityGroup)
create_in_progress  NATSecurityGroup: (AWS::EC2::SecurityGroup) Resource creation Initiated
create_complete  NATSecurityGroup: (AWS::EC2::SecurityGroup)
update_complete_cleanup_in_progress  convection-demo-vpc: (AWS::CloudFormation::Stack)
update_complete  convection-demo-vpc: (AWS::CloudFormation::Stack)

Looking at our new security group in the Amazon web console, we can see it doesn't have any inbound rules. Lets update our security group to allow inbound web traffic from our private subnet.

Within the ec2_security_group method, Convection provides the ingress_rule helper method for defining inbound rules. The method takes a traffic type, a port number, and a block for setting the rule's source attribute.

Update the block in your ec2_security_group method to look like the one below.

ec2_security_group 'NATSecurityGroup' do
  description 'NAT access for private subnet'
  vpc fn_ref('DemoVPC')
  tag 'Name', "#{stack.cloud}-#{stack.name}-nat-security-group"
  ingress_rule :tcp, 443 do
    source '10.10.10.0/24'
  end
  ingress_rule :tcp, 80 do
    source '10.10.10.0/24'
  end
end

This locks down our NAT router so it can only receive requests for web traffic from our private subnet. Having ingress rules for ports 443 and 80 allows us to handle both HTTPS and HTTP traffic.

By default, Amazon sets an outbound rule on our security group that allows all traffic. Since our NAT router only handles web traffic, we can add egress rules as well and lock down outbound requests. Convection has an egress_rule method for setting output traffic rules. It has the same syntax as the ingress_rule method.

Your vpc.rb template should now look like the one below.

require 'convection'

module Templates
  VPC = Convection.template do
    description 'VPC with Public and Private Subnets (NAT)'

    ec2_vpc 'DemoVPC' do
      network '10.10.10.0/23'
      tag 'Name', "#{stack.cloud}-#{stack.name}"
    end

    ec2_subnet 'PrivateSubnet' do
      network '10.10.10.0/24'
      tag 'Name', "#{stack.cloud}-#{stack.name}-private"
      vpc fn_ref('DemoVPC')
    end

    ec2_subnet 'PublicSubnet' do
      network '10.10.11.0/24'
      tag 'Name', "#{stack.cloud}-#{stack.name}-public"
      vpc fn_ref('DemoVPC')
      public_ips true
    end

    ec2_security_group 'NATSecurityGroup' do
      description 'NAT access for private subnet'
      vpc fn_ref('DemoVPC')
      tag 'Name', "#{stack.cloud}-#{stack.name}-nat-security-group"
      ingress_rule :tcp, 443 do
        source '10.10.10.0/24'
      end
      ingress_rule :tcp, 80 do
        source '10.10.10.0/24'
      end
      egress_rule :tcp, 443 do
        source '0.0.0.0/0'
      end
      egress_rule :tcp, 80 do
        source '0.0.0.0/0'
      end
    end

  end
end

Our security group's locked down, so we can diff our template and see what changes.

$> convection diff

compare  Compare local state of stack vpc (convection-demo-vpc) with remote template
 create  .Resources.NATSecurityGroup.Properties.SecurityGroupIngress.0.IpProtocol: 6
 create  .Resources.NATSecurityGroup.Properties.SecurityGroupIngress.0.FromPort: 443
 create  .Resources.NATSecurityGroup.Properties.SecurityGroupIngress.0.ToPort: 443
 create  .Resources.NATSecurityGroup.Properties.SecurityGroupIngress.0.CidrIp: 10.10.10.0/24
 create  .Resources.NATSecurityGroup.Properties.SecurityGroupIngress.1.IpProtocol: 6
 create  .Resources.NATSecurityGroup.Properties.SecurityGroupIngress.1.FromPort: 80
 create  .Resources.NATSecurityGroup.Properties.SecurityGroupIngress.1.ToPort: 80
 create  .Resources.NATSecurityGroup.Properties.SecurityGroupIngress.1.CidrIp: 10.10.10.0/24
 create  .Resources.NATSecurityGroup.Properties.SecurityGroupEgress.0.IpProtocol: 6
 create  .Resources.NATSecurityGroup.Properties.SecurityGroupEgress.0.FromPort: 443
 create  .Resources.NATSecurityGroup.Properties.SecurityGroupEgress.0.ToPort: 443
 create  .Resources.NATSecurityGroup.Properties.SecurityGroupEgress.0.CidrIp: 0.0.0.0/0
 create  .Resources.NATSecurityGroup.Properties.SecurityGroupEgress.1.IpProtocol: 6
 create  .Resources.NATSecurityGroup.Properties.SecurityGroupEgress.1.FromPort: 80
 create  .Resources.NATSecurityGroup.Properties.SecurityGroupEgress.1.ToPort: 80
 create  .Resources.NATSecurityGroup.Properties.SecurityGroupEgress.1.CidrIp: 0.0.0.0/0

It's exactly what we expect. Looks like it's just the ingress and egress rules, so we can converge the template and apply the changes.

$> convection converge

converge  Stack vpc
update_in_progress  convection-demo-vpc: (AWS::CloudFormation::Stack) User Initiated
update_in_progress  NATSecurityGroup: (AWS::EC2::SecurityGroup)
update_complete  NATSecurityGroup: (AWS::EC2::SecurityGroup)
update_complete_cleanup_in_progress  convection-demo-vpc: (AWS::CloudFormation::Stack)
update_complete  convection-demo-vpc: (AWS::CloudFormation::Stack)

Create the NAT router

Now that we've got our security group, we need to set up an EC2 instance that will function as a NAT router. We'll be using one of Amazon's pre-built NAT images since we don't need anything custom. Open up the Amazon web console and search for AMIs with the string "amzn-ami-vpc-nat-hvm" in their name. The most recent one, from September 2016, has ID ami-d2ee95c5. Note that the default instance type is m1.small, which doesn't work with hvm AMIs, so you'll need to choose a different instance type, such as t2.small.

The AWS::EC2::Instance resource handles creating our router instance from the given AMI. Since our router provides internet access, it needs to be in the public subnet. We also need to disable source/destination checking so it can perform network address translation. Finally, we'll make sure the instance is in the security group we just created.

Update your vpc.rb template to look like the one below.

require 'convection'

module Templates
  VPC = Convection.template do
    description 'VPC with Public and Private Subnets (NAT)'

    ec2_vpc 'DemoVPC' do
      network '10.10.10.0/23'
      tag 'Name', "#{stack.cloud}-#{stack.name}"
    end

    ec2_subnet 'PrivateSubnet' do
      network '10.10.10.0/24'
      tag 'Name', "#{stack.cloud}-#{stack.name}-private"
      vpc fn_ref('DemoVPC')
    end

    ec2_subnet 'PublicSubnet' do
      network '10.10.11.0/24'
      tag 'Name', "#{stack.cloud}-#{stack.name}-public"
      vpc fn_ref('DemoVPC')
      public_ips true
    end

    ec2_security_group 'NATSecurityGroup' do
      description 'NAT access for private subnet'
      vpc fn_ref('DemoVPC')
      tag 'Name', "#{stack.cloud}-#{stack.name}-nat-security-group"
      ingress_rule :tcp, 443 do
        source '10.10.10.0/24'
      end
      ingress_rule :tcp, 80 do
        source '10.10.10.0/24'
      end
      egress_rule :tcp, 443 do
        source '0.0.0.0/0'
      end
      egress_rule :tcp, 80 do
        source '0.0.0.0/0'
      end
    end

    ec2_instance 'NATInstance' do
      tag 'Name', "#{stack.cloud}-#{stack.name}-nat"
      image_id 'ami-c02b04a8'
      instance_type 't2.small'
      subnet fn_ref('PublicSubnet')
      security_group fn_ref('NATSecurityGroup')
      src_dst_checks false
    end

  end
end

We can diff our template to see what's going to change.

$> convection diff

compare  Compare local state of stack vpc (convection-demo-vpc) with remote template
 create  .Resources.NATInstance.Type: AWS::EC2::Instance
 create  .Resources.NATInstance.Properties.ImageId: ami-c02b04a8
 create  .Resources.NATInstance.Properties.SubnetId.Ref: PublicSubnet
 create  .Resources.NATInstance.Properties.SecurityGroupIds.0.Ref: NATSecurityGroup
 delete  .Resources.NATInstance.Properties.SourceDestCheck
 create  .Resources.NATInstance.Properties.Tags.0.Key: Name
 create  .Resources.NATInstance.Properties.Tags.0.Value: convection-demo-vpc-nat

It's just our NAT router that's new, so let's converge it.

$> convection converge

converge  Stack vpc
update_in_progress  convection-demo-vpc: (AWS::CloudFormation::Stack) User Initiated
create_in_progress  NATInstance: (AWS::EC2::Instance)
create_in_progress  NATInstance: (AWS::EC2::Instance) Resource creation Initiated
create_complete  NATInstance: (AWS::EC2::Instance)
update_complete_cleanup_in_progress  convection-demo-vpc: (AWS::CloudFormation::Stack)
update_complete  convection-demo-vpc: (AWS::CloudFormation::Stack)

Create a route table for the public subnet

In order for instances in our public subnet to reach the internet, our VPC needs an AWS::EC2::InternetGateway resource. Using stock CloudFormation, we'd create the gateway and wire it up to to our VPC with an AWS::EC2::VPCGatewayAttachment. We'd also need a with AWS::EC2::RouteTable resource for the gateway with a default AWS::EC2::Route resource that lets it connect to the world.

We could use Convection to create each of those resources. However, there's a simpler way. Convection provides an add_route_table method that can generate an internet gateway and wire it up to our VPC.

Update your ec2_vpc block to look like the one below. NOTE we added enable_dns and add_route_table.

  ec2_vpc 'DemoVPC' do
    network '10.10.10.0/23'
    tag 'Name', "#{stack.cloud}-#{stack.name}"
    enable_dns true
    add_route_table 'InternetGateway', gateway_route: true
  end

Diffing our template, we can see our VPC will get a Route, RouteTable, InternetGateway, and InternetGateway attachment. The route lets the internet gateway talk to the world, which is exactly what we want. Notice that we're also enabling DNS support and hostnames on our VPC.

$> convection diff

compare  Compare local state of stack vpc (convection-demo-vpc) with remote template
 create  .Resources.DemoVPCTableInternetGateway.Type: AWS::EC2::RouteTable
 create  .Resources.DemoVPCTableInternetGateway.Properties.VpcId.Ref: DemoVPC
 create  .Resources.DemoVPCTableInternetGateway.Properties.Tags.0.Key: Name
 create  .Resources.DemoVPCTableInternetGateway.Properties.Tags.0.Value: DemoVPCTableInternetGateway
 create  .Resources.DemoVPCIGVPCAttachmentDemoVPC.Type: AWS::EC2::VPCGatewayAttachment
 create  .Resources.DemoVPCIGVPCAttachmentDemoVPC.Properties.VpcId.Ref: DemoVPC
 create  .Resources.DemoVPCIGVPCAttachmentDemoVPC.Properties.InternetGatewayId.Ref: DemoVPCIG
 create  .Resources.DemoVPCIG.Type: AWS::EC2::InternetGateway
 create  .Resources.DemoVPCIG.Properties.Tags.0.Key: Name
 create  .Resources.DemoVPCIG.Properties.Tags.0.Value: DemoVPCInternetGateway
 create  .Resources.DemoVPCTableInternetGatewayRouteDefault.Type: AWS::EC2::Route
 create  .Resources.DemoVPCTableInternetGatewayRouteDefault.Properties.RouteTableId.Ref: DemoVPCTableInternetGateway
 create  .Resources.DemoVPCTableInternetGatewayRouteDefault.Properties.DestinationCidrBlock: 0.0.0.0/0
 create  .Resources.DemoVPCTableInternetGatewayRouteDefault.Properties.GatewayId.Ref: DemoVPCIG
 create  .Resources.DemoVPC.Properties.EnableDnsSupport: true
 create  .Resources.DemoVPC.Properties.EnableDnsHostnames: true

Everything looks good, so we can converge the stack and create new resources.

$> convection converge

converge  Stack vpc
update_in_progress  convection-demo-vpc: (AWS::CloudFormation::Stack) User Initiated
create_in_progress  DemoVPCIG: (AWS::EC2::InternetGateway)
create_in_progress  DemoVPCIG: (AWS::EC2::InternetGateway) Resource creation Initiated
update_in_progress  DemoVPC: (AWS::EC2::VPC)
update_complete  DemoVPC: (AWS::EC2::VPC)
create_in_progress  DemoVPCTableInternetGateway: (AWS::EC2::RouteTable)
create_in_progress  DemoVPCTableInternetGateway: (AWS::EC2::RouteTable) Resource creation Initiated
create_complete  DemoVPCTableInternetGateway: (AWS::EC2::RouteTable)
create_complete  DemoVPCIG: (AWS::EC2::InternetGateway)
create_in_progress  DemoVPCTableInternetGatewayRouteDefault: (AWS::EC2::Route)
create_in_progress  DemoVPCIGVPCAttachmentDemoVPC: (AWS::EC2::VPCGatewayAttachment)
create_in_progress  DemoVPCIGVPCAttachmentDemoVPC: (AWS::EC2::VPCGatewayAttachment) Resource creation Initiated
create_in_progress  DemoVPCTableInternetGatewayRouteDefault: (AWS::EC2::Route) Resource creation Initiated
create_complete  DemoVPCIGVPCAttachmentDemoVPC: (AWS::EC2::VPCGatewayAttachment)
create_complete  DemoVPCTableInternetGatewayRouteDefault: (AWS::EC2::Route)
update_complete_cleanup_in_progress  convection-demo-vpc: (AWS::CloudFormation::Stack)
update_complete  convection-demo-vpc: (AWS::CloudFormation::Stack)

Now that we have an internet gateway, we need to associate its route table with the public subnet. We can create an AWS::EC2::SubnetRouteTableAssociation resource to do that. The ec2_subnet_route_table_association method in Convection will do that.

Add the below block to the bottom of your vpc.rb template below your ec2_instance 'NATInstance' block.

ec2_subnet_route_table_association 'DemoVPCRouteTable' do
  route_table fn_ref('DemoVPCTableInternetGateway')
  subnet fn_ref('PublicSubnet')
end

Where did the reference to "DemoVPCTableInternetGateway" come from? Convection took the name of our VPC "DemoVPC", added "Table" to it, and appended the name of our route table "InternetGateway". Looking at the output from our previous converge shows the "DemoVPCTableInternetGateway" resource being created. Use diff to check that the route table association will be created.

$> convection diff

compare  Compare local state of stack vpc (convection-demo-vpc) with remote template
 create  .Resources.DemoVPCRouteTable.Type: AWS::EC2::SubnetRouteTableAssociation
 create  .Resources.DemoVPCRouteTable.Properties.RouteTableId.Ref: DemoVPCTableInternetGateway
 create  .Resources.DemoVPCRouteTable.Properties.SubnetId.Ref: PublicSubnet

Now go ahead and converge the stack.

$> convection converge

converge  Stack vpc
update_in_progress  convection-demo-vpc: (AWS::CloudFormation::Stack) User Initiated
create_in_progress  DemoVPCRouteTable: (AWS::EC2::SubnetRouteTableAssociation)
create_in_progress  DemoVPCRouteTable: (AWS::EC2::SubnetRouteTableAssociation) Resource creation Initiated
create_complete  DemoVPCRouteTable: (AWS::EC2::SubnetRouteTableAssociation)
update_complete_cleanup_in_progress  convection-demo-vpc: (AWS::CloudFormation::Stack)
update_complete  convection-demo-vpc: (AWS::CloudFormation::Stack)

Create a route table for the private subnet

Just like we created a route table for the public subnet, we now need a route table for the private subnet. Our route table will reference our VPC and define a single route for all traffic from our private instances through our NAT. Convection's ec2_route_table method can be used to explicitly create a route table.

Add the below block to the bottom of your vpc.rb template

ec2_route_table 'PrivateRouteTable' do
  vpc fn_ref('DemoVPC')
  route 'PrivateRoute' do
    destination '0.0.0.0/0'
    instance fn_ref('NATInstance')
  end
end

Now we need to link the private route table to the private subnet. Like we did for the public subnet, we can use Convection's ec2_subnet_route_table_association method.

Add the below to the bottom of your vpc.rb template.

ec2_subnet_route_table_association 'PrivateRouteAssoc' do
  route_table fn_ref('PrivateRouteTable')
  subnet fn_ref('PrivateSubnet')
end

Diffing the template shows three new resources, one for the route, one for the route table, and one for the subnet association.

$> convection diff

compare  Compare local state of stack vpc (convection-demo-vpc) with remote template
 create  .Resources.PrivateRouteTableRoutePrivateRoute.Type: AWS::EC2::Route
 create  .Resources.PrivateRouteTableRoutePrivateRoute.Properties.RouteTableId.Ref: PrivateRouteTable
 create  .Resources.PrivateRouteTableRoutePrivateRoute.Properties.DestinationCidrBlock: 0.0.0.0/0
 create  .Resources.PrivateRouteTableRoutePrivateRoute.Properties.InstanceId.Ref: NATInstance
 create  .Resources.PrivateRouteTable.Type: AWS::EC2::RouteTable
 create  .Resources.PrivateRouteTable.Properties.VpcId.Ref: DemoVPC
 create  .Resources.PrivateRouteAssoc.Type: AWS::EC2::SubnetRouteTableAssociation
 create  .Resources.PrivateRouteAssoc.Properties.RouteTableId.Ref: PrivateRouteTable
 create  .Resources.PrivateRouteAssoc.Properties.SubnetId.Ref: PrivateSubnet

Converge the stack and create the new resources.

$> convection converge

converge  Stack vpc
update_in_progress  convection-demo-vpc: (AWS::CloudFormation::Stack) User Initiated
create_in_progress  PrivateRouteTable: (AWS::EC2::RouteTable)
create_in_progress  PrivateRouteTable: (AWS::EC2::RouteTable) Resource creation Initiated
create_complete  PrivateRouteTable: (AWS::EC2::RouteTable)
create_in_progress  PrivateRouteTableRoutePrivateRoute: (AWS::EC2::Route)
create_in_progress  PrivateRouteAssoc: (AWS::EC2::SubnetRouteTableAssociation)
create_in_progress  PrivateRouteTableRoutePrivateRoute: (AWS::EC2::Route) Resource creation Initiated
create_in_progress  PrivateRouteAssoc: (AWS::EC2::SubnetRouteTableAssociation) Resource creation Initiated
create_complete  PrivateRouteAssoc: (AWS::EC2::SubnetRouteTableAssociation)
create_complete  PrivateRouteTableRoutePrivateRoute: (AWS::EC2::Route)
update_complete_cleanup_in_progress  convection-demo-vpc: (AWS::CloudFormation::Stack)
update_complete  convection-demo-vpc: (AWS::CloudFormation::Stack)

Where to go from here

We used Convection to build an Amazon VPC with public and private subnets, complete with a NAT router for handling internet traffic and a security group for locking down access. From here we could add bastion servers to get SSH access to EC2 instances in the private subnet, or network ACLs to further harden the VPC.

Whatever we do, we now have a solid workflow for making infrastructure improvements. Make a small change. Diff to see what's going to be updated. Converge the change if it looks good.