Saturday, March 4, 2017

Automate hostname and DNS setup on EC2 instance

1 Introduction

When we automate an EC2 instance provision with CloudFormation, we can assign a name tag to the EC2 instance. Naturally, we would like to have the OS hostname match the name tag, but surprisingly, this is not the default AWS behavior and need some efforts to automate it. 

2 The challenges

2.1 Hostname is derived from dynamic IP address

When we provision a new Amazon Linux EC2 instance on AWS, the default hostname is derived from the IP address that is dynamically assigned to the instance at startup.

For example, your hostname might look like this
ip-12-34-56-78

Very likely, this name is not desirable and need to be changed, but how?

Amazon has instruction on how to make the  change manually, but it involve a reboot, something is not nice if we want to use script to automate this task.

2.2 Using on-premises AD service

On the other hand, changing the hostname is part of the story, we also need to update the DNS record.

If you are using Active Directory and extend the on-premises AD service to the AWS like the following picture, you would like to know how to automatic the DNS update too.


3 2 The solution

3.1 Enable DNS dynamic update

We need to enable dynamic update on the DNS service provided by the AD server.

The following enable dynamic updates using the Windows interface
  1. Open DNS Manager.
  2. In the console tree, right-click the applicable zone, and then click Properties.
  3. On the General tab, verify that the zone type is either Primary or Active Directory-integrated.
  4. In Dynamic Updates, click Nonsecure and secure.


In my example script, it only works with the “Nonsecure and secure” setting only. In case you need more security, you could set up Secure Dynamic Update, check out the details on this Microsoft document and modify the script accordingly.

3.2 Automate all the updates with a python script

I had written a script in Python to automate the task to update the hostname and DNS record based on the name tag assigned to the EC2 instance.

In details, this script perform the following:
  • Get the instance name from EC2 metadata, which is the EC2 name tag
  • Get the instance IP address from EC2 metadata
  • Update the /etc/sysconfig/network file with the name FQDN name
  • Update the /etc/hosts with the IP and FQDN
  • Set the hostname
  • Update the DNS record on AD

Following is the python script, you can include it in your bootstrap step script during the EC2 provision or in your Chef/Puppet/Ansible configuration steps.
.
#!/usr/bin/python
import boto3
import requests
import json
import re
import os
import subprocess
import StringIO

## Change the domain name to your need
DOMAIN='cloud.operative.com'

## replace content in a file
def replace(file, pattern, subst):
    # Read contents from file as a single string
    file_handle = open(file, 'r')
    file_string = file_handle.read()
    file_handle.close()

    # Use RE package to allow for replacement (also allowing for (multiline) REGEX)
    file_string = (re.sub(pattern, subst, file_string))

    # Write contents to file.
    # Using mode 'w' truncates the file.
    file_handle = open(file, 'w')
    file_handle.write(file_string)
    file_handle.close()

## get the name tag of the ec2 instance
def get_instance_name():
    ## get the ec2 instance id and region
    r = requests.get('http://169.254.169.254/latest/dynamic/instance-identity/document/')
    r_arr=json.loads(r.text)

    region =  r_arr['region']
    instance_id = r_arr['instanceId']

    ## get EC2 information
    ec2 = boto3.resource('ec2', region_name=region)
    ec2instance = ec2.Instance(instance_id)
    instancename = ''
    for tags in ec2instance.tags:
       if tags["Key"] == 'Name':
           instancename = tags["Value"]
    return instancename

def get_ip_addr():
    r = requests.get("http://169.254.169.254/latest/meta-data/local-ipv4")
    return r.text

def grep(file,pattern):
    for line in open(file).readlines():
       if re.match(pattern, line):
           return line
    return ""

def append(file,data):
  f = open(file, 'a')
  f.write(data)
  f.close()

def update_dns(hostname):
    cmd='nslookup ' + hostname
    args = cmd.split()
    process = subprocess.Popen(args, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    stdout, stderr = process.communicate()
    exit_code = process.wait()
    print "return code = " , exit_code

    if exit_code == 0:
       print "DNS record already existed"
       cmd = """
    else:
       ## update dns record
       print "Update DNS record"
       cmd = """
nsupdate << EOF
zone $ZONE
update delete $fqdn
update add $fqdn 86400 A $ip_addr
show
send
EOF
"""
       os.system(cmd)

##### Main Program #####

instance_name = get_instance_name()
ip_addr = get_ip_addr()

fqdn = instance_name + '.' + DOMAIN
host_line = 'HOSTNAME=' + instance_name + '.' + DOMAIN

## Update /etc/sysconfig/network file
print "update /etc/sysconfig/network with " + host_line
replace("/etc/sysconfig/network", 'HOSTNAME=.*', host_line )

## Update /etc/hosts file
ret = grep("/etc/hosts", ip_addr)
ip_w_fqdn = ip_addr + " " + fqdn + " " + instance_name
if ret == "":
  print "add fqdn info to /etc/hosts"
  append("/etc/hosts", ip_w_fqdn + "\n")
else:
  print "Replace line in /etc/hosts with '" + ip_w_fqdn  + "'"
  replace("/etc/hosts", ip_addr + '.*' , ip_w_fqdn)

## Run hostname command
cmd='hostname ' + instance_name
os.system(cmd)

## Update DNS record
update_dns(fqdn)

3.3 Additional tip

If your shop only allow one DNS server for dynamic update and it is not the one defined in /etc/resolv.conf, you can modify the script and add the server option for nsupdate like the following

…..
…..
nsupdate << EOF
server <your dns server allow dynamic update>
zone $ZONE
update delete $fqdn
update add $fqdn 86400 A $ip_addr
show
send
EOF
"""
….
….

4 comments: