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

5

7

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";
}
}
Pogrom answered 24/10, 2017 at 22:47 Comment(6)
Do you know how this would be achieved manually? If you can share the manual process you found ("using EC2Config Windows GUI"), someone may be able to help you translate that into Terraform or Chef, but currently this question includes both an AWS question and a Terraform/Chef question so it's less likely that someone will know the answer to both parts.Eucalyptol
docs.aws.amazon.com/AWSEC2/latest/WindowsGuide/… has instructions though it notes that the results may not always match expectations.Opinionated
So the subject line has terraform, but I also mentioned chef and powershell because I'm really not sure of the best way of doing this. Is it to use terraform (i'm now thinking perhaps not) - it may likely require powershell and/or chef.Pogrom
I've edited the original question to include a (non-working) powershell example. It gets the drives, but it won't set the drive letter or the label. I believe that's because Drives and Volumes are different. I'm just not sure how to map one to the other.Pogrom
Now a working example although I'm still wondering if this is the best way to do this. It seems fairly convoluted.Pogrom
@Brad: You might find the following helpful Mapping Disks to Volumes on Your Windows Instance to grok the Disk/Volume differences. Our AWS-hosted SQL Servers reassign drive letters by unmounting them with Remove-PartitionAccessPath -AccessPath "$($WrongDriveLetter):" and remounting them with Set-Partition -DiskNumber $DiskNumber -PartitionNumber $Partition+1 -NewDriveLetter $NewDriveLetter. Beware that Win32_DiskPartition numbers are 0-based numbers but Set-Partition expects 1-based.Threepence
W
3

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>
Whitefaced answered 6/12, 2019 at 22:6 Comment(0)
K
2

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.

Kiwanis answered 24/9, 2019 at 16:40 Comment(1)
With LaunchConfig you cannot tell which drive is which letter. And you cannot setup multiple drives.Banna
N
1

Slightly More complicated Solution. Setup For this to work

  1. Tag each volume with "DriveLetter" and value what you want to assign it. Without the ":"
  2. Give EC2 instance a "ec2:DescribeTags" and "ec2:DescribeVolumes" rights in IAM
  3. Run The following script by either passing it into User-data or create a ssm document and run it post boot
Get-Disk|where-Object IsSystem -eq $False|Foreach-Object {
  if ( $_.PartitionStyle -Eq 'RAW') {
      Initialize-Disk -Number $_.Number –PartitionStyle MBR
      Set-Disk -Number $_.Number -IsOffline $False
      $VolumeId=$_.SerialNumber -replace "_[^ ]*$" -replace "vol", "vol-"
      $InstanceId = (Invoke-WebRequest -Uri "http://169.254.169.254/latest/meta-data/instance-id" -UseBasicParsing).Content
      $DriveLetter = Get-EC2Volume -Filter @{Name="volume-id";Values=$VolumeId},@{Name="attachment.instance-id";Values=$instanceId}  |ForEach-Object {$_.Tags}|where Key -eq "DriveLetter"|Select-Object -Property Value |foreach-Object {$_.Value}
      New-Partition -DiskNumber $_.Number -DriveLetter $DriveLetter –UseMaximumSize
      Format-Volume -DriveLetter $DriveLetter
    }
Nahshunn answered 30/4, 2020 at 19:4 Comment(2)
This is the real solution in my opinion (provided it fully works). All drives with proper tags can be initialized and mapped according to the tags. I would add also a tag with the label, though. Kudos.Banna
might require update now with metadata v2Nahshunn
C
1

On AWS windows servers 2016 and up , you can use the below line in userdata to initialize the secondary volumes during EC2 creation

C:\ProgramData\Amazon\EC2-Windows\Launch\Scripts\InitializeDisks.ps1

More info here :

Amazon Support

Above AWS script initializes the disks as only MBR Type . (we can not extend volume > 2tb with MBR type)

My use case was to initialize GPT type volumes.

So I ended up passing the below script to userdata and sending it to a file saved in C drive (I have referred Manpreet Nehras suggestion to frame userscript https://mcmap.net/q/1509512/-programmatically-set-ebs-volumes-windows-drive-letters-using-terraform-chef-or-powershell)

$disks = Get-Disk|where-Object  partitionstyle -eq 'RAW' 
foreach ($diski in $disks) {
      Initialize-Disk -Number $diski.Number
      Set-Disk -Number $diski.Number -IsOffline $False
      $VolumeId=$diski.SerialNumber -replace "_[^ ]*$" -replace "vol", "vol-"
      $InstanceId = (Invoke-WebRequest -Uri "http://169.254.169.254/latest/meta-data/instance-id" -UseBasicParsing).Content
      $DriveLetter = (Get-EC2Tag -Filter @{Name="resource-type";Value="volume"},@{Name="resource-id";Value=$VolumeId} | where-object Key -eq "driveletter").value
      New-Partition -DiskNumber $diski.Number -DriveLetter $DriveLetter –UseMaximumSize
      Format-Volume -DriveLetter $DriveLetter
    }
Start-Sleep -s 120
'@
$initializescript | Out-File C:\initializescript.ps1
Start-Sleep -s 30  

Then, I called the above script through AWS SSM by creating and associating it with instance . Below is the code


resource "aws_ssm_association" "initialize" {
  count = length(var.ec2_name)
  name        = var.ssm_document_name
  targets {
    key    = "InstanceIds"
    values = [element(aws_instance.ec2server[*].id, count.index)]
  }
    }


resource "aws_ssm_document" "InitializeDrives" {
  name          = "initializedriveswindows"
  document_type = "Command"

  content = <<DOC
{
  "schemaVersion": "2.2",
  "description": "Run command to initialize drives",
  "parameters": {
    "Message": {
      "type": "String",
      "description": "Run command to initialize drives",
      "default": "Run command to initialize drives"
    }
  },
  "mainSteps": [
    {
      "action": "aws:runPowerShellScript",
      "name": "powershell",
      "inputs": {
        "runCommand": [
          "C:\\initializescript.ps1",
          "Restart-Computer -Force"
        ]
      }
    }
  ]
}
DOC
} ```

I have included sleep time to avoid race issues.
Countersubject answered 6/7, 2020 at 19:18 Comment(0)
D
0

First we enforce a mounting convention that simply states for non-root volumes use the xvdDRIVE convention for your devices, where DRIVE is same as the drive letter you want to mount to

   xvdd - D:
   xvde - E:
   xvdm = M:

to support drive assignment .. including "skip level" mounting

format the volumes .. with the drive letter or some other convention you like
  we run diskpart with an input file.. but basically
      format fs=ntfs label=D quick

then we update the DriveletterConfig.xml or DriveLetterConfig.json (depending on ec2config or ec2launch)

    for xml looks like:
        <Mapping> <VolumeName>D</VolumeName> <DriveLetter>D:</DriveLetter></Mapping>
        <Mapping> <VolumeName>E</VolumeName> <DriveLetter>E:</DriveLetter> </Mapping>
        <Mapping> <VolumeName>M</VolumeName> <DriveLetter>M:</DriveLetter> </Mapping>  
    

poof

Dworman answered 29/4, 2021 at 17:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.