How to Execute EC2 User Data Script using CloudFormation

How to Execute EC2 User Data Script using CloudFormation

Dear reader, In this post, I will help you execute EC2 user data scripts using CloudFormation. As you might already know, EC2 user data script, lets you bootstrap your EC2 instance by executing some commands(that you specify) dynamically after your instance is booted.

In my previous post, I talked about doing it via AWS console. In this post, we will learn to do it using CloudFormation. Feel free to add a comment in case you need me to cover using CLI as well.

Suggested Read:

A Bit of Backgroud on EC2 User Data

In a very simple terms if I say, user data is user data/commands that you can specify at the time of launching your instance. These data/command executes after your EC2 instance starts.

You don’t need to SSH into your EC2 instance and run those command one by one. Rather all you need is to specify the whole script in the user data section and they get executed once your instance boots up.

Let’s understand by an example-

You want to create a file log.txt in dev folder as soon as your instance starts. You can specify below user data to achieve that.

#!/bin/bash
touch /dev/log.txt

Note: User data scripts run as root user so you don’t need to specify sudo with your commands

User Data and CloudFormation

When you launch an EC2 instance, you can specify your user data like below.

Resources:
  DemoInstance:
    Type: AWS::EC2::Instance
    Properties: 
      ...
      ...
      UserData: String
      ...
      ...

That means all you need is the parameter UserData of AWS::EC2::Instance resource type.

Sounds very easy right?

Well, there is a small catch, which if not known can be a time waster for you.

The property UserData must be a base64-encoded text. Also, the limit on user data size is 16 KB.

Therefore, always remeber to base64-encode your user data script while specifying your user data.

Don’t worry 🙂

CloudFormation provides a real simple way to do it on the go while specifying your user data using function Fn::Base64 ike you can see below. All you need is to prefix this function before your userdata.

Resources:
  DemoInstance:
    Type: AWS::EC2::Instance
    Properties: 
      ...
      ...
      UserData:
        Fn::Base64: 
          !Sub |
            #!/bin/bash
            ....
            ....
      ...
      ...

Usecase that we will implement today

Similar to other posts on this topic, we are going to install apache web server on an EC2 instance using EC2 User Data. I do have script handy for that. Have a look on the script below in case you need that.

EC2 User Data to Install Apache Web Server

#!/bin/bash
yum update -y
yum install -y httpd.x86_64
systemctl start httpd.service
systemctl enable httpd.service
echo ?Hello World from $(hostname -f)? > /var/www/html/index.html

This is how it looks like in a CloudFormation Template

Resources:
  DemoInstance:
    Type: 'AWS::EC2::Instance'
    Properties: 
      ImageId: !Ref ImageId
      .....
      ..... other properties
      UserData:
        Fn::Base64: 
          !Sub |
            #!/bin/bash
            yum update -y
            yum install -y httpd.x86_64
            systemctl start httpd.service
            systemctl enable httpd.service
            echo ?Hello World from $(hostname -f)? > /var/www/html/index.html

Steps to Execute EC2 User Data Script using CloudFormation

I hope we are clear on the script by now. So let’s go ahead and see the step by step instruction to execute EC2 user data script using CloudFormation

Step 1: Provide proper permission

If you are not an admin user, you should explicitly provide ec2:* permission for your user/role. Additionally, you will also needs cloudformation:* as well to be able to do CloudFormation stack creation, updation etc.

I have specified * for simplicity. But please revise your permission based on uses to follow principal of least priviledge.

Step 2: Prepare a template

You can use YAML or JSON for your template. I prefer YAML for writing my templates. But don’t worry, If you want it in JSON, I will provide JSON template as well.

Template to Execute EC2 User Data Script using CloudFormation : YAML

In this template, we are launching an EC2 instance with user data specified. The user data says to install apache web server on your instance.

AWSTemplateFormatVersion: '2010-09-09'
Description: Template to Create an EC2 instance in a VPC
   
Parameters:
  VpcId:
    Type: String
    Description: VPC id
    Default: vpc-8854eef1
  ImageId:
    Type: String
    Description: 'Linux 2 AMI for Ireland eu-west1 Region'
    Default: 'ami-0fc970315c2d38f01'
  InstanceType:
    Type: String
    Description: Choosing  t2 micro because it is free
    Default: t2.micro
  KeyName:
    Description: SSH Keypair to login to the instance
    Type: AWS::EC2::KeyPair::KeyName
    Default: DemoKeyPair

