Create VPC in CloudFormation and launch it in PowerShell

CloudFormationSometimes, you just want a working, complete but basic CloudFormation template to use as a basis for something more complex. I’ve wanted such a CloudFormation template that creates a bare-bones VPC for quite some time but couldn’t find one. AWS’s samples are old (their templates create old-school NAT instances for example) and are either too simple or too complex. And most other sample CloudFormation templates I found on the net weren’t, IMHO, all that useful.

So, I wrote my own sample CloudFormation template for a VPC. The template is below and you can use it either as-is or as a building block. I tried to make it as clear and self-documenting as possible. I put extra effort into tagging each resource so that it would be clear exactly what was done when you review assets created by the template in the AWS Console. Be sure to check the “Outputs” tab of the stack in the CloudFormation console to see everything in one place.

It produces a functional VPC with one public subnet, one private subnet, a NAT gateway and route tables for the subnets. It also creates and associates two VPC security groups, one for each subnet. The private security group allows all traffic from all instances in the VPC’s CIDR; the public security group permits inbound traffic from 0.0.0.0/0 on port 22. (I am going to post an enhanced version of this template in the near future that launches an EC2 AWS Linux instance you can use in the VPC as a jump server or bastion host. (2017-05-02: And here it is.)

I find launching CloudFormation templates via the AWS Console tedious. That presented the opportunity to get familiar with the AWS PowerShell cmdlets for CloudFormation in a script that launches this template. That PowerShell script is also available below. It uses Test-CFNTemplate to validate the CloudFormation VPC template.

Learning Test-CFNTemplate gave me the opportunity to explore the $AWSHistory variable. The documentation details an important concept to keep in mind when using the AWS PowerShell cmdlets: error information is in the automatic variable, not necessarily in the return from the cmdlet you are using. So, the CloudFormation launch script includes this statement

$AWSAPIResponse = (($AWSHistory.LastServiceResponse).GetType()).Name

This statement looks complex but it really isn’t. It simply gets the typename of the last response from AWS. In this case, that should be ValidTemplateResponse so that’s what we check for in the switch statement.

If the template passes validation, we pass it to New-CFNStack. This cmdlet requires that parameter defaults in the template which are to be overridden are to passed as an array of hash tables of type Amazon.CloudFormation.Model.Parameter. It would be easy to modify the script to accept parameters for the hash table values from the PowerShell console. To me, it’s just as easy (and more self-documenting) to edit the values in the hash table and save a version of the script that can be used to recreate the stack if necessary.

There are a number of ways you can create an array of hash tables of a certain object type. See this post on Amazon.EC2.Model.Filter and this post for a discussion of how to create arrays of PowerShell hash tables.

While the stack is being built, we loop and every 30 seconds return the current status via Get-CFNStackSummary. I can’t remember why I used Get-CFNStackSummary, which is really designed to produce the last 90 days’ worth of stack status for all stacks in the account. I could’ve just used Get-CFNStack but Get-CFNStackSummary is cooler.

I hope you find the CloudFormation template and PowerShell script useful.

Import-Module AWSPowerShell
<#
    .SYNOPSIS
       Tests and creates a CloudFormation stack using Test-CFNStack & New-CFNStack using a template passed as a parameter
 
    .DESCRIPTION
        Using a template file on the local disk, calls Test-CFNTemplate to validate a single YAML or JSON CloudFormation template and, if valid, launches that stack into the current AWS account and region. Also shows elapsed stack creation time and stack creation status.
 
    .PARAMETER Template
        -Template [String]
 
    .EXAMPLE
        PS C:\> ./Create CloudFormation stack from-VPC-template.ps1 -Template .\MyCloudFormationTemplate.yaml
 
 
    .NOTES
        (c) 2017 Air11 Technology LLC -- licensed under the Apache OpenSource 2.0 license, https://opensource.org/licenses/Apache-2.0
        Licensed under the Apache License, Version 2.0 (the "License");
        you may not use this file except in compliance with the License.
        You may obtain a copy of the License at
 
        http://www.apache.org/licenses/LICENSE-2.0
 
        Unless required by applicable law or agreed to in writing, software
        distributed under the License is distributed on an "AS IS" BASIS,
        WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
        See the License for the specific language governing permissions and
        limitations under the License.
 
        Author's blog: https://yobyot.com
#>

param
(
    [parameter(Mandatory = $true)]
    [string]
    $Template
)
$templateBody = Get-Content -Path $Template -raw
Test-CFNTemplate -TemplateBody $templateBody
$AWSAPIResponse = (($AWSHistory.LastServiceResponse).GetType()).Name
$start = Get-Date
switch ($AWSAPIResponse)
{
    "ValidateTemplateResponse" {
        $timestamp = Get-Date -Format yyyy-MM-dd-HH-mm
        $stack = New-CFNStack -TemplateBody $templateBody `
                     -StackName "Stack-test-$timestamp" `
                     -DisableRollback $true `
                     -Parameter @(@{ Key = "VPCCIDR"; Value = "10.10.0.0/16"; UsePreviousValue = $false }; @{ Key = "PublicSubnetCIDR"; Value = "10.10.50.0/24"; UsePreviousValue = $false }; @{ Key = "PrivateSubnetCIDR"; Value = "10.10.60.0/24"; UsePreviousValue = $false }; @{ Key = "SSHLocation"; Value = "0.0.0.0/0"; UsePreviousValue = $false })
        do {
            Start-Sleep -Seconds 30
            $status = (Get-CFNStackSummary | Where-Object -Property StackID -EQ $stack).StackStatus
            "Stack status: $status Elapsed time: $( New-TimeSpan -Start $start -End (Get-Date) )" -f {g}
        } until ( ($status -eq "CREATE_COMPLETE") -or ($status -eq "CREATE_FAILED") )
    }
    default
    {
        "New-CFNStack failure: $AWSAPIResponse"
    }
}
"Last stack creation status $status"
"Total elapsed time: $( New-TimeSpan -Start $start -End (Get-Date) )" -f {g}
{
    "AWSTemplateFormatVersion": "2010-09-09",
    "Description": "Creates a two-subnet VPC (public w/ NAT gateway and private) (c) 2017 Air11 Technology LLC -- licensed under the Apache OpenSource 2.0 license, https://opensource.org/licenses/Apache-2.0",
    "Metadata": {
        "AWS::CloudFormation::Interface": {
            "ParameterGroups": [{
                "Label": {
                    "default": "VPC configuration parameters"
                },
                "Parameters": [
                    "VPCCIDR",
                    "PublicSubnetCIDR",
                    "PrivateSubnetCIDR",
                    "SSHLocation"
                ]
            }],
            "ParameterLabels": {
                "VPCCIDR": {
                    "default": "Enter CIDR of new VPC"
                },
                "PublicSubnetCIDR": {
                    "default": "Enter CIDR of the public subnet"
                },
                "PrivateSubnetCIDR": {
                    "default": "Enter CIDR of the private subnet"
                },
                "SSHLocation": {
                    "default": "Subnet allowed to ssh on TCP to public subnet"
                }
            }
        }
    },
    "Parameters": {

        "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.10.0.0/16",
            "Description": "CIDR block for entire VPC.",
            "Type": "String"
        },
        "PublicSubnetCIDR": {
            "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.10.10.0/24",
            "Description": "CIDR block for the public subnet",
            "Type": "String"
        },
        "PrivateSubnetCIDR": {
            "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.10.20.0/24",
            "Description": "CIDR block for the private subnet",
            "Type": "String"
        },
        "SSHLocation": {
            "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])(\/([0-9]|[1-2][0-9]|3[0-2]))$",
            "ConstraintDescription": "CIDR block parameter must be in the form x.x.x.x/0-28",
            "Default": "0.0.0.0/0",
            "Description": "Network allowed to ssh to instances in public subnet.",
            "Type": "String"

        }

    },
    "Mappings": {},

    "Resources": {

        "VPC": {
            "Type": "AWS::EC2::VPC",
            "Properties": {
                "EnableDnsSupport": "true",
                "EnableDnsHostnames": "true",
                "CidrBlock": {
                    "Ref": "VPCCIDR"
                },
                "Tags": [{
                        "Key": "Name",
                        "Value": {
                            "Fn::Join": [
                                "", [
                                    "VPC ",
                                    {
                                        "Ref": "VPCCIDR"
                                    }
                                ]
                            ]
                        }
                    },
                    {
                        "Key": "CloudFormationStack",
                        "Value": {
                            "Ref": "AWS::StackId"
                        }
                    }
                ]
            }
        },
        "InternetGateway": {
            "Type": "AWS::EC2::InternetGateway",
            "Properties": {
                "Tags": [{
                        "Key": "Name",
                        "Value": "IGW"
                    },
                    {
                        "Key": "CloudFormationStack",
                        "Value": {
                            "Ref": "AWS::StackId"
                        }
                    }
                ]
            }
        },
        "PublicSubnet": {
            "Type": "AWS::EC2::Subnet",
            "Properties": {
                "VpcId": {
                    "Ref": "VPC"
                },
                "CidrBlock": {
                    "Ref": "PublicSubnetCIDR"
                },
                "Tags": [{
                        "Key": "Name",
                        "Value": {
                            "Fn::Join": [
                                "", [
                                    "Public ",
                                    {
                                        "Ref": "PublicSubnetCIDR"
                                    }
                                ]
                            ]
                        }
                    },
                    {
                        "Key": "CloudFormationStack",
                        "Value": {
                            "Ref": "AWS::StackId"
                        }
                    }
                ]
            }
        },
        "PrivateSubnet": {
            "Type": "AWS::EC2::Subnet",
            "Properties": {
                "VpcId": {
                    "Ref": "VPC"
                },
                "CidrBlock": {
                    "Ref": "PrivateSubnetCIDR"
                },
                "Tags": [{
                        "Key": "Name",
                        "Value": {
                            "Fn::Join": [
                                "", [
                                    "Private ",
                                    {
                                        "Ref": "PrivateSubnetCIDR"
                                    }
                                ]
                            ]
                        }
                    },
                    {
                        "Key": "CloudFormationStack",
                        "Value": {
                            "Ref": "AWS::StackId"
                        }
                    }
                ]
            }
        },
        "AttachGateway": {
            "Type": "AWS::EC2::VPCGatewayAttachment",
            "Properties": {
                "VpcId": {

                    "Ref": "VPC"
                },
                "InternetGatewayId": {
                    "Ref": "InternetGateway"
                }
            }
        },
        "EIP": {
            "Type": "AWS::EC2::EIP",
            "Properties": {
                "Domain": "vpc"
            }
        },
        "NAT": {
            "DependsOn": "AttachGateway",
            "Type": "AWS::EC2::NatGateway",
            "Properties": {
                "AllocationId": {
                    "Fn::GetAtt": ["EIP", "AllocationId"]
                },
                "SubnetId": {
                    "Ref": "PublicSubnet"
                }
            }
        },
        "PublicSubnetRouteTable": {
            "Type": "AWS::EC2::RouteTable",
            "Properties": {
                "VpcId": {
                    "Ref": "VPC"
                },
                "Tags": [{
                        "Key": "Name",
                        "Value": {
                            "Fn::Join": [
                                "", [
                                    "Public ",
                                    {
                                        "Ref": "PublicSubnetCIDR"
                                    }
                                ]
                            ]
                        }
                    },
                    {
                        "Key": "CloudFormationStack",
                        "Value": {
                            "Ref": "AWS::StackId"
                        }
                    }
                ]
            }
        },
        "PublicRoute": {
            "Type": "AWS::EC2::Route",
            "DependsOn": "AttachGateway",
            "Properties": {
                "RouteTableId": {
                    "Ref": "PublicSubnetRouteTable"
                },
                "DestinationCidrBlock": "0.0.0.0/0",
                "GatewayId": {
                    "Ref": "InternetGateway"
                }
            }
        },
        "PublicSubnetRouteTableAssociation": {
            "Type": "AWS::EC2::SubnetRouteTableAssociation",
            "Properties": {
                "SubnetId": {
                    "Ref": "PublicSubnet"
                },
                "RouteTableId": {
                    "Ref": "PublicSubnetRouteTable"
                }
            }
        },
        "PublicInstanceSG": {
            "Type": "AWS::EC2::SecurityGroup",
            "Properties": {
                "VpcId": {
                    "Ref": "VPC"
                },
                "GroupDescription": "Enable SSH access via port 22",
                "SecurityGroupIngress": [{
                        "IpProtocol": "tcp",
                        "FromPort": "22",
                        "ToPort": "22",
                        "CidrIp": {
                            "Ref": "SSHLocation"
                        }
                    },
                    {
                        "IpProtocol": "tcp",
                        "FromPort": "80",
                        "ToPort": "80",
                        "CidrIp": "0.0.0.0/0"
                    }
                ],
                "Tags": [{
                        "Key": "Name",
                        "Value": {
                            "Fn::Join": [
                                "", [
                                    "PublicSG ",
                                    {
                                        "Ref": "VPCCIDR"
                                    }
                                ]
                            ]
                        }
                    },
                    {
                        "Key": "CloudFormationStack",
                        "Value": {
                            "Ref": "AWS::StackId"
                        }
                    }
                ]
            }
        },
        "PrivateSubnetRouteTable": {
            "Type": "AWS::EC2::RouteTable",
            "Properties": {
                "VpcId": {
                    "Ref": "VPC"
                },
                "Tags": [{
                        "Key": "Name",
                        "Value": {
                            "Fn::Join": [
                                "", [
                                    "Private ",
                                    {
                                        "Ref": "PrivateSubnetCIDR"
                                    }
                                ]
                            ]
                        }
                    },
                    {
                        "Key": "CloudFormationStack",
                        "Value": {
                            "Ref": "AWS::StackId"
                        }
                    }
                ]
            }
        },
        "PrivateSubnetRoute": {
            "Type": "AWS::EC2::Route",
            "Properties": {
                "RouteTableId": {
                    "Ref": "PrivateSubnetRouteTable"
                },
                "DestinationCidrBlock": "0.0.0.0/0",
                "NatGatewayId": {
                    "Ref": "NAT"
                }
            }
        },
        "PrivateSubnetRouteTableAssociation": {
            "Type": "AWS::EC2::SubnetRouteTableAssociation",
            "Properties": {
                "SubnetId": {
                    "Ref": "PrivateSubnet"
                },
                "RouteTableId": {
                    "Ref": "PrivateSubnetRouteTable"
                }
            }
        },
        "PrivateSubnetInstanceSG": {
            "Type": "AWS::EC2::SecurityGroup",
            "Properties": {
                "VpcId": {
                    "Ref": "VPC"
                },
                "GroupDescription": "Enable all TCP ports from instances in this VPC",
                "SecurityGroupIngress": [{
                    "IpProtocol": "tcp",
                    "FromPort": "1",
                    "ToPort": "65535",
                    "CidrIp": {
                        "Ref": "VPCCIDR"
                    }
                }],
                "Tags": [{
                        "Key": "Name",
                        "Value": {
                            "Fn::Join": [
                                "", [
                                    "PrivateSG ",
                                    {
                                        "Ref": "VPCCIDR"
                                    }
                                ]
                            ]
                        }
                    },
                    {
                        "Key": "CloudFormationStack",
                        "Value": {
                            "Ref": "AWS::StackId"
                        }
                    }
                ]
            }
        }
    },
    "Outputs": {
        "VPCId": {
            "Description": "VPCId of the newly created VPC",
            "Value": {
                "Ref": "VPC"
            }
        },
        "NatGateway": {
            "Description": "NAT gateway instance",
            "Value": {
                "Ref": "NAT"
            }
        },
        "EIPAddress": {
            "Description": "EIP allocated to NAT gateway",
            "Value": {
                "Ref": "EIP"
            }
        },
        "PublicSubnet": {
            "Description": "SubnetId of the public subnet",
            "Value": {
                "Ref": "PublicSubnet"
            }
        },
        "PublicSubnetRouteTable": {
            "Description": "Public route table",
            "Value": {
                "Ref": "PublicSubnetRouteTable"
            }
        },
        "PublicInstanceSG": {
            "Description": "SG for instances in public subnet",
            "Value": {
                "Ref": "PublicInstanceSG"
            }
        },
        "PrivateSubnet": {
            "Description": "SubnetId of the public subnet",
            "Value": {
                "Ref": "PrivateSubnet"
            }
        },
        "PrivateSubnetRouteTable": {
            "Description": "Private subnet route table",
            "Value": {
                "Ref": "PrivateSubnetRouteTable"
            }
        },
        "PrivateSubnetInstanceSG": {
            "Description": "SG for instances in the private subnet",
            "Value": {
                "Ref": "PrivateSubnetInstanceSG"
            }
        }

    }
}

 


Posted

in

, , ,

by

Comments

2 responses to “Create VPC in CloudFormation and launch it in PowerShell”

  1. Paul G Avatar
    Paul G

    Windows in the cloud… too resource heavy. Powershell … painful. So much simpler, cost effective and efficient with Unix based systems. Yes I’ve used both. I agree though that AWS really needs to update their cloudformation templates. Two thumbs up for the improvements.

    1. Alex Neihaus Avatar
      Alex Neihaus

      Thanks for the kudos. But I am not takin’ the bait for the tired “Linux vs. Windows” argument. I enjoy working with both. With PowerShell in alpha for Linux and macOS, I am sure this argument will continue consume the days of many a tech.

Leave a Reply

Your email address will not be published. Required fields are marked *