Run AWS CloudFormation via CLI + Use Cases (Template and Resources)
Tags: aws, cloudformation, infrastructureascode, cloud
Published: 2022-05-09 02:34:30

AWS CloudFormation is a "free" service that helps us to provide and manage AWS resources only through a template. One of the benefits that I love is we don't need to look for which modules are still valid to use or which ones that have been deprecated because AWS provides the complete information on the documentation, click [here](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html). What we need to define in the template are provided there. One of the most important things about this service is you have to remember that we are free to use the CloudFormation service but we will be billed for every service created through the stack (the ones that you define in the template).

Alright! We will do anything with CloudFormation through AWS CLI. So, make sure you have installed [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) and [set up the credential](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html). If you're ready with them all, let's get started!

![CloudFormation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3jsaigfbc5q9w4fyxg0p.png)

## 1\. Create Template

In the first version of the template, I want to create an S3 bucket with versioning enabled. I prepare this for the next step because when we create a stack by uploading the template (stored locally) via CLI, it doesn't show us where it will be stored (I'll tell you in a second below). Ok, I named the template cfn-demo.yaml and it will be:

```yaml
Resources:
  S3Bucket:
    Type: 'AWS::S3::Bucket'
    Properties:
      BucketName: cfn-bucket-template
      VersioningConfiguration:
        Status: Enabled
```

## 2\. Create Stack

As I mentioned above when we create a stack by uploading the template (stored locally) via CLI, it doesn't show us where it will be stored. It's different when we create a stack by uploading the file via Console which will be stored in the S3 bucket generated by CloudFormation itself, but that bucket isn't deleted even though the stack has been deleted.

![Upload Template via Console](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0vantihessn6veqjcuyv.png)

![Bucket Created](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jafzakd6di2rowdv18w1.png)

![Stack Deleted](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tjxmrv4rqojkxcpyn12f.png)

![Bucket Still Exist](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6abca468nhtsixytcaw1.png)

Then, to prevent that undeleted bucket off the stack. We will create the bucket first, upload the template to the bucket, and use the object as the template source. I just wanna let you know that we can do this.

Let's create the stack!

```bash
$ aws cloudformation create-stack --stack-name cfn-demo --template-body file://cfn-demo.yaml
{
    "StackId": "arn:aws:cloudformation:ap-southeast-3:01234567890:stack/cfn-demo/2953bb10-ced2-11ec-a796-06bb617a2d70"
}
```

Please wait till the status is complete.

```bash
$ aws cloudformation describe-stacks --stack-name cfn-demo
{
    "Stacks": [
        {
            "StackId": "arn:aws:cloudformation:ap-southeast-3:01234567890:stack/cfn-demo/2953bb10-ced2-11ec-a796-06bb617a2d70",
            "StackName": "cfn-demo",
            "CreationTime": "2022-05-08T13:24:17.386Z",
            "RollbackConfiguration": {},
            "StackStatus": "CREATE_COMPLETE",
            "DisableRollback": false,
            "NotificationARNs": [],
            "Tags": [],
            "EnableTerminationProtection": false,
            "DriftInformation": {
                "StackDriftStatus": "NOT_CHECKED"
            }
        }
    ]
}
```

Let's verify the S3 bucket! Has it been created or not?

```bash
$ aws s3 ls
2022-05-08 20:24:45 cfn-bucket-template
```

## 3\. Update Stack

To update the stack, we will have some steps to do to follow the scenario. Those are:

* Make any changes to the template. In this case, I'll create more resources (VPC, Subnet, Internet Gateway, Route Table, 2 Security Groups, and an EC2 instance).
    
* Upload the second version of the template to the S3 bucket.
    
* Update the stack by using the S3 object as a template source.
    

cfn-demo.yaml (second version)

