1.3 AWS CloudFormation Lab

AWS CloudFormation - provides a common language for you to describe and provision all the infrastructure resources in your cloud environment. CloudFormation allows you to use programming languages or a simple text file to model and provision, in an automated and secure manner, all the resources needed for your applications across all regions and accounts. This gives you a single source of truth for your AWS resources. AWS CloudFormation is available at no additional charge, and you pay only for the AWS resources needed to run your applications.

AWS Cloud9 - AWS Cloud9 is a cloud-based integrated development environment (IDE) that lets you write, run, and debug your code with just a browser. It includes a code editor, debugger, and terminal. Cloud9 comes prepackaged with essential tools for popular programming languages, including JavaScript, Python, PHP,and more, so you don’t need to install files or configure your development machine to start new projects. Since your Cloud9 IDE is cloud-based, you can work on your projects from your office, home, or anywhere using an internet-connected machine.

Opening AWS Cloud9 Environment

We will be using Cloud9 so that everyone in the workshop has an consistent experience.

Please note: Your Cloud9 environment has already been created for you, but we need to make one minor tweak.

  • Make sure you’re using a supported browser
    • The latest version of Chrome, Firefox, or Edge
    • The latest version of Apple Safari for macOS
  • Once there, be sure you are operating in the eu-west-1 region by selecting “EU (Ireland)” from the chooser in the upper right hand corner of your AWS Console. This is the region we will be using for the workshop.
  1. Select EU(Ireland) Region:
  2. Then head over to the Amazon Elastic Cloud Compute (EC2) Console. From the services drop-down menu, select EC2:
  3. Then select Instances from the left side menu pane:
  4. We need to change the instance role assigned to our Cloud9 EC2 instance. Select the aws-cloud9-MGT312 instance, then select the Actions > Instance Settings > Attach/Replace IAM Role:
  5. Select the role names MGT312EC2InstanceProfile in the name and then hit Apply:
  6. Now let’s head over to the Cloud9 console. From the services drop-down menu, search for “cloud9””
    You can also find Cloud9 under “Developer Tools” on the main AWS Console page.
  7. Click the “Open IDE” button”
  8. Once in the IDE, click on AWS Cloud9 on the upper left hand side, Preferences
  9. Then click on AWS Settings and on the right side under Credentials turn off AWS managed temporary credentials. This allows our instance role to take effect, you can hit X on the preferences tab to close it.
    Note the 3 sections highlighted in the next image (Environment File Browser, main Tabbed Editor Window, and Integrated Terminal). We will use Cloud9 IDE to build and test our AWS CloudFormation template.
  10. Before we move on, let’s install a tool we will use later. In the terminal section run the command in the next code block. We will only use cfn-lint in this lab, but note there are other open-source tools available from various AWS teams.
sudo pip install cfn-lint

cfn-lint - A tool that helps validate CloudFormation yaml/json templates against the CloudFormation spec and run additional checks. Includes checking valid values for resource properties and best practices.

Now that we have our IDE ennvironment setup, let’s create a CloudFormation template.

Overview for creating CloudFormation Templates

One of the keys to success with CloudFormation is knowing where to go for resource reference material, the jump-page for this is the AWS Resource and Property Types Reference. Here you’ll find the definitive list of available resources and properties.

In this lab, we’re working with an Elastic Cloud Compute (EC2) Instance. Take a moment to review the resource documentation for the AWS::EC2::Instance type, note the following:

  • Syntax examples: Provided for both JSON and YAML

  • Resource properties: The description, whether the property is required, the type, and what happens to the resource when you update the property are all documented here

  • Return values: These are the values you can reference in your template as logical IDs (e.g. the Logical ID of the instance) or attributes that you can utilize with intrinsic functions (e.g. capturing the DNS name of a created instance as an output)

  • Examples: Having trouble with syntax? Look at the examples section for help.

Every supported resource has a corresponding page that you can use for reference.

What will be creating?

We are going to create a template that we can use later in the AWS Service Catalog Lab. However, this template will achieve the following:

  • Define a set of parameters so we can generalize and re-use the template
  • Use SSM Parameters to dynamically get the latest AMI Id
  • Deploy an Instance using the Parameters defined in a Specific Subnet
  • The instance will have a tag configuring a Name key with the value MGMT312-EC2
  • The instance will be configured with a Role to register with AWS Systems Manager

Create Template

  1. In Cloud9 , click File > New File to create a new file

  2. Review the template below and recreate it in your IDE

    NOTE While you can copy/paste the contents as is, there is value in typing this out in order to familiarize yourself with the syntax/structure of YAML.

  3. First let’s setup our templates scaffolding. There are nine sections that can exist within a CloudFormation template, view the template anatomy section of CloudFormation documentation for more information on template sections. We are going to focus on five sections that are commonly used. You can also note use of these in Appendix section of this workshop that include all the CloudFormation Templates to build the lab environment.

1. Template Structure:

AWSTemplateFormatVersion: "2010-09-09"
Description:
Parameters:
Resources:
Outputs:

The Template has the following structure:

  • The AWSTemplateFormatVersion section (optional) identifies the capabilities of the template. The latest template format version is 2010-09-09 and is currently the only valid value.
  • The Description section (optional) enables you to include comments about your template.
  • Use the optional Parameters section to customize your templates. Parameters enable you to input custom values to your template each time you create or update a stack.
  • The required Resources section declares the AWS resources that you want to include in the stack, such as an Amazon EC2 instance or an Amazon S3 bucket.
  • The optional Outputs section declares output values that you can import into other stacks (to create cross-stack references), return in response (to describe stack calls), or view on the AWS CloudFormation console.

Now, lets add in our parameters. Each parameter we need to specify a name for the parameter and a type for the parameter. Supported types include: String, Number, CommaDelimitedList, List<Number>, AWS-Specific Parameter Types and SSM Parameter Types. Below is what we can learn from each parameter in the code block:

  • EC2InstanceType - shows use of AllowedValues property to specify what Instance Types you will accept in this template. You will also note that we are specifying a default value for this parameter as well.
  • LatestAmiId - shows use of an SSM Parameter Type. This allows for dynamically grabbing the latest AMI ID and populating it into the template.
  • SubnetID - Shows the use of a AWS-Specific Parameter Type to get all the Subnet IDs available in the AWS region.
  • SourceLocation - Shows how you can use regex in the AllowedPattern property to enusre the proper value is typed in, along with the MinLength and MaxLength.
  • VPCID - shows use of another AWS-Specific Parameter Type to get the VPCs available in the AWS region.

2. Template Parameters:

AWSTemplateFormatVersion: "2010-09-09"
Description: "Deploy Single EC2 Linux Instance as part of MGT312 Workshop"
Parameters:
  EC2InstanceType:
    AllowedValues:
      - t3.nano
      - t3.micro
      - t3.small
      - t3.medium
      - t3.large
      - t3.xlarge
      - t3.2xlarge
      - m5.large
      - m5.xlarge
      - m5.2xlarge
    Default: t3.small
    Description: Amazon EC2 instance type
    Type: String
  LatestAmiId:
    Type: 'AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>'
    Default: "/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2"
  SubnetID:
    Description: ID of a Subnet.
    Type: AWS::EC2::Subnet::Id
  SourceLocation:
    Description : The CIDR IP address range that can be used to RDP 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.
  VPCID:
    Description: ID of the target VPC (e.g., vpc-0343606e).
    Type: AWS::EC2::VPC::Id

3. Resource: Security Group

Now that we have some parameters that make our CloudFormaton re-useble, lets define our required section, Resources. We will focus on each resource we add, keep in mind that referencing AWS Resource and Property Types Reference becomes your best friend when authoring CloudFormation Templates. The first resource we add is a Security Group with a rule that allows traffic in on port 80. Notice that we are using an Intrinsic Function Ref to return the value of the VPCID Parameter. AWS CloudFormation provides several built-in functions that help you manage your stacks. Use intrinsic functions in your templates to assign values to properties that are not available until runtime.

Resources:
  EC2InstanceSG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: EC2 Instance Security Group
      VpcId: !Ref 'VPCID'
      SecurityGroupIngress:
      - IpProtocol: tcp
        FromPort: 80
        ToPort: 80
        CidrIp: !Ref SourceLocation

4. Resource: Instance Role

Now we will create a Role to be used by our EC2 instance that allows it to report and communicate with the AWS Systems Manager Service (SSM). In addition to creating the role, we are creating an Instance Profile that we assign the role to. Notice the use of the Intrinsic Function Sub which will substitute values in a string. This is a good way to create Amazon Resource Names as shown in this code block. Also note the use of Pseudo Parameters such AWS::Region and AWS::Partition. Pseudo parameters are parameters that are predefined by AWS CloudFormation. You do not declare them in your template. Use them the same way as you would a parameter.

  SSMInstanceRole:
    Type : AWS::IAM::Role
    Properties:
      Policies:
        - PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Action:
                  - s3:GetObject
                Resource:
                  - !Sub 'arn:aws:s3:::aws-ssm-${AWS::Region}/*'
                  - !Sub 'arn:aws:s3:::aws-windows-downloads-${AWS::Region}/*'
                  - !Sub 'arn:aws:s3:::amazon-ssm-${AWS::Region}/*'
                  - !Sub 'arn:aws:s3:::amazon-ssm-packages-${AWS::Region}/*'
                  - !Sub 'arn:aws:s3:::${AWS::Region}-birdwatcher-prod/*'
                  - !Sub 'arn:aws:s3:::patch-baseline-snapshot-${AWS::Region}/*'
                Effect: Allow
          PolicyName: ssm-custom-s3-policy
      Path: /
      ManagedPolicyArns:
        - !Sub 'arn:${AWS::Partition}:iam::aws:policy/AmazonSSMManagedInstanceCore'
        - !Sub 'arn:${AWS::Partition}:iam::aws:policy/CloudWatchAgentServerPolicy'
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
        - Effect: "Allow"
          Principal:
            Service:
            - "ec2.amazonaws.com"
            - "ssm.amazonaws.com"
          Action: "sts:AssumeRole"
  SSMInstanceProfile:
    Type: "AWS::IAM::InstanceProfile"
    Properties:
      Roles:
      - !Ref SSMInstanceRole

5. Resource: EC2 Instance

Let’s continue to build on our template and finally define our EC2 Instance. Notice here we are bringing together all the pieces we defined in previous resources and parameters. We are using !Ref to dynamically assign or create these resources and values at run-time. Using !Ref also impacts the order in which resources are created by CloudFormation.

  EC2Instance:
    Type: "AWS::EC2::Instance"
    Properties:
      ImageId: !Ref LatestAmiId
      InstanceType: !Ref EC2InstanceType
      IamInstanceProfile: !Ref SSMInstanceProfile
      NetworkInterfaces:
        - DeleteOnTermination: true
          DeviceIndex: '0'
          SubnetId: !Ref 'Subnet'
          GroupSet:
            - !Ref EC2InstanceSG
      Tags:
      - Key: "Name"
        Value: "MGMT312-EC2"

6. Outputs

Finally, lets add in an Output section, that will provide the Private IP for the Instance. However, it also demonstrates the use of another Intrinsic Function GettAtt which gets an attribute that of a Resource, in this case the private ip address of the EC2 Instance we created. The AWS Resource and Property Types Reference will help in determining what information can be grabbed via GetAtt or Ref under each Resource type.

Outputs:
  EC2InstancePrivateIP:
    Value: !GetAtt 'EC2Instance.PrivateIp'
    Description: Private IP for EC2 Instances

Validate the Final Template

Our final template should look like the next code block. In Cloud9 , click File > Save As to save the file and name it singleec2instance.yaml to the home directory (/home/ec2-user-environment). Let’s test our template with cfn-lint

AWSTemplateFormatVersion: "2010-09-09"
Description: "Deploy Single EC2 Linux Instance"
Parameters:
  EC2InstanceType:
    AllowedValues:
      - t3.nano
      - t3.micro
      - t3.small
      - t3.medium
      - t3.large
      - t3.xlarge
      - t3.2xlarge
      - m5.large
      - m5.xlarge
      - m5.2xlarge
    Default: t3.small
    Description: Amazon EC2 instance type
    Type: String
  LatestAmiId:
    Type: 'AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>'
    Default: "/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2"
  SubnetID:
    Description: ID of a Subnet.
    Type: AWS::EC2::Subnet::Id
  SourceLocation:
    Description : The CIDR IP address range that can be used to RDP 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.
  VPCID:
    Description: ID of the target VPC (e.g., vpc-0343606e).
    Type: AWS::EC2::VPC::Id

Resources:

  EC2InstanceSG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: EC2 Instance Security Group
      VpcId: !Ref 'VPCID'
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: !Ref SourceLocation

  SSMInstanceRole:
    Type : AWS::IAM::Role
    Properties:
      Policies:
        - PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Action:
                  - s3:GetObject
                Resource:
                  - !Sub 'arn:aws:s3:::aws-ssm-${AWS::Region}/*'
                  - !Sub 'arn:aws:s3:::aws-windows-downloads-${AWS::Region}/*'
                  - !Sub 'arn:aws:s3:::amazon-ssm-${AWS::Region}/*'
                  - !Sub 'arn:aws:s3:::amazon-ssm-packages-${AWS::Region}/*'
                  - !Sub 'arn:aws:s3:::${AWS::Region}-birdwatcher-prod/*'
                  - !Sub 'arn:aws:s3:::patch-baseline-snapshot-${AWS::Region}/*'
                Effect: Allow
          PolicyName: ssm-custom-s3-policy
      Path: /
      ManagedPolicyArns:
        - !Sub 'arn:${AWS::Partition}:iam::aws:policy/AmazonSSMManagedInstanceCore'
        - !Sub 'arn:${AWS::Partition}:iam::aws:policy/CloudWatchAgentServerPolicy'
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Principal:
              Service:
              - "ec2.amazonaws.com"
              - "ssm.amazonaws.com"
            Action: "sts:AssumeRole"

  SSMInstanceProfile:
    Type: "AWS::IAM::InstanceProfile"
    Properties:
      Roles:
        - !Ref SSMInstanceRole

  EC2Instance:
    Type: "AWS::EC2::Instance"
    Properties:
      ImageId: !Ref LatestAmiId
      InstanceType: !Ref EC2InstanceType
      IamInstanceProfile: !Ref SSMInstanceProfile
      NetworkInterfaces:
        - DeleteOnTermination: true
          DeviceIndex: '0'
          SubnetId: !Ref 'Subnet'
          GroupSet:
            - !Ref EC2InstanceSG
      Tags:
        - Key: "Name"
          Value: "MGMT312-EC2"

Outputs:
  EC2InstancePrivateIP:
    Value: !GetAtt 'EC2Instance.PrivateIp'
    Description: Private IP for EC2 Instances

As a best practice, you should run some type of automated check to validate syntax of your template. Below we will cover two ways you can do this. From the terminal, run:

cfn-lint singleec2instance.yaml

You can also run the validate-template command below into your Cloud9 terminal. This will submit your template to the CloudFormation service for syntax validation:

aws cloudformation validate-template  --template-body file://singleec2instance.yaml --region eu-west-1

Whoops! We have a problem, we mistyped a parameter on line 92, we typed Subnet instead of SubnetID.

Fix that line, and rerun cfn-lint and re-validate template.

When running the validate-template command successfully should return something similar to:

{
    "CapabilitiesReason": "The following resource(s) require capabilities: [AWS::IAM::Role]",
    "Description": "Deploy Single EC2 Linux Instance",
    "Parameters": [
        {
            "DefaultValue": "0.0.0.0/0",
            "NoEcho": false,
            "Description": "The CIDR IP address range that can be used to RDP to the EC2 instances",
            "ParameterKey": "SourceLocation"
        },
        {
            "NoEcho": false,
            "Description": "ID of the target VPC (e.g., vpc-0343606e).",
            "ParameterKey": "VPCID"
        },
        {
            "DefaultValue": "/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2",
            "NoEcho": false,
            "ParameterKey": "LatestAmiId"
        },
        {
            "DefaultValue": "t3.small",
            "NoEcho": false,
            "Description": "Amazon EC2 instance type",
            "ParameterKey": "EC2InstanceType"
        },
        {
            "NoEcho": false,
            "Description": "ID of a Subnet.",
            "ParameterKey": "SubnetID"
        }
    ],
    "Capabilities": [
        "CAPABILITY_IAM"
    ]
}

Create Stack

We now have a valid CloudFormation Template, lets create a test stack.

The CloudFormation create-stack command below will create a new stack named mgt312-test using the template we created above in the Ireland region.

  1. First you are going to need the VPCId and SubnetId that is in your lab account. From the terminal section run the commands in the next code block and save the values that are returned.
   aws ssm get-parameters --names MGMT312PrivSub1 --query 'Parameters[0].[Value]' --output text --region eu-west-1
   aws ssm get-parameters --names MGMT312VPCID --query 'Parameters[0].[Value]' --output text --region eu-west-1
  1. Use the AWS CLI in the terminal to launch the template. Replace the YourVPCID and YourSubnetID values with values you obtained from the previous step.
   aws cloudformation create-stack --stack-name mgt312-test --template-body file://singleec2instance.yaml --parameters ParameterKey=SubnetID,ParameterValue=YourSubnetID ParameterKey=VPCID,ParameterValue=YourVPCID --capabilities CAPABILITY_IAM --region eu-west-1
  1. Let’s head over to the AWS Cloudformation console. From the services drop-down menu, search for “Cloudformation”. Once there confirm that you see the mgt312-test stack created successfully. We are going to use this stack for the next lab.

  2. Finally right click on the template in the file browser of Cloud9 and download it locally to use in the service catalog lab later in the workshop.

Review

In this lab we became familiar with AWS CloudFormation. We created a template and learned some of the features of CloudFormation, and validated our template. We then launched the template from the AWS CLI to test it.