Deploying Windows 10 to AWS using Packer and the AWS PowerShell module

The reason for this post is that I wanted to create a one click solution to deploy a Window 10 machine into AWS.  I wanted the AWS instance to be named in Windows, added to the domain, install applications, name the AWS instance tag an much more.  The tools I initially decided on to create this were VMware, awscli, PowerShell, Terraform and Packer.

Reference Image VM

The first stage is to build a Windows VM within vCenter.  Simply create the VM and install Windows 10 Enterprise.  When you build the VM use the Intel NIC not the VMXNET3 NIC as this will require you to install the VMware tools.

Login to the VM and create an administrator account that you will be using later.  Install all available Windows updates.  Then disable UAC, the Windows firewall and enable remote connections for RDP access.

We then need to run the below script on the new VM, this will setup a secure HTTPS WinRM listener.  Its not needed now but Packer will use this later on to accept commands from your desktop machine to the AWS VM over secure WinRM on port 5986. (ensure this is open on the firewall).

Set-ExecutionPolicy Unrestricted -Scope LocalMachine -Force -ErrorAction Ignore

$ErrorActionPreference = "stop"

Remove-Item -Path WSMan:\Localhost\listener\listener* -Recurse

$Cert = New-SelfSignedCertificate -CertstoreLocation Cert:\LocalMachine\My -DnsName "packer"
New-Item -Path WSMan:\LocalHost\Listener -Transport HTTPS -Address * -CertificateThumbPrint $Cert.Thumbprint -Force

write-output "Setting up WinRM"
write-host "(host) setting up WinRM"

cmd.exe /c winrm quickconfig -q
cmd.exe /c winrm set "winrm/config" '@{MaxTimeoutms="1800000"}'
cmd.exe /c winrm set "winrm/config/winrs" '@{MaxMemoryPerShellMB="1024"}'
cmd.exe /c winrm set "winrm/config/service" '@{AllowUnencrypted="true"}'
cmd.exe /c winrm set "winrm/config/client" '@{AllowUnencrypted="true"}'
cmd.exe /c winrm set "winrm/config/service/auth" '@{Basic="true"}'
cmd.exe /c winrm set "winrm/config/client/auth" '@{Basic="true"}'
cmd.exe /c winrm set "winrm/config/service/auth" '@{CredSSP="true"}'
cmd.exe /c winrm set "winrm/config/listener?Address=*+Transport=HTTPS" "@{Port=`"5986`";Hostname=`"packer`";CertificateThumbprint=`"$($Cert.Thumbprint)`"}"
cmd.exe /c netsh advfirewall firewall set rule group="remote administration" new enable=yes
cmd.exe /c netsh firewall add portopening TCP 5986 "Port 5986"
cmd.exe /c net stop winrm
cmd.exe /c sc config winrm start= auto
cmd.exe /c net start winrm 

 

Then run the below script to install the AD PowerShell modules.  This has to be run at this stage as it cannot be run as a Packer provisioner.

Add-WindowsCapability online Name Rsat.ActiveDirectory.DS-LDS.Tools~~~~0.0.1.0 

 

Once you are happy with the reference image shut it down and take a snapshot.

Exporting the reference image VM

We now need to extract the reference VM as an .ova file that we can use in AWS.  To do this login to PowerCLI and run the below commands:

Import-Module VMware.VimAutomation.Core

Connect-VIServer vCenter1

Export-VApp -Destination 'C:\Temp\AWSAMI\Windows 10 2019 LTSC Template.ova' -VM 'Windows102019LTSCTemplate' -Format Ova 
 

 

Uploading the VM image to AWS

We now need to create an S3 bucket so that we can store the .ova file that we just took from vCenter.  To do this login to AWS and go to S3, create a bucket as below.

Deploying Windows 10 to AWS using Packer and Terraform-1

Create a new file called containers.json

Use the below code as a template changing the description, S3Bucket and the S3Key (Bucket folder).

Deploying Windows 10 to AWS using Packer and Terraform-2

 

[
  {
    "Description": "Windows 10 2019 LTSC Template",
    "Format": "ova",
    "UserBucket": {
        "S3Bucket": "windows10bucketami1",
        "S3Key": "disks/Windows 10 2019 LTSC Template.ova"
    }
}]

 

Once created upload the .ova file you exported from vCenter using the AWS S3 console.

Use the awscli to upload as the file will probably be too large for the GUI.

