In God we trust, the rest we automate
— unknown DevOps Engineer
— unknown DevOps Engineer
The use of virtualization and cloud platforms allows launching and maintaining
IT infrastructure ten time faster. A single person can manage tens, hundreds and
even thousands of virtual servers: easily launch them, stop, clone, configure
hardware, and use to create pre-configured system images.
In case all of your servers have a
similar configuration, it is all quite simple – you can configure a server
once, create its image and launch as many machines, as you like using the
image. However, if you use multiple operating systems with different software packages,
or need to quickly launch and stop complex cluster configurations, maintaining even
a few dozens of such servers becomes very time-consuming. Naturally, you can have
a set of regularly updated scripts and images for all occasions. Still, it might
be more rational to use a single script with several images and provide all
required parameters during system launch. Many cloud computing platforms offer a
so-called metadata or user-data mechanism. It enables you to pass all necessary
data on configuration of a certain virtual machine or even send the whole
script to be launched on startup.
This article addresses the following cloud platforms, to some extent:
This article addresses the following cloud platforms, to some extent:
·
Amazon EC2
·
Eucalyptus
·
Nimbula Director
·
VMWare vCloud Director
1. User-Data Concept Review. Use Cases for Different Platforms via CLI or Common Scripts
1.1 Amazon EC2
Amazon allows providing user-data in free format on instance startup. You can later receive them by following a certain link:
Link example:
curl 169.254.169.254/latest/user-data
The IP address above is a virtual one and all requests it receives are forwarded to internal API of the EC2 service, in accordance with the source’s IP address.
All standard system images, provided by Amazon have an embedded ability to perform Bash and Power Shell scripts, transmitted in user-data. If user-data begin with shebang (#!), the system will try and launch the script using the specified interpreter. This ability had initially been implemented in a separate ‘cloud init’ package for Ubuntu, however by now it has been included in all standard system images, including Windows.
Windows allows specifying execution of both common console commands,
<script>
netsh
advfirewall set allprofiles state off
</script>
and Power Shell code:
<powershell>
$source = "http://www.example.com/myserverconfig.xml"
$destination =
"c:\myapp\myserverconfig.xml"
$wc =
New-Object System.Net.WebClient
$wc.DownloadFile($source, $destination)
</powershell>
This functionality can be used jointly with Cloud Formation templates to run whole stacks of servers, having provided correct user-data:
{
"AWSTemplateFormatVersion" : "2010-09-09",
"Parameters" : {
"AvailabilityZone" : {
"Description" : "Name of an availability zone to create instance",
"Default" : "us-east-1c",
"Type" : "String"
},
"KeyName" : {
"Description" : "Name of an existing EC2 KeyPair to enable SSH access
to the instance",
"Default" : "test",
"Type" : "String"
},
"InstanceSecurityGroup" : {
"Description" : "Name of an existing security group",
"Default" : "default",
"Type" : "String"
}
},
"Resources" : {
"autoconftest" : {
"Type" : "AWS::EC2::Instance",
"Properties" : {
"AvailabilityZone" : { "Ref" : "AvailabilityZone" },
"KeyName" : { "Ref" : "KeyName" },
"SecurityGroups" : [{ "Ref" : "InstanceSecurityGroup" }],
"ImageId" : "ami-31308xxx",
"InstanceType" : "t1.micro",
"UserData" : { "Fn::Base64" : { "Fn::Join" : ["",[
"#!/bin/bash","\n",
"instanceTag=WebServer","\n",
"confDir=/etc/myconfig","\n",
"mkdir $confDir","\n",
"touch $confDir/$instanceTag","\n",
"IPADDR=$(ifconfig eth0 | grep inet | awk
'{print $2}' | cut -d ':' -f 2)","\n",
"echo $IPADDR myhostname","\n",
"hostname myhostname","\n" ]]
}
}
}
Sometimes launching a script on instance launch can be unfit for you purpose. Suppose you want other people to use your images without the need to sort out your code. In this case, you can install the script into the system, add it to startup list and create a system image.
Then you should provide your users a
description of all parameters that can be provided using user-data. The
following example is a list of key=value parameters, separated by semicolons:
graylogserver=«192.168.1.1»;chefnodename=«chef_node_name1»;chefattributes=«recipe1.attribute1=value1,recipe1.attribute2=value2,customparameter1=value1»;chefserver=«192.168.1.38:4000»;chefrole=«apache,mysql,php»;
The whole line can be obtained using Bash as follows:
graylogserver=«192.168.1.1»;chefnodename=«chef_node_name1»;chefattributes=«recipe1.attribute1=value1,recipe1.attribute2=value2,customparameter1=value1»;chefserver=«192.168.1.38:4000»;chefrole=«apache,mysql,php»;
The whole line can be obtained using Bash as follows:
function get_userdata {
user_data=$(curl
-w "%{http_code}" -s http://169.254.169.254/latest/user-data)
result_code=${user_data:(-3)}
if [ -z "$user_data" ] || [ $result_code != "200" ]
then
echo "$CurrentDate: Couldn't receive user-data. Result code: $result_code"
return 1
else
export
user_data=${user_data%%$result_code}
return 0
fi
}
You will then be able to get the required value from the received list:
function get_userdata_value {
IFS=';'
for user_data_list in $user_data
do
user_data_name=${user_data_list%%=*}
if [ $user_data_name = $1 ]
then
user_data_value=${user_data_list#*=}
user_data_value=$(echo $user_data_value | tr -d '\"')
return 0
fi
done
return 1
}
This done, you can proceed with system configuration in accordance with the received data. You do not have to store all scripts inside an image. It is enough that you have a startup script that reads user-data and then downloads and launches all necessary parameters or delegates control to Chef or Puppet.
Similar functionality can be implemented using Power Shell.
1.2 Eucaliptus
This product is compatible with Amazon AWS and shares its user-data implementation.
1.3 Nimbula
The system is relatively young and rapidly developed. Nimbula’s purpose is creation of private cloud systems using KVM virtualization. Its creators, once Amazon employees themselves, announced full AWS compatibility. However, it not so. They support user-data mechanism over virtual IPs, yet it should be provided in the ‘key=value’ fashion.
The list of all keys is available at:
192.0.0.192/latest/attributes or 169.254.169.254/latest/attributes
Example:
curl 169.254.169.254/latest/attributes
nimbula_compressed_size
nimbula_decompressed_size
chefserver
hostname
Getting the exact key value:
curl 169.254.169.254/latest/attributes/chefserver
192.168.1.45:4000
You cannot, however, pass whole scripts this way. To do this you should create your own system images with an embedded startup script.
Bash code example:
curl 169.254.169.254/latest/attributes/chefserver
192.168.1.45:4000
You cannot, however, pass whole scripts this way. To do this you should create your own system images with an embedded startup script.
Bash code example:
function get_value {
user_data_value=$(curl curl -w "%{http_code}" -s http://169.254.169.254/latest/attributes/"$1")
result_code=${user_data_value:(-3)}
if [ -z "$user_data_value" ] || [ $result_code != "200" ]
then
echo "$CurrentDate: $1 variable is not set, skip it, return code: $result_code" >> $LogFile
return 1
else
user_data_value=${user_data_value%%$result_code}
return 0
fi
}
1.4 VMWare vCloud Director
Starting from version 1.5 vCloud Director supports use of metadata within vApp (VM container). Data must be specified as ‘key=value’. In order to provide metadata you should create an XML-file with their description:
<Metadata xmlns="http://www.vmware.com/vcloud/v1.5">
<MetadataEntry>
<Key>app-owner</Key>
<Value>Foo Bar</Value>
</MetadataEntry>
<MetadataEntry>
<Key>app-owner-contact</Key>
<Value>415-123-4567</Value>
</MetadataEntry>
<MetadataEntry>
<Key>system-owner</Key>
<Value>John Doe</Value>
</MetadataEntry>
</Metadata>
Next step is executing a POST request via URL of the respective vApp:
$ curl -i -k -H
«Accept:application/*+xml;version=1.5» -H «x-vcloud-authorization:
jmw43CwPAKdQS7t/EWd0HsP0+9/QFyd/2k/USs8uZtY=» -H «Content-Type:application/vnd.vmware.vcloud.metadata+xml»
-X POST 10.20.181.101/api/vApp/vapp-1468a37d-4ede-4cac-9385-627678b0b59f/metadata -d @metadata-request.
You can read all metadata with the following GET request:
You can read all metadata with the following GET request:
$ curl -i -k -H «application/*+xml;version=1.5» -H «x-vcloud-authorization: jmw43CwPAKdQS7t/EWd0HsP0+9/QFyd/2k/USs8uZtY=» -X GET 10.20.181.101/api/vApp/vapp-1468a37d-4ede-4cac-9385-627678b0b59f/metadata.
In order to read value of a certain key your request must be arranged like this:
$ curl -i -k -H «application/*+xml;version=1.5» -H «x-vcloud-authorization: jmw43CwPAKdQS7t/EWd0HsP0+9/QFyd/2k/USs8uZtY=» -X GET 10.20.181.101/api/vApp/vapp-1468a37d-4ede-4cac-9385-627678b0b59f/metadata/asset-tag
the answer will be provided as XML.
Details on metadata in vCloud are described here: blogs vmware
2. User-Data Management Using Chef, Puppet and Similar Systems
2.1 Chef
Chef-client installation is up to you: you can install it manually and create system images afterwards, or you can install it automatically on system startup. Both ways have their pros and cons: the first one reduces time on instance startup configuration, while the second one enables you to install the latest client version, or the one that suits you best, whatever works for you. In any case, we should send a list of roles and recipes to be executed on the machine (this list can be obtained via user-data) and configure our Chef client on system startup. Also, in case we do install and configure the client on system startup, we should download the validation.pem key for the respective Chef server (the data on which can be passed using user-data)
Below is an example of Bash script retrieving a list of roles:
rolefile="/etc/chef/role.json"
function get_role {
get_value "chefrole"
if [ $? = 0 ]
then
chefrole=$user_data_value
else
echo "$CurrentDate: Couldn't get any Chef role, use base role only."
chefrole="base"
fi
commas_string=${chefrole//[!,]/}
commas_count=${#commas_string}
echo '{
"run_list": [ ' > $rolefile
IFS=","
for line in $ep_chefrole
do
if [ $commas_count = 0 ]
then
echo "\"role[$line]\" " >> $rolefile
else
echo "\"role[$line]\", " >> $rolefile
fi
commas_count=$(($commas_count-1))
done
echo ' ] }' >> $rolefile
}
It also created a client configuration file:
function set_chef {
if [ -d $chef_dir ] && [ -e $chef_bin ]
then
service $chef_service stop
sleep 10
echo -e "chef_server_url
\"http://$1\"" > $chef_dir/client.rb
echo -e "log_location \"$chef_log\"" >> $chef_dir/client.rb
echo -e "json_attribs \"$rolefile\"" >> $chef_dir/client.rb
echo -e "interval
$chef_interval" >> $chef_dir/client.rb
echo "$CurrentDate: Writing $chef_dir/client.rb"
service $chef_service start
else
echo "$CurrentDate: Chef directory $chef_dir or chef binary $chef_bin does not exist. Exit."
exit 1
fi
}
The ‘json_attributes’ parameter set a path to the JSON file with a list of roles and recipes.
Once control has been delegated to the Chef client, it will register on a server, download the list of recipes and proceed with their execution. However, there are some nuances:
·
It takes a lot of time to execute certain
recipes, we should know once system configuration has been finished and whether
it has been successful.
·
Sometimes we don’t want to execute recipes
with default recipes, changing some attributes instead, e.g. install LAMP with
Apache working over port 8080, not 80.
The first issue can be solved using a cookbook by Opscode, called ‘chef_handler’. It provides a mechanism, called Exception and Report Handlers. It is called after Chef client finishes executing recipes. This cookbook enables us to check the result of the last client’s execution and perform certain actions. We can send messages containing execution results (example from an Opscode manual) or upload the results to Chef server to check this value using own applications and display the execution status.
Recipe example:
Setting default attribute values
default['lastrun']['state'] = "unknown"
default['lastrun']['backtrace'] = "none"
Specifying what needs to be executed
include_recipe "chef_handler"
chef_handler "NodeReportHandler::LastRun" do
source "#{node.chef_handler.handler_path}/nodereport.rb"
action :nothing
end.run_action(:enable)
Configuring execution
module NodeReportHandler
class LastRun < Chef::Handler
def report
if success? then
node.override[:lastrun][:state] = "successful"
node.override[:lastrun][:backtrace] = "none"
else
node.override[:lastrun][:state] = "failed"
node.override[:lastrun][:backtrace] = "#{run_status.formatted_exception}"
end
node.save
end
end
end
As a result we get values of ‘lastrun.state’ and ‘lastrun.backtrace’ parameters initially set as 'unknown' and 'none' respectively, and once the client finishes execution, receive either ‘successful’ or ‘failed’ record, with the error description in lastrun.backtrace.
This recipe should be placed on top of the execution list to cover all errors received during execution of any recipes.
Change default attributes requires us to get them somehow, then store and start executing recipes. Once again, we can receive those using user-data.
Example of a recipe, retrieving user-data from Amazon:
Getting the whole string
# Get whole user-data string
def GetUserData(url)
uri = URI.parse(url)
http = Net::HTTP.new(uri.host, uri.port)
http.open_timeout = 5
http.read_timeout = 5
proto =
url.split(":", 2)
if proto[0] == "https"
http.use_ssl
= true
http.verify_mode
= OpenSSL::SSL::VERIFY_NONE
end
request = Net::HTTP::Get.new(uri.request_uri)
begin
http.open_timeout = 5
http.read_timeout = 5
request =
Net::HTTP::Get.new(uri.request_uri)
response
= http.request(request)
result =
response.body.to_s
if response.is_a?(Net::HTTPSuccess)
Chef::Log.info("Successfuly
get user-data.")
return result
else
return false
end
rescue Exception => e
Chef::Log.info("HTTP
request failed.")
return false
end
end
getting the value of a certain parameter
# Get specified user-data value
def GetValue(user_data,attribute)
user_data.split(";").each do |i|
attribute_name=i.split("=", 2)
if attribute_name[0] ==
attribute
return attribute_name[1].strip
end
end
return false
end
Now that we can receive values of certain parameters from passed data, we can set them:
сhefnodename=«chef_node_name1»;chefattributes=«recipe1.attribute1=value1,recipe1.attribute2=value2,customparameter1=value1»;chefserver=«192.168.1.38:4000»;chefrole=«apache,mysql,php»
We used the ‘chefattributes’ parameter to send a list of properties we would like changed. They are set in the ‘cookbookname.attributename=value’ fashion. In order to change the default Apache port, we should set ‘chefattributes=apache.port=8080’.
Below is the recipe that reads this value and saves it:
We used the ‘chefattributes’ parameter to send a list of properties we would like changed. They are set in the ‘cookbookname.attributename=value’ fashion. In order to change the default Apache port, we should set ‘chefattributes=apache.port=8080’.
Below is the recipe that reads this value and saves it:
chefattributes = GetValue("#{node[:user_data]}","chefattributes")
if
chefattributes != false
сhefattributes.split(",").each do |i|
attribute_name=i.split("=")
recipe_name=attribute_name[0].split(".", 2)
node.override[:"#{recipe_name[0]}"][:"#{recipe_name[1].strip}"]="#{attribute_name[1].strip}"
Chef::Log.info("Save
node attributes.")
node.save
else
Chef::Log.info("Couldn't
get Chef attributes. Skip.")
end
This recipe should be executed in the first place.
Drawbacks of the aforementioned recipes
The ‘node.save’ operation sends the whole
JSON array for a certain node to a server for storing, including information,
gathered by Ohai. Thousands of machines that constantly try to rewrite their attributes
on a server will have a negative impact of the server’s performance. This also concerns
using a flexible and powerful search, like the one provided by Chef. The search
operation is very time-consuming and placing heavy load on a server. In this case
we should use different means, not described here.
2.2 Puppet
2.2 Puppet
Receiving user-data with Puppet is similar to using Chef. Addresses of a Puppet server and the rest of necessary data for agent configuration are received by means of a startup script. Facter add-on enables handy uploading of your facts to a server.
Below is an example of a ruby script that receives necessary data from user-data and sends them to a server as additional facts for each machine:
require 'facter'
user_data = `curl http://169.254.169.254/latest/user-data`
user_data = user_data.split(";")
user_data.each do |line|
user_data_key_value = line.split('=', 2)
user_data_key =
user_data_key_value[0]
user_data_value =
user_data_key_value[1]
Facter.add(user_data_key) do
setcode {
user_data_value }
end
end
instance_id = `curl
http://169.254.169.254/latest/meta-data/instance-id`
Facter.add('instance-id') do
setcode {
instance_id }
end
Naturally, it can all appear simple and not requiring such complex layouts. You can just create the required set of images with pre-installed software and make all changes using scripts that go by SSH and edit configuration files.
This article, however, covers
elementary steps only. In case you require a Hadoop or MySQL cluster, not to
mention a cluster of Front-End, Back-End, App, and DB servers, supporting
automatic configuration of all machines automatic scalability, you will not be
able to go without the aforementioned techniques.
Should you be aware of the ways to transmit metadata for other cloud platforms, or other means of controlling VM configuration on startup, you are welcome to discuss it in the comments section.
Should you be aware of the ways to transmit metadata for other cloud platforms, or other means of controlling VM configuration on startup, you are welcome to discuss it in the comments section.
No comments:
Post a Comment