```yaml
AWSTemplateFormatVersion: 2010-09-09

Parameters:
  EC2Type:
    Description: Type of instance
    Type: String
    Default: t3.micro
    AllowedValues:
    - t3.micro
    - t3.small
    - t3.medium
    - t3.large

Resources:
  S3Bucket:
    Type: 'AWS::S3::Bucket'
    Properties:
      BucketName: cfn-bucket-template
      VersioningConfiguration:
        Status: Enabled
  VirtualPrivateCloud:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 172.16.0.0/16
      Tags:
      - Key: "Name"
        Value: "CFN VPC"
  Subnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VirtualPrivateCloud
      CidrBlock: 172.16.1.0/28
      AvailabilityZone: ap-southeast-3a
      Tags:
      - Key: "Name"
        Value: "CFN Subnet"
  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
      - Key: "Name"
        Value: "CFN IGW"
  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VirtualPrivateCloud
      InternetGatewayId: !Ref InternetGateway
  RouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VirtualPrivateCloud
      Tags:
      - Key: "Name"
        Value: "CFN Route"
  Route:
    Type: AWS::EC2::Route
    Properties: 
      RouteTableId: !Ref RouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway
  SubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref Subnet 
      RouteTableId: !Ref RouteTable
  SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties: 
      GroupDescription: Allow SSH HTTP
      SecurityGroupIngress:
      - CidrIp: 0.0.0.0/0
        IpProtocol: tcp
        FromPort: 22
        ToPort: 22
      - CidrIp: 0.0.0.0/0
        IpProtocol: tcp
        FromPort: 80
        ToPort: 80
      VpcId: !Ref VirtualPrivateCloud
      Tags:
      - Key: "Name"
        Value: "CFN SG"
  SecurityGroup2:
    Type: AWS::EC2::SecurityGroup
    Properties: 
      GroupDescription: Only Allow SSH
      SecurityGroupIngress:
      - CidrIp: 0.0.0.0/0
        IpProtocol: tcp
        FromPort: 22
        ToPort: 22
      VpcId: !Ref VirtualPrivateCloud
      Tags:
      - Key: "Name"
        Value: "CFN SG2"
  KeyPair:
    Type: AWS::EC2::KeyPair
    Properties:
      KeyName: ec2-user
      PublicKeyMaterial: ssh-rsa VWXYZ.....(your pubkey goes here)
  EC2Instance:
    Type: AWS::EC2::Instance
    Properties:
      InstanceType: !Ref EC2Type
      AvailabilityZone: ap-southeast-3a
      ImageId: "ami-021fb2b73ff1efc96"
      KeyName: "ec2-user"
      BlockDeviceMappings:
      - DeviceName: "/dev/xvda"
        Ebs:
          DeleteOnTermination: "true"
          VolumeSize: "8"
          VolumeType: "gp2"
      NetworkInterfaces:
      - AssociatePublicIpAddress: "true"
        DeleteOnTermination: "true"
        DeviceIndex: 0
        SubnetId: !Ref Subnet
        GroupSet: 
          - Ref: SecurityGroup2
      UserData:
        Fn::Base64:
          !Sub |
            #!/bin/bash
            yum update -y
            yum install -y httpd
            systemctl enable httpd
            systemctl start httpd
      Tags:
      - Key: "Name"
        Value: "CFN Instance"

Outputs:
  EC2IpAddress:
    Description: "EC2 Public IP Address"
    Value: !GetAtt EC2Instance.PublicIp
```

Upload the file to the S3 bucket by using the copy:

```bash
$ aws s3 cp cfn-demo.yaml s3://cfn-bucket-template
upload: ./cfn-demo.yaml to s3://cfn-bucket-template/cfn-demo.yaml
$ aws s3 ls s3://cfn-bucket-template
2022-05-08 20:31:46       4256 cfn-demo.yaml
```

Then, let's update the stack by using the S3 object URL as the template source!

```bash
$ aws cloudformation update-stack --stack-name cfn-demo --template-url https://cfn-bucket-template.s3.ap-southeast-3.amazonaws.com/cfn-demo.yaml
{
    "StackId": "arn:aws:cloudformation:ap-southeast-3:01234567890:stack/cfn-demo/2953bb10-ced2-11ec-a796-06bb617a2d70"
}
```

Please wait till the status is complete and we get the output.

