Automatically mount NVME volumes in AWS EC2 on Windows with Cloudformation and Powershell Userdata

Introduction


This post explains how I go about with AWS Cloudformation and Powershell userdata scripts.

  • attaching EBS volumes to Windows EC2 instances
  • identifying them in the OS
  • initializing them
  • formatting them
  • and assigning labels and drive letters.

Prerequisites

You should know something about Cloudformation and Powershell.
The AWS Image you use for your EC2 instance should be one provided by AWS and should have the following utility already deployed to C:\ProgramData\Amazon\Tools\ebsnvme-id.exe

AWS Cloudformation Step

I typically do not declare EBS Volumes together with the EC2 Instance declaration in Cloudformation but prefer to declare them as separate resources along with their own ebs attachment resources.  Here is an example EC2 instance with three additional volume declarations on top of the root volume which is automatically mounted on C:\
 WindowsServerInstance:
    Type: AWS::EC2::Instance
    Properties:
      BlockDeviceMappings:
        - DeviceName: /dev/sda1
          Ebs:
            VolumeSize: 65
            Encrypted: true
            VolumeType: gp3
      ImageId: !Ref WindowsImageId
      KeyName: !Ref WindowsKeyPairName
      InstanceType: !WindowsServerInstanceType
      CreditSpecification:
        CPUCredits: standard
      IamInstanceProfile: !Ref WindowsServerInstanceProfile
      SecurityGroupIds:
        - !Ref ManagementSecurityGroup
        - !Ref ApplicationSecurityGroup
      SubnetId: !Ref WindowsServerSubnetId
      Tags:
        - Key: Name
          Value: WindowsServerInstance
      UserData:
        Fn::Base64: |
          <powershell>
            mkdir C:\setup
            Read-S3Object -BucketName example-bucket -Key /userdata/windows_bootstrap.ps1 -File C:\setup\bootstrap.ps1
            . C:\setup\bootstrap.ps1
          </powershell>

  DataVolume:
    Type: AWS::EC2::Volume
    Properties:
      Size: 100
      VolumeType: gp3
      Encrypted: true
      AvailabilityZone: !GetAtt WindowsServerInstance.AvailabilityZone
      Tags:
        - Key: Name
          Value: DataVolume
  DataVolumeAttachment:
    Type: AWS::EC2::VolumeAttachment
    Properties:
      InstanceId: !Ref WindowsServerInstance
      VolumeId: !Ref DataVolume
      Device: /dev/sdb

  BackupVolume:
    Type: AWS::EC2::Volume
    Properties:
      Size: 100
      VolumeType: gp3
      Encrypted: true
      AvailabilityZone: !GetAtt WindowsServerInstance.AvailabilityZone
      Tags:
        - Key: Name
          Value: BackupVolume
  BackupVolumeAttachment:
    Type: AWS::EC2::VolumeAttachment
    Properties:
      InstanceId: !Ref WindowsServerInstance
      VolumeId: !Ref BackupVolume
      Device: /dev/sdc

  ScriptsVolume:
    Type: AWS::EC2::Volume
    Properties:
      Size: 5
      VolumeType: gp3
      Encrypted: true
      AvailabilityZone: !GetAtt WindowsServerInstance.AvailabilityZone
      Tags:
        - Key: Name
          Value: ScriptsVolume
  ScriptsVolumeAttachment:
    Condition: DeployB2BResources
    Type: AWS::EC2::VolumeAttachment
    Properties:
      InstanceId: !Ref WindowsServerInstance
      VolumeId: !Ref ScriptsVolume
      Device: /dev/sdd

When this template builds, a windows instance will be available with 3 additional and un-initialized volumes attached.

The userdata property is configured to pull down a powershell script from S3 as permitted by the WindowsServerInstanceProfile.  I've not bothered to explain how all that works.  I assume if you are reading this, you already know how to allow an EC2 Instance access to objects in S3 via Instance Profiles and Policies.

Userdata Script

The userdata script below, declares a function and calls the function once for each of the volume attachments.
### Function to format a disk based on the disk number, label and drive letter.
function setup_disk {
	param (
		$DiskNumber,
		$Label,
		$DriveLetter
	)
	Initialize-Disk "$DiskNumber" -PartitionStyle MBR
	New-Partition -DiskNumber "$DiskNumber" -DriveLetter "$DriveLetter" -UseMaximumSize
	Format-Volume -DriveLetter "$DriveLetter" -FileSystem NTFS -NewFileSystemLabel "$Label" -Confirm:$false
}

# Create a hashtable of disk numbers and block device mappings
$hash = $null
$hash = @{}

get-disk | where partitionstyle -eq 'raw' | where-object IsSystem -eq $False| select Number | foreach-Object {
  $num = $_.Number
  $blk = (c:\ProgramData\Amazon\Tools\ebsnvme-id.exe $num | Select-String -Pattern 'Device Name: .*$'| ConvertFrom-String -Delimiter ':' | Select P2).P2
  $blk = $blk -replace '^......'

  $hash.add($blk, $num)
}

# Set up each of the volume attachments
setup_disk $hash.sdb "Data" "D"
setup_disk $hash.sdc "Backup" "E"
setup_disk $hash.sdd "Scripts" "I"

Explanation


The get-disk command returns a list of all the disks attached to the system.   We can use the get-disk command to iterate through each attached disk that is not a system disk (exclude C:\) and then call the AWS Provided ebsnvme-id.exe tool to find out which device name it has and add it to a hashmap called $hash.  This just maps Disk Number to Volume Attachment Device Name.  Unfortunately Windows does not always enumerate disks in the same order so we have to build this map correctly.  As a general rule it is never a good idea to rely on the order of things in computers.

Seeing as though our Cloudformation VolumeAttachment resources declare these device names, we have the mapping we need to proceed.  
PS C:\setup> $hash

Name                           Value
----                           -----
sdb                            2
sdc                            1
sdd                            3


Finally, our little script, calls the setup_disk function with the value of the hash for each volume we want and the parameters for Label and Drive Letter.

Comments

Popular posts from this blog

Extending the AD Schema on Samba4 - Part 2

Python + inotify = Pyinotify [ how to watch folders for file activity ]