From 129f036fae66733c02d0bdd7e3c2da98187e0895 Mon Sep 17 00:00:00 2001 From: Spike Curtis Date: Fri, 5 May 2023 16:57:14 +0400 Subject: [PATCH 1/6] WIP Azure template for windows machine Signed-off-by: Spike Curtis --- .../azure-windows/FirstLogonCommands.xml | 12 ++ examples/templates/azure-windows/main.tf | 190 ++++++++++++++++++ 2 files changed, 202 insertions(+) create mode 100644 examples/templates/azure-windows/FirstLogonCommands.xml create mode 100644 examples/templates/azure-windows/main.tf diff --git a/examples/templates/azure-windows/FirstLogonCommands.xml b/examples/templates/azure-windows/FirstLogonCommands.xml new file mode 100644 index 0000000000000..6b2777b88461c --- /dev/null +++ b/examples/templates/azure-windows/FirstLogonCommands.xml @@ -0,0 +1,12 @@ + + + cmd /c "copy C:\AzureData\CustomData.bin C:\AzureData\CoderAgent.ps1" + Move the CustomData file to the working directory + 10 + + + powershell.exe -sta -ExecutionPolicy Unrestricted -Command "C:\AzureData\CoderAgent.ps1 | Out-File C:\AzureData\CoderAgent.log + Execute CustomData as powershell. + 20 + + diff --git a/examples/templates/azure-windows/main.tf b/examples/templates/azure-windows/main.tf new file mode 100644 index 0000000000000..9cb7092dae5d3 --- /dev/null +++ b/examples/templates/azure-windows/main.tf @@ -0,0 +1,190 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + version = "0.7.0" + } + azurerm = { + source = "hashicorp/azurerm" + version = "=3.52.0" + } + } +} + +provider "azurerm" { + features {} +} + +provider "coder" { +} + +data "coder_workspace" "me" {} + +data "coder_parameter" "location" { + description = "What location should your workspace live in?" + display_name = "Location" + name = "location" + default = "eastus" + mutable = false + option { + value = "eastus" + name = "East US" + } + option { + value = "centralus" + name = "Central US" + } + option { + value = "southcentralus" + name = "South Central US" + } + option { + value = "westus2" + name = "West US 2" + } +} +resource "coder_agent" "main" { + arch = "amd64" + auth = "azure-instance-identity" + os = "windows" + startup_script = < Date: Mon, 8 May 2023 16:20:30 +0400 Subject: [PATCH 2/6] WIP windows uses data disk Signed-off-by: Spike Curtis --- .../azure-windows/FirstLogonCommands.xml | 16 +++- examples/templates/azure-windows/main.tf | 82 ++++++++++++++++--- 2 files changed, 84 insertions(+), 14 deletions(-) diff --git a/examples/templates/azure-windows/FirstLogonCommands.xml b/examples/templates/azure-windows/FirstLogonCommands.xml index 6b2777b88461c..d8c5849e3c44c 100644 --- a/examples/templates/azure-windows/FirstLogonCommands.xml +++ b/examples/templates/azure-windows/FirstLogonCommands.xml @@ -1,12 +1,22 @@ + + powershell.exe -sta -ExecutionPolicy Unrestricted -Command "Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) | Out-File C:\AzureData\chocolatey.log" + Install Chocolatey Package Manager + 1 + + + powershell.exe -sta -ExecutionPolicy Unrestricted -Command "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12" + Set SecurityProtocol to TLS1.2 + 2 + cmd /c "copy C:\AzureData\CustomData.bin C:\AzureData\CoderAgent.ps1" Move the CustomData file to the working directory - 10 + 3 - powershell.exe -sta -ExecutionPolicy Unrestricted -Command "C:\AzureData\CoderAgent.ps1 | Out-File C:\AzureData\CoderAgent.log + powershell.exe -sta -ExecutionPolicy Unrestricted -Command "C:\AzureData\CoderAgent.ps1 | Out-File C:\AzureData\CoderAgent.log" Execute CustomData as powershell. - 20 + 4 diff --git a/examples/templates/azure-windows/main.tf b/examples/templates/azure-windows/main.tf index 9cb7092dae5d3..b49ef7532dbbd 100644 --- a/examples/templates/azure-windows/main.tf +++ b/examples/templates/azure-windows/main.tf @@ -44,13 +44,57 @@ data "coder_parameter" "location" { } } resource "coder_agent" "main" { - arch = "amd64" - auth = "azure-instance-identity" - os = "windows" + arch = "amd64" + auth = "azure-instance-identity" + os = "windows" + dir = "F:\\Users\\coder" + login_before_ready = false startup_script = < Date: Tue, 9 May 2023 14:44:17 +0400 Subject: [PATCH 3/6] Data drive working Signed-off-by: Spike Curtis --- .../azure-windows/FirstLogonCommands.xml | 18 +--- .../azure-windows/Initialize.ps1.tftpl | 85 +++++++++++++++++++ examples/templates/azure-windows/main.tf | 81 ++---------------- 3 files changed, 96 insertions(+), 88 deletions(-) create mode 100644 examples/templates/azure-windows/Initialize.ps1.tftpl diff --git a/examples/templates/azure-windows/FirstLogonCommands.xml b/examples/templates/azure-windows/FirstLogonCommands.xml index d8c5849e3c44c..ac4a9d80796c0 100644 --- a/examples/templates/azure-windows/FirstLogonCommands.xml +++ b/examples/templates/azure-windows/FirstLogonCommands.xml @@ -1,22 +1,12 @@ - powershell.exe -sta -ExecutionPolicy Unrestricted -Command "Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) | Out-File C:\AzureData\chocolatey.log" - Install Chocolatey Package Manager - 1 - - - powershell.exe -sta -ExecutionPolicy Unrestricted -Command "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12" - Set SecurityProtocol to TLS1.2 - 2 - - - cmd /c "copy C:\AzureData\CustomData.bin C:\AzureData\CoderAgent.ps1" - Move the CustomData file to the working directory + cmd /c "copy C:\AzureData\CustomData.bin C:\AzureData\Initialize.ps1" + Copy Initialize.ps1 to file from CustomData 3 - powershell.exe -sta -ExecutionPolicy Unrestricted -Command "C:\AzureData\CoderAgent.ps1 | Out-File C:\AzureData\CoderAgent.log" - Execute CustomData as powershell. + powershell.exe -sta -ExecutionPolicy Unrestricted -Command "C:\AzureData\Initialize.ps1 *> C:\AzureData\Initialize.log" + Execute Initialize.ps1 script 4 diff --git a/examples/templates/azure-windows/Initialize.ps1.tftpl b/examples/templates/azure-windows/Initialize.ps1.tftpl new file mode 100644 index 0000000000000..d9d928703f104 --- /dev/null +++ b/examples/templates/azure-windows/Initialize.ps1.tftpl @@ -0,0 +1,85 @@ +# This script gets run once when the VM is first created. + +# Initialize the data disk & home directory. +$disk = Get-Disk -Number 2 +if ($disk.PartitionStyle -Eq 'RAW') +{ + "Initializing data disk" + $disk | Initialize-Disk +} else { + "data disk already initialized" +} + +$partitions = Get-Partition -DiskNumber $disk.Number | Where-Object Type -Ne 'Reserved' +if ($partitions.Count -Eq 0) { + "Creating partition on data disk" + $partition = New-Partition -DiskNumber $disk.Number -UseMaximumSize +} else { + $partition = $partitions[0] + $s = "data disk already has partition of size {0:n1} GiB" -f ($partition.Size / 1073741824) + Write-Output $s +} + +$volume = Get-Volume -Partition $partition +if ($volume.FileSystemType -Eq 'Unknown') +{ + "Formatting data disk" + Format-Volume -InputObject $volume -FileSystem NTFS -Confirm:$false +} else { + "data disk is already formatted" +} + +# Above operations are careful not to repartition & overwrite data since the disk +# might come from a previous build, but this should only be run once per VM and is not idempotent +#$h = "C:\Users\coder" +# We can only mount to empty directory, so if there is data in the home dir, move it +#if (Test-Path -Path $home) { +# "Home directory exists" +# Rename-Item -Path $h -NewName "coderold" +#} +# Create the directory and mount the partition +#New-Item -ItemType "directory" -Path $h +#Add-PartitionAccessPath -InputObject $partition -AccessPath $h + +# Mount the partition +Add-PartitionAccessPath -InputObject $partition -AccessPath "F:" + +# Enable RDP +Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server' -name "fDenyTSConnections" -value 0 +# Enable RDP through Windows Firewall +Enable-NetFirewallRule -DisplayGroup "Remote Desktop" +# Disable Network Level Authentication (NLA) +# Clients will connect via Coder's tunnel +(Get-WmiObject -class "Win32_TSGeneralSetting" -Namespace root\cimv2\terminalservices -ComputerName $env:COMPUTERNAME -Filter "TerminalName='RDP-tcp'").SetUserAuthenticationRequired(0) + +# Install Chocolatey package manager +Set-ExecutionPolicy Bypass -Scope Process -Force +[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072 +iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) +# Reload path so sessions include "choco" and "refreshenv" +$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User") + +# Install Git and reload path +choco install -y git +$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User") + +# Set protocol to TLS1.2 for agent download +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +# Set Coder Agent to run immediately, and on each restart +$init_script = @' +${init_script} +'@ +Out-File -FilePath "C:\AzureData\CoderAgent.ps1" -InputObject $init_script +$task = @{ + TaskName = 'CoderAgent' + Action = (New-ScheduledTaskAction -Execute 'powershell.exe' -Argument '-sta -ExecutionPolicy Unrestricted -Command "C:\AzureData\CoderAgent.ps1 *>> C:\AzureData\CoderAgent.log"') + Trigger = (New-ScheduledTaskTrigger -AtStartup), (New-ScheduledTaskTrigger -Once -At (Get-Date).AddSeconds(15)) + Settings = (New-ScheduledTaskSettingsSet -DontStopOnIdleEnd -ExecutionTimeLimit ([TimeSpan]::FromDays(3650)) -Compatibility Win8) + Principal = (New-ScheduledTaskPrincipal -UserId 'vm\coder' -RunLevel Highest -LogonType S4U) +} +Register-ScheduledTask @task -Force + +# Additional Chocolatey package installs (optional, uncomment to enable) +# choco feature enable -n=allowGlobalConfirmation +# choco install visualstudio2022community --package-parameters "--add=Microsoft.VisualStudio.Workload.ManagedDesktop;includeRecommended --passive --locale en-US" diff --git a/examples/templates/azure-windows/main.tf b/examples/templates/azure-windows/main.tf index b49ef7532dbbd..2637e3fb43359 100644 --- a/examples/templates/azure-windows/main.tf +++ b/examples/templates/azure-windows/main.tf @@ -43,69 +43,15 @@ data "coder_parameter" "location" { name = "West US 2" } } + resource "coder_agent" "main" { arch = "amd64" auth = "azure-instance-identity" os = "windows" - dir = "F:\\Users\\coder" + #dir = "F:\\Users\\coder" login_before_ready = false - startup_script = < Date: Tue, 9 May 2023 15:51:01 +0400 Subject: [PATCH 4/6] Add az cli commands to start & stop Signed-off-by: Spike Curtis --- .../azure-windows/Initialize.ps1.tftpl | 12 --- examples/templates/azure-windows/README.md | 23 +++++ examples/templates/azure-windows/main.tf | 89 ++++++++++++++----- 3 files changed, 92 insertions(+), 32 deletions(-) create mode 100644 examples/templates/azure-windows/README.md diff --git a/examples/templates/azure-windows/Initialize.ps1.tftpl b/examples/templates/azure-windows/Initialize.ps1.tftpl index d9d928703f104..5a195f589ec66 100644 --- a/examples/templates/azure-windows/Initialize.ps1.tftpl +++ b/examples/templates/azure-windows/Initialize.ps1.tftpl @@ -29,18 +29,6 @@ if ($volume.FileSystemType -Eq 'Unknown') "data disk is already formatted" } -# Above operations are careful not to repartition & overwrite data since the disk -# might come from a previous build, but this should only be run once per VM and is not idempotent -#$h = "C:\Users\coder" -# We can only mount to empty directory, so if there is data in the home dir, move it -#if (Test-Path -Path $home) { -# "Home directory exists" -# Rename-Item -Path $h -NewName "coderold" -#} -# Create the directory and mount the partition -#New-Item -ItemType "directory" -Path $h -#Add-PartitionAccessPath -InputObject $partition -AccessPath $h - # Mount the partition Add-PartitionAccessPath -InputObject $partition -AccessPath "F:" diff --git a/examples/templates/azure-windows/README.md b/examples/templates/azure-windows/README.md new file mode 100644 index 0000000000000..b9b5d6f5313ef --- /dev/null +++ b/examples/templates/azure-windows/README.md @@ -0,0 +1,23 @@ +--- +name: Develop in Windows on Azure +description: Get started with Windows development on Microsoft Azure. +tags: [cloud, azure, windows] +icon: /icon/azure.png +--- + +# azure-windows + +To get started, run `coder templates init`. When prompted, select this template. +Follow the on-screen instructions to proceed. + +## Authentication + +This template assumes that coderd is run in an environment that is authenticated +with Azure. For example, run `az login` then `az account set --subscription=` +to import credentials on the system and user running coderd. For other ways to +authenticate [consult the Terraform docs](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs#authenticating-to-azure). + +## Dependencies + +This template depends on the Azure CLI tool (`az`) to start and stop the Windows VM. Ensure this +tool is installed and available in the path on the machine that runs coderd. diff --git a/examples/templates/azure-windows/main.tf b/examples/templates/azure-windows/main.tf index 2637e3fb43359..ddf62ae820fa8 100644 --- a/examples/templates/azure-windows/main.tf +++ b/examples/templates/azure-windows/main.tf @@ -44,6 +44,19 @@ data "coder_parameter" "location" { } } +data "coder_parameter" "data_disk_size" { + description = "Size of your data (F:) drive in GB" + display_name = "Data disk size" + name = "data_disk_size" + default = 20 + mutable = "false" + type = "number" + validation { + min = 5 + max = 5000 + } +} + resource "coder_agent" "main" { arch = "amd64" auth = "azure-instance-identity" @@ -52,18 +65,22 @@ resource "coder_agent" "main" { login_before_ready = false } +resource "random_password" "admin_password" { + length = 16 + special = true + # https://docs.microsoft.com/en-us/windows/security/threat-protection/security-policy-settings/password-must-meet-complexity-requirements#reference + # we remove characters that require special handling in XML, as this is how we pass it to the VM + # namely: <>&'" + override_special = "~!@#$%^*_-+=`|\\(){}[]:;,.?/" +} + locals { - prefix = "spike" + prefix = "coder-win" admin_username = "coder" - # Password to log in via RDP - # - # Must meet Windows password complexity requirements: - # https://docs.microsoft.com/en-us/windows/security/threat-protection/security-policy-settings/password-must-meet-complexity-requirements#reference - admin_password = "coderRDP!" } resource "azurerm_resource_group" "main" { - name = "${local.prefix}-${data.coder_workspace.me.name}-resources" + name = "${local.prefix}-${data.coder_workspace.me.id}" location = data.coder_parameter.location.value tags = { Coder_Provisioned = "true" @@ -71,15 +88,15 @@ resource "azurerm_resource_group" "main" { } // Uncomment here and in the azurerm_network_interface resource to obtain a public IP -resource "azurerm_public_ip" "main" { - name = "publicip" - resource_group_name = azurerm_resource_group.main.name - location = azurerm_resource_group.main.location - allocation_method = "Static" - tags = { - Coder_Provisioned = "true" - } -} +#resource "azurerm_public_ip" "main" { +# name = "publicip" +# resource_group_name = azurerm_resource_group.main.name +# location = azurerm_resource_group.main.location +# allocation_method = "Static" +# tags = { +# Coder_Provisioned = "true" +# } +#} resource "azurerm_virtual_network" "main" { name = "network" address_space = ["10.0.0.0/24"] @@ -104,7 +121,7 @@ resource "azurerm_network_interface" "main" { subnet_id = azurerm_subnet.internal.id private_ip_address_allocation = "Dynamic" // Uncomment for public IP address as well as azurerm_public_ip resource above - public_ip_address_id = azurerm_public_ip.main.id +# public_ip_address_id = azurerm_public_ip.main.id } tags = { Coder_Provisioned = "true" @@ -133,14 +150,14 @@ resource "azurerm_managed_disk" "data" { resource_group_name = azurerm_resource_group.main.name storage_account_type = "Standard_LRS" create_option = "Empty" - disk_size_gb = 20 + disk_size_gb = data.coder_parameter.data_disk_size.value } # Create virtual machine resource "azurerm_windows_virtual_machine" "main" { name = "vm" admin_username = local.admin_username - admin_password = local.admin_password + admin_password = random_password.admin_password.result location = azurerm_resource_group.main.location resource_group_name = azurerm_resource_group.main.name network_interface_ids = [azurerm_network_interface.main.id] @@ -160,7 +177,7 @@ resource "azurerm_windows_virtual_machine" "main" { version = "latest" } additional_unattend_content { - content = "${local.admin_password}true1${local.admin_username}" + content = "${random_password.admin_password.result}true1${local.admin_username}" setting = "AutoLogon" } additional_unattend_content { @@ -175,9 +192,41 @@ resource "azurerm_windows_virtual_machine" "main" { } } +resource "coder_metadata" "rdp_login" { + resource_id = azurerm_windows_virtual_machine.main.id + item { + key = "Username" + value = local.admin_username + } + item { + key = "Password" + value = random_password.admin_password.result + sensitive = true + } +} + resource "azurerm_virtual_machine_data_disk_attachment" "main_data" { managed_disk_id = azurerm_managed_disk.data.id virtual_machine_id = azurerm_windows_virtual_machine.main.id lun = "10" caching = "ReadWrite" } + +# Stop the VM +resource "null_resource" "stop_vm" { + count = data.coder_workspace.me.transition == "stop" ? 1 : 0 + depends_on = [azurerm_windows_virtual_machine.main] + provisioner "local-exec" { + # Use deallocate so the VM is not charged + command = "az vm deallocate --ids ${azurerm_windows_virtual_machine.main.id}" + } +} + +# Start the VM +resource "null_resource" "start" { + count = data.coder_workspace.me.transition == "start" ? 1 : 0 + depends_on = [azurerm_windows_virtual_machine.main] + provisioner "local-exec" { + command = "az vm start --ids ${azurerm_windows_virtual_machine.main.id}" + } +} From 70364bc30601409315eed964c4efdce8ad473999 Mon Sep 17 00:00:00 2001 From: Spike Curtis Date: Tue, 9 May 2023 17:49:01 +0400 Subject: [PATCH 5/6] Remove commented line Signed-off-by: Spike Curtis --- examples/templates/azure-windows/main.tf | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/templates/azure-windows/main.tf b/examples/templates/azure-windows/main.tf index ddf62ae820fa8..681a25e05fe0d 100644 --- a/examples/templates/azure-windows/main.tf +++ b/examples/templates/azure-windows/main.tf @@ -61,7 +61,6 @@ resource "coder_agent" "main" { arch = "amd64" auth = "azure-instance-identity" os = "windows" - #dir = "F:\\Users\\coder" login_before_ready = false } From 9043f049c69b5f820d01d4b35eed18aa6962e1a5 Mon Sep 17 00:00:00 2001 From: Spike Curtis Date: Wed, 10 May 2023 05:46:00 +0000 Subject: [PATCH 6/6] Prettierify Signed-off-by: Spike Curtis --- examples/templates/azure-windows/README.md | 2 +- examples/templates/azure-windows/main.tf | 28 +++++++++++----------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/examples/templates/azure-windows/README.md b/examples/templates/azure-windows/README.md index b9b5d6f5313ef..768e5f5e9ed79 100644 --- a/examples/templates/azure-windows/README.md +++ b/examples/templates/azure-windows/README.md @@ -19,5 +19,5 @@ authenticate [consult the Terraform docs](https://registry.terraform.io/provider ## Dependencies -This template depends on the Azure CLI tool (`az`) to start and stop the Windows VM. Ensure this +This template depends on the Azure CLI tool (`az`) to start and stop the Windows VM. Ensure this tool is installed and available in the path on the machine that runs coderd. diff --git a/examples/templates/azure-windows/main.tf b/examples/templates/azure-windows/main.tf index 681a25e05fe0d..6bfe25e78e2c0 100644 --- a/examples/templates/azure-windows/main.tf +++ b/examples/templates/azure-windows/main.tf @@ -45,12 +45,12 @@ data "coder_parameter" "location" { } data "coder_parameter" "data_disk_size" { - description = "Size of your data (F:) drive in GB" + description = "Size of your data (F:) drive in GB" display_name = "Data disk size" - name = "data_disk_size" - default = 20 - mutable = "false" - type = "number" + name = "data_disk_size" + default = 20 + mutable = "false" + type = "number" validation { min = 5 max = 5000 @@ -65,8 +65,8 @@ resource "coder_agent" "main" { } resource "random_password" "admin_password" { - length = 16 - special = true + length = 16 + special = true # https://docs.microsoft.com/en-us/windows/security/threat-protection/security-policy-settings/password-must-meet-complexity-requirements#reference # we remove characters that require special handling in XML, as this is how we pass it to the VM # namely: <>&'" @@ -74,7 +74,7 @@ resource "random_password" "admin_password" { } locals { - prefix = "coder-win" + prefix = "coder-win" admin_username = "coder" } @@ -120,7 +120,7 @@ resource "azurerm_network_interface" "main" { subnet_id = azurerm_subnet.internal.id private_ip_address_allocation = "Dynamic" // Uncomment for public IP address as well as azurerm_public_ip resource above -# public_ip_address_id = azurerm_public_ip.main.id + # public_ip_address_id = azurerm_public_ip.main.id } tags = { Coder_Provisioned = "true" @@ -161,8 +161,8 @@ resource "azurerm_windows_virtual_machine" "main" { resource_group_name = azurerm_resource_group.main.name network_interface_ids = [azurerm_network_interface.main.id] size = "Standard_DS1_v2" - custom_data = base64encode( - templatefile("${path.module}/Initialize.ps1.tftpl", {init_script = coder_agent.main.init_script}) + custom_data = base64encode( + templatefile("${path.module}/Initialize.ps1.tftpl", { init_script = coder_agent.main.init_script }) ) os_disk { name = "myOsDisk" @@ -194,12 +194,12 @@ resource "azurerm_windows_virtual_machine" "main" { resource "coder_metadata" "rdp_login" { resource_id = azurerm_windows_virtual_machine.main.id item { - key = "Username" + key = "Username" value = local.admin_username } item { - key = "Password" - value = random_password.admin_password.result + key = "Password" + value = random_password.admin_password.result sensitive = true } }