AWS CloudFormation Adventures: Part1 — Build Your Own VPC

Christian Vecchiola
26 min readJun 8, 2021

Recently I started playing with CloudFormation to programmatically build infrastructure in AWS. At first sight the tool is very powerful but sometimes it may become a bit daunting because the verbosity of the prose as well as the variety of options available.

The documentation is quite good and usually you can find all the information you need if you’re willing to spend enough time surfing through the documentation and Stack Overflow. Nevertheless, sometimes you may get stuck especially if you get to it the first time and you want to do something that differs a bit from the standard deployment examples, that you can find online and in the AWS Labs Github website.

Photo by Markus Spiske on Unsplash

In this series of articles I will explain my learning journey in building infrastructure programmatically in AWS. More importantly, I will try to point out all the gotchas that I went through to get my examples working. The intent is to help others that went through the same journey as me, and also keep a note for myself as I get more and more familiar and I want to quickly go back to some useful templates built on the way.

Therefore in these series of articles we will start building up our infrastructure from simple pieces: VPC, storage in S3, EC2 to progress (as I learn and experiment) to build more and more complex systems (time permitting :-).

Disclaimer: while today one of the most convenient ways to build infrastructure in AWS is to use the AWS CDK, in this series I decided to go back to the roots and work directly with CloudFormation, as this tool is the closest to Terraform, which essentially uses the same declarative model (more or less).

Before You Start

First of all I had to thank Ken Krueger’s article on Quora that has been my primary reference for building the VPC. I have mostly followed the structure that he outlined in the article and complemented what was there with some additional features and better configurability. Thanks Ken for the article it was very informative and useful!

Another important source whenever you need to work on CloudFormation templates is the AWS CloudFormation Documentation, which should always be open in one of your tabs as you go along. Since most of the resources are quite complex and have a lot of customisation, the ability to check the meaning of all the parameters is quite useful.

Finally, if you use Visual Studio Code, I would suggest you to install the AWS CloudFormation YAML Template Maker, which is integrated with the editor and facilitates writing the boilerplate code required for each of the resources we wan to use in our template. It does not work perfectly, but it is a great time (and typing) saver!

In this series of articles we will also be using the AWS CLI. You can download it here. And finally, you will need an AWS Account that with an API KEY that you can use for the CLI.

You can find all the associated templates in the following repository: https://github.com/hyp0th3rmi4/aws-cloudformation-adventures.

VPC Definition and Use Case

Why Virtual Private Cloud (VPC)?

A Virtual Private Cloud (VPC) is construct that AWS makes available to customer to build your own network in the AWS network. It allows you to isolate your workloads from the workloads of other customers and to finely control the network traffic in and out. If you are not using server-less solutions a VPC is an essential building block of your cloud infrastructure as it constitutes the foundation required to deploy many of the other services that you would need to build a cloud system: EC2 instances, RDS databases, K8s clusters and so on. This is the reason why we start from here.
A VPC is a partition of the AWS network that is allocated to you, which you can further subdivide into subnets to segregate and isolate workloads according to different networking rules.

VPC Structure and Configuration

A common pattern is to define a VPC that contains at least two subnets: one public subnet and one private subnet. The former is provided access to the Internet by means of an Internet Gateway, while the latter is only accessible from within the VPC. This is what we are going to build in this session.

The reason for partitioning a VPC into public and private subnets is to support the most common deployment patterns for web applications, where you need to have resources exposed to the Internet (i.e. web proxies and web servers) that handle most of the incoming traffic, and resources that contain your business application logic and data (i.e. web application servers and databases) shielded away in a more secure network only accessible from within the VPC. For instance, if you are developing your web site, you are likely to put your reverse proxy and web servers such as NginX, Apache, in the public network and your application servers such as Tomcat, JBoss, together with databases such as MySQL into the private network. An example is shown in the figure below.

Figure 1. VPC Structure and Partitioning.

