cloud/aws/deploy.ps1 (257 lines of code) (raw):

Param ( [parameter(HelpMessage = "Remove existing resources created by a previous run")] [switch] $Clean, [parameter(HelpMessage = "The AWS region in which to deploy resources")] $Region = 'us-east-1', [parameter(HelpMessage = "The name to use for the custom worker node AMI")] $AmiName = 'eks-worker-node', [parameter(HelpMessage = "The name to use for the EKS cluster")] $ClusterName = 'demo-cluster' ) # Halt execution if we encounter an error $ErrorActionPreference = 'Stop' # Replaces the placeholders in a template file with values and writes the output to a new file function FillTemplate { Param ( $Template, $Rendered, $Values ) $filled = Get-Content -Path $Template -Raw $Values.GetEnumerator() | ForEach-Object { $filled = $filled.Replace($_.Key, $_.Value) } Set-Content -Path $Rendered -Value $filled -NoNewline } # Represents the output of a native process class ProcessOutput { ProcessOutput([string] $stdout, [string] $stderr) { $this.StandardOutput = $stdout $this.StandardError = $stderr } [string] $StandardOutput [string] $StandardError } # Helper functions for executing native commands class ExecutionHelpers { # Escapes command-line arguments for passing to a native command static [string] EscapeArguments([string[]] $arguments) { $escaped = @() foreach ($arg in $arguments) { if ($arg.Contains(' ')) { $escaped += @("`"$arg`"") } else { $escaped += @($arg) } } return $escaped -join ' ' } # Executes a command and throws an error if it returns a non-zero exit code static [ProcessOutput] RunCommand([string] $command, [string[]] $arguments, [bool] $captureStdOut, [bool] $captureStdErr) { # Log the command $escapedArgs = [ExecutionHelpers]::EscapeArguments($arguments) $formatted = "[$command $escapedArgs]" Write-Host "$formatted" -ForegroundColor DarkYellow # Execute the command and wait for it to complete, retrieving the exit code, stdout and stderr $info = New-Object System.Diagnostics.ProcessStartInfo $info.FileName = $command $info.Arguments = $escapedArgs $info.RedirectStandardError = $captureStdErr $info.RedirectStandardOutput = $captureStdOut $info.UseShellExecute = $false $info.WorkingDirectory = (Get-Location).ToString() $process = New-Object System.Diagnostics.Process $process.StartInfo = $info $process.Start() $process.WaitForExit() $exitCode = $process.ExitCode $stdout = if ($captureStdOut) { $process.StandardOutput.ReadToEnd() } else { '' } $stderr = if ($captureStdErr) { $process.StandardError.ReadToEnd() } else { '' } # If the command terminated with a non-zero exit code then throw an error if ($exitCode -ne 0) { throw "Command $formatted terminated with exit code $exitCode, stdout $stdout and stderr $stderr" } # Return the output return [ProcessOutput]::new($stdout, $stderr) } # Do not capture stdout and stderr of child processes unless the caller explicitly requests it static [void] RunCommand([string] $command, [string[]] $arguments) { [ExecutionHelpers]::RunCommand($command, $arguments, $false, $false) } # Tests whether the specified command exists, by attempting to execute it with the supplied arguments static [bool] CommandExists([string] $command, [string[]] $testArguments) { try { [ExecutionHelpers]::RunCommand($command, $testArguments, $true, $true) return $true } catch { return $false } } } # Represents the Packer manifest data for our EKS worker node AMI class PackerManifest { PackerManifest([string] $path) { $this.ManifestPath = $path } [bool] Exists() { return (Test-Path -Path $this.ManifestPath) } [void] Parse() { # Parse the Packer manifest JSON and validate the AMI details $manifestDetails = Get-Content -Path $this.ManifestPath -Raw | ConvertFrom-Json $amiDetails = ($manifestDetails.builds[0].artifact_id -split ':') if ($amiDetails.Length -lt 2) { throw "Malformed 'artifact_id' field in Packer build manifest: '$amiDetails'" } # Extract the region and AMI ID $this.AmiRegion = $amiDetails[0] $this.AmiID = $amiDetails[1] # If the manifest data doesn't contain the snapshot ID for the AMI then populate it $this.SnapshotID = $manifestDetails.builds[0].custom_data.snapshot_id if ($this.SnapshotID.Length -lt 1) { # Attempt to retrieve the snapshot ID from the AWS API Write-Host 'Retrieving the snapshot ID for the AMI...' -ForegroundColor Green $queryOutput = [ExecutionHelpers]::RunCommand('aws', @('ec2', 'describe-images', "--region=$($this.AmiRegion)", "--image-ids=$($this.AmiID)"), $true, $true) $snapshotDetails = $queryOutput.StandardOutput | ConvertFrom-Json $this.SnapshotID = $snapshotDetails.Images[0].BlockDeviceMappings[0].Ebs.SnapshotId if ($amiDetails.Length -lt 1) { throw "Failed to retrieve snapshot ID for AMI: '$this.AmiID'" } # Inject the snapshot ID into the manifest data $manifestDetails.builds[0].custom_data.snapshot_id = $this.SnapshotID # Write the updated manifest data back to the JSON file $manifestJson = ConvertTo-Json $manifestDetails -Depth 32 Set-Content -Path $this.ManifestPath -Value $manifestJson -NoNewline } } [void] Delete() { # De-register the AMI [ExecutionHelpers]::RunCommand('aws', @('ec2', 'deregister-image', "--region=$($this.AmiRegion)", "--image-id=$($this.AmiID)")) # Remove the snapshot [ExecutionHelpers]::RunCommand('aws', @('ec2', 'delete-snapshot', "--region=$($this.AmiRegion)", "--snapshot-id=$($this.SnapshotID)")) # Delete the manifest JSON file Remove-Item -Force $this.ManifestPath } [string] $ManifestPath [string] $AmiID [string] $AmiRegion [string] $SnapshotID } # Represents an EKS cluster managed by eksctl class EksCluster { EksCluster([string] $name) { $this.Name = $name } [bool] Exists() { try { [ExecutionHelpers]::RunCommand('eksctl', @('get', 'cluster', "--name=$($this.Name)", "--region=$($global:Region)"), $true, $true) return $true } catch { return $false } } [void] Create([string] $yamlFile) { [ExecutionHelpers]::RunCommand('eksctl', @('create', 'cluster', '-f', $yamlFile.Replace('\', '/'))) } [void] Delete() { [ExecutionHelpers]::RunCommand('eksctl', @('delete', 'cluster', "--name=$($this.Name)", "--region=$($global:Region)")) } [string] $Name } # Verify that all of the native commands we require are available $requiredCommands = @{ 'the AWS CLI' = [ExecutionHelpers]::CommandExists('aws', @('help')); 'eksctl' = [ExecutionHelpers]::CommandExists('eksctl', @('version')); 'kubectl' = [ExecutionHelpers]::CommandExists('kubectl', @('help')); 'HashiCorp Packer' = [ExecutionHelpers]::CommandExists('packer', @('version')) } foreach ($command in $requiredCommands.GetEnumerator()) { if ($command.Value -eq $false) { throw "Error: $($command.Name) must be installed to run this script!" } } # Resolve the path to the Packer manifest file and create a helper object to represent the manifest data $packerDir = "$PSScriptRoot\node" $packerManifest = [PackerManifest]::new("$packerDir\manifest.json") # Create a helper object to represent our test EKS cluster $eksCluster = [EksCluster]::new($global:ClusterName) # Determine whether we are removing existing resources created by a previous run if ($Clean) { # Remove the EKS cluster if it exists if ($eksCluster.Exists()) { Write-Host 'Removing existing EKS cluster...' -ForegroundColor Green $eksCluster.Delete() } # Delete the AMI and its accompanying snapshot if they exist if ($packerManifest.Exists()) { Write-Host 'Removing AMI and its accompanying snapshot...' -ForegroundColor Green $packerManifest.Parse() $packerManifest.Delete() } Exit } # Build the custom worker node AMI if it doesn't already exist if ($packerManifest.Exists() -eq $false) { # Populate the Packer template $packerfile = "$packerDir\eks-worker-node.pkr.hcl" FillTemplate ` -Template "$packerDir\eks-worker-node.pkr.hcl.template" ` -Rendered $packerfile ` -Values @{ '__AWS_REGION__' = $global:Region; '__AMI_NAME__' = $global:AmiName } # Build the AMI Write-Host 'Building the EKS custom worker node AMI...' -ForegroundColor Green Push-Location "$packerDir" [ExecutionHelpers]::RunCommand('packer', @('init', 'eks-worker-node.pkr.hcl')) [ExecutionHelpers]::RunCommand('packer', @('build', 'eks-worker-node.pkr.hcl')) Pop-Location } # Parse the Packer manifest JSON and validate the AMI details $packerManifest.Parse() # Populate the cluster template YAML with the values for the AMI $clusterDir = "$PSScriptRoot\cluster" $configFile = "$clusterDir\test-cluster.yml" FillTemplate ` -Template "$clusterDir\test-cluster.template" ` -Rendered $configFile ` -Values @{ '__CLUSTER_NAME__' = $global:ClusterName; '__AWS_REGION__' = $packerManifest.AmiRegion; '__AMI_ID__' = $packerManifest.AmiID } # Deploy the test EKS cluster if it doesn't already exist if ($eksCluster.Exists() -eq $false) { Write-Host 'Deploying a test EKS cluster with a Windows worker node group using the custom AMI...' -ForegroundColor Green $eksCluster.Create($configFile) } # Deploy the device plugin DaemonSets to the test cluster Write-Host 'Deploying the DirectX device plugin DaemonSets to the test EKS cluster...' -ForegroundColor Green $deploymentsYaml = "$PSScriptRoot\..\..\deployments\default-daemonsets.yml" [ExecutionHelpers]::RunCommand('kubectl', @('apply', '-f', $deploymentsYaml.Replace('\', '/')))