Friday, May 30, 2014

Setting the Hostname in a SysPreped AMI

When you create an Windows AMI (Amazon Machine Image) it is configured to generate a random server name.  Often this name does not meet your needs.  Maybe your company has a specific naming convention (e.g US-NYC-1234) or you just want to use a descriptive name (e.g. WEB01).  Whatever the reason, let's look at how to set the name when you launch the machine.

In this post we will use PowerShell to read the name from a Tag on the instance.  When done, you set the hostname in launch wizard by simply filling in the Name tag.  See the image below.  Our script will read this tag and rename the server when it boots for the first time.

It is important to automate the name change.  As your cloud adoption matures, you quickly realize that you cannot have an admin log in and rename the server when it's launched.  First, it takes too long.  Second, you want servers to launch automatically, for example, in response to an auto-scaling event.

So how can you set the name?  You will find a ComputerName element in the SysPrep2008.xml file that ships with the EC2 Config Service (or in the unattended.xml file if you're not using the EC2 Config Service.)  The computer name is in the specialize section.  In the snippet below, you can see the default value of "*".  The star means that windows should generate a random name.

<component language="neutral" xmlns:xsi="" xmlns:wcm="" versionScope="nonSxS" publicKeyToken="31bf3856ad364e35" processorArchitecture="amd64" name="Microsoft-Windows-Shell-Setup">
    <TimeZone>Eastern Standard Time</TimeZone>

If you want to change the name you can simply hard-code whatever you want here.  Of course, if you hard-code if before you run SysPrep, every machine you create from the AMI will have the same name.  That's not what we want.  So the trick is to set the name when the machine first boots and before specialize runs.

Let's quickly review how SysPrep works.  When you run SysPrep, it wipes any identifying information form the machine (e.g. Name, SIDs, etc.)  This is known as the generalize phase.  After the generalize phase you shutdown the machine and take the image.

When a SysPreped image first boots, it runs windows setup (WinDeploy.exe).  This is known as the specialize phase.  If you have ever bought a new home computer, you have experienced the setup wizard that allows you to configure your timezone, etc.  In the cloud you cannot answer questions so you have to supply an unattended.xml file with the answers to all the questions.  

We need to inject our script into the specialize phase before setup runs.  Our script will get the machine name from the EC2 API and modify the unattended.xml file.  Here is a sample script to do just that.  The script has three parts.

  • The first part uses the meta-data service to discover the identity of the instance and the region the machine is running in.
  • The second part of the script uses the EC2 API to get the name tag from for the instance.  Note that I have not included any credentials.  I assume that the instance is in a role that allows access to the Get-EC2Tag API call.
  • The third part of the script modifies the unattended.xml file.  This is the same file shown earlier.  The script simply finds the ComputerName node and replaces the * with the correct name.

Write-Host "Discovering instance identity from meta-data web service"
$InstanceId = (Invoke-RestMethod '').ToString()
$AvailabilityZone = (Invoke-RestMethod '').ToString()
$Region = $AvailabilityZone.Substring(0,$AvailabilityZone.Length-1)

Write-Host "Getting Tags for the instance"
$Tags = Get-EC2Tag -Filters @{Name='resource-id';Value=$InstanceId} -Region $Region
$InstanceName = ($Tags | Where-Object {$_.Key -eq 'Name'}).Value
Write-Host "`tFound Instance Name: $InstanceName"
Write-Host "`tFound Instance Owner: $InstanceOwner"

If($InstanceName -ne $null) {
 Write-Host "Setting the machine name to $InstanceName"
 $AnswerFilePath = "C:\Windows\Panther\unattend.xml"
$AnswerFile = [xml](Get-Content -Path $AnswerFilePath) $ns = New-Object System.Xml.XmlNamespaceManager($AnswerFile.NameTable) $ns.AddNamespace("ns", $AnswerFile.DocumentElement.NamespaceURI) $ComputerName = $AnswerFile.SelectSingleNode('/ns:unattend/ns:settings[@pass="specialize"]/ns:component[@name="Microsoft-Windows-Shell-Setup"]/ns:ComputerName', $ns) $ComputerName.InnerText = $InstanceName $AnswerFile.Save($AnswerFilePath) }

So how do we get this script to run before setup? That's the tricky part.  Let's dig a bit deeper.  I said earlier that when a SysPreped image first boots it will run WinDeploy.exe.  To be more specific, it will run whatever it finds in the HKLM:\System\Setup registry key.  SysPrep will put c:\Windows\System32\oobe\windeploy.exe in the registry key before shutdown.

So we need to change that registry key after SysPrep runs, but before the system shuts down.  To do that we need to pass the /quit flag rather than /shutdown.  I'm writing about AWS, so I assume you are calling SysPrep from the EC2Config service.  If you are, you need to edit the switches element of the  BundleConfig.xml file in the EC2Config folder.  The switches element is about midway down the file.  See the example below.  Just remove /shutdown and replace it with /quit.

<Sysprep version="6.0">
      <PreSysprepRunCmd>C:\Program Files\Amazon\Ec2ConfigService\Scripts\BeforeSysprep.cmd</PreSysprepRunCmd>
      <Switches>/oobe /shutdown /generalize<syspr/Switches>

Alright, we are almost there.  Now you can run SysPrep and it will give you a chance to make changes before shutting down.  You want to replace the HKLM:\System\Setup registry key with the script we created above.  Don't forget to add a line to call WinDeploy.exe at the end of the script.

With all that done (it's not as bad it sounds) you can shutdown and take an image.  It will take a few tries to get all this working correctly.  I recommend that you log the output of the script using Start-Transcript.  If the server fails to boot you can attach the volume to another instance and read the log.