In my first post on AWS CloudFormation I talked about how to create a machine instance with specific properties. That’s very useful. But what I really like about CloudFormation is how it lets me declaratively provision my new server with necessary software, content of my own, and even running services. I’m going to cover that in this post. But fair warning: there’s a trick needed to make it work. I don’t feel that it’s clearly documented by Amazon, and it took me a while to figure it out. I’ll cover that near the end of this post.
I said that CloudFormation Resources contain Type and Properties keys. But they can optionally have another key: Metadata. Any metadata object defined here can be retrieved using the CloudFormation API. A new instance can retrieve the metadata, and, if it’s the right kind of object, provision the instance according to that specification. The “right kind of metdata” object for this is an AWS::CloudFormation::Init resource. I think you can declare that as another resource and reference it by name in the metadata, but for now we will just put it directly in the metadata. We defined our new server resource last time as:
"NewServer": { "Type": "AWS::EC2::Instance", "Properties": { "ImageId": "ami-1624987f", "InstanceType": "t1.micro", "KeyName": "cloudformation" } }
Now we can add the needed Metadata property:
"NewServer": { "Type": "AWS::EC2::Instance", "Properties": { "ImageId": "ami-1624987f", "InstanceType": "t1.micro", "KeyName": "cloudformation" }, "Metadata": { "AWS::CloudFormation::Init": { provisioning stuff goes here } } }
What kind of things can you specify in the configuration? The full documentation is here, but let’s complete an example. We will provision a web server with static content that’s stored in an S3 object. That’s pretty simple provisioning: install the Apache web server using the yum package manager, fetch my zipped up content from S3 and expand it in the right place, and start the httpd service. Here’s an AWS::CloudFormation::Init object that will do all that:
"AWS::CloudFormation::Init": { "config": { "packages": { "yum": { "httpd": [] } }, "sources": { "/var/www/html": "https://s3.amazonaws.com/engelke/public/webcontent.zip" }, "services": { "sysvinit": { "httpd": { "enabled": "true", "ensureRunning": "true" } } } } }
It’s pretty obvious what most of this does. The packages key lets you specify a variety of package managers, and which packages each one should install. We’re using the yum manager to install httpd, the Apache web server. The empty list as the value of the httpd key is how you specify that you want the latest available version to be installed. You can also use the apt package manager, or Python’s easy_install or Ruby’s rubygems package managers here. The sources key gives a URL (or local file name) for a zip or tgz file containing content to fetch and install. The key (/var/www/html here) is the directory to expand the fetched file to. Finally, the services key lists the services to run on boot. The ensureRunning key value of true specifies that the service should start on every boot. The only tricky part of the services key is that it has one value, always called sysvinit, and that key has the actual services as its children. Putting this all together gives the following template:
{ "AWSTemplateFormatVersion": "2010-09-09", "Description": "Create and provision a web server", "Resources": { "NewServer": { "Type": "AWS::EC2::Instance", "Properties": { "ImageId": "ami-1624987f", "InstanceType": "t1.micro", "KeyName": "cloudformation" }, "Metadata": { "AWS::CloudFormation::Init": { "config": { "packages": { "yum": { "httpd": [] } }, "sources": { "/var/www/html": "https://s3.amazonaws.com/engelke/public/webcontent.zip" }, "services": { "sysvinit": { "httpd": { "enabled": "true", "ensureRunning": "true" } } } } } } } } }
I’ve made the zip file at that URL public, so you can copy this template and try to launch it yourself. Remember, you need to have created a key pair named cloudformation first. Did you try it? Did you notice that all this new stuff had no effect at all? The httpd package wasn’t installed, there is nothing at /var/www/html, and there’s no httpd service running. I had the hardest time figuring out what was wrong, but it turned out to be simple. The Amazon Linux AMI doesn’t do anything with this metadata automatically. You have to run a command as root to have it provision the instance according to the metadata:
/opt/aws/bin/cfn-init -s WebTest --region us-east-1 -r NewServer
The cfn-init utility is the program that understands the metadata and performs the steps it specifies, and the Linux AMI doesn’t run it automatically. If you log on to your new instance and run this command, though, it will do it all for you. You will have to replace WebTest in the command with whatever name you give the stack when you create it. If you’re running in a different region than us-east-1, change that part of the command, too. The -r NewServer option gives the name of the resource containing the metadata you want to use; we called that NewServer in the template above.
That’s nice, but not yet what we wanted. We want CloudFormation to handle the provisioning itself. To do that we have to get the new instance to run the cfn-init command for us when it first boots. And that’s what the UserData property of an instance lets us do. We can just put a simple shell script as the value of the UserData key to make that happen:
#!/bin/sh /opt/aws/bin/cfn-init -s WebTest --region us-east-1 -r NewServer
Well, as you might guess, it’s not quite that simple. The value of the UserData key has to be a base 64 encoded string of this shell script. There’s a built-in CloudFormation function to base 64 encode a string, and we will use that:
UserData: { "Fn::Base64": "#!/bin/sh\n/opt/aws/bin/cfn-init -s WebTest --region us-east-1 -r NewServer\n" }
Note the \n characters to terminate each line. Put this in as a property of the server, giving the complete template:
{ "AWSTemplateFormatVersion": "2010-09-09", "Description": "Create and provision a web server", "Resources": { "NewServer": { "Type": "AWS::EC2::Instance", "Properties": { "ImageId": "ami-1624987f", "InstanceType": "t1.micro", "KeyName": "cloudformation", "UserData": { "Fn::Base64": "#!/bin/sh\n/opt/aws/bin/cfn-init -s WebTest --region us-east-1 -r NewServer\n" } }, "Metadata": { "AWS::CloudFormation::Init": { "config": { "packages": { "yum": { "httpd": [] } }, "sources": { "/var/www/html": "https://s3.amazonaws.com/engelke/public/webcontent.zip" }, "services": { "sysvinit": { "httpd": { "enabled": "true", "ensureRunning": "true" } } } } } } } } }
If you create a stack called WebTest with this template, you should get a new instance already running the Apache web server, with a couple of pages of content installed and already available. Give it a try. For me, at least, this was a success!
There are still a lot of rough edges. What if you don’t want to call your new stack WebTest? What if you want to run it in other regions? How about dealing with protected resources? Creating resources that interact with each other? Getting better reports of how to access resources that are created? Letting the user specify parameters to control the stack? I’ll cover some of that in future posts.
Thanks for this! Banging my head against an Amazon AMI wall trying to get it to work! Given it’s an AWS AMI – you kind of expect it to do the right thing. Thanks for posting!
Comment by Peter Hancock — April 11, 2013 @ 2:06 am