#4 user-data, cloud-init and CloudFormation

Episode #4 - 12 minutes - Friday 10/25/2013
Every EC2 instance has access to meta-data, which can be used in combination with cloud-init to automation processes as an instance boots. CloudFormation parameters add extra flexibility to passing data to instances, and this episode will show you how to use them all.

Show Notes

Instance meta-data can be accessed by hitting this URL from within an EC2 instance:

http://169.254.169.254/

On Ubuntu, you can use the ec2metadata script to list out one or all of the fields. On Amazon Linux, use the ec2-metadata script.

$ ec2metadata
ami-id: ami-a73264ce
ami-launch-index: 0
ami-manifest-path: (unknown)
ancestor-ami-ids: unavailable
availability-zone: us-east-1a
block-device-mapping: ami
root
instance-action: none
instance-id: i-91d66fe8
instance-type: t1.micro
local-hostname: ip-10-73-174-101.ec2.internal
local-ipv4: 10.73.174.101
kernel-id: aki-88aa75e1
mac: unavailable
profile: default-paravirtual
product-codes: unavailable
public-hostname: ec2-54-205-181-234.compute-1.amazonaws.com
public-ipv4: 54.205.181.234
public-keys: ['ssh-rsa AAAAB...']
ramdisk-id: unavailable
reserveration-id: unavailable
security-groups: mysg
user-data: unavailable

Both Amazon Linux and Ubuntu come with a package called cloud-init that checks the meta-data for user-data, and performs various actions including running scripts and chef recipes on boot.

An example user-data script to update Ubuntu packages and install Apache is:

#!/bin/bash
apt-get update
apt-get upgrade -y
apt-get install apache2 -y
echo "<html><body><h1>Welcome</h1>" > /var/www/index.html
echo "I was generated from user-data and cloud-init" >> /var/www/index.html
echo "</body></html>" >> /var/www/index.html

You can pass CloudFormation parameters to instances via user-data by using either the AWS::EC2::Instance or AWS::AutoScaling::LaunchConfiguration resources in your template. Strings inside the template can use the Fn::Join intrinsic function. An example template is here:

{
  "Description": "Episode 4 example of user-data and cloud-init",
  "Parameters": {
    "KeyPair": {
      "Description": "Name of the keypair to use for SSH access",
      "Type": "String"
    },
    "Environment": {
      "Description": "Name of this environment",
      "Type": "String",
      "Default": "Production"
    },
    "Role": {
      "Description": "The role this server should be",
      "Type": "String",
      "Default": "Web"
    }
  },
  "Resources": {
    "MyElasticLoadBalancer": {
      "Properties": {
        "AvailabilityZones": {
          "Fn::GetAZs": ""
        },
        "HealthCheck": {
          "HealthyThreshold": 3,
          "Interval": 30,
          "Target": "HTTP:80/",
          "Timeout": 5,
          "UnhealthyThreshold": 5
        },
        "Listeners": [
          {
            "InstancePort": 80,
            "LoadBalancerPort": 80,
            "Protocol": "HTTP"
          }
        ]
      },
      "Type": "AWS::ElasticLoadBalancing::LoadBalancer"
    },
    "MySecurityGroup": {
      "Properties": {
        "GroupDescription": "Allow access to MyInstance",
        "SecurityGroupIngress": [
          {
            "CidrIp": "0.0.0.0/0",
            "FromPort": 22,
            "IpProtocol": "tcp",
            "ToPort": 22
          },
          {
            "FromPort": 80,
            "IpProtocol": "tcp",
            "SourceSecurityGroupName": {
              "Fn::GetAtt": [
                "MyElasticLoadBalancer",
                "SourceSecurityGroup.GroupName"
              ]
            },
            "SourceSecurityGroupOwnerId": {
              "Fn::GetAtt": [
                "MyElasticLoadBalancer",
                "SourceSecurityGroup.OwnerAlias"
              ]
            },
            "ToPort": 80
          }
        ]
      },
      "Type": "AWS::EC2::SecurityGroup"
    },
    "MyASG": {
      "Properties": {
        "AvailabilityZones": {
          "Fn::GetAZs": ""
        },
        "Cooldown": 120,
        "LaunchConfigurationName": {
          "Ref": "MyLaunchConfig"
        },
        "LoadBalancerNames": [
          {
            "Ref": "MyElasticLoadBalancer"
          }
        ],
        "MaxSize": 1,
        "MinSize": 1,
        "Tags": [
          {
            "Key": "Name",
            "PropagateAtLaunch": "true",
            "Value": "Episode 4"
          }
        ]
      },
      "Type": "AWS::AutoScaling::AutoScalingGroup"
    },
    "MyLaunchConfig": {
      "Properties": {
        "ImageId": "ami-ef277b86",
        "InstanceType": "t1.micro",
        "KeyName": {
          "Ref": "KeyPair"
        },
        "SecurityGroups": [
          {
            "Ref": "MySecurityGroup"
          }
        ],
        "UserData": {
          "Fn::Base64": {
            "Fn::Join": [
              "\n",
              [
                "#!/bin/bash",
                "apt-get update",
                "apt-get upgrade -y",
                "apt-get install apache2 -y",
                "echo \"<html><body><h1>Welcome</h1>\" > /var/www/index.html",
                {
                  "Fn::Join": [
                    "",
                    [
                      "echo \"<h2>Environment: ",
                      {
                        "Ref": "Environment"
                      },
                      "</h2>\" >> /var/www/index.html"
                    ]
                  ]
                },
                {
                  "Fn::Join": [
                    "",
                    [
                      "echo \"<h2>Role: ",
                      {
                        "Ref": "Role"
                      },
                      "</h2>\" >> /var/www/index.html"
                    ]
                  ]
                },
                "echo \"</body></html>\" >> /var/www/index.html"
              ]
            ]
          }
        }
      },
      "Type": "AWS::AutoScaling::LaunchConfiguration"
    }
  },
  "Outputs": {
    "URL": {
      "Description": "URL of the sample website",
      "Value": {
        "Fn::Join": [
          "",
          [
            "http://",
            {
              "Fn::GetAtt": [
                "MyElasticLoadBalancer",
                "DNSName"
              ]
            }
          ]
        ]
      }
    }
  }
}

The same template and user-data script is easier to write using troposphere.

Resources

comments powered by Disqus