AWS CloudFormation Introduction

CloudFormation can be simply defined by infrastructure as code. You define the AWS services which have to be created and configured in a written form inside a file. Then you upload the file to AWS and it will be executed. This will speed up the whole creation process and provides consistency. Amazon won’t charge you for the CloudFormation service but you have to pay all the resulting costs like if you are creating an EC2 instance, you have to pay the runtime fee. The template is either in YAML or JSON format and you either upload it to an S3 bucket and specify the URL or you can directly upload it via CloudFormation. Even if you upload the template file with CloudFormation, it will internally stored in a S3 bucket.

It is recommended to write a template for each layer of architecture. For example, separate templates for networking components, database servers, web servers, and so on. As a result, the required down-time during the maintenance and its impact on the business can be minimized.

Template Structure

So lets take a look on an example template:

AWSTemplateFormatVersion: "2010-09-09"
Description: "Template to create an EC2 instance"
Metadata:
  Instances:
    Description: "Web Server Instance"

Parameters: #input values
  EnvType:
    Description: "Environment type"
    Type: String
    AllowedValues:
      - prod
      - test

Conditions:
  CreateProdResources: !Equals [ !Ref EnvType, prod ]

Mappings: #e.g. set values based on a region
  RegionMap:
    eu-west-1:
      "ami": "ami-0bd1d6c15a40392c"

Transform: # include snippets of code outside the main template
  Name: 'AWS::Include'
  Parameters:
    Location: 's3://MyAmazonS3BucketName/MyFileName.yaml'

Resources: # the AWS resources you are deploying
  EC2Instance:
    Type: AWS::EC2::Instance
    Properties:
    Outputs:
  InstanceID:
    Description: The Instance ID
    Value: !Ref EC2Instance
      InstanceType: t2.micro
      ImageID: ami-0bdb1d6c15a40392c

Outputs:
  InstanceID:
    Description: The Instance ID
    Value: !Ref EC2Instance
      InstanceType: t2.micro
      ImageId: ami-0bd1d6c15a40392c

Each template can have up to nine sections, where only the section Resources is mandatory. But as it looks quite cryptic, let’s go through all sections and clarify their usage:

  • Format Version (optional): The AWS CloudFormation template version that the template conforms to. The template format version isn’t the same as the API or WSDL version. The template format version can change independently of the API and WSDL versions.
  • Description (optional): A text string that describes the template. This section must always follow the template format version section.
  • Metadata (optional): Objects that provide additional information about the template.
  • Parameters (optional): Values to pass to your template at runtime (when you create or update a stack). You can refer to parameters from the Resources and Outputs sections of the template. In this case you can choose between prod and test.
  • Mappings (optional): A mapping of keys and associated values that you can use to specify conditional parameter values, similar to a lookup table. You can match a key to a corresponding value by using the Fn::FindInMap intrinsic function in the Resources and Outputs sections.
  • Conditions (optional): Conditions that control whether certain resources are created or whether certain resource properties are assigned a value during stack creation or update. For example, you could conditionally create a resource that depends on whether the stack is for a production or test environment.
  • Transform (optional): For serverless applications (also referred to as Lambda-based applications), specifies the version of the AWS Serverless Application Model (AWS SAM) to use. When you specify a transform, you can use AWS SAM syntax to declare resources in your template. The model defines the syntax that you can use and how it’s processed. You can also use AWS::Include transforms to work with template snippets that are stored separately from the main AWS CloudFormation template. You can store your snippet files in an Amazon S3 bucket and then reuse the functions across multiple templates.
  • Resources (required): Specifies the stack resources and their properties, such as an Amazon Elastic Compute Cloud instance or an Amazon Simple Storage Service bucket. You can refer to resources in the Resources and Outputs sections of the template.
  • Outputs (optional): Describes the values that are returned whenever you view your stack’s properties. For example, you can declare an output for an S3 bucket name and then call the aws cloudformation describe-stacks AWS CLI command to view the name.