```bash
$ aws cloudformation describe-stacks --stack-name cfn-demo
{
    "Stacks": [
        {
            "StackId": "arn:aws:cloudformation:ap-southeast-3:0123456789:stack/cfn-demo/2953bb10-ced2-11ec-a796-06bb617a2d70",
            "StackName": "cfn-demo",
            "Parameters": [
                {
                    "ParameterKey": "EC2Type",
                    "ParameterValue": "t3.micro"
                }
            ],
            "CreationTime": "2022-05-08T13:24:17.386Z",
            "LastUpdatedTime": "2022-05-08T13:33:13.916Z",
            "RollbackConfiguration": {},
            "StackStatus": "UPDATE_COMPLETE",
            "DisableRollback": false,
            "NotificationARNs": [],
            "Outputs": [
                {
                    "OutputKey": "EC2IpAddress",
                    "OutputValue": "108.136.169.98",
                    "Description": "EC2 Public IP Address"
                }
            ],
            "Tags": [],
            "EnableTerminationProtection": false,
            "DriftInformation": {
                "StackDriftStatus": "NOT_CHECKED"
            }
        }
    ]
}
```

Let's verify the EC2 instance port by accessing it via SSH and a web browser!

```bash
$ ssh ec2-user@108.136.169.98 

       __|  __|_  )
       _|  (     /   Amazon Linux 2 AMI
      ___|\___|___|

https://aws.amazon.com/amazon-linux-2/
[ec2-user@ip-172-16-1-8 ~]$ exit
logout
Connection to 108.136.169.98 closed.
```

![First Instance](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sk7f4n3ym504xlrdf1e1.png)

Why do we verify the ports? We have created 2 security groups and we already used the first one for our EC2 instance. Now, let's make a change to the template and upload it again to the S3 bucket. From this action, we will have two versions of the object (template that is stored in the S3 bucket).

Before (to allow SSH and HTTP):

```yaml
        GroupSet: 
          - Ref: SecurityGroup
```

After (to only allow SSH):

```yaml
        GroupSet: 
          - Ref: SecurityGroup2
```

Then, upload that file!

```bash
$ aws s3 cp cfn-demo.yaml s3://cfn-bucket-template
upload: ./cfn-demo.yaml to s3://cfn-bucket-template/cfn-demo.yaml
```

Now, let's update the stack again by using the S3 object URL as the template source! (It will use the newest version of the template)

```bash
$ aws cloudformation update-stack --stack-name cfn-demo --template-url https://cfn-bucket-template.s3.ap-southeast-3.amazonaws.com/cfn-demo.yaml
{
    "StackId": "arn:aws:cloudformation:ap-southeast-3:0123456789:stack/cfn-demo/2953bb10-ced2-11ec-a796-06bb617a2d70"
}
```

The output:

```bash
$ aws cloudformation describe-stacks --stack-name cfn-demo
{
    "Stacks": [
        {
            "StackId": "arn:aws:cloudformation:ap-southeast-3:0123456789:stack/cfn-demo/2953bb10-ced2-11ec-a796-06bb617a2d70",
            "StackName": "cfn-demo",
            "Parameters": [
                {
                    "ParameterKey": "EC2Type",
                    "ParameterValue": "t3.micro"
                }
            ],
            "CreationTime": "2022-05-08T13:24:17.386Z",
            "LastUpdatedTime": "2022-05-08T13:40:42.678Z",
            "RollbackConfiguration": {},
            "StackStatus": "UPDATE_COMPLETE",
            "DisableRollback": false,
            "NotificationARNs": [],
            "Outputs": [
                {
                    "OutputKey": "EC2IpAddress",
                    "OutputValue": "108.136.160.193",
                    "Description": "EC2 Public IP Address"
                }
            ],
            "Tags": [],
            "EnableTerminationProtection": false,
            "DriftInformation": {
                "StackDriftStatus": "NOT_CHECKED"
            }
        }
    ]
}
```

Even though we only change the security group, through the stack it will also replace the EC2 instance with the new one. So, it can be a review for us to choose which will be executed through CloudFormation or using other ways such as via the Console, CLI, or others. Here I'll just show you the main points when we use CloudFormation via CLI and use S3 to store templates.

Okay, let's continue to check! The current instance should be only accessible via SSH.

```bash
$ telnet 108.136.160.193 80
Trying 108.136.160.193...
^C
$ ssh ec2-user@108.136.160.193

       __|  __|_  )
       _|  (     /   Amazon Linux 2 AMI
      ___|\___|___|

https://aws.amazon.com/amazon-linux-2/
[ec2-user@ip-172-16-1-12 ~]$ exit
logout
Connection to 108.136.160.193 closed.
```

![HTTP Closed](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oe1r9fujqc9ldbih1jw8.png)

It works! Now, I want to make one more change but I'll only use the first (oldest) object version without making any changes to the template. I just want to be back by creating a new instance using the first security group that allows SSH and HTTP. This is one of the benefits of using S3 versioning to store the template.

Get the version ID and I'll take the last one!

```bash
$ aws s3api list-object-versions --bucket cfn-bucket-template --query 'Versions[].[Key, VersionId]'
[
    [
        "cfn-demo.yaml",
        "vtPszUgfpn8S9VZj5r6uHWKYL9HSlDRV"
    ],
    [
        "cfn-demo.yaml",
        "0DY8PO_LdRI_1WcppeMBSFc_ey5Djuxf"
    ]
]
```

Now, let's do our last update to the stack by using the S3 object URL as the template source! Please don't forget to mention the version ID by adding `?versionId=xxx` right after the object URL.

```bash
$ aws cloudformation update-stack --stack-name cfn-demo --template-url https://cfn-bucket-template.s3.ap-southeast-3.amazonaws.com/cfn-demo.yaml?versionId=0DY8PO_LdRI_1WcppeMBSFc_ey5Djuxf
{
    "StackId": "arn:aws:cloudformation:ap-southeast-3:01234567890:stack/cfn-demo/2953bb10-ced2-11ec-a796-06bb617a2d70"
}
```

The output:

```bash
$ aws cloudformation describe-stacks --stack-name cfn-demo
{
    "Stacks": [
        {
            "StackId": "arn:aws:cloudformation:ap-southeast-3:0123456789:stack/cfn-demo/2953bb10-ced2-11ec-a796-06bb617a2d70",
            "StackName": "cfn-demo",
            "Parameters": [
                {
                    "ParameterKey": "EC2Type",
                    "ParameterValue": "t3.micro"
                }
            ],
            "CreationTime": "2022-05-08T13:24:17.386Z",
            "LastUpdatedTime": "2022-05-08T13:49:09.816Z",
            "RollbackConfiguration": {},
            "StackStatus": "UPDATE_COMPLETE",
            "DisableRollback": false,
            "NotificationARNs": [],
            "Outputs": [
                {
                    "OutputKey": "EC2IpAddress",
                    "OutputValue": "108.136.160.169",
                    "Description": "EC2 Public IP Address"
                }
            ],
            "Tags": [],
            "EnableTerminationProtection": false,
            "DriftInformation": {
                "StackDriftStatus": "NOT_CHECKED"
            }
        }
    ]
}
```

Then, the HTTP port of our newest EC2 instance should be opened back.

![HTTP Back](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0ruf9ux91zw1cgdxe6ie.png)

## 4\. Delete Template

We have uploaded 2 versions of the template to the S3 bucket. So, we have to delete the object versions before we delete the stack because we upload the files manually (aren't included in the stack).

```bash
$ aws s3api delete-object --bucket cfn-bucket-template --key cfn-demo.yaml --version-id vtPszUgfpn8S9VZj5r6uHWKYL9HSlDRV
{
    "VersionId": "vtPszUgfpn8S9VZj5r6uHWKYL9HSlDRV"
}
$ aws s3api delete-object --bucket cfn-bucket-template --key cfn-demo.yaml --version-id 0DY8PO_LdRI_1WcppeMBSFc_ey5Djuxf
{
    "VersionId": "0DY8PO_LdRI_1WcppeMBSFc_ey5Djuxf"
}
```

## 5\. Delete Stack

Now, the bucket is empty and we're ready to delete the stack.

```bash
$ aws cloudformation delete-stack --stack-name cfn-demo
$ aws cloudformation describe-stacks --stack-name cfn-demo

An error occurred (ValidationError) when calling the DescribeStacks operation: Stack with id cfn-demo does not exist
```

---

It's a wrap! We already reached the end of this post.

What we have done:

1. Create a CloudFormation template with the following tasks: create S3 Bucket and create EC2 (VPC, Subnet, Internet Gateway, Route Table, Security Group, and EC2 instance)
    
2. Create, update, and delete CloudFormation stack via AWS CLI
    
3. Upload object to the S3 bucket via AWS CLI
    
4. Delete S3 object version via AWS CLI
    
5. Use 2 different CloudFormation template sources, local file and S3 URL.
    

That's it! Thank you for coming and I'm looking forward to your feedback. Follow me to get notified when my new post is published!