Resources:
  DemoInstance:
    Type: 'AWS::EC2::Instance'
    Properties: 
      ImageId: !Ref ImageId
      InstanceType: !Ref InstanceType
      KeyName: !Ref KeyName
      SecurityGroupIds: 
        - !Ref DemoSecurityGroup
      UserData:
        Fn::Base64: 
          !Sub |
            #!/bin/bash
            yum update -y
            yum install -y httpd.x86_64
            systemctl start httpd.service
            systemctl enable httpd.service
            echo ?Hello World from $(hostname -f)? > /var/www/html/index.html
  DemoSecurityGroup:
    Type: 'AWS::EC2::SecurityGroup'
    Properties:
      VpcId: !Ref VpcId
      GroupDescription: SG to allow SSH access via port 22
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: '22'
          ToPort: '22'
          CidrIp: '0.0.0.0/0'
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: 0.0.0.0/0
      Tags:
        - Key: Name
          Value: EC2-SG
Outputs:
  DemoInstanceId:
    Description: Instance Id 
    Value: !Ref DemoInstance

Template to Execute EC2 User Data Script using CloudFormation : JSON

{
    "AWSTemplateFormatVersion": "2010-09-09",
    "Description": "Template to Create an EC2 instance in a VPC",
    "Parameters": {
        "VpcId": {
            "Type": "String",
            "Description": "VPC id",
            "Default": "vpc-8854eef1"
        },
        "ImageId": {
            "Type": "String",
            "Description": "Linux 2 AMI for Ireland eu-west1 Region",
            "Default": "ami-0fc970315c2d38f01"
        },
        "InstanceType": {
            "Type": "String",
            "Description": "Choosing  t2 micro because it is free",
            "Default": "t2.micro"
        },
        "KeyName": {
            "Description": "SSH Keypair to login to the instance",
            "Type": "AWS::EC2::KeyPair::KeyName",
            "Default": "DemoKeyPair"
        }
    },
    "Resources": {
        "DemoInstance": {
            "Type": "AWS::EC2::Instance",
            "Properties": {
                "ImageId": {
                    "Ref": "ImageId"
                },
                "InstanceType": {
                    "Ref": "InstanceType"
                },
                "KeyName": {
                    "Ref": "KeyName"
                },
                "SecurityGroupIds": [
                    {
                        "Ref": "DemoSecurityGroup"
                    }
                ],
                "UserData": {
                    "Fn::Base64": {
                        "Fn::Sub": "#!/bin/bash\nyum update -y\nyum install -y httpd.x86_64\nsystemctl start httpd.service\nsystemctl enable httpd.service\necho ?Hello World from $(hostname -f)? > /var/www/html/index.html\n"
                    }
                }
            }
        },
        "DemoSecurityGroup": {
            "Type": "AWS::EC2::SecurityGroup",
            "Properties": {
                "VpcId": {
                    "Ref": "VpcId"
                },
                "GroupDescription": "SG to allow SSH access via port 22",
                "SecurityGroupIngress": [
                    {
                        "IpProtocol": "tcp",
                        "FromPort": "22",
                        "ToPort": "22",
                        "CidrIp": "0.0.0.0/0"
                    },
                    {
                        "IpProtocol": "tcp",
                        "FromPort": 80,
                        "ToPort": 80,
                        "CidrIp": "0.0.0.0/0"
                    },
                    {
                        "IpProtocol": "tcp",
                        "FromPort": 443,
                        "ToPort": 443,
                        "CidrIp": "0.0.0.0/0"
                    }
                ],
                "Tags": [
                    {
                        "Key": "Name",
                        "Value": "EC2-SG"
                    }
                ]
            }
        }
    },
    "Outputs": {
        "DemoInstanceId": {
            "Description": "Instance Id",
            "Value": {
                "Ref": "DemoInstance"
            }
        }
    }
}

Step3: Create a Stack using prepared template

Now, we know the basics and we have the template so let’s go and create the stack.

  1. Grab the YAML or JSON template from above as per your convenience.
  2. Change parameters like ImageId, InstanceType and KeyName with your own AMI Id, instance type and name of keypair respectivey
  3. Save the template with .yml or .json as per the choice of template and follow below steps.
  4. Login to AWS Management Console, navigate to CloudFormation and click on Create stack
  5. Click on “Upload a template file”, upload your saved .yml  or .json file and click Next
  6. Enter the stack name and click on Next. In configuration, keep everything as default and click on Next.
  7. In the events tab of stack, you can view the status.
  8. Once stack is successfully created, you can check “Resources” tab to see all that’s created by this template.
  9. Navigate to EC2 instance, grab the instance public IP from instance details screen and hit the pubic IP. You should see hello world response fro your server.

Clean Up

If you are creating this EC2 instance just for learning purpose. Don’t forget to delete your CloudFormation stack so that your instance is terminated and you don’t bear any cost.

Happy Learning !!!

Conclusion:

In this post, we learnt to execute EC2 user data script using CloudFormation.

We learnt-

  • About user data and how it lets you bootstrap instance
  • A catch regarding specifying user data correctly
  • Testing your installed server

I hope you found this post helpful. If you find any issue, please fee free to reach me in comment section. I would be more than happy to reply to your comment.

Enjoyed the content?

Subscribe to our newsletter below to get awesome AWS learning materials delivered straight to your inbox.

Don’t forget to motivate me by-

Suggested Read:

4 thoughts on “How to Execute EC2 User Data Script using CloudFormation

  1. How to update the powershell script in the user data.It will be helpful if you update the same. updating the code below.
    {
    “AWSTemplateFormatVersion”: “2010-09-09”,
    “Description”: “Template to Create an EC2 instance in a VPC”,
    “Parameters”: {
    “VpcId”: {
    “Type”: “String”,
    “Description”: “VPC id”,
    “Default”: “vpc-58c9a833”
    },
    “ImageId”: {
    “Type”: “String”,
    “Description”: “windows machine”,
    “Default”: “ami-0c4a11a8d0e503812”
    },
    “InstanceType”: {
    “Type”: “String”,
    “Description”: “Choosing t2 micro because it is free”,
    “Default”: “t2.micro”
    },
    “KeyName”: {
    “Description”: “SSH Keypair to login to the instance”,
    “Type”: “AWS::EC2::KeyPair::KeyName”
    }
    },
    “Resources”: {
    “DemoInstance”: {
    “Type”: “AWS::EC2::Instance”,
    “Properties”: {
    “ImageId”: {
    “Ref”: “ImageId”
    },
    “InstanceType”: {
    “Ref”: “InstanceType”
    },
    “KeyName”: {
    “Ref”: “KeyName”
    },
    “SecurityGroupIds”: [
    {
    “Ref”: “DemoSecurityGroup”
    }
    ],
    “UserData”: {
    “Fn::Base64”: {
    “Fn::Sub”: “if ( Get-Service “AWSXRayDaemon” -ErrorAction SilentlyContinue ) {
    sc.exe stop AWSXRayDaemon
    sc.exe delete AWSXRayDaemon
    }

    $targetLocation = “C:\Program Files\Amazon\XRay”
    if ((Test-Path $targetLocation) -eq 0) {
    mkdir $targetLocation
    }

    $zipFileName = “aws-xray-daemon-windows-service-3.x.zip”
    $zipPath = “$targetLocation\$zipFileName”
    $destPath = “$targetLocation\aws-xray-daemon”
    if ((Test-Path $destPath) -eq 1) {
    Remove-Item -Recurse -Force $destPath
    }

    $daemonPath = “$destPath\xray.exe”
    $daemonLogPath = “$targetLocation\xray-daemon.log”
    $url = “https://s3.dualstack.us-west-2.amazonaws.com/aws-xray-assets.us-west-2/xray-daemon/aws-xray-daemon-windows-service-3.x.zip”

    Invoke-WebRequest -Uri $url -OutFile $zipPath
    Add-Type -Assembly “System.IO.Compression.Filesystem”
    [io.compression.zipfile]::ExtractToDirectory($zipPath, $destPath)

    New-Service -Name “AWSXRayDaemon” -StartupType Automatic -BinaryPathName “`”$daemonPath`” -f `”$daemonLogPath`””
    sc.exe start AWSXRayDaemon”
    }
    }
    }
    },
    “DemoSecurityGroup”: {
    “Type”: “AWS::EC2::SecurityGroup”,
    “Properties”: {
    “VpcId”: {
    “Ref”: “VpcId”
    },
    “GroupDescription”: “SG to allow SSH access via port 22”,
    “SecurityGroupIngress”: [
    {
    “IpProtocol”: “tcp”,
    “FromPort”: “22”,
    “ToPort”: “22”,
    “CidrIp”: “0.0.0.0/0”
    },
    {
    “IpProtocol”: “tcp”,
    “FromPort”: 80,
    “ToPort”: 80,
    “CidrIp”: “0.0.0.0/0”
    },
    {
    “IpProtocol”: “tcp”,
    “FromPort”: 443,
    “ToPort”: 443,
    “CidrIp”: “0.0.0.0/0”
    }
    ],
    “Tags”: [
    {
    “Key”: “Name”,
    “Value”: “EC2-SG”
    }
    ]
    }
    }
    },
    “Outputs”: {
    “DemoInstanceId”: {
    “Description”: “Instance Id”,
    “Value”: {
    “Ref”: “DemoInstance”
    }
    }
    }
    }

Leave a Reply

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