How AWS CloudFormation Boosts Your Productivity with Reusable Templates

How AWS CloudFormation Boosts Your Productivity with Reusable Templates

This is a continuation of CloudFormation and YAML(Part 1). There, I explained Infrastructure as Code and AWS CloudFormation, the elements of CloudFormation, then dived further into some challenge task using CloudFormation.

For this article, we will focus on the reusability of our templates and we will be covering the features and capabilities within AWS CloudFormation that can help us with that.

Imagine a scenario where you need to deploy similar infrastructure components(AWS Resources) across multiple projects or environments. Writing separate templates from scratch can be time-consuming and error-prone. However, you can significantly enhance your workflow by designing modular and parameterized templates. We will explore how to structure templates in a way that allows you to effortlessly adapt and reuse them, thereby boosting productivity and maintaining consistency.

Prerequisites

Features and Capabilities of AWS CloudFormation

Conditions

Like traditional programming's "if-else if-else" conditional statements, CloudFormation offers a way to use conditions within your templates to enhance the reusability of existing configurations.

For instance, let's consider a scenario where you need to provision an EC2 instance. In a test environment, you want the instance type to be "t2.micro," and in a production environment, you prefer "t2.medium." Instead of creating two separate templates, you can use Conditions to handle these cases within a single template.

To use conditions in your template, you include statements in the following template elements:

  • Parameters: Here you specify your template's input parameter which will be evaluated by your conditions.

  • Conditions: This is where you use the "Fn::If" intrinsic function and "Condition" attribute to define your requirements. will be using both in the sample below

  • Resource and Outputs: Lastly, every condition should be associated with either a resource creation or what needs to be in the outputs.

To further explain the above, we will be creating a Simple S3 Bucket which will have access control of either BucketOwnerFullControl(prod) or BucketOwnerRead(test) based on the environment,

Parameters

AWSTemplateFormatVersion: '2010-09-09'
Description: S3 Bucket Access Control Conditional Example

Parameters:
  Environment:
    Type: String
    Default: test
    AllowedValues: [test, prod]
    Description: Access Control Conditional
  • We specified our template version as well as defined the parameter "Environment"

  • we also defined the parameter attributes which are "type", "description", "AllowedValues" and lastly the "Default".

  • Within the AllowedValues attributes, we specified an array of acceptable responses from our user which can be test or prod.

Conditions

Conditions:
  IsProdEnvironment: !Equals [!Ref Environment, prod]
  • defined a condition with the name "IsProdEnvironment"

  • Within the conditional, use the intrinsic function "!Equals" which compares two values for equality and returns either true or false. Here it compares the constant "prod" and the value passed by the user.

  • [!Ref Environment, prod] is the expression being evaluated by the !Equals function. The expression [!Ref Environment, prod] checks if the value of the Environment parameter is equal to the string "prod".

  • In summary, when the template is being processed, the condition IsProdEnvironment will be evaluated based on whether the value of the Environment parameter is "prod" or "test", respectively. If the value matches, the condition will be true, and if it doesn't match, the condition will be false.

Resources

Resources:
  MyS3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: 
        Fn::If:
          - IsProdEnvironment
          - my-prod-bucket-2
          - my-test-bucket-3
      AccessControl:
        Fn::If:
          - IsProdEnvironment
          - BucketOwnerFullControl
          - BucketOwnerRead
  • I defined a resource "MyS3Bucket" which has the attributes "Type" and "Properties".

  • I dynamically named my S3 Bucket using the intrinsic function "Fn::If".

  • Such that if the condition "IsProdEnvironment" returns true, my bucket name becomes "my-prod-bucket-2"

    else if it returns false, my bucket name becomes "my-test-bucket-3"

  • Did the same thing for the value of the "AccessControl" attribute such that a truevalue gives the S3 Bucket BucketOwnerFullControl access and vice versa.

Outputs

Outputs:
  MyS3BucketName:
    Description: Name of the S3 bucket
    Value: !Ref MyS3Bucket
  • Based on the conditions, the name of my S3 Bucket will be displayed in the outputs.

Using the Service Console, here you will choose the environment type from the list of accepted environment types. choosing "prod" as my environment i have both read and write access to my bucket and "my-prod-bucket-2" gets outputted to me.

You can see from here that we have the S3 bucket created from the prod environment having BucketFullAccess control

Resource Dependencies

Many resources rely on other resources to be created before they can be successfully provisioned e.g. to provision an Elastic Compute Cloud (EC2 Instance) we need to provision a Security Group(the inbound and outbound traffic rules), VPC, and Subnet resources first.

In this case, you will need to explicitly state and define resource creation order in your CloudFormation template using the DependsOn attribute. as well as use !Ref and !GetAtt intrinsic functions to have CloudFormation handle the creation order when these dependencies have been established.

To practice what we have learned about resource dependencies. We will be deploying two resources; Amazon Elastic Compute Cloud (EC2 Instance) and Amazon S3 Bucket.

Amazon Elastic Compute Cloud (EC2 Instance) will depend on three other resources; VPC, Subnets, and Security Groups.

Amazon S3 Bucket resource will be created only when the EC2 Instance has been successfully created.

AWSTemplateFormatVersion: "2010-09-09"
Description: Resource Dependencies Lab 
Parameters:
  LatestAmiId:
    Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
    Default: /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2
  • We defined a Parameter with a logical name "LatestAmid" that has the "Type" and "Default" attributes.

  • The Type AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>: defines the data type of the parameter.

  • <AWS::EC2::Image::Id> is a type constraint that specifies that the parameter value must be a valid Amazon Machine Image (AMI) ID in the format typically used for EC2 instances.

  • This allows us to dynamically fetch the ID of an Amazon Machine Image (AMI) from the SSM parameter store or provide the one we prefer. This is useful for keeping track of the latest available AMI without hardcoding its ID in your CloudFormation template.

  • We also set the default value in case of situations where we don't specify any AMI.

Resources:
  MyVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
  • Just like I mentioned earlier, to create the EC2 instance resource, we will have to create three other sub-resources VPC, Subnets, and Security Group

  • We created our first sub-resource "MyVPC" with Type and Properties attributes

  • Within the properties, we have specified our CidrBlock.

MySubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref MyVPC
      CidrBlock: 10.0.0.0/24
  • We created the second sub-resource with the logical name of "MySubnet"

  • We referenced the initial resource we created "MyVPC" as the value of the VpcId within the properties attribute.

InstanceSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Metadata:
      cfn_nag:
        rules_to_suppress:
          - id: F1000
            reason: This is using default VPC where we dont know VpcId to support egress. Missing egress rule means all traffic is allowed outbound.
    Properties:
      GroupDescription: Allow http to client host
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0
      VpcId: !Ref MyVPC
  • Here we have our third sub-resource with the logical name "InstanceSecurityGroup" which has the different attributes we require.

  • We also have the "SecurityGroupIngress" that specifies our security group's different inbound and outbound traffic rules.

  • And we also referenced "MyVPC" as the value of the "VpcId" for the security group.

Ec2Instance:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !Ref LatestAmiId
      InstanceType: t2.micro
      SubnetId: !Ref MySubnet
      Tags:
        - Key: Name
          Value: Resource-dependencies-workshop
  • Finally, we can now create our EC2 Instance resource by referencing the previous sub-resources we have created.

  • We have also referenced our "LatestAmiId" parameter as the ImageId

  • Then we declared the InstanceType as "t2.micro" and used tags for cost allocation.

S3Bucket:
  Type: AWS::S3::Bucket
  DependsOn: Ec2Instance
  Properties:
    Tags:
      - Key: Name
        Value: Resource-dependencies-workshop
  • Lastly, we have our final resource; the S3 Bucket.

  • which uses the "DependsOn" attribute to ensure that the S3 bucket is created only when the EC2 instance has been successfully created.

Now using the event info tab in the CloudFormation Console, we can monitor the order to which our resources were created.

From the images above, we can see the hierachy of how our resources were provisioned from bottom to top.
MyVPC -> MySubnet -> InstanceSecurityGroup -> EC2Instance -> S3Bucket.

Congratulations on making it to the end of this article, we talked about the Reusability of our CloudFormation Templates and also discussed some of the features and capabilities of CloudFormation.
In the next article which will be the last of the series, we will discuss more features and how they can improve the Reusability of our defined Templates.