A few months ago, AWS introduced an extremely useful little feature to their consistently developing EC2 platform: Instance Tagging. Before tags, it was relatively tricky to define a clear instance nomenclature, forcing you to implement a vague approach using security groups if you wanted something close to the same functionality. Since there is also a full API surrounding tags, they can be put to work in many useful ways.
Naming instances
Besides attaching up to ten custom nuggets of arbitrary data to instances, tags can be extremely handy when it comes to naming your EC2 instances, and more importantly, using these names to differentiate between instances and their roles. For example, you have three instances. One holds your master database and website, the second is a slave database in replication, and the third is a staging/test server. You’ll want to make pretty damn sure that when you’re SSH’d into all three of your instances, the commands you send are received by the intended recipient server.
In the EC2 management console, you can define a tag key for an instance which is then mapped to a value. There is a default tag key called “Name”. In the value for that “Name” key, you can type in the name you want to assign to that instance. For our servers in this example we can call the master server master-0, the slave server slave-0, and the staging server staging-0. I like this naming system because on the left of the hyphen you can define the type or classification of that server representing the role it performs, and the number following the hyphen can be incremented for each server of the same type, therefore acting as a unique ID for each server.
Conveniently, you can define which of these tags become a column in the table listing all of your instances. If you have a large number of instances, having this custom column for tags present can make it far quicker to find the instance you’re looking for at a glance, rather than trying to derive the right one from all of the other parameters displayed.
Doing some magic with .bashrc
I am a big fan of customising the bash prompt so that it displays only useful contextual information I need to know when I send commands. One of the problems I came across whilst working simultaneously on multiple instances is making sure I am sending commands to the correct one. Sure, most bash prompts include the server’s hostname in the prompt, but beware – there are flaws with this. For example, if you reboot an EBS-backed instance, the hostname it shows is not necessarily the one it is operating from. I needed something more definitive than that.
Here’s some example code you can place in your .bashrc file to display the instance ID and server name in the command prompt:
INSTANCEID=`curl -s http://169.254.169.254/latest/meta-data/instance-id`
SERVER_IDENTIFIER=$INSTANCEID
SERVER_TAG=0
if [ -f /var/opt/server-tag ]; then
SERVER_TAG=`cat /var/opt/server-tag`
SERVER_IDENTIFIER="$SERVER_TAG:$SERVER_IDENTIFIER"
fi
if [ "$color_prompt" = yes ]; then
PS1='${debian_chroot:+($debian_chroot)}[ 33[01;32m]u@${SERVER_IDENTIFIER}[ 33[00m]:[ 33[01;34m]w[ 33[00m]$ '
else
PS1='${debian_chroot:+($debian_chroot)}u@${SERVER_IDENTIFIER}:w$ '
fi
unset color_prompt force_color_prompt
The great thing about this bashrc code is that if you have a file saved at /var/opt/server-tag which contains the server’s tagged name, the prompt will look something like this:
That prompt clearly defines that I am working as user Ubuntu on a server called Node#1 with instance ID of i-b92990d5, so I don’t need to think twice about that.
Using tags as hostnames
Now that we have named our instances using tags, we can turn them into hostnames so we can address them directly in our scripts, avoiding the need for expensive lookups against Amazon’s API to find server internal IPs. I’ve written a simple script to add entries to the /etc/hosts file for each of your instances that have a tag called “Name”. The value in this “Name” tag will become that server’s hostname, which then forwards to its internal IP address. While we’re at it, we also create the aforementioned /var/opt/server-tag file so our .bashrc script can display the server name in the command prompt. Note this script requires the AWS SDK for PHP and because we’re modifying the /etc/hosts file, you need to run it as root.
< ?php
// Install the AWS PHP SDK from http://docs.amazonwebservices.com/AWSSDKforPHP/latest/ and follow the setup instructions on that page.
require_once "aws-php-sdk/sdk.class.php";
define('SERVER_TAG', '/var/opt/server-tag');
try{
// Parse hosts file into an array
$hosts_file = file_get_contents('/etc/hosts');
$hosts_lines = explode(PHP_EOL, $hosts_file);
foreach($hosts_lines as $hlkey => $hline){
if(empty($hline)) continue;
$host_lines[$hlkey] = preg_split('/s/', $hline);
}
$hosts_updated = false;
$this_instance_id = file_get_contents('http://169.254.169.254/latest/meta-data/instance-id');
// Check server tag directory
$server_tag_dir = dirname(SERVER_TAG);
if(!file_exists($server_tag_dir)) mkdir($server_tag_dir, 0777, true);
// Get the server tag
$server_tag = file_get_contents(SERVER_TAG);
$EC2 = new AmazonEC2();
$result = $EC2->describe_instances();
if(!$result->isOK()) throw new Exception("ec2->describe_instances failed");
// Each "reservationSet" is essentially an instance or compute "reservation"
foreach($result->body->reservationSet->item as $rset){
$iset = $rset->instancesSet->item;
if($iset->instanceState->code != 16) continue; // Instance is not in "running" state, so ignore
if(isset($iset->tagSet)){
// Server is tagged. We should note it down in the /etc/hosts file
$tset = $iset->tagSet->item;
$server_identifier = false;
if(is_array($tset) && count($tset) > 1){
foreach($tset as $tag){
if($tag->key == 'Name') $server_identifier = (string)$tag->value;
}
}
else{
if($tset->key == 'Name') $server_identifier = (string)$tset->value;
}
if($server_identifier){
// This server is tagged with an identifier. Make sure we've got it recorded in the server-tag file if this is our server
if((string)$iset->instanceId == $this_instance_id && $server_tag != $server_identifier) file_put_contents (SERVER_TAG, $server_identifier);
// Now me ensure the correct host entry is written or up to date
$nice_si = str_replace('#', '-', $server_identifier); // Replace # with - to avoid conflicts
$hosts_match = false;
foreach($host_lines as $hlkey => $hline){
if(strstr($hline[0], '#')) continue; // Skip comment lines
while($hostname = next($hline)){ // Loop through the hostnames to see if this server exists in the host file
if($hostname == $nice_si){ // Found a hostname match, so check the mapped IP address
$hosts_match = true;
$ip_addr = (string)$iset->privateIpAddress;
if($hline[0] != $ip_addr){
// Private IP address has changed. Update it after a quick sanity check on IP address
if(!empty($ip_addr) && strlen($ip_addr) >= 7) $hline[0] = $ip_addr;
$host_lines[$hlkey] = $hline;
$hosts_updated = true;
}
}
}
}
if(!$hosts_match){
// This server does not exist in the hosts list. Add it.
$ip_addr = (string)$iset->privateIpAddress;
if(!empty($ip_addr) && strlen($ip_addr) >= 7){
$host_lines[] = array($ip_addr, $nice_si);
$hosts_updated = true;
}
}
}
}
}
if($hosts_updated){
foreach($host_lines as $hlkey => $hline){
$host_lines[$hlkey] = implode(" ", $hline);
}
if(file_put_contents('/etc/hosts', implode("n", $host_lines))){
echo "/etc/hosts has been updated successfullyn";
}
else echo "Could not update /etc/hosts filen";
}
else echo "No changes to maken";
}
catch(Exception $e){
die($e->getMessage());
}
?>
After running this script, we will have entries in our /etc/hosts file for all of our tagged servers, and because these are now defined as operating system hostnames, you can use these hostnames for connections between your servers (for example, if you were connecting to mysql from slave-0 to master-0 you could define the hostname for the connection as master-0:3306 and the connection will be made via Amazon’s internal DNS network. If you set this script to a regular cron job, you can ensure that the hostnames are kept updated and new ones are added as you tag new instances.