Stacks

A stack is created on a successful execution of a template in CloudFormation. Executing a template creates a defined set of AWS resources. The group of these AWS resources defined in CloudFormation is called a stack. During template execution, if CloudFormation is unable to create any resource, the whole stack creation fails.

An existing CloudFormation stack can be updated by submitting a modified version of the original stack template or by giving different input parameter values during the execution.

Nested stacks are stacks created as part of other stacks. You create a nested stack within another stack by using the AWS::CloudFormation::Stack resource. This is useful if you want to declare the same components in multiple templates. You create a dedicated template for them, then use the resource in your template to reference other templates. For example if you are using a load balancer configuration in multiple stacks, you can create a dedicated template for the load balancer and reference it inside another template. It is also possible to reference a nested stack inside a nested stack, so you are building a hierarchy of stacks.

Hierachy of nested stacks - iamge by AWS official documentation
Hierachy of nested stacks – iamge by AWS official documentation

For referencing output values from other stacks you can use the Fn::ImportValue inside your CloudFormation template.

{
    "Fn::ImportValue": {
        "Fn::Sub": "${NetworkStack}-SubnetID"
    }
}

AWS Serverless Application Model (SAM)

AWS SAM is an open-source framework to build serverless applications on AWS. Serverless applications are a combination of Lambda functions, event sources and other resources that work together to perform tasks. They can include additional resources such as APIs, databases and event source mappings.

There are several advantages of using AWS SAM, for example you just have a single deployment configuration. There is also an integration of CloudFormation, which means you can use the same template structure. Another advantage is the capability of local debugging and testing as the AWS SAM CLI lets you locally build, test and debug serverless applications.

AWS SAM consists of two components:

Deployment of a CloudFormation Template

To replicate the whole process of uploading and deploying a CloudFormation template, the following chapter will guide you through all the steps using the AWS Console.

First of all you need to have a template prepared. A good first step is to look through the predefined templates by AWS. The following link provides some examples and documentation for CloudFormation https://aws.amazon.com/cloudformation/resources/templates/. We picked the EC2 instance with an assigned Security Group which the following source code:

{
    "AWSTemplateFormatVersion" : "2010-09-09",
  
    "Description" : "AWS CloudFormation Sample Template EC2InstanceWithSecurityGroupSample: Create an Amazon EC2 instance running the Amazon Linux AMI. The AMI is chosen based on the region in which the stack is run. This example creates an EC2 security group for the instance to give you SSH access. **WARNING** This template creates an Amazon EC2 instance. You will be billed for the AWS resources used if you create a stack from this template.",
  
    "Parameters" : {
      "KeyName": {
        "Description" : "Name of an existing EC2 KeyPair to enable SSH access to the instance",
        "Type": "AWS::EC2::KeyPair::KeyName",
        "ConstraintDescription" : "must be the name of an existing EC2 KeyPair."
      },
  
      "InstanceType" : {
        "Description" : "WebServer EC2 instance type",
        "Type" : "String",
        "Default" : "t2.small",
        "AllowedValues" : [ "t1.micro", "t2.nano", "t2.micro", "t2.small", "t2.medium", "t2.large", "m1.small", "m1.medium", "m1.large", "m1.xlarge", "m2.xlarge", "m2.2xlarge", "m2.4xlarge", "m3.medium", "m3.large", "m3.xlarge", "m3.2xlarge", "m4.large", "m4.xlarge", "m4.2xlarge", "m4.4xlarge", "m4.10xlarge", "c1.medium", "c1.xlarge", "c3.large", "c3.xlarge", "c3.2xlarge", "c3.4xlarge", "c3.8xlarge", "c4.large", "c4.xlarge", "c4.2xlarge", "c4.4xlarge", "c4.8xlarge", "g2.2xlarge", "g2.8xlarge", "r3.large", "r3.xlarge", "r3.2xlarge", "r3.4xlarge", "r3.8xlarge", "i2.xlarge", "i2.2xlarge", "i2.4xlarge", "i2.8xlarge", "d2.xlarge", "d2.2xlarge", "d2.4xlarge", "d2.8xlarge", "hi1.4xlarge", "hs1.8xlarge", "cr1.8xlarge", "cc2.8xlarge", "cg1.4xlarge"]
  ,
        "ConstraintDescription" : "must be a valid EC2 instance type."
      },
  
      "SSHLocation" : {
        "Description" : "The IP address range that can be used to SSH to the EC2 instances",
        "Type": "String",
        "MinLength": "9",
        "MaxLength": "18",
        "Default": "0.0.0.0/0",
        "AllowedPattern": "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})",
        "ConstraintDescription": "must be a valid IP CIDR range of the form x.x.x.x/x."
     }
    },
  
    "Mappings" : {
      "AWSInstanceType2Arch" : {
        "t1.micro"    : { "Arch" : "HVM64"  },
        "t2.nano"     : { "Arch" : "HVM64"  },
        "t2.micro"    : { "Arch" : "HVM64"  },
        "t2.small"    : { "Arch" : "HVM64"  },
        "t2.medium"   : { "Arch" : "HVM64"  },
        "t2.large"    : { "Arch" : "HVM64"  },
        "m1.small"    : { "Arch" : "HVM64"  },
        "m1.medium"   : { "Arch" : "HVM64"  },
        "m1.large"    : { "Arch" : "HVM64"  },
        "m1.xlarge"   : { "Arch" : "HVM64"  },
        "m2.xlarge"   : { "Arch" : "HVM64"  },
        "m2.2xlarge"  : { "Arch" : "HVM64"  },
        "m2.4xlarge"  : { "Arch" : "HVM64"  },
        "m3.medium"   : { "Arch" : "HVM64"  },
        "m3.large"    : { "Arch" : "HVM64"  },
        "m3.xlarge"   : { "Arch" : "HVM64"  },
        "m3.2xlarge"  : { "Arch" : "HVM64"  },
        "m4.large"    : { "Arch" : "HVM64"  },
        "m4.xlarge"   : { "Arch" : "HVM64"  },
        "m4.2xlarge"  : { "Arch" : "HVM64"  },
        "m4.4xlarge"  : { "Arch" : "HVM64"  },
        "m4.10xlarge" : { "Arch" : "HVM64"  },
        "c1.medium"   : { "Arch" : "HVM64"  },
        "c1.xlarge"   : { "Arch" : "HVM64"  },
        "c3.large"    : { "Arch" : "HVM64"  },
        "c3.xlarge"   : { "Arch" : "HVM64"  },
        "c3.2xlarge"  : { "Arch" : "HVM64"  },
        "c3.4xlarge"  : { "Arch" : "HVM64"  },
        "c3.8xlarge"  : { "Arch" : "HVM64"  },
        "c4.large"    : { "Arch" : "HVM64"  },
        "c4.xlarge"   : { "Arch" : "HVM64"  },
        "c4.2xlarge"  : { "Arch" : "HVM64"  },
        "c4.4xlarge"  : { "Arch" : "HVM64"  },
        "c4.8xlarge"  : { "Arch" : "HVM64"  },
        "g2.2xlarge"  : { "Arch" : "HVMG2"  },
        "g2.8xlarge"  : { "Arch" : "HVMG2"  },
        "r3.large"    : { "Arch" : "HVM64"  },
        "r3.xlarge"   : { "Arch" : "HVM64"  },
        "r3.2xlarge"  : { "Arch" : "HVM64"  },
        "r3.4xlarge"  : { "Arch" : "HVM64"  },
        "r3.8xlarge"  : { "Arch" : "HVM64"  },
        "i2.xlarge"   : { "Arch" : "HVM64"  },
        "i2.2xlarge"  : { "Arch" : "HVM64"  },
        "i2.4xlarge"  : { "Arch" : "HVM64"  },
        "i2.8xlarge"  : { "Arch" : "HVM64"  },
        "d2.xlarge"   : { "Arch" : "HVM64"  },
        "d2.2xlarge"  : { "Arch" : "HVM64"  },
        "d2.4xlarge"  : { "Arch" : "HVM64"  },
        "d2.8xlarge"  : { "Arch" : "HVM64"  },
        "hi1.4xlarge" : { "Arch" : "HVM64"  },
        "hs1.8xlarge" : { "Arch" : "HVM64"  },
        "cr1.8xlarge" : { "Arch" : "HVM64"  },
        "cc2.8xlarge" : { "Arch" : "HVM64"  }
      },
  
      "AWSInstanceType2NATArch" : {
        "t1.micro"    : { "Arch" : "NATHVM64"  },
        "t2.nano"     : { "Arch" : "NATHVM64"  },
        "t2.micro"    : { "Arch" : "NATHVM64"  },
        "t2.small"    : { "Arch" : "NATHVM64"  },
        "t2.medium"   : { "Arch" : "NATHVM64"  },
        "t2.large"    : { "Arch" : "NATHVM64"  },
        "m1.small"    : { "Arch" : "NATHVM64"  },
        "m1.medium"   : { "Arch" : "NATHVM64"  },
        "m1.large"    : { "Arch" : "NATHVM64"  },
        "m1.xlarge"   : { "Arch" : "NATHVM64"  },
        "m2.xlarge"   : { "Arch" : "NATHVM64"  },
        "m2.2xlarge"  : { "Arch" : "NATHVM64"  },
        "m2.4xlarge"  : { "Arch" : "NATHVM64"  },
        "m3.medium"   : { "Arch" : "NATHVM64"  },
        "m3.large"    : { "Arch" : "NATHVM64"  },
        "m3.xlarge"   : { "Arch" : "NATHVM64"  },
        "m3.2xlarge"  : { "Arch" : "NATHVM64"  },
        "m4.large"    : { "Arch" : "NATHVM64"  },
        "m4.xlarge"   : { "Arch" : "NATHVM64"  },
        "m4.2xlarge"  : { "Arch" : "NATHVM64"  },
        "m4.4xlarge"  : { "Arch" : "NATHVM64"  },
        "m4.10xlarge" : { "Arch" : "NATHVM64"  },
        "c1.medium"   : { "Arch" : "NATHVM64"  },
        "c1.xlarge"   : { "Arch" : "NATHVM64"  },
        "c3.large"    : { "Arch" : "NATHVM64"  },
        "c3.xlarge"   : { "Arch" : "NATHVM64"  },
        "c3.2xlarge"  : { "Arch" : "NATHVM64"  },
        "c3.4xlarge"  : { "Arch" : "NATHVM64"  },
        "c3.8xlarge"  : { "Arch" : "NATHVM64"  },
        "c4.large"    : { "Arch" : "NATHVM64"  },
        "c4.xlarge"   : { "Arch" : "NATHVM64"  },
        "c4.2xlarge"  : { "Arch" : "NATHVM64"  },
        "c4.4xlarge"  : { "Arch" : "NATHVM64"  },
        "c4.8xlarge"  : { "Arch" : "NATHVM64"  },
        "g2.2xlarge"  : { "Arch" : "NATHVMG2"  },
        "g2.8xlarge"  : { "Arch" : "NATHVMG2"  },
        "r3.large"    : { "Arch" : "NATHVM64"  },
        "r3.xlarge"   : { "Arch" : "NATHVM64"  },
        "r3.2xlarge"  : { "Arch" : "NATHVM64"  },
        "r3.4xlarge"  : { "Arch" : "NATHVM64"  },
        "r3.8xlarge"  : { "Arch" : "NATHVM64"  },
        "i2.xlarge"   : { "Arch" : "NATHVM64"  },
        "i2.2xlarge"  : { "Arch" : "NATHVM64"  },
        "i2.4xlarge"  : { "Arch" : "NATHVM64"  },
        "i2.8xlarge"  : { "Arch" : "NATHVM64"  },
        "d2.xlarge"   : { "Arch" : "NATHVM64"  },
        "d2.2xlarge"  : { "Arch" : "NATHVM64"  },
        "d2.4xlarge"  : { "Arch" : "NATHVM64"  },
        "d2.8xlarge"  : { "Arch" : "NATHVM64"  },
        "hi1.4xlarge" : { "Arch" : "NATHVM64"  },
        "hs1.8xlarge" : { "Arch" : "NATHVM64"  },
        "cr1.8xlarge" : { "Arch" : "NATHVM64"  },
        "cc2.8xlarge" : { "Arch" : "NATHVM64"  }
      }
  ,
      "AWSRegionArch2AMI" : {
        "af-south-1"       : {"HVM64" : "ami-064cc455f8a1ef504", "HVMG2" : "NOT_SUPPORTED"},
        "ap-east-1"        : {"HVM64" : "ami-f85b1989", "HVMG2" : "NOT_SUPPORTED"},
        "ap-northeast-1"   : {"HVM64" : "ami-0b2c2a754d5b4da22", "HVMG2" : "ami-09d0e0e099ecabba2"},
        "ap-northeast-2"   : {"HVM64" : "ami-0493ab99920f410fc", "HVMG2" : "NOT_SUPPORTED"},
        "ap-northeast-3"   : {"HVM64" : "ami-01344f6f63a4decc1", "HVMG2" : "NOT_SUPPORTED"},
        "ap-south-1"       : {"HVM64" : "ami-03cfb5e1fb4fac428", "HVMG2" : "ami-0244c1d42815af84a"},
        "ap-southeast-1"   : {"HVM64" : "ami-0ba35dc9caf73d1c7", "HVMG2" : "ami-0e46ce0d6a87dc979"},
        "ap-southeast-2"   : {"HVM64" : "ami-0ae99b503e8694028", "HVMG2" : "ami-0c0ab057a101d8ff2"},
        "ca-central-1"     : {"HVM64" : "ami-0803e21a2ec22f953", "HVMG2" : "NOT_SUPPORTED"},
        "cn-north-1"       : {"HVM64" : "ami-07a3f215cc90c889c", "HVMG2" : "NOT_SUPPORTED"},
        "cn-northwest-1"   : {"HVM64" : "ami-0a3b3b10f714a0ff4", "HVMG2" : "NOT_SUPPORTED"},
        "eu-central-1"     : {"HVM64" : "ami-0474863011a7d1541", "HVMG2" : "ami-0aa1822e3eb913a11"},
        "eu-north-1"       : {"HVM64" : "ami-0de4b8910494dba0f", "HVMG2" : "ami-32d55b4c"},
        "eu-south-1"       : {"HVM64" : "ami-08427144fe9ebdef6", "HVMG2" : "NOT_SUPPORTED"},
        "eu-west-1"        : {"HVM64" : "ami-015232c01a82b847b", "HVMG2" : "ami-0d5299b1c6112c3c7"},
        "eu-west-2"        : {"HVM64" : "ami-0765d48d7e15beb93", "HVMG2" : "NOT_SUPPORTED"},
        "eu-west-3"        : {"HVM64" : "ami-0caf07637eda19d9c", "HVMG2" : "NOT_SUPPORTED"},
        "me-south-1"       : {"HVM64" : "ami-0744743d80915b497", "HVMG2" : "NOT_SUPPORTED"},
        "sa-east-1"        : {"HVM64" : "ami-0a52e8a6018e92bb0", "HVMG2" : "NOT_SUPPORTED"},
        "us-east-1"        : {"HVM64" : "ami-032930428bf1abbff", "HVMG2" : "ami-0aeb704d503081ea6"},
        "us-east-2"        : {"HVM64" : "ami-027cab9a7bf0155df", "HVMG2" : "NOT_SUPPORTED"},
        "us-west-1"        : {"HVM64" : "ami-088c153f74339f34c", "HVMG2" : "ami-0a7fc72dc0e51aa77"},
        "us-west-2"        : {"HVM64" : "ami-01fee56b22f308154", "HVMG2" : "ami-0fe84a5b4563d8f27"}
      }
  
    },
  
    "Resources" : {
      "EC2Instance" : {
        "Type" : "AWS::EC2::Instance",
        "Properties" : {
          "InstanceType" : { "Ref" : "InstanceType" },
          "SecurityGroups" : [ { "Ref" : "InstanceSecurityGroup" } ],
          "KeyName" : { "Ref" : "KeyName" },
          "ImageId" : { "Fn::FindInMap" : [ "AWSRegionArch2AMI", { "Ref" : "AWS::Region" },
                            { "Fn::FindInMap" : [ "AWSInstanceType2Arch", { "Ref" : "InstanceType" }, "Arch" ] } ] }
        }
      },
  
      "InstanceSecurityGroup" : {
        "Type" : "AWS::EC2::SecurityGroup",
        "Properties" : {
          "GroupDescription" : "Enable SSH access via port 22",
          "SecurityGroupIngress" : [ {
            "IpProtocol" : "tcp",
            "FromPort" : "22",
            "ToPort" : "22",
            "CidrIp" : { "Ref" : "SSHLocation"}
          } ]
        }
      }
    },
  
    "Outputs" : {
      "InstanceId" : {
        "Description" : "InstanceId of the newly created EC2 instance",
        "Value" : { "Ref" : "EC2Instance" }
      },
      "AZ" : {
        "Description" : "Availability Zone of the newly created EC2 instance",
        "Value" : { "Fn::GetAtt" : [ "EC2Instance", "AvailabilityZone" ] }
      },
      "PublicDNS" : {
        "Description" : "Public DNSName of the newly created EC2 instance",
        "Value" : { "Fn::GetAtt" : [ "EC2Instance", "PublicDnsName" ] }
      },
      "PublicIP" : {
        "Description" : "Public IP address of the newly created EC2 instance",
        "Value" : { "Fn::GetAtt" : [ "EC2Instance", "PublicIp" ] }
      }
    }
  }