The figure also shows another important aspect. The VPC also requires a CIDR block assigned to it, which defines the address space of the resources hosted in the network. In the same manner, the subnets that are are created inside the VPC require their own CIDR blocks, which must be contained in the CIDR block of the VPC and not overlapping with each other. In Figure 1 we have chosen 10.0.0.0/16 for the entire VPC network, 10.0.0.0/24 for the public network, and 10.0.1.0/24 for the private network. This allocation will allow us to host a total of 65536 addresses in the VPC and 256 addresses into each of the two subnets. This partition allows us to create 254 additional subnets of the same type in our VPC should we need to.

NOTE: it is quite important to select the right CIDR blocks for both the VPC and the subnets, since once they are created we can no longer changed them. Things to consider include:

  1. the total number of IP addresses we expect to use in each subnet.
  2. the address space of our local infrastructure should we need to connect this VPC (and its subnets) to our local infrastructure, as we cannot have overlapping address spaces.
  3. the fact that AWS retains 5 addresses for each subnet that is created for their internal purposes.

With this in mind, building a VPC with /16 prefix gives a good amount of flexibility and allowance for IP addresses and subnets with /24 prefix are of a reasonable size as well.

In terms of connectivity we want the public subnet to have Internet access both for incoming and outgoing traffic, while the private subnet will only be accessible locally from within the public subnet. Figure 1 also shows a setup whereby the private subnet is enabled to reach out to the Internet when connections are originated by entities within the private network (outgoing traffic). This is not necessary but it may come handy if we plan to install software in the boxes in the private network or we need to apply patches to them.

When setting up the same type of network with real infrastructure to enable this configuration we would have used:

  1. a gateway to provide bidirectional access to the Internet.
  2. a bunch of routing tables to configure the traffic rules in each subnet.
  3. a NAT box to enable outgoing Internet traffic from within the private subnet.

In building the same network configuration in AWS we will be creating essentially the same entities, but rather than being physical hardware they will be virtual and software defined.

Tools of the Trade: AWS CloudFormation

You can provision new infrastructure by defining a CloudFormation template. A template provides a mean to declaratively define infrastructure. The listing below shows the structure of a template in YAML format (JSON is the other accepted format to write templates in).

AWSTemplateFormatVersion: 2010-09-09
Description: This is a description for the template.
# This section contains additional metadata
# such as initialisation for EC2 instances or
# information used by the CloudFormation console.
Metadata:
# This section contains the definition of the input
# parameters that can be used to specialise the behaviour
# of the template.
Parameters:
# This section allows for defining a collection of rules
# to perform validation across a combination of parameters.
Rules:
# This sections allows the definition of mappings from a given
# given key to a collection of values. Mappings are particularly
# useful when we need to use different set of values according to
# the region or availability zone.
Mappings:
# Conditions are used to allow for conditional resource definition.
# Resources can be subject to a condition, which if determined to be
# false, will prevent the resource from being created.
Conditions:
# This section allows for the definition of macros that
# CloudFormation will use to process the template.
Transform:
# This section allows for defining the the resources that the
# template will manage once executed.
Resources:
# This section allows for declaring output parameters that can be
# used by other templates. These usually expose information about
# useful attributes of the resources that have been created.
Outputs:

Not all of these sections are required and a minimal scaffold contains only the following sections:

AWSTemplateFormatVersion: 2010-09-09
Resources:

All the other sections are optional and we will use some of them as we build our VPC template.

Once we have defined the template we can manage it either via loading the template file into the AWS CloudFormation Console or by using the AWS CLI. In this tutorial we will be using the AWS CLI, which is assumed to be already configured with an API KEY to execute commands.

In order to access the capabilities of AWS CloudFormation via the CLI we need to type aws cloudformation <command> . By typing aws cloudformation help we can have an overview of all the possible commands that we can use to interact with AWS CloudFormation. In our case we will be focusing on the following commands:

  1. validate-template : this command allows for validating that the template is correct in its definition.
  2. create-stack : this command is used to create a stack from a template.
  3. deploy : this command is used to create and deploy a stack from a template.
  4. delete-stack: this command is used delete an existing stack

