Hands-on AWS CloudFormation - Part 3. Intrinsic functions in Action
Tags: aws, cloud, cloudformation
Published: 2020-12-19 05:58:24

In the previous post, we discussed Pseudo parameters and Intrinsic functions such as *Ref*, *GetAtt*, *GetAZs*, *FindInMap*, *Select*, *Join*, and *Split*. There is one more subset of intrinsic functions left. Let’s quickly walk through Condition functions and then create a template and a stack using most of the intrinsic functions we’ve already learned.

#Conditionally provisioned resources

Assume that you are working in two different environments - 'dev' and 'prod'. You need to provision a new EC2 resource. You also want to create and attach a new EBS volume but **only for prod env** because you want to save extra money by using reduced capabilities of 'dev'. In that case you might want to use the intrinsic function **Condition**.  It returns the evaluated result of the specified condition. For e.g.:

```YAML
Parameters:
  paramEnvironmentType: # ask a user to define whether it is 'dev' or 'prod'
    Description: Environment type
    Default: dev # by default it is 'dev' environment
    Type: String
    AllowedValues: [dev, prod]
    ConstraintDescription: Must specify 'dev' or 'prod'
 
Conditions:
  isProd: !Equals [!Ref paramEnvironmentType, prod] # if 'prod' then TRUE, otherwise FALSE
 
Resources:
  myVolume: # create a new EBS volume only if environment is 'prod'
    Type: 'AWS::EC2::Volume'
    Condition: isProd # conditionally create EBS volume (only if environment is 'prod')
    Properties:
      Size: 100
      # etc.
```
As you can see, in "Parameters" section we declared a new "paramEnvironmentType" input parameter which value - either 'dev' or 'prod' - should be defined by a user at the time of stack creation.
 
Next, in "Conditions" section we declared a new "isProd" boolean variable which checks whether a user provisions resources for 'dev' or 'prod' based on "paramEnvironmentType" value.

And finally, in "Resources" section we used *Condition* function  to refer to "isProd" condition. Thus, AWS CLoudFormation will check the condition and create EBS volume only if environment is 'prod'.

#Intrinsic functions for conditions

You can use five intrinsic functions to conditionally create stack resources.

### Fn::Equals
Compares two values and returns *true* if the two values are equal, or returns *false* if they aren't.
```YAML
Conditions:
  isProd: !Equals [!Ref paramEnvironmentType, prod] # if 'prod' then TRUE, otherwise FALSE
```

### Fn::And
Returns *true* if all the specified conditions are true, or returns *false* if any one of the conditions is false. E.g., let’s create a “isCreateS3Bucket” condition that evaluates to *true* only if both month is 'January' and environment is 'prod':

```YAML
Conditions:
  isProd: !Equals [!Ref paramEnvironmentType, prod] # if 'prod' then TRUE, otherwise FALSE
  isCreateS3Bucket: !And
    - !Equals [!Ref paramMonth, 'January'] # if 'January' then TRUE
    - !Condition isProd  # if 'prod' then TRUE
  # isCreateS3Bucket  condition is TRUE only if both month is 'January' and environment is 'prod'
```
Note, don’t exceed the limit of 10 conditions per Fn::And.

### Fn::Or
Returns *true* if any one of the specified conditions is true, or returns *false* if all of the conditions are false. E.g., let’s create a “isCreateS3Bucket” condition that evaluates to *true* if  either month is 'January' or environment is 'prod', or both month is 'January' and environment is 'prod':

```YAML
Conditions:
  isProd: !Equals [!Ref paramEnvironmentType, prod] # if 'prod' then TRUE, otherwise FALSE
  isCreateS3Bucket: !Or
    - !Equals [!Ref paramMonth, 'January'] # if 'January' then TRUE
    - !Condition isProd  # if 'prod' then TRUE
  # isCreateS3Bucket  condition is TRUE if either month is 'January' or environment is 'prod'
```
Note, don’t exceed the limit of 10 conditions per Fn::Or.

### Fn::Not
Returns *true* if false and vice versa. E.g., let’s create a “isNonProd” condition that evaluates to *false* if the environment is 'prod' and *true* otherwise.

```YAML
Conditions:
  isNonProd: !Not [!Ref paramEnvironmentType, prod] # if 'prod' then FALSE, otherwise TRUE
```
### Fn::If
Returns one value if the specified condition is *true* and another value if the specified condition is *false*. E.g., if environment is 'prod'  then let’s use “t2.large” as EC2 instance type, otherwise use  “t2.micro”.

```YAML
Conditions:
  isProd: !Equals [!Ref paramEnvironmentType, prod] # if 'prod' then TRUE, otherwise FALSE
 
Resources:
  myEC2Instance: # create a new EC2 instance
    Type: 'AWS::EC2::Instance'
    Properties:
      InstanceType: !If [isProd, t2.large, t2.micro] ] # if 'prod', then use t2.micro, otherwise use t1.micro
      # etc.
```
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/rpk4pq2oc8tgrmjfly9e.jpg)


#Let's get our hands dirty!

Time to apply all our knowledge about Intrinsic functions. We are going to create a simple CloudFormation template and provision a new EC2 instance. Also let's add conditionally provisioned EBS volume which must be created and attached to instance only if environment is 'prod'.

###Step 1. Create a template

Create a new ["2-IntrinsicFunctions.yaml" file](https://github.com/Tiamatt/Mastering-AWS-CloudFormation/blob/main/2-IntrinsicFunctions.yaml) in your favorite code editor and add the following code:

```YAML
## =================== DESCRIPTION ===================
Description: >-
  AWS CloudFormation sample template. 
  Create a new EC2 instance and if environment is 'prod' then attach a new EBS volume to it

## =================== PARAMETERS ===================
Parameters:
  paramEnvironmentType: # ask a user to define whether it is 'dev', 'qa' or 'prod' environment
    Description: Environment type
    Default: dev # by default it is 'dev' environment
    Type: String
    AllowedValues: [dev, qa, prod]
    ConstraintDescription: Must specify 'dev', 'qa' or 'prod'
  paramTagValues: #ask a user to specify some values for tags
    Description: 'Comma-delimited list of tag values'
    Type: CommaDelimitedList
    Default: 'JonSnow, DaenerysTargaryen, AryaStark'

## =================== MAPPINGS ===================
Mappings: # map image ids with regions
  mapRegion:
    us-east-1:
      AMI: ami-1853ac65
    us-west-1:
      AMI: ami-bf5540df
    eu-west-1:
      AMI: ami-3bfab942
    ap-southeast-1:
      AMI: ami-e2adf99e
    ap-southeast-2:
      AMI: ami-43874721

## =================== CONDITIONS ===================
Conditions:
  isProd: !Equals [!Ref paramEnvironmentType, prod] # if 'prod' then TRUE, otherwise FALSE
  isDev: !Equals [!Ref paramEnvironmentType, dev]   # if 'dev' then TRUE, otherwise FALSE

## =================== RESOURCES ===================
Resources:
  myEC2Instance: # create a new EC2 instance
    Type: 'AWS::EC2::Instance'
    Properties:
      ImageId: !FindInMap # define imageId based on region
        - mapRegion # map's name
        - !Ref 'AWS::Region' # top level key which is a region where a new instance is being created
        - AMI # second level key - e.g. for 'us-east-1' the value for ImageId is 'ami-0ff8a91507f77f867'  
      InstanceType: !If [isProd, t2.micro, !If [isDev, t2.nano, t1.micro] ] # if 'prod', then t2.micro, (else) if 'dev' then t2.nano otherwise (if 'qa') then t1.micro  
      Tags:
      - Key: CloudFormationLab
        Value: !Join [' ', ['EC2 Instance for', !Ref AWS::Region] ] 

  myVolume: # create a new EBS volume only if environment is 'prod'
    Type: 'AWS::EC2::Volume'
    Condition: isProd # conditionally create EBS volume (only if environment is 'prod')
    Properties:
      Size: 100 # 100 GiB
      AvailabilityZone: !GetAtt myEC2Instance.AvailabilityZone # get AZ of a new EC2 instance
      Tags:
        - Key: CloudFormationLab
          Value: !Select [ 0, !Ref paramTagValues ] # output is 'JonSnow'
  
  myMountPoint: # attach an Amazon EBS volume to an EC2 instance only i environment is 'prod'
    Type: 'AWS::EC2::VolumeAttachment'
    Condition: isProd # conditionally attach EBS volume (only if environment is 'prod')
    Properties:
      InstanceId: !Ref myEC2Instance # ref to a new EC2 instance
      VolumeId: !Ref myVolume # ref to a new EBS volume
      Device: /dev/sdh
```
During stack creation we are going to specify environment type. For either 'dev' or 'qa' a new EC2 instance only should be provisioned ignoring an EBS volume and a Mount Point. The instance type also depends on environment type: *t2.nano* for 'dev' and *t1.micro* for 'qa'.

Once we re-select the environment type switching to 'prod', an instance type should be changed to *t2.micro* and a new EBS volume should be created and attached to the instance.

###Step 2. Create a stack

Log in to AWS Console, search for CloudFormation service. Click on "Create stack" and upload your yaml file:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/hew9fint305sldw0e7cb.png)

