While performing a lift and shift migration of Windows SQL Server using the AWS Application Migration Service I was challenged with wanting the newly migrated instance to have a Windows OS license ‘included’ but additionally the SQL Server Standard license billed to the account. The customer was moving away from their current hosting platform where both licenses were covered under SPLA. Rather then going to a license reseller and purchasing SQL Server it was preferred to have all the Windows OS and SQL Server software licensing to be payed through their AWS account.
In the Application Migration Service, under launch settings > Operating System Licensing. We can see all we have is OS licence options available to toggle between license-included and BYOL.
Choose whether you want to Bring Your Own Licenses (BYOL) from the source server into the Test or Cutover instance. This defines whether the launched test or cutover instance will include the license for the operating system (License-included), or if the licensing will be based on that of the migrated server (BYOL: Bring Your Own License).
If we review a migrated instance where ‘license-included’ was selected during launch, using Powershell on instance itself we see only a singular ‘BillingProduct = bp-6ba54002’ for Windows:
((Invoke-WebRequest http://169.254.169.254/latest/dynamic/instance-identity/document).Content | ConvertFrom-Json).billingProducts bp-6ba54002
AWS Preferred Approach
There are a lots of options for migrating SQL Server to AWS, so we weren’t without choices.
- Leverage the AWS Database Migration Service (DMS) to migrate on-premises Windows SQL Server to a Relation Database Services (RDS).
- Leverage the AWS Database Migration Service (DMS) to migrate on-premises Windows SQL Server to AWS EC2 Instance provisioned from a Marketplace AMI which includes SQL licensing.
- Leverage SQL Server native tooling between an on-premises Windows SQL Server to AWS EC2 Instance provisioned from a Marketplace AMI which includes SQL licensing. Use either
- Native backup and restore
- Log shipping
- Database mirroring
- Always On availability groups
- Basic Always On availability groups
- Distributed availability groups
- Transactional replication
- Detach and attach
- Import/export
The only concern our customer had with all the above approaches was that there was technical configuration on the source server that wasn’t well understand. The risk of reimplementation on a new EC2 instance and missing configuration was perceived to be high impact.
Solution
The solution was to create a new EC2 instance from a AWS Marketplace AMI that we would like to be billed for. In my case I chose ‘Microsoft Windows Server 2019 with SQL Server 2017 Standard – ami-09ee4321c0e1218c3’.
The procedure is to detach all the volumes (including root) from the migrated EC2 instance that has all the lovely SQL data and attach it to the newly created instance with the updated BillingProducts of ‘bp-6ba54002′ for Windows and ‘bp-6ba54003′ for SQL Standard assigned to it.
If we review a Marketplace EC2 instance where SQL Server Standard was selected using Powershell on the instance:
((Invoke-WebRequest http://169.254.169.254/latest/dynamic/instance-identity/document).Content | ConvertFrom-Json).billingProducts bp-6ba54002 bp-6ba54003
How will it work?
This process will require a little outage as both EC2 Instances will have to be stopped to detach the volumes and re-attach. This all happens pretty fast so only expect it to last a minute.
NOTE: The primary ENI interface cannot be changed so there will be an IP swap, so be aware of any DNS updates you may need to do post to resolve the SQL Server being available via hostname to other servers.
The high level process of the script:
- Get Original Instance EBS mappings
- Stop the instances
- Detach the volumes from both instances
- Add the Original Instance’s EBS mappings to the New Instance
- Tag the New Instance with the Original Instance’s tags
- Tag the New Instance with the tag ‘Key=convertedFrom’ and ‘Value=<Original Instance ID>’
- Update the Name tag on the Original Instance with ‘Key=Name’ and ‘Value=<OldValue+.old>
- Update the Original Instance tags with its original BlockMapping for reference e.g. ‘Key=xvdc’ and ‘Value=vol-0c2174621f7fc2e4c’
- Start the New Instance
After the script completes the Original Instance will have the following information:
The New Instance will have the following information:
The volumes connected on the New Instance:
$orginalInstanceID = "i-0ca332b0b062dbe76" $newInstanceID = "i-0ce3eeadfa27e2f64" $AccessKey = "" $Secret = "" $Region = "ap-southeast-2" If (!(get-module -ListAvailable | ? {$_.Name -like "*AWS.Tools.EC2*"})) { Write-Output "WARNING: EC2 AWS Modules Not Installed Yet..." Exit } $getModuleResults = Get-Module "AWS.Tools.EC2" If (!$getModuleResults) { Write-Output "INFO: Loading AWS Module..." Import-Module AWS.Tools.Common -ErrorAction SilentlyContinue -Force Import-Module AWS.Tools.EC2 -ErrorAction SilentlyContinue -Force } else{ Write-Output "INFO: AWS Module Already Loaded" } Set-AWSCredential -AccessKey $AccessKey -SecretKey $Secret -ProfileLocation $Region Write-Output "INFO: Getting details $($orginalInstanceID)" $originalInstance = (Get-EC2Instance -InstanceId $orginalInstanceID).Instances $orginalBlockMappings = $originalInstance.BlockDeviceMappings $originalVolumes = @() Write-Output "INFO: Getting EBS volumes from $($orginalInstanceID)" ForEach($device in $orginalBlockMappings){ $Object = New-Object System.Object #Get EBS volumes for the machine $Object | Add-Member -type NoteProperty -name "DeviceName" -Value $device.DeviceName $Object | Add-Member -type NoteProperty -name "VolumeId" -Value $device.ebs.VolumeId $Object | Add-Member -Type NoteProperty -name "Status" -Value $device.ebs.Status $volume = Get-EC2Volume -VolumeId $device.ebs.VolumeId $Object | Add-Member -Type NoteProperty -name "AvailabilityZone" -Value $volume.AvailabilityZone $Object | Add-Member -Type NoteProperty -name "Iops" -Value $volume.Iops $Object | Add-Member -Type NoteProperty -name "CreateTime" -Value $volume.CreateTime $Object | Add-Member -Type NoteProperty -name "Size" -Value $volume.Size $Object | Add-Member -Type NoteProperty -name "VolumeType" -Value $volume.VolumeType $originalVolumes += $Object } Write-Output $originalVolumes | Format-Table $tempInstance = (Get-EC2Instance -InstanceId $newInstanceID).Instances $tempBlockMappings = $tempInstance.BlockDeviceMappings $tempVolumes = @() Write-Output "INFO: Getting details $($newInstanceID)" ForEach($device in $tempBlockMappings){ $Object = New-Object System.Object #Get EBS volumes for the machine $Object | Add-Member -type NoteProperty -name "DeviceName" -Value $device.DeviceName $Object | Add-Member -type NoteProperty -name "VolumeId" -Value $device.ebs.VolumeId $Object | Add-Member -Type NoteProperty -name "Status" -Value $device.ebs.Status $volume = Get-EC2Volume -VolumeId $device.ebs.VolumeId $Object | Add-Member -Type NoteProperty -name "AvailabilityZone" -Value $volume.AvailabilityZone $Object | Add-Member -Type NoteProperty -name "Iops" -Value $volume.Iops $Object | Add-Member -Type NoteProperty -name "CreateTime" -Value $volume.CreateTime $Object | Add-Member -Type NoteProperty -name "Size" -Value $volume.Size $Object | Add-Member -Type NoteProperty -name "VolumeType" -Value $volume.VolumeType $tempVolumes += $Object } Write-Output $tempVolumes | Format-Table #Lets do the work Write-Output "INFO: Stop the instance $($orginalInstanceID)...." try{ Stop-EC2Instance -InstanceId $originalInstance -ErrorAction Stop }catch{ Write-Output "ERROR: $_" exit } While((Get-EC2Instance -InstanceId $orginalInstanceID).Instances[0].State.Name -ne 'stopped'){ Write-Verbose "INFO: Waiting for instance to stop..." Start-Sleep -s 10 } Write-Output "INFO: Stop the instance $($newInstanceID)...." try{ Stop-EC2Instance -InstanceId $newInstanceID -Force -ErrorAction Stop }catch{ Write-Output "ERROR: $_" exit } While((Get-EC2Instance -InstanceId $newInstanceID).Instances[0].State.Name -ne 'stopped'){ Write-Verbose "INFO: Waiting for instance to stop..." Start-Sleep -s 10 } Write-Output "INFO: detaching the EBS volumes from $($orginalInstanceID)...." ForEach($volume in $originalVolumes){ try{ Dismount-EC2Volume -VolumeId $volume.VolumeId -InstanceId $orginalInstanceID -Device $volume.DeviceName -ErrorAction Stop }catch{ Write-Output "ERROR: $_" exit } } Write-Output "INFO: detaching the EBS volumes from $($newInstanceID)...." ForEach($volume in $tempVolumes){ try{ Dismount-EC2Volume -VolumeId $volume.VolumeId -InstanceId $newInstanceID -Device $volume.DeviceName -ErrorAction Stop }catch{ Write-Output "ERROR: $_" exit } } Write-Output "INFO: Migrating $($orginalInstanceID) to $($newInstanceID) with $($originalVolumes.Count) connected volumes" Write-Output "INFO: attaching the EBS volumes to $($newInstanceID)...." ForEach($volume in $originalVolumes){ try{ Add-EC2Volume -VolumeId $volume.VolumeId -InstanceId $newInstanceID -Device $volume.DeviceName -ErrorAction Stop }catch{ Write-Output "ERROR: $_" exit } } Write-Output "INFO: Tagging the $($newInstanceID) with original instance tags" $orginalInstanceTags = $originalInstance.tags ForEach($T in $orginalInstanceTags){ try{ $tag = New-Object Amazon.EC2.Model.Tag $tag.Key = $T.Key $value = $T.Value $tag.Value = $value New-EC2Tag -Resource $newInstanceID -Tag $tag -ErrorAction Stop }catch{ Write-Output "ERROR: $_" } } Try{ $tag = New-Object Amazon.EC2.Model.Tag $tag.Key = "convertedFrom" $value = $orginalInstanceID $tag.Value = $value New-EC2Tag -Resource $newInstanceID -Tag $tag -ErrorAction Stop }catch{ Write-Output "ERROR: $_" } Write-Output "INFO: Marking the $($orginalInstanceID) as old" $orginalInstanceName = ($originalInstance.tags | ? {$_.Key -like "Name"}).Value If($orginalInstanceName){ try{ $tag = New-Object Amazon.EC2.Model.Tag $tag.Key = "Name" $value = $orginalInstanceName+".old" $tag.Value = $value New-EC2Tag -Resource $orginalInstanceID -Tag $tag -ErrorAction Stop }catch{ Write-Output "ERROR: $_" } } Write-Output "INFO: Tagging the $($orginalInstanceID) with original volumes for failback" ForEach($device in $orginalBlockMappings){ try{ $tag = New-Object Amazon.EC2.Model.Tag $tag.Key = $device.DeviceName $value = $device.ebs.VolumeId $tag.Value = $value New-EC2Tag -Resource $orginalInstanceID -Tag $tag -ErrorAction Stop }catch{ Write-Output "ERROR: $_" } } Write-Output "INFO: Starting the instance $($newInstanceID) with newly attached drives...." try{ Start-EC2Instance -InstanceId $newInstanceID -Force -ErrorAction Stop }catch{ Write-Output "ERROR: $_" exit } While((Get-EC2Instance -InstanceId $newInstanceID).Instances[0].State.Name -ne 'Running'){ Write-Verbose "INFO: Waiting for instance to start..." Start-Sleep -s 10 } $filterENI = New-Object Amazon.EC2.Model.Filter -Property @{Name = "attachment.instance-id"; Values = $newInstanceID} $newInterface = Get-EC2NetworkInterface -Filter $filterENI Write-Output "INFO: Conversion complete to $($newInstanceID)" Write-Output "SUCCESS: Try logging into $($newInterface.PrivateIpAddress)"
Thanks Rene and Evan for passing on the idea.