These commands will be sufficient to manage our VPC within the context of our article. In order to know more details about each of these commands it is possible to access the inline help by executing the following command:

aws cloudformation <command> help

Building our VPC

Step 1: Defining the VPC Resource

To build a VPC it is sufficient to define a resource of type: AWS::EC2::VPC . As any other resource the entity is defined by a set of properties that allow us to configure the details of the VPC. The listing below shows the resource definition for the VPC. It is possible to see the full set of properties that can be used to configure this type of resource in the documentation.

Step 1 — Defining the VPC.

The template above defines a resource named VPC representing our virtual private cloud. It is important to notice that the name VPC is completely arbitrary and any other name that is suitable for a YAML attribute could be used. What matters is the specification of the type that — as mention before — must be AWS::EC2::VPC .

In order to customise our VPC the Properties section specifies the CIDR block that we want to allocate to the VPC and some additional configuration parameters that control the configuration of the DNS for the VPC. In this case we want the VPC to have DNS support (i.e. EnableDnsSupport: true ) and we want to allocate automatically DNS name to each EC2 instance we deploy in the VPC (i.e. EnableDnsHostnames: true ). The last parameter we configure is the tenancy behaviour for the EC2 instances created within the VPC. We have three options: default , dedicated , and host . This parameter has only effect if we don’t specify a tenancy for the EC2 instance during launch, and its current setting is default , which triggers the default behaviour of AWS. If we want to segregate workloads we can use the options host or dedicated (see the documentation for more details).

Finally, in order to facilitate the housekeeping of resources we have also added a tag that stores the name of the resources as shown in the AWS console. Tags are quite useful to classify and categorise resources and in this case we want to highlight the fact that the resource belongs to a specific CloudFormation stack. As a result the value of the name is the result of the concatenation of the stack name and the suffix -VPC.

The name of the VPC provides us with the opportunity to introduce two CloudFormation intrinsic functions: Fn::Join and Ref . These two functions are expressed in their abbreviated form !Join and !Ref that allow the inline expression of the function, without creating a YAML node.

  1. Fn::Join is used to concatenate strings and accepts a list of strings to combine together in the order we have specified them. The first parameter is an empty string and the second parameter an array of strings.
  2. Ref is used to reference an entity in the CloudFormation template, either a resource, parameter, or other runtime entity whose value we are interested. In this particular case we reference the name of the stack that is identified by AWS::StackName.

We have now defined a complete VPC and we can try our stack by saving the file to disk, validating the content, and then deploying it to AWS.

> aws cloudformation \\ 
validate-template --template-body file://vpc-only.yaml
{
"Parameters": [],
"Description": "This template is used to deploy a simple vpc that is partitioned in a private and public network, with a configured NAT gateway for the private network and an internet gateway for the public network.\n"
}
> aws cloudformation \\
create-stack --stack-name MyVPC
\\
--template-body file://vpc-only.yaml

{
"StackId": "arn:aws:cloudformation:ap-southeast-2:775255536166:stack/MyVPC/49427010-b628-11eb-a69d-027d8657c1ec"
}

The listing above shows the successful execution of the validate-template and the create-stackcommands. Both of them accept the parameter --template-body which is followed by the argument specifying the where the template is located. In this particular case it is a local file so we prefix the location of the file (current directory) with the file:// protocol.

The validate-template command if successful will print out the Parameters and Description sections of the template in JSON format as shown above, while the create-stack command will return the unique identifier of the stack created expressed as an ARN (Amazon Resource Number).

NOTE: when we created the stack we have also specified the parameter --stack-name followed by an argument that defines the name of the stack. It is important that as we modify the template we are building we retain the same name of the stack so that we can operate always on the same entity. Changing the name will cause operating on a different stack rather than updating the one that was created before.

Figure 1 — Event log of the MyVPC stack.

The figure above shows the CloudFormation console executing all the operations that are required to create the resources in the template defining the stack.

Step 2: Subnets

Now that we have created our VPC we want to partition it into two subnets:

  1. a public network that will contain the Internet-facing resources
  2. a private network that will contain the protected resources not directly exposed to the Internet.

