Sometimes, 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" } } } }
Leave a Reply