From d35a458767861b22cb83c3a109c2fd487f71d298 Mon Sep 17 00:00:00 2001 From: Spike Curtis Date: Wed, 10 May 2023 09:56:19 +0400 Subject: [PATCH] feat: Windows on Azure example template (#7469) * WIP Azure template for windows machine Signed-off-by: Spike Curtis * WIP windows uses data disk Signed-off-by: Spike Curtis * Data drive working Signed-off-by: Spike Curtis * Add az cli commands to start & stop Signed-off-by: Spike Curtis * Remove commented line Signed-off-by: Spike Curtis * Prettierify Signed-off-by: Spike Curtis --------- Signed-off-by: Spike Curtis --- .../azure-windows/FirstLogonCommands.xml | 12 + .../azure-windows/Initialize.ps1.tftpl | 73 ++++++ examples/templates/azure-windows/README.md | 23 ++ examples/templates/azure-windows/main.tf | 231 ++++++++++++++++++ 4 files changed, 339 insertions(+) create mode 100644 examples/templates/azure-windows/FirstLogonCommands.xml create mode 100644 examples/templates/azure-windows/Initialize.ps1.tftpl create mode 100644 examples/templates/azure-windows/README.md 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 0000000000..ac4a9d8079 --- /dev/null +++ b/examples/templates/azure-windows/FirstLogonCommands.xml @@ -0,0 +1,12 @@ + + + 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\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 0000000000..5a195f589e --- /dev/null +++ b/examples/templates/azure-windows/Initialize.ps1.tftpl @@ -0,0 +1,73 @@ +# 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" +} + +# 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/README.md b/examples/templates/azure-windows/README.md new file mode 100644 index 0000000000..768e5f5e9e --- /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 new file mode 100644 index 0000000000..6bfe25e78e --- /dev/null +++ b/examples/templates/azure-windows/main.tf @@ -0,0 +1,231 @@ +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" + } +} + +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" + os = "windows" + 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 = "coder-win" + admin_username = "coder" +} + +resource "azurerm_resource_group" "main" { + name = "${local.prefix}-${data.coder_workspace.me.id}" + location = data.coder_parameter.location.value + tags = { + Coder_Provisioned = "true" + } +} + +// 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_virtual_network" "main" { + name = "network" + address_space = ["10.0.0.0/24"] + location = azurerm_resource_group.main.location + resource_group_name = azurerm_resource_group.main.name + tags = { + Coder_Provisioned = "true" + } +} +resource "azurerm_subnet" "internal" { + name = "internal" + resource_group_name = azurerm_resource_group.main.name + virtual_network_name = azurerm_virtual_network.main.name + address_prefixes = ["10.0.0.0/29"] +} +resource "azurerm_network_interface" "main" { + name = "nic" + resource_group_name = azurerm_resource_group.main.name + location = azurerm_resource_group.main.location + ip_configuration { + name = "internal" + 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 + } + tags = { + Coder_Provisioned = "true" + } +} +# Create storage account for boot diagnostics +resource "azurerm_storage_account" "my_storage_account" { + name = "diag${random_id.storage_id.hex}" + location = azurerm_resource_group.main.location + resource_group_name = azurerm_resource_group.main.name + account_tier = "Standard" + account_replication_type = "LRS" +} +# Generate random text for a unique storage account name +resource "random_id" "storage_id" { + keepers = { + # Generate a new ID only when a new resource group is defined + resource_group = azurerm_resource_group.main.name + } + byte_length = 8 +} + +resource "azurerm_managed_disk" "data" { + name = "data_disk" + location = azurerm_resource_group.main.location + resource_group_name = azurerm_resource_group.main.name + storage_account_type = "Standard_LRS" + create_option = "Empty" + 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 = 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] + size = "Standard_DS1_v2" + custom_data = base64encode( + templatefile("${path.module}/Initialize.ps1.tftpl", { init_script = coder_agent.main.init_script }) + ) + os_disk { + name = "myOsDisk" + caching = "ReadWrite" + storage_account_type = "Premium_LRS" + } + source_image_reference { + publisher = "MicrosoftWindowsServer" + offer = "WindowsServer" + sku = "2022-datacenter-azure-edition" + version = "latest" + } + additional_unattend_content { + content = "${random_password.admin_password.result}true1${local.admin_username}" + setting = "AutoLogon" + } + additional_unattend_content { + content = file("${path.module}/FirstLogonCommands.xml") + setting = "FirstLogonCommands" + } + boot_diagnostics { + storage_account_uri = azurerm_storage_account.my_storage_account.primary_blob_endpoint + } + tags = { + Coder_Provisioned = "true" + } +} + +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}" + } +}