The creation of the two subnets entails the reserving a partition of the CIDR block that we have assigned to our VPC. Again, as happens for the VPC the sizing of the VPC, it is not possible to change the address range of a subnet without recreating it. Therefore, consideration must be given to how many IPs would be required for each of the subnets. In our case we have decide to reserve 256 addresses for each subnet:

  1. public subnet: 10.0.0.0/24
  2. private subnet: 10.0.1.0/24

We have allocated the subnets right at the bottom of the address range assigned to the VPC in a contiguous manner. This gives us an ample range left to create subnets of bigger sizes should we need to. A bitmask of 24 bits, leaves 8 bits free which cater for 2⁸ = 256 addresses, which differ one from the other only by the last number of the IP address. This is also the reason why the second subnet starts from 10.0.1.0, as we have exhausted all the IP addresses starting with 10.0.0.

Step 2 — Defining the Public and Private Subnets

The listing above extends the one previously created and adds two additional resources to create which represent the two subnets previously described. The description of a subnet is quite simple since we only have to specify the following parameters:

  1. VpcId: specifies the VPC that contains the subnet. In our case we have identify the containing VPC by referring to the previously created VPC resource named VPC.
  2. AvailabilityZone: specifies the placement of the subnet within the region where the VPC has been provisioned. In our case the VPC has been deployed in Asia Pacific (Sydney) region, and therefore we have chosen ap-southeast-2a as availability zone.
  3. CidrBlock: specifies the address range associated to the subnet, which must be contained in the address range of the containing VPC.

Another important parameter that we have set is the behaviour with regards to mapping public IPs when EC2 instances are created within the subnet. At present time this parameter is not very relevant, but as we would like to create instances within such network in the future we have specified that the public network should map public IP addresses for EC2 instances when such instances are created (i.e. MapPublicIpOnLaunch: true), while we have disabled the same behaviour for the private subnet (this is the default). Finally, as we did for the VPC we have assigned a name to the subnets and tagged it in a manner that shows their belonging to a CloudFormation stack.

More documentation on all the available parameters for a subnet resource can be found in the documentation.

We can now save the content of the listing into a separate file (i.e. vpc-with-subnets.yml) and update our stack by using the deploy command. This command is a combination of the update and create functions, it will first check the status of the current stack and if present it will update it, otherwise it will create it from scratch. The update of the stack analyses first the changes made and then determines the change set to apply, thus minimising the amount of resources that are removed and recreated.

> aws cloudformation deploy \\
--stack-name MyVPC \\
--template-file vpc-with-subnets.yml
Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - MyVPC

An important thing to notice is that we need to specify the same stack name we used before to create the stack to ensure that our changes will be applied to the same infrastructure. The fact that we use a different file is completely irrelevant.

NOTE: it is worth noting that the deploy command uses the --template-file parameter and not the --template-body parameter used by create-stack and validate-template. The --template-file does not require the specification of the protocol as part of the designation of the resource, because it implicitly assumes it to be a file, as the name of the parameter suggests.

Step 3: Defining and Attaching the Internet Gateway

Setting the MapPublicIpOnLauch: true is not sufficient to make a subnet public. As happens in the physical world we need to provide the subnet with network connectivity to the Internet. In the real world this is done by connecting to the subnet a network appliance that has connectivity to the Internet and configuring it to enable bidirectional traffic. In a very similar manner, we would need to create a virtual gateway and then attach it to the subnet.

AWS CloudFormation provides two resources to implement these operations:

  1. InternetGateway: this resource models a gateway that provides connectivity to the Internet.
  2. AttachGateway: this resources models the configuration of the gateway for a given VPC.

These resources have minimal configuration and work out of the box once they have been included into our CloudFormation stack.

Step 3 — Adding the Internet Gateway.

The gist above shows the configuration of the gateway for our VPC. We have added the two resources at the bottom of the file. The InternetGateway resource requires no configuration, while the AttachGateway resource is the component that holds the connectivity between the gateway and the VPC, as it requires specifying the unique identifiers of the both the VPC and the gateway to connect.

NOTE: It is worth noting the specification of DependsOn: VPC for the InternetGateway resource. The gateway resource needs to be created after the VPC resource, but because the mapping is held in a different resource (i.e. AttachGateway) CloudFormation is not able to resolve the correct order of creation by itself. Adding DependsOn: VPC on the InternetGateway resources forces CloudFormation to schedule the correct sequence of operations.

The update of the stack can now be performed by invoking the deploy command again as shown in Step 2.

> aws cloudformation deploy \\
--stack-name MyVPC \\
--template-file vpc-with-gateway.yml
Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - MyVPC

Step 4: Defining the NAT Gateway

Another piece of infrastructure that we need is a NAT gateway. A NAT gateway provides network address translation capabilities for those machines that are not directly exposed to the network we need connectivity to and from. In our case we would be using a NAT gateway to provide outgoing network connectivity to the Internet for those EC2 instances that we will be deploying in the private subnet. This connectivity is useful to ensure that:

  1. We can install, patch, and update packages in such instances.
  2. We can run services that require making request to the internet.

A detailed discussion of how NAT works is outside the scope of this article and the reader can find more documentation here.

In AWS there are two way in which you can enable NAT for a private subnet: using a NAT instance or a NAT gateway (more details here). In this article we will be defining a NAT gateway resource, which is the preferred approach unless special conditions apply.

NOTE: by default a NAT Gateway is a highly available configuration composed by three instances deployed in the same availability zone of the subnet. As a result it is more expensive of a NAT instance, but it comes with the perks of being already configured to operate in a production environment.

To configure a NAT gateway for our private subnet we need two resources:

  1. NatGateway: this resource models the gateway itself.
  2. EIP: this resource models the elastic (public) IP address that the gateway requires to be exposed to the Internet.

The listing below shows how to configure these two resources for our VPC (some lines are omitted as irrelevant for the NAT configuration).

Step 4 — Adding the Internet Gateway

The ElasticIPAddress resource requires the specification of theDomain property. This can have two values based on the type of infrastructure we are using:

  1. For EC2 classic infrastructure the designated value is standard .
  2. For VPC-based infrastructure the designated value is vpc (default if omitted).

Since we are using a VPC, the value we are specifying is vpc. EC2 classic infrastructure refers to legacy deployments of EC2, where your instances run in a flat network that is shared by other customers. This deployment is no longer the preferred approach and this configuration is retained for backward compatibility.

The NatGateway resource requires two properties: AllocationId and SubnetId. The former provides information about the elastic IP allocation to use for the gateway and the the latter is the unique identifier of the private subnet associated to the gateway. The AllocationId property provides us with the opportunity to use another built-in function of AWS CloudFormation: Fn::GetAtt. This function is used to extract an attribute from a resource for the purpose of referencing it as property value, for instance. The function takes two parameters: the attribute to reference and the name of the resource containing the attribute. In our case we have created an EIP resource and we require the AllocationId of such resource to configure the NAT. Therefore we would require the value of the AllocationId property of ElasticIPAddress.

Deployment and update of the existing stack is performed as usual, this time by specifying the vpc-with-nat.yml file.

> aws cloudformation deploy \\
--stack-name MyVPC \\
--template-file vpc-with-nat.yml
Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - MyVPC

Step 5: Defining the Routing Tables

The deployment and configuration of the Internet Gateway and the NAT Gateway are not sufficient to enable the connectivity of the EC2 resources, we also need to define routing tables for each of the subnet as we would do for a physical network.

AWS automatically configures a default routing table in our subnet that enables the local traffic in the VPC. We can verify the existence of this table by selecting either the MyVPC-Pub-SubNet or the MyVPC-Priv-SubNet in the AWS console and inspecting the Route Table tab of the resource. Figure 2 shows the default route table for the MyVPC-Priv-SubNet.

Figure 2 — Default Routing Table of the MyVPC-Priv-SubNet subnet.

The default configuration does not enable us to enable connectivity to the public Internet as well as impose the restrictions we previously discussed for the instances deployed in both subnets. What we want is the following:

  1. All the resources in the public network should be able to connect to the Internet and be reachable from the Internet via the InternetGateway resource.
  2. All the resources in the private network should be able to connect to the Internet via the NatGateway resource.
  3. All the resources in the private and public subnets should be able to connect to each other (enable local traffic as per default).

To configure this behaviour we need to be able to create routing tables, add entries to these table that specify how to route traffic, and associate the routing tables to the subnets. To facilitate these tasks AWS provides us with three different resources: RouteTable, Route, and SubnetRoutTableAssociation. We will create two routing tables: PublicRouteTable and PrivateRouteTable, with the corresponding association resources, and for each of them we will define the routes necessary to achieve the configuration discussed above.

Step 5 — Adding Routing Tables.

The listing above shows the configuration required for both routing table. Their configuration is quite specular and this is due to the flexibility of the` AWS::EC2::Route resource that provides a variety of properties to express the different routing rules (i.e. SubnetId, GatewayId, NatGatewayId). We have defined two resources: RouteTrafficToIGW and RouteTrafficToNGW. The former configures the routing rule that enables connectivity via the Internet Gateway and the latter connectivity via the NAT Gateway. Both of them are applied to the CIDR block 0.0.0.0/0, which means that they will apply to all IP addresses. We have then assigned a specific route table identifier to both rules to include them in the PublicRouteTable and PrivateRouteTable routing tables, respectively. These make the rules effective in the subnets that are associated to such routing tables.

NOTE: it is worth noting that we did not have to specify any additional routing rules, as the default routing rules still apply, and this means that the following rules will be added to the default routing rules already existing to enable local traffic.

> aws cloudformation deploy \\
--stack-name MyVPC \\
--template-file vpc-with-routing.yml
Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - MyVPC

Once we have deployed the stack, we can observe the configuration of the two subnets being modified as specified in the stack.

Figure 3 — Updated routing table for MyVPC-Priv-SubNet
Figure 4 — Updated routing table for MyVPC-Pub-SubNet

As we can see now for each of the two routing tables, whose name match the resources defined in the stack, we have an additional entry which provides connectivity to the Internet Gateway (i.e. igw-0f25ffbe8840bb7e5) and the NAT Gateway (i.e. nat-0a12eb96de6a02ff2).

NOTE: to enable the behaviour described, we only had to specify the rules that apply to instances operating in the subnet and not the routing rules for the two gateways, which are automatically configured by AWS as part of deploying the resources in the VPC.

Step 6: Customising Our Template

We have now created a functional VPC deployment where we can host multiple EC2 instances (or other resources). While the automation of the entire procedure already provides a great advantage, the current stack suffers from the limitation of being only usable out of the box for a single VPC, this is because the template hard-codes most of the values that are used to configure the VPC and associated resources.

The real value of a template relies on being a recipe that can be used multiple times to create the multiple instances of itself. This allows the effort spent in writing the template to scale across many deployments of the VPC. To achieve this result we need to find a way to configure or customise the template when we create/deploy the stack, without editing the CloudFormation template, which is a tedious operations besides being prone to human error.

Fortunately, AWS CloudFormation comes to the rescue with the ability to parameterise a stack template via the definition of parameters. The value of these parameters can then be externalised thus allowing the template to be reused across deployments unchanged. As an act of convenience, parameters may have default values and this provides us with the flexibility to specify only those parameters that require changing for a given deployment.

We can define parameters for a stack by adding to the template the top level Parameters node, where for each parameter identified by its name we can define type, default value (if it does make sense) and a description. To use these parameters in the template we can use the Ref function in the following form: !Ref <ParameterName>.

To make more usable our stack across deployment we have defined the following parameters: VpcCidrBlock, PublicSubnetCidrBlock, PrivateSubnetCidrBlock, PublicSubnetAZ, PrivateSubnetAZ. The gist below shows how to modify the template to include the definition of the parameters. The listing also shows the use of VpcCidrBlock to configure the VPC resource.

Step 6 — Adding Parameters to Customise the Template

In the listing we have defined all parameters with defaults. This allows us to use the file without any other additional resources, since all parameters will be supplied with the default values. If we want to customise the configuration of the stack, we can define different value in a separate file, which we can supply during the deployment of the stack. The listing below shows the content and structure of the parameter file for our example.

[{
"ParameterKey" : "VpcCidrBlock",
"ParamererValue" : "10.1.0.0/16"
},{
"ParameterKey" : "PublicSubnetCidrBlock",
"ParameterValue" : "10.1.0.0/24"
},{
"ParameterKey" : "PublicSubnetAZ",
"ParameterValue" : "ap-southeast-2b"
},{
"ParameterKey" : "PrivateSubnetCidrBlock",
"ParameterValue" : "10.1.1.0/24"
},{
"ParameterKey" : "PrivateSubnetAZ",
"ParameterValue" : "ap-southeast-2c"
}]

Reference: vpc-params.json

The file updates all parameters to different value from the defaults. If we now deploy the stack with the associated vpc-params.json as parameter file we can observe the different configuration of the stack. The specification of runtime parameters is possible by adding the --parameter-overrides switch to the deploy command. This switch accepts both the inline specification of the parameter overrides and the specifications by means of a file.

Inline specification:

--parameter-overrides ParamName1=ParamValue1,ParamName2=ParamValue2

File specification:

--parameter-overrides file://vpc-params.json

In our example, we will be using the file specification and the command to deploy the updated stack is shown in the listing below.

> aws cloudformation deploy \\
--stack-name MyVPC \\
--template-file vpc-with-parameters.yml \\
--parameter-overrides file://vpc-params.json
Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - MyVPC

NOTE: the parameters file can also be specified with the CodePipeline file format and in that case the file will look like the listing specified below.

[
"Parameters" : {
"VpcCidrBlock": "10.1.0.0/16",
"PublicSubnetCidrBlock": "10.1.0.0/24",
"PublicSubnetAZ": "ap-southeast-2b",
"PrivateSubnetCidrBlock": "10.1.1.0/24",
"PrivateSubnetAZ": "ap-southeast-2c"
}
]

You can explore different combinations of the parameters by either modifying the file and removing entries that you don’t want to change or by specifying the parameters to modify directly as part of the command line.

The parameters now defined in the template can also be visualised in the AWS CloudFormation console, under the corresponding Parameters tab.

Figure 5 — Parameters view of the MyVPC stack

NOTE: the parameters introduced require the complete removal of the VPC and its recreation. This causes a sequence of updates of the stack that needs to be executed in the right order in order to avoid failures. While CloudFormation can statically determine most of the dependencies, sometime there are configurations that reveal implicit dependencies. In particular, the following needs to be considered:

  1. A NAT Gateway requires an Internet Gateway in the VPC to supply Internet connectivity. If we try to create a NAT Gateway before the Internet Gateway is fully attached to the VPC the update of the stack will fail.
  2. A VPC to be removed requires to not to have any public IP mapped otherwise the update will fail. If we have an elastic IP associated to any instance (for instance a NAT Gateway) this will cause a dependency that will prevent the VPC update.

If we don’t make the dependency of the NAT Gateway on the setup of the Internet Gateway in the VPC the following error will occur:

NatGateway nat-xxxxxx is in state failed and hence failed to stabilize. Detailed failure message: Network vpc-xxxxxxxx has no Internet gateway attached.

If we don’t consider the dependency of the VPC on the elastic IP addresses we may encounter the following error:

Network vpc-xxxxxxxx has some mapped public address(es). Please unmap those public address(es) before detaching the gateway.

In this scenario, while trying to update the stack with such a drastic set of changes, we found the following implicit dependencies:

  1. The NAT Gateway requires an Internet Gateway attached to the VPC to obtain a public IP. Hence, the attachment must occur before the creation of the NAT.
  2. The Internet Gateway cannot be detached if there are mapped public IP to the VPN. Hence, before detaching the Internet Gateway we need to release all the EIP associations (including the one allocated to the NAT).

Therefore, during creation we need the Internet Gateway setup to complete before the NAT creation. During update we need the reverse process to ensure the correct release. If we are unable to ensure that during the update procedure the NAT is released before the Internet Gateway is detached, the update will fail.

Figure 6 — Sequence of events triggered during the stack update

The figure above shows clearly that during the update of the stack the update of the AWS::EC@::VPCGatewayAttachment resource (i.e. AttachGateway) is triggered before any update to the NAT instance, thus causing the corresponding error that we see in the CloudTrail logs:

Figure 7 — Failure of the DetachInternetGateway operation.

Strangely, if we do invoke aws cloudformation delete-stack --stack-name MyVPC CloudFormation executes the right sequence for the removal of the resources, thus not causing any dependency violation error as shown here in Figure 8. The only conclusion I can make is that during stack update the necessity to update the NAT gateway is not recognised correctly thus causing the dependency violation.

Figure 8 — Order of events during the stack deletion.

I have tried to play with DependsOn but I have been unable to sequence the operations in CloudFormation in a way that causes the proper update sequence. It is also to be said that such a change in a stack is rather drastic and uncommon. Therefore, in scenarios such as the one explored here it is better to delete and recreate the stack from scratch rather than trying to update it.

Step 7: Generating Output Parameters

If we use the CloudFormation template in a much larger system setup it may be useful to reference some of the values associated to the resources defined in the template. This may be necessary, for instance, to configure other resources that rely on the resources defined in the template.

AWS provides the ability of exporting information from a template by defining a set of output parameters. These are named references to attributes of the resources that are defined in the template. Output parameters are defined by introducing an Output top level block, which contains a list of named parameters specified by a description and a value.

Step 7 — Adding Output Parameters to Export Information

The gist above shows the output section for our VPC, where we export the unique identifiers of the VPC, PublicSubnet, and PrivateSubnet resources.

NOTE: The Ref function by default references the identifier of the named resources passed as argument, please consult the documentation of the function to understand how it applies to the different types of resources available in AWS CloudFormation, as the default attribute returned per type of resource may be different.

> aws cloudformation deploy \\
--stack-name MyVPC \\
--template-file vpc-with-parameters.yml \\
--parameter-overrides file://vpc-params.json
Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - MyVPC

Once we have redeployed the updated stack, we can see the output parameters in the Output tab of the AWS CloudFormation console view that describes the stack.

Figure 9 — View of the Output tab of the MyVPC stack.

Final Observations

In this articles we have shown how to create a VPC composed by two subnets and configure it so that one subnet has bidirectional public Internet access (public subnet) and the other has only outgoing connection to the Internet (private subnet). This is a setup quite common in AWS to deploy web applications and other types of systems.

NOTE: the setup discussed can be further enriched by duplicating the number of subnets (public and private) and distribute them across availability zones to support highly available configurations.

We have explored the capabilities of AWS CloudFormation, which uses a declarative approach for deploying virtual infrastructure and services. The configuration of the VPC has allowed us to explore the following resources:

  1. AWS::EC2::VPC
  2. AWS::EC2::Subnet
  3. AWS::EC2::InternetGateway
  4. AWS::EC2::VPCGatewayAttachment
  5. AWS::EC2::EIP
  6. AWS::EC2::NatGateway
  7. AWS::EC2::RouteTable
  8. AWS::EC2::Route
  9. AWS::EC2::RouteTableAssociation

We have also explored how to make the most use of AWS CloudFormation template by enriching the template with input and output parameters. Input parameters enable the customisation of templates by abstracting away some of the values that specialise a template, and delay their specification at deployment time. Output parameters enable templates to export attributes which may be of interest in other templates in scenario where resources defined in one template reference resources in other templates. This is a key capability enabling the definition of larger systems as a composition of smaller building blocks.

In the next sections, we will build upon what we have discussed here to deploy additional services and capabilities and showcase what can be done with AWS CloudFormation.

--

--

Christian Vecchiola

I am a passionate and hands-on thought leader that loves the challenge of translating complex requirements into innovative and game changing solutions.