Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Programmatically set EBS Volumes Windows Drive Letters using Terraform, Chef or Powershell

I'm using terraform and chef to create multiple aws ebs volumes and attach them to an EC2 instance.

The problem is I want to be able to give each ebs volume a specific windows drive letter. The problem is when the EC2 instance is instantiated window just gives it sequential drive letters (D,E,F,etc)

Some of the drives are identically sized so I can't necessarily rename based on drive size. Does anyone know of a way to do this with terraform or chef. My google foo isn't finding anything.

Certainly this must come up for other folks?

I did see reference to using EC2Config Windows GUI to set them but the whole point is to automate the process, as ultimately I want chef to install SQL server and certain data is expected to go on certain drive letters.

This seems to work - although I do wonder if there isn't an easier way.

function Convert-SCSITargetIdToDeviceName
{
param([int]$SCSITargetId)
If ($SCSITargetId -eq 0) {
    return "/dev/sda1"
}
$deviceName = "xvd"
If ($SCSITargetId -gt 25) {
    $deviceName += [char](0x60 + [int]($SCSITargetId / 26))
}
$deviceName += [char](0x61 + $SCSITargetId % 26)
return $deviceName
}

Get-WmiObject -Class Win32_DiskDrive | ForEach-Object {
$DiskDrive = $_
$Volumes = Get-WmiObject -Query "ASSOCIATORS OF {Win32_DiskDrive.DeviceID='$($DiskDrive.DeviceID)'} WHERE AssocClass=Win32_DiskDriveToDiskPartition" | ForEach-Object {
    $DiskPartition = $_
    Get-WmiObject -Query "ASSOCIATORS OF {Win32_DiskPartition.DeviceID='$($DiskPartition.DeviceID)'} WHERE AssocClass=Win32_LogicalDiskToPartition"
}
If ($DiskDrive.PNPDeviceID -like "*PROD_PVDISK*") {
    $BlockDeviceName = Convert-SCSITargetIdToDeviceName($DiskDrive.SCSITargetId)
    If ($BlockDeviceName -eq "xvdf") { $drive = gwmi win32_volume -Filter "DriveLetter = '$($Volumes.DeviceID)'"; Set-WmiInstance -input $drive -Arguments @{DriveLetter="D:"; Label="SQL Data"} };
    If ($BlockDeviceName -eq "xvdg") { $drive = gwmi win32_volume -Filter "DriveLetter = '$($Volumes.DeviceID)'"; Set-WmiInstance -input $drive -Arguments @{DriveLetter="L:"; Label="SQL Logs"} };
    If ($BlockDeviceName -eq "xvdh") { $drive = gwmi win32_volume -Filter "DriveLetter = '$($Volumes.DeviceID)'"; Set-WmiInstance -input $drive -Arguments @{DriveLetter="R:"; Label="Report Data"} };
    If ($BlockDeviceName -eq "xvdi") { $drive = gwmi win32_volume -Filter "DriveLetter = '$($Volumes.DeviceID)'"; Set-WmiInstance -input $drive -Arguments @{DriveLetter="T:"; Label="Temp DB"} };
    If ($BlockDeviceName -eq "xvdj") { $drive = gwmi win32_volume -Filter "DriveLetter = '$($Volumes.DeviceID)'"; Set-WmiInstance -input $drive -Arguments @{DriveLetter="M:"; Label="MSDTC"} };
    If ($BlockDeviceName -eq "xvdk") { $drive = gwmi win32_volume -Filter "DriveLetter = '$($Volumes.DeviceID)'"; Set-WmiInstance -input $drive -Arguments @{DriveLetter="B:"; Label="Backups"} };
} ElseIf ($DiskDrive.PNPDeviceID -like "*PROD_AMAZON_EC2_NVME*") {
    $BlockDeviceName = Get-EC2InstanceMetadata "meta-data/block-device-mapping/ephemeral$($DiskDrive.SCSIPort - 2)"
    If ($BlockDeviceName -eq "xvdf") { $drive = gwmi win32_volume -Filter "DriveLetter = '$($Volumes.DeviceID)'"; Set-WmiInstance -input $drive -Arguments @{DriveLetter="D:"; Label="SQL Data"} };
    If ($BlockDeviceName -eq "xvdg") { $drive = gwmi win32_volume -Filter "DriveLetter = '$($Volumes.DeviceID)'"; Set-WmiInstance -input $drive -Arguments @{DriveLetter="L:"; Label="SQL Logs"} };
    If ($BlockDeviceName -eq "xvdh") { $drive = gwmi win32_volume -Filter "DriveLetter = '$($Volumes.DeviceID)'"; Set-WmiInstance -input $drive -Arguments @{DriveLetter="R:"; Label="Report Data"} };
    If ($BlockDeviceName -eq "xvdi") { $drive = gwmi win32_volume -Filter "DriveLetter = '$($Volumes.DeviceID)'"; Set-WmiInstance -input $drive -Arguments @{DriveLetter="T:"; Label="Temp DB"} };
    If ($BlockDeviceName -eq "xvdj") { $drive = gwmi win32_volume -Filter "DriveLetter = '$($Volumes.DeviceID)'"; Set-WmiInstance -input $drive -Arguments @{DriveLetter="M:"; Label="MSDTC"} };
    If ($BlockDeviceName -eq "xvdk") { $drive = gwmi win32_volume -Filter "DriveLetter = '$($Volumes.DeviceID)'"; Set-WmiInstance -input $drive -Arguments @{DriveLetter="B:"; Label="Backups"} };
} Else {
    write-host "Couldn't find disks";
}
}
like image 264
Brad Avatar asked Oct 24 '17 22:10