aws s3 cp "C:\Temp\AWSAMI\Windows 10 2019 LTSC Template.ova" s3://import-images-dev --no-verify-ssl

 

Importing the ova from S3 to EC2

Creating the vmimport service role

Before you can import the VM into EC2 you need to create a vmimport service role as defined by AWS here

The policy is easy to create just go to IAM and then go to roles.  Create a new role called vmimport and then just create an inline policy

Deploying Windows 10 to AWS using Packer and Terraform-3

You can use the visual editor to create the policy as below and then just make sure that you change the Resources to your S3 bucket.

Deploying Windows 10 to AWS using Packer and Terraform-4

If you prefer to use json the code is below:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "s3:ListBucket",
                "s3:GetBucketLocation"
            ],
            "Resource": [
                "arn:aws:s3:::import-images-dev"
            ],
            "Effect": "Allow"
        },
        {
            "Action": [
                "s3:GetObject"
            ],
            "Resource": [
                "arn:aws:s3:::import-images-dev/*"
            ],
            "Effect": "Allow"
        },
        {
            "Action": [
                "ec2:ModifySnapshotAttribute",
                "ec2:CopySnapshot",
                "ec2:RegisterImage",
                "ec2:Describe*"
            ],
            "Resource": "*",
            "Effect": "Allow"
        }
    ]
}

 

IAM policy permissions for importing images

Now that we have an S3 bucket and a service role configured we need to create a policy that we can apply to a user so that they have permissions to upload the image to EC2.

Create a new policy in IAM called Import_Image.  Using the visual editor add the below permissions making sure that under the S3 section you specify the correct S3 bucket name.  You can see that the policy will allow the user to import new images into import-images-dev.  The policy also specifies which specific actions they can perform.

Deploying Windows 10 to AWS using Packer and Terraform-5

Below is the json code for this policy

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "s3:DeleteObject",
                "s3:PutObject",
                "s3:GetObject"
            ],
            "Resource": "arn:aws:s3:::import-images-dev/*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "ec2:CopyImage",
                "ec2:CreateTags",
                "ec2:DeregisterImage",
                "ec2:DescribeImages",
                "ec2:DescribeImportImageTasks",
                "ec2:ImportImage",
                "ec2:CancelImportTask",
                "ec2:ModifyImageAttribute"
            ],
            "Resource": "*",
            "Effect": "Allow"
        }
    ]
}

 

IAM Policy permissions for creating the AMI

Ultimately once the images has been imported to EC2 we will be performing some actions on it.  These actions will include creating an instance, preparing the instance and then creating an AMI based on this instance.  We will then be terminating the instance and its volumes when we are finished.

Create a policy called amazon_ebs and give it the below permissions using the visual editor.

Deploying Windows 10 to AWS using Packer and Terraform-6

Here are the same permissions in json format:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "ec2:AttachVolume",
                "ec2:AuthorizeSecurityGroupIngress",
                "ec2:CopyImage",
                "ec2:CreateImage",
                "ec2:CreateKeypair",
                "ec2:CreateSecurityGroup",
                "ec2:CreateSnapshot",
                "ec2:CreateTags",
                "ec2:CreateVolume",
                "ec2:DeleteKeypair",
                "ec2:DeleteSecurityGroup",
                "ec2:DeleteSnapshot",
                "ec2:DeleteVolume",
                "ec2:DeregisterImage",
                "ec2:DescribeImageAttribute",
                "ec2:DescribeImages",
                "ec2:DescribeInstances",
                "ec2:DescribeRegions",
                "ec2:DescribeSecurityGroups",
                "ec2:DescribeSnapshots",
                "ec2:DescribeSubnets",
                "ec2:DescribeTags",
                "ec2:DescribeVolumes",
                "ec2:DetachVolume",
                "ec2:GetPasswordData",
                "ec2:ModifyImageAttribute",
                "ec2:ModifyInstanceAttribute",
                "ec2:ModifySnapshotAttribute",
                "ec2:RegisterImage",
                "ec2:RunInstances",
                "ec2:StopInstances",
                "ec2:TerminateInstances"
            ],
            "Resource": "*",
            "Effect": "Allow"
        }
    ]
}

 

IAM user account creation and group

For simplicity we are going to use one account for importing the VM and creating the AMI.  As we will be doing this with Packer and creating a Windows based AMI call this account winpacker.

Then create a group, this will make it easy to apply both the policies created above.  Call the group ami_import and attach the policies just created as below.

Deploying Windows 10 to AWS using Packer and Terraform-7