Just copy it and save it as cloudformationec2securitygroup.json locally. Then login into your AWS Console and look for the service CloudFormation. On the dashboard, hit the button Create stack on the top right corner.

Cloudformation create new stack - image by author
Cloudformation create new stack – image by author

Just change the template source from S3 URL to upload a template file and choose the cloudformationEc2SecurityGroup.json file.

AWS CloudFormation uploading the template - image by author
AWS CloudFormation uploading the template – image by author

Change the parameters if necessary. The only mandatory part to fill in is to select a predefined KeyName which will be used to ssh into the EC2 instance.

AWS CloudFormation stack details - image by author
AWS CloudFormation stack details – image by author

Finally click through the next pages and finish the creation process. You will be navigated to an overview interface of the CloudFormation stack. It takes some time to finish all the steps, just wait few minutes and refresh the page afterwards. In case something went wrong, a log will be displayed and all the services will be deleted again. Otherwise you face the status CREATE/COMPLETE and you are good to go!

AWS CloudFormation events - image by author
AWS CloudFormation events – image by author

And that is all it takes to create and deploy a CloudFormation Stack. As pointed out before, the best practice is to first look for a predefined template and update it so it fits to your requirements. You will also have to look up a lot of attributes in the official documentation. But with time it becomes more easy and you will get used to.

Leave a Comment

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