Brad


2 Answers

If you take the tables in this link into account: https://docs.aws.amazon.com/AWSEC2/latest/WindowsGuide/ec2-windows-volumes.html

You can see that on EBS, the first rows are:

Bus Number 0, Target ID 0, LUN 0 /dev/sda1
Bus Number 0, Target ID 1, LUN 0 xvdb

Disk 0 (/dev/sda1) is always setup for you by EC2 as C:

So you know when you run "New-Partition -DiskNumber 1 -UseMaximumSize -IsActive -AssignDriveLetter" you are going to get D: given to it.

So if you provision an AMI image with Packer using the following volumes in Builders (just two here in this example but you could do however many):

        "launch_block_device_mappings": [{
        "device_name": "/dev/sda1",
        "volume_size": 30,
        "volume_type": "gp2",
        "delete_on_termination": true
    },
    {
        "device_name": "xvdb",
        "volume_size": 30,
        "volume_type": "gp2",
        "delete_on_termination": true    
    }]

..You can plan, knowing xvd[b] is actually two letters behind what will get mapped.

Then spin up an EC2 instance of this multi-volume AMI with Terraform and have this in the user_data section of the aws_instance resource:

    user_data = <<EOF
    <powershell>
    Initialize-Disk -Number 1 -PartitionStyle "MBR"
    New-Partition -DiskNumber 1 -UseMaximumSize -IsActive -AssignDriveLetter
    Format-Volume -DriveLetter d -Confirm:$FALSE
    Set-Partition -DriveLetter D -NewDriveLetter S
    </powershell>
    EOF

The Set-Partition -DriveLetter D -NewDriveLetter S line(s) is what you use to rename your known sequential drive(s) to whatever letter(s) you are used to. In my case, they wanted D: as S: - just repeat this line to rename E: as X: or whatever you need.

Hope this helps.

UPDATE: There is another way (Server 2016 up), which I discovered when I discovered Sysprep nukes all the mappings being baked into the AMI image.

You have to provide a DriveLetterMappingConfig.json file in C:\ProgramData\Amazon\EC2-Windows\Launch\Config to do the mapping. The format of the file is:

{
  "driveLetterMapping": [
    {
      "volumeName": "sample volume",
      "driveLetter": "H"
    }
  ]
}

...Only, my drives, by default, didn't have a volumeName; they were blank. So back to the 1980-something good old "LABEL" command. Labeled the D: drive as volume2. So the file looks like:

{
  "driveLetterMapping": [
    {
      "volumeName": "volume2",
      "driveLetter": "S"
    }
  ]
}

Running C:\ProgramData\Amazon\EC2-Windows\Launch\Scripts\InitializeDisks.ps1 tested this worked (D: became S:)

So now, back in Packer, I need to also provision the image with this DriveLetterMappingConfig.json file in C:\ProgramData\Amazon\EC2-Windows\Launch\Config to make sure all the drive work I did on the AMI's S: comes back as S: on the instance. (I put the file in an S3 bucket along with all the other crap we are going to install on the box.)

I put the disk stuff into a .ps1 and call it from a provisioner:

{ "type": "powershell", "script": "./setup_two_drive_names_c_and_s.ps1"
},

Where the above .ps1 is:

# Do volume config of the two drives
write-host "Setting up drives..."
Initialize-Disk -Number 1 -PartitionStyle "MBR"
New-Partition -DiskNumber 1 -UseMaximumSize -IsActive -AssignDriveLetter
Format-Volume -DriveLetter d -Confirm:$FALSE
label c: "volume1"
label d: "volume2"
Set-Partition -DriveLetter D -NewDriveLetter S