The final step is then to apply the group to the user account and the account is ready to perform all of the necessary tasks.

Deploying Windows 10 to AWS using Packer and Terraform-8

Creating a reference AMI from the reference ova image

Now that we have the ova in the S3 bucket and we have an account with all the necessary permissions we can import the ova file into EC2 and convert it to an AMI that we can use with Packer to customise within EC2.

Login to AWS CLI using the account we created above and then run the below command to start the import of the ova file into an AMI in EC2:

aws ec2 import-image --description "Windows 10 2019 LTSC Template" --disk-containers file://C:\Temp\AWSAMI\containers.json --no-verify-ssl

 

The AMI is now being created, run the below command to check the status:

aws ec2 describe-import-image-tasks --no-verify-ssl

 

Deploying Windows 10 to AWS using Packer and Terraform-9

Building the customised AMI with Packer

Packer is basically a tool that allows you to build in customisations to your reference image AMI.  In the previous step we uploaded and prepared what started as a vCenter VM into an AMI.  We will now pack the AMI with all the customisations that we require.  Packer will first create an AWS instance, make all the necessary changes, terminate the instance and then create the final AMI.

The easiest way to install Packer is to use Chocolatey.  Once Chocolatey is installed used the PowerShell command as below to install Packer:

cinst packer 

 

We now need to tell Packer how to prepare the image.  To do this we just need a json file with all the configuration needed.  As we will also be using scripts and xml files we can keep these organised as below in folders.

Deploying Windows 10 to AWS using Packer and Terraform-10

Inside the json folder I have a file called windows10.json.  The file looks like the below and I’ll run through what each part does.

  {
  "variables": {
    "aws_access_key": "youraccesskey",
    "aws_secret_key": "youraccesskey"
  },
  "builders": [{

    "type": "amazon-ebs",
    "access_key": "{{user `aws_access_key`}}",
    "secret_key": "{{user `aws_secret_key`}}",
	"vpc_id": "vpc-452145e4",
    "subnet_id": "subnet-26124125",
    "region": "eu-west-1",
    "source_ami": "ami-0361245252151245",
	"user_data_file": "./scripts/setupwinrm.ps1",
    "instance_type": "t2.medium",
	"communicator": "winrm",
	"winrm_username": "packer",
	"winrm_password": "yourpassword",
	"winrm_insecure": true,
	"winrm_use_ssl": true,
    "ami_name": "windows10-ami {{timestamp}}"
  }],

    "provisioners": [
        {
	  "type": "powershell",	
      "scripts":
      [
            "./scripts/disableuac.ps1",
            "./scripts/chocolateyinstall.ps1",
            "./scripts/windowsupdates.ps1"			
      ]					
        },
		{
  "type": "file",
  "source": "./xml/BundleConfig.xml",
  "destination": "c:\\Sysprep\\BundleConfig.xml"
},
		{
  "type": "file",
  "source": "./xml/config.xml",
  "destination": "c:\\Sysprep\\config.xml"
},
		{
  "type": "file",
  "source": "./scripts/Sysprep.bat",
  "destination": "c:\\Sysprep\\Sysprep.bat"
},
		{
  "type": "file",
  "source": "./scripts/Sysprep.ps1",
  "destination": "c:\\Sysprep\\Sysprep.ps1"
},
		{
  "type": "file",
  "source": "./xml/sysprep2008.xml",
  "destination": "c:\\Sysprep\\sysprep2008.xml"
},
		{
  "type": "file",
  "source": "./scripts/Windeploy.bat",
  "destination": "c:\\Sysprep\\Windeploy.bat"
},
		{
  "type": "file",
  "source": "./scripts/EC2Tags.ps1",
  "destination": "c:\\Sysprep\\EC2Tags.ps1"
},
		{
  "type": "file",
  "source": "./scripts/InstanceNaming.exe",
  "destination": "c:\\Sysprep\\InstanceNaming.exe"
},
		{
  "type": "file",
  "source": "./scripts/ccmsetup.msi",
  "destination": "c:\\Sysprep\\ccmsetup.msi"
},
		{
  "type": "file",
  "source": "./scripts/installsccm.bat",
  "destination": "c:\\Sysprep\\installsccm.bat"
},
		{
  "type": "windows-shell",
  "script": "./scripts/installsccm.bat"
},
		{
  "type": "windows-shell",
  "script": "./scripts/Sysprep.bat"
}
    ]
}

 