You can view your template in **AWS CloudFormation Designer**, which is a graphic tool for creating, viewing, and modifying templates:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/0arwi125y5qvsn1ej5nf.png)

Give a name to your stack and specify parameters values. First, let's go with 'dev' environment type:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/2ctn12g04dgcozjhgm51.png)

Click next, add tags if you want, review your stack and hit on "Create stack". Wait couple of seconds and click on refresh button to get the updated list of events. Voila!

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/me3uylo9ytju2s6lz258.png)

###Step 3. New resources for 'dev'

Click on "Resources" tab to see the list of newly created resources:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/r6ukcyfyuviec5wf2j0k.png)

As you can see, a new EC2 instance with Logical ID of 'myEC2Intance' has been created. Once you click on its physical ID you will be navigated to Amazon EC2:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/skho72vhm4idhq7nr4iz.png)

Note, as we have specified 'dev' environment type, AWS CloudFormation used *t2.nano* as an instance type.

```YAML
InstanceType: !If [isProd, t2.micro, !If [isDev, t2.nano, t1.micro] ] # if 'prod', then t2.micro, (else) if 'dev' then t2.nano otherwise (if 'qa') then t1.micro  
```

We can also check "Tags" tab to verify that *Fn::Join* intrinsic function concatenated a string with the region.

```YAML
    Tags:
      - Key: CloudFormationLab
        Value: !Join [' ', ['EC2 Instance for', !Ref AWS::Region] ] 
```

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/6jd31pkfzztdmliu3nvy.png)

###Step 4. Change parameters and update the stack

Now, let's change environment type from 'dev' to 'prod' and re-run the stack. For that, click on **Stack actions** dropdown and select **Create change set for current stack**:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/w6bnylfa35p87qe8bjyu.png)

Keep the current template:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/1b5v2uh1sllzwm2sn8gr.png)

Next, re-select 'prod' as a new value of "paramEnvironmentType" parameter: 

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/76n1vmtr22wahgxogx42.png)

Then hit *Create change set*. Look at the list of changes:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/gisk80wd529xctquxwyk.png)

**Change sets** allow you to preview how proposed changes to a stack might impact your running resources. In our case, AWS CloudFormation is going to add two more resources (EBS volume and Mount Point) and also update EC2 instance type from *t2.nano* to *t2.micro*.

Go ahead and execute the changes:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/c03wmpbp6x946ghp1wuy.png)

###Step 5. New resources for 'prod'

Click on "Resources" tab to see the list of created/updated resources:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/pjroo9t5q9mmj7d42485.png)

Let's examine EC2 instance. Click on its physical ID:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/tb5xlp0h81elpsjqdnqx.png)

Note three changes:

1. Instance type has been changed from *t2.nano* for 'dev' to *t2.micro* for 'prod'. 

2. A new EBS volume has been created.

3. The EBS volume has been attached to the instance.

Now, click on Volume ID:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/zmeyir3a8wbis0pfisdt.png)

Notice that EBS volume's size is 100 GiB as we specified in the template. Also the volume was provisioned in the same AZ as that of the instance. 

```YAML
myVolume: # create a new EBS volume only if environment is 'prod'
    Type: 'AWS::EC2::Volume'
    Condition: isProd # conditionally create EBS volume (only if environment is 'prod')
    Properties:
      Size: 100 # 100 GiB
      AvailabilityZone: !GetAtt myEC2Instance.AvailabilityZone # get AZ of a new EC2 instance
```

We can also check "Tags" tab to verify that *Fn::Select* intrinsic function picked the first element of "paramTagValues" list.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/gtycaml9anrwg04a4wy1.png)

```YAML
 Parameters:
  paramTagValues: #ask a user to specify some values for tags
    Description: 'Comma-delimited list of tag values'
    Type: CommaDelimitedList
    Default: 'JonSnow, DaenerysTargaryen, AryaStark'
    #etc.

      Tags:
        - Key: CloudFormationLab
          Value: !Select [ 0, !Ref paramTagValues ] # output is 'JonSnow'
```
###Step 6. Cleanup
Don't forget to clean all provisioned resources once you are done with the stack. Otherwise you might be charged for running resources. 

By deleting a stack, all its resources will be deleted as well.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/38hhq86e42xkvij2nk0o.png)

#Summary

In the template above we tried to implement the most of the intrinsic functions such as *GetAtt*, *Ref*, *FindInMap*, *Join*, *Select*, *Equals*, etc. and showed how to specify conditionally provisioned resources.

I hope this post was easy to follow and helped you to better understand the implementation of intrinsic functions in AWS CloudFormation.