# Now insert DriveLetterMappingConfig.json file into C:\ProgramData\Amazon\EC2-Windows\Launch\Config to ensure instance starts with correct drive mappings
Write-Host "S3 Download: DriveLetterMappingConfig.json"
Read-S3Object -BucketName ********* -Key DriveLetterMappingConfig.json -File 'c:\temp\DriveLetterMappingConfig.json'
Write-Host "Copying DriveLetterMappingConfig.json to C:\ProgramData\Amazon\EC2-Windows\Launch\Config..."
Copy-Item "c:\temp\DriveLetterMappingConfig.json" -Destination "C:\ProgramData\Amazon\EC2-Windows\Launch\Config\DriveLetterMappingConfig.json" -Force
Write-Host "Set Initialze Disks to run on every boot..."
C:\ProgramData\Amazon\EC2-Windows\Launch\Scripts\InitializeDisks.ps1 -Schedule

Yeah, there is no reason to label c: But I was on a roll...

The final line with the "-Schedule" parameter means this happens on every boot.

like image 163
Faye Smelter Avatar answered Sep 27 '22 21:09

Faye Smelter


I needed a Windows Server 2016 with 4 drives of identical sizes, but I did not care which block device became which drive letter. Below are the steps I took (using Packer) to obtain this:

First, in the in the builders area of the template, add as many block devices as you need (in my case - 4 entries under launch_block_device_mapping). Then, in the provisioners list run the following commands:

  1. initialize the disks using the script available on any Windows 2016 Amazon instance; this will bring every disk online, add a partion to it, extend the partition to maximum possible size, format it and assign a Windows drive letter to it.

    {
        "type": "powershell",        
        "inline": [
            "C:\\ProgramData\\Amazon\\EC2-Windows\\Launch\\Scripts\\InitializeDisks.ps1"        
        ]
    }
    

    Notes:

    If you add the '-Schedule' parameter, the disks will not be initialized at this point, as this option will only add the script to a task scheduled to run one time at the next boot of the instance (afterwards it's de-activated).

    The drive letters are assigned in alphabetical order, starting with D (because C is reserved for the root drive).

    The order in which volumes are attached to an instance is not related to the block device name and will not have a 1-on-1 correspondance (xvdb will not become the D:\ drive, xvdc will not become E:\, etc.)

  2. Assign the label you desire to each drive letter of the already initialized disks.

    {
        "type": "powershell",
        "inline": [
            "write-output \"Label partitions after initializing disks\"",
            "label C: \"OS\"",
            "label D: \"Programs\"",
            "label E: \"Data\"",
            "label F: \"Backup\"",
            ...
        ]
    }
    

    Note: Another possible option would be to add the labels directly in the DriveLetterMapping.json file (available on any Windows 2016 Amazon AMI) before runnning the disks initialization script (I could not make this work).

  3. After you add any other provisioners you might need (e.g. activate Windows components, install applications or check for Windows updates), as the last entry in the provisioners list make sure the instance initialization and SysPrep scripts are added

    {
        "type": "powershell",
        "inline": [
            "C:/ProgramData/Amazon/EC2-Windows/Launch/Scripts/InitializeInstance.ps1 -Schedule",
            "C:/ProgramData/Amazon/EC2-Windows/Launch/Scripts/SysprepInstance.ps1 -NoShutdown"
        ]
    }
    

    Note: This last step is specific to EC2Launch and applies from Windows 2016 onwards. For older versions (like Windows 2012), the syntax differs and it's based on EC2Config.

Once an AMI is obtained from this configuration, the drive letters of any instance launched from it should be as desired.

If drive letters and their labels are not mapped as expected, you can also try to force the re-labeling of the drives by using the instance's user data. Just before you launch it, a powershell script can easily be passed as clear text; below is just one possible example:

<powershell>
write-output "Force re-map of drive letters based on labels, after disk initialization"
# remove drive letters, but keep labels
Get-Volume -Drive D | Get-Partition | Remove-PartitionAccessPath -accesspath "D`:\"
Get-Volume -Drive E | Get-Partition | Remove-PartitionAccessPath -accesspath "E`:\"
Get-Volume -Drive F | Get-Partition | Remove-PartitionAccessPath -accesspath "F`:\"
# add drive letters based on labels
get-volume | where filesystemlabel -match "Programs" | Get-Partition | Set-Partition -NewDriveLetter D
get-volume | where filesystemlabel -match "Data" | Get-Partition | Set-Partition -NewDriveLetter E
get-volume | where filesystemlabel -match "Backup" | Get-Partition | Set-Partition -NewDriveLetter F
</powershell>
like image 38
Soo Avatar answered Sep 27 '22 23:09

Soo