Break-down of json config

Here we enter our AWS credentials, we are using the winpacker account.

  {
  "variables": {
    "aws_access_key": "youraccesskey",
    "aws_secret_key": "yoursecretkey"
  },
  "builders": [{

    "type": "amazon-ebs",
    "access_key": "{{user `aws_access_key`}}",
    "secret_key": "{{user `aws_secret_key`}}",

 

Here we enter the VPC/subnet we want to build the instance in and the region.

	"vpc_id": "vpc-452145e4",
    "subnet_id": "subnet-26124125",
    "region": "eu-west-1",

 

We then enter the source AMI ID – this is the ID of the AMI that we uploaded from the ova file

"source_ami": "ami-0361245252151245",

 

Here we can specify the user data file we want to use.  This is a PowerShell script call setupwinrm.ps1.  It does some initial config on the VM to prepare it for everything we need to do with Packer.

	"user_data_file": "./scripts/setupwinrm.ps1",

 

We then setup the instance type and enter the credentials we want to use to connect to winrm with.  We will use the packer account that we had already setup on the VM.  We then just choose to use SSL and give the resulting AMI a name when it is complete.

    "instance_type": "t2.medium",
	"communicator": "winrm",
	"winrm_username": "packer",
	"winrm_password": "yourpassword",
	"winrm_insecure": true,
	"winrm_use_ssl": true,
    "ami_name": "windows10-ami {{timestamp}}"
  }],

Provisioners

These are the scripts and files that we will use to make changes to the source image.  There are many different types of provisioners that can be used.

The PowerShell provisioner allows us to run all the scripts as below from the scripts folder.  DisableUAC just turns off UAC on the remote system.  Chocolateyinstall installs Chocolatey and a few basic apps such as Chrome, Notepad++, Putty and Keepass.

"provisioners": [
        {
	  "type": "powershell",	
      "scripts":
      [
            "./scripts/disableuac.ps1",
            "./scripts/chocolateyinstall.ps1",
            "./scripts/windowsupdates.ps1"			
      ]					
        },

 

The file provisioner allows us to copy files to the VM to be used later on

 "type": "file",
  "source": "./xml/BundleConfig.xml",
  "destination": "c:\\Sysprep\\BundleConfig.xml"
},
		{
  "type": "file",
  "source": "./xml/config.xml",
  "destination": "c:\\Sysprep\\config.xml"
},
		{
  "type": "file",
  "source": "./scripts/Sysprep.bat",
  "destination": "c:\\Sysprep\\Sysprep.bat"
},
		{
  "type": "file",
  "source": "./scripts/Sysprep.ps1",
  "destination": "c:\\Sysprep\\Sysprep.ps1"
},
		{
  "type": "file",
  "source": "./xml/sysprep2008.xml",
  "destination": "c:\\Sysprep\\sysprep2008.xml"
},
		{
  "type": "file",
  "source": "./scripts/Windeploy.bat",
  "destination": "c:\\Sysprep\\Windeploy.bat"
},
		{
  "type": "file",
  "source": "./scripts/EC2Tags.ps1",
  "destination": "c:\\Sysprep\\EC2Tags.ps1"
},
		{
  "type": "file",
  "source": "./scripts/InstanceNaming.exe",
  "destination": "c:\\Sysprep\\InstanceNaming.exe"
},
		{
  "type": "file",
  "source": "./scripts/ccmsetup.msi",
  "destination": "c:\\Sysprep\\ccmsetup.msi"
},
		{
  "type": "file",
  "source": "./scripts/installsccm.bat",
  "destination": "c:\\Sysprep\\installsccm.bat"
},

 

The Windows-Shell provisioner allows us to run batch files and Windows shell commands.  The first script installsccm.bat installs the sccm client.  The reason we run this at this stage is to get the SCCM packages installed as soon as the machine is built.  The next command starts the sysprep process.

		{
  "type": "windows-shell",
  "script": "./scripts/installsccm.bat"
},
		{
  "type": "windows-shell",
  "script": "./scripts/Sysprep.bat"
}
    ]
}

 

Packer Scripts and sequence

As you could see above a bunch of scripts were copied over to the VM once we initiate the Packer build.  The final part of the Packer build runs the below batch file on the VM which is described below.

1. First we stop the EC2Config service. This service is installed by default when we converted the ova to an AWS AMI. This is so that we can make changes to the EC2 config.

REM Stop the EC2Config service so we can overwrite the configuration
net stop ec2config

 

2. Now we copy over our BundleConfig.xml in place of the AWS one. This basically changes the sysprep process to quit rather than shutdown so that we can perform further functions.

REM Change Sysprep to quit rather than shutdown
copy C:\SysPrep\BundleConfig.xml "C:\Program Files\Amazon\Ec2ConfigService\Settings\BundleConfig.xml"

 

3. This makes a bunch of sysprep changes that are handled by AWS (mainly turning things off like AWS naming computer)

REM Enable password generation and user data
copy C:\SysPrep\config.xml "C:\Program Files\Amazon\Ec2ConfigService\Settings\config.xml"

 

4. This copies our configured sysprep file over the top of the AWS sysprep file. (We will look at this in more detail)

REM Copy the sysprep template with computer name node
copy C:\SysPrep\sysprep2008.xml "C:\Program Files\Amazon\Ec2ConfigService\sysprep2008.xml"

 

5. This runs the AWS sysprep setup – but will not shut down the machine.

REM Run sysprep but do not shutdown
"c:\Program Files\Amazon\Ec2ConfigService\Ec2Config.exe" -sysprep

 

6. This runs our custom sysprep PowerShell script

REM Configure our customization script to run before setup
PowerShell.exe -ExecutionPolicy Unrestricted -File c:\SysPrep\SysPrep.ps1

 

7. This will now shutdown the machine that is sysprep’d an ready to go

REM Ready to shutdown.
shutdown /s

 

Sysprep unattend file

Below is the unattend file from step 4 above.  It does the following:

  • Set local to UK
  • Hide EULA and skip startup questions
  • Activate Windows

 

<?xml version="1.0" encoding="utf-8"?>
<unattend xmlns="urn:schemas-microsoft-com:unattend">
    <settings pass="generalize">
        <component name="Microsoft-Windows-PnpSysprep" processorArchitecture="amd64" publicKeyToken="12345856ad312345" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <PersistAllDeviceInstalls>true</PersistAllDeviceInstalls>
            <DoNotCleanUpNonPresentDevices>true</DoNotCleanUpNonPresentDevices>
        </component>
    </settings>
    <settings pass="oobeSystem">
        <component name="Microsoft-Windows-International-Core" processorArchitecture="amd64" publicKeyToken="12345856ad312345" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <InputLocale>en-GB</InputLocale>
            <SystemLocale>en-GB</SystemLocale>
            <UILanguage>en-GB</UILanguage>
            <UserLocale>en-GB</UserLocale>
        </component>
        <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="12345856ad312345" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <OOBE>
                <HideEULAPage>true</HideEULAPage>
                <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
                <NetworkLocation>Work</NetworkLocation>
                <ProtectYourPC>3</ProtectYourPC>
                <HideLocalAccountScreen>true</HideLocalAccountScreen>
                <HideOEMRegistrationScreen>true</HideOEMRegistrationScreen>
                <HideOnlineAccountScreens>true</HideOnlineAccountScreens>
                <SkipMachineOOBE>true</SkipMachineOOBE>
                <SkipUserOOBE>true</SkipUserOOBE>
            </OOBE>
            <BluetoothTaskbarIconEnabled>false</BluetoothTaskbarIconEnabled>
            <TimeZone>GMT Standard Time</TimeZone>
            <RegisteredOrganization>MyCompany.com</RegisteredOrganization>
            <RegisteredOwner>MyCompany</RegisteredOwner>
            <FirstLogonCommands>
                <SynchronousCommand wcm:action="add">
                    <CommandLine>cmd /C start /wait c:\windows\system32\slmgr.vbs -ipk MSNCF-SMDKF-WLSJD-ALSDF-ALMSD //b</CommandLine>
                    <Order>1</Order>
                    <RequiresUserInput>false</RequiresUserInput>
                </SynchronousCommand>
                <SynchronousCommand wcm:action="add">
                    <CommandLine>cscript.exe c:\windows\system32\slmgr.vbs /ato</CommandLine>
                    <Order>2</Order>
                    <RequiresUserInput>false</RequiresUserInput>
                </SynchronousCommand>
                <SynchronousCommand wcm:action="add">
                    <CommandLine>C:\Sysprep\InstanceNaming.exe</CommandLine>
                    <Order>3</Order>
                    <RequiresUserInput>false</RequiresUserInput>
                </SynchronousCommand>
            </FirstLogonCommands>
            <AutoLogon>
                <Password>
                    <Value>RSJdfsgbAHkANsksh4AZsslslsFAAYQshsSMAdwSIAZAA=</Value>
                    <PlainText>false</PlainText>
                </Password>
                <Enabled>true</Enabled>
                <LogonCount>1</LogonCount>
                <Username>packer</Username>
            </AutoLogon>
        </component>
    </settings>
    <settings pass="specialize">
        <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="12345856ad312345" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <!-- The ComputerName parameter can be used to specify the computer name. Note:: This will cause the machine name to be changed twice;  initial by sysprep, then again with the new parameter.
     The second name change will break the SQL Server name, which is corrected in the 'scripts/SysprepSpecializePhase.cmd' file, so will need to be manually updated and MSSQLService restarted.-->
            <ComputerName>*</ComputerName>
            <CopyProfile>true</CopyProfile>
            <RegisteredOrganization>MyCompany</RegisteredOrganization>
            <TimeZone>GMT Standard Time</TimeZone>
        </component>
        <component name="Microsoft-Windows-Deployment" processorArchitecture="amd64" publicKeyToken="12345856ad312345" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <RunSynchronous>
                <RunSynchronousCommand wcm:action="add">
                    <Order>1</Order>
                    <Path>net user Administrator /ACTIVE:YES /LOGONPASSWORDCHG:NO /EXPIRES:NEVER /PASSWORDREQ:YES</Path>
                </RunSynchronousCommand>
                <RunSynchronousCommand wcm:action="add">
                    <Order>2</Order>
                    <Path>"C:\Program Files\Amazon\Ec2ConfigService\ScramblePassword.exe" -u Administrator</Path>
                </RunSynchronousCommand>
                <RunSynchronousCommand wcm:action="add">
                    <Order>3</Order>
                    <Path>"C:\Program Files\Amazon\Ec2ConfigService\Scripts\SysprepSpecializePhase.cmd"</Path>
                </RunSynchronousCommand>
            </RunSynchronous>
        </component>
        <component name="Microsoft-Windows-ServerManager-SvrMgrNc" processorArchitecture="amd64" publicKeyToken="12345856ad312345" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <DoNotOpenServerManagerAtLogon>true</DoNotOpenServerManagerAtLogon>
        </component>
    </settings>
    <settings pass="windowsPE">
        <component name="Microsoft-Windows-International-Core-WinPE" processorArchitecture="amd64" publicKeyToken="12345856ad312345" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <SetupUILanguage>
                <UILanguage>en-GB</UILanguage>
            </SetupUILanguage>
            <InputLocale>en-GB</InputLocale>
            <SystemLocale>en-GB</SystemLocale>
            <UILanguage>en-GB</UILanguage>
            <UserLocale>en-GB</UserLocale>
        </component>
        <component name="Microsoft-Windows-Setup" processorArchitecture="amd64" publicKeyToken="12345856ad312345" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <UserData>
                <ProductKey>
                    <Key></Key>
                    <WillShowUI>OnError</WillShowUI>
                </ProductKey>
                <AcceptEula>true</AcceptEula>
                <Organization>MyCompany</Organization>
            </UserData>
        </component>
    </settings>
    <cpi:offlineImage cpi:source="wim:c:/temp/sw_dvd5_win_ent_ltsc_2019_64bit_english_-2_mlf_x22-05056/sources/install.wim#Windows 10 Enterprise LTSC 2019" xmlns:cpi="urn:schemas-microsoft-com:cpi" />
</unattend>

Sysprep PowerShell script

The sysprep PowerShell script from step 6 is below:

Write-Host "Check the current setup command line"
Get-ItemProperty -Path "HKLM:\System\Setup" -Name "CmdLine"
Write-Host "Replace the setup command line with script"
Set-ItemProperty -Path "HKLM:\System\Setup" -Name "CmdLine" -Value "C:\SysPrep\WinDeploy.bat"
Write-Host "Verify the new setup command line"
Get-ItemProperty -Path "HKLM:\System\Setup" -Name "CmdLine" 

 

This script does the following things:

Write-Host "Check the current setup command line"
Get-ItemProperty -Path "HKLM:\System\Setup" -Name "CmdLine"
Write-Host "Replace the setup command line with script"

 

Change the registry to reflect the path of the windeploy.bat file that we copied over to the VM.

Set-ItemProperty -Path "HKLM:\System\Setup" -Name "CmdLine" -Value "C:\SysPrep\WinDeploy.bat"
Write-Host "Verify the new setup command line"
Get-ItemProperty -Path "HKLM:\System\Setup" -Name "CmdLine" 

 

Windeploy

The Windeploy batch file is as below, it starts the windeploy.exe process.

c:\Windows\System32\oobe\windeploy.exe

 

Domain Join account

Before you can run the first-logon scripts you need to create an AD account that can (only) join machines to the domain.  Add the below permissions to the OU where the machines are to be added for your domain join service account:

Go to the Security Properties for the OU

Deploying Windows 10 to AWS using Packer and Terraform-12

Add the account you created for domain join

Deploying Windows 10 to AWS using Packer and Terraform-13

First Select Descendant Computer Objects

Deploying Windows 10 to AWS using Packer and Terraform-14

Clear all existing permissions

Deploying Windows 10 to AWS using Packer and Terraform-15

Add these permissions

Deploying Windows 10 to AWS using Packer and Terraform-16

and these…

Deploying Windows 10 to AWS using Packer and Terraform-17

Change the drop-down to This Object and all descendant objects

Deploying Windows 10 to AWS using Packer and Terraform-18

Add these permissions

Deploying Windows 10 to AWS using Packer and Terraform-19

Post-Logon Scripts

As you will have noticed  from the Windows10.json file we had a file provisoner called InstanceNaming.exe.  You may also have noticed the following lines in the sysprep2008.xml file:

<SynchronousCommand wcm:action="add">
                    <CommandLine>C:\Sysprep\InstanceNaming.exe</CommandLine>
                    <Order>3</Order>

 

This basically forces a script called InstanceNaming.exe to run after the first login. The script below is PowerShell but I converted to an .exe using ISESteroids

$domain = "MyCompany.com"
$computerCN = 'OU=Contoso,OU=AWS,OU=Win10 Policies,OU=Workstations,OU=MyCompany,DC=MyCompany,DC=com'
$password = "MyPassword" | ConvertTo-SecureString -asPlainText -Force
$username = "MyDomain\domainjoinacount" 
$credential = New-Object System.Management.Automation.PSCredential($username,$password)
$Computers = @(Get-ADComputer -Server domaincontroller.MyCompany.com -Credential $credential -Filter 'Name -like "WORKSTATION*"' -SearchBase $computerCN -SearchScope OneLevel)
$MachineName = "WORKSTATION$('{0:d2}' -f ([int]($Computers.foreach({($_.Name).split('WORKSTATION') | Select-Object -Last 1}) | Sort-Object {$_ -as [int]} | Select-Object -Last 1) +1))"
Write-Host "Machine name is $MachineName"

$Trigger= New-ScheduledTaskTrigger –AtStartup
$User= "NT AUTHORITY\SYSTEM"
$Action= New-ScheduledTaskAction -Execute "PowerShell.exe" -Argument "C:\Sysprep\EC2Tags.ps1" 
Register-ScheduledTask -TaskName "Rename Tag" -Trigger $Trigger -User $User -Action $Action -RunLevel Highest –Force

Rename-Computer -NewName $MachineName

Write-Host "Joining domain"
Add-Computer -DomainName $domain -Credential $credential -OUPath "OU=Contoso,OU=AWS,OU=Win10 Policies,OU=Workstations,OU=MyCompany,DC=MyCompany,DC=com" -Options JoinWithNewName,AccountCreate -Force
Restart-Computer

 

The script will do the following:

First get the domain name and credentials of the user we created above that can add this machine to the domain.  The script will then filter all machines with a particular name (this depends what you call your machines ie workstation003).  It will also only search within a particular OU.

$domain = "MyCompany.com"
$computerCN = 'OU=Contoso,OU=AWS,OU=Win10 Policies,OU=Workstations,OU=MyCompany,DC=MyCompany,DC=com'
$password = "MyPassword" | ConvertTo-SecureString -asPlainText -Force
$username = "MyDomain\domainjoinacount" 
$credential = New-Object System.Management.Automation.PSCredential($username,$password)
$Computers = @(Get-ADComputer -Server domaincontroller.MyCompany.com -Credential $credential -Filter 'Name -like "WORKSTATION*"' -SearchBase $computerCN -SearchScope OneLevel)

 

The next part will create a machine name for you.  This will take the last available machine name and +1 to it.  If you need to use three numbers change the d2 to d3.

$MachineName = "WORKSTATION$('{0:d2}' -f ([int]($Computers.foreach({($_.Name).split('WORKSTATION') | Select-Object -Last 1}) | Sort-Object {$_ -as [int]} | Select-Object -Last 1) +1))"
Write-Host "Machine name is $MachineName"

 

The next part will create a scheduled task to run a PowerShell script called EC2Tags.ps1.

I created an AWS account and policy for this called describeinstances with the below restricted permissions

Deploying Windows 10 to AWS using Packer and Terraform-20

This script will set the AWS name tag to the same as the domain computer name of the machine in AD.

$Trigger= New-ScheduledTaskTrigger –AtStartup
$User= "NT AUTHORITY\SYSTEM"
$Action= New-ScheduledTaskAction -Execute "PowerShell.exe" -Argument "C:\Sysprep\EC2Tags.ps1" 
Register-ScheduledTask -TaskName "Rename Tag" -Trigger $Trigger -User $User -Action $Action -RunLevel Highest –Force

 

We then just rename the computer, join the domain and reboot.

Rename-Computer -NewName $MachineName

Write-Host "Joining domain"
Add-Computer -DomainName $domain -Credential $credential -OUPath "OU=Contoso,OU=AWS,OU=Win10 Policies,OU=Workstations,OU=MyCompany,DC=MyCompany,DC=com" -Options JoinWithNewName,AccountCreate -Force
Restart-Computer

 

The machine will now reboot and the scheduled task will kick in that will run the below EC2tags.ps1 file.  This script will set the EC2tag to the same name as the domain computer name.  It will enable UAC and disable the Windows firewall.  Finally the script will delete itself and the entire scripts folder

Set-AWSCredential -AccessKey 'youraccesskey' -SecretKey 'yoursecretkey'

$instanceId = (New-Object System.Net.WebClient).DownloadString("http://169.254.169.254/latest/meta-data/instance-id")
$versionTag =  Get-EC2Tag | ` Where-Object {$_.ResourceId -eq $instanceId -and $_.Key -eq 'Name'}
$versionTag = $versionTag.value
$machinename = $env:COMPUTERNAME


New-EC2Tag -Resource $instanceId -Tag @{Key="Name"; Value=$machinename}

Unregister-ScheduledTask -TaskName "Rename Tag" -Confirm:$false

Set-NetFirewallProfile -Profile Domain,Public,Private -Enabled False

Write-Host “enabling UAC…”
New-ItemProperty -Path HKLM:Software\Microsoft\Windows\CurrentVersion\Policies\System -Name EnableLUA -PropertyType DWord -Value 1 -Force
New-ItemProperty -Path HKLM:Software\Microsoft\Windows\CurrentVersion\Policies\System -Name ConsentPromptBehaviorAdmin -PropertyType DWord -Value 1 -Force

# this script deletes itself.
Remove-Item $MyINvocation.InvocationName

Remove-Item -LiteralPath "C:\Sysprep\" -Force -Recurse

 

Building the final AMI with Packer

Now that we have all the above scripts in place and files ready we can build the AMI with Packer.  To do this we just run packer build  and the path to our json file.  Below are a few variations on how you could do this:

packer build C:\Temp\AWSAMI\Packer\Windows10.json

packer build "K:\Documentation\AWS\Config\Packer\json\Windows10.json"

packer build ./json/Windows10.json

Deploying the VM

There are of course a number of ways you could do this.  I prefer to use PowerShell.  All you now need to do is change the -ImageId ami to the newly created ami that you got from Packer and select a number of instances to deploy.

Add-Type -AssemblyName Microsoft.VisualBasic

import-module awspowershell

$AWS_ACCESS_KEY = [Microsoft.VisualBasic.Interaction]::InputBox('Enter AWS Access Key', 'AWS Access Key')
$AWS_SECRET_KEY = [Microsoft.VisualBasic.Interaction]::InputBox('Enter AWS Secret Key', 'AWS Secret Key')

$NoOfInstances = [Microsoft.VisualBasic.Interaction]::InputBox('Enter number of instances to create', 'Number of instances')

Set-DefaultAWSRegion eu-west-1

Set-AWSCredential -AccessKey $AWS_ACCESS_KEY -SecretKey $AWS_SECRET_KEY

New-EC2Instance -ImageId ami-15db2dc612547854 -MinCount $NoOfInstances -MaxCount $NoOfInstances -SecurityGroupId sg-1212452125da -InstanceType t2.medium -SubnetId subnet-212458412

Leave a Reply

Your email address will not be published. Required fields are marked *