Skip to content

Commit 4010c1c

Browse files
spikecurtispull[bot]
authored andcommitted
feat: Windows on Azure example template (coder#7469)
* WIP Azure template for windows machine Signed-off-by: Spike Curtis <spike@coder.com> * WIP windows uses data disk Signed-off-by: Spike Curtis <spike@coder.com> * Data drive working Signed-off-by: Spike Curtis <spike@coder.com> * Add az cli commands to start & stop Signed-off-by: Spike Curtis <spike@coder.com> * Remove commented line Signed-off-by: Spike Curtis <spike@coder.com> * Prettierify Signed-off-by: Spike Curtis <spike@coder.com> --------- Signed-off-by: Spike Curtis <spike@coder.com>
1 parent 108a8f1 commit 4010c1c

File tree

4 files changed

+339
-0
lines changed

4 files changed

+339
-0
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<FirstLogonCommands>
2+
<SynchronousCommand>
3+
<CommandLine>cmd /c "copy C:\AzureData\CustomData.bin C:\AzureData\Initialize.ps1"</CommandLine>
4+
<Description>Copy Initialize.ps1 to file from CustomData</Description>
5+
<Order>3</Order>
6+
</SynchronousCommand>
7+
<SynchronousCommand>
8+
<CommandLine>powershell.exe -sta -ExecutionPolicy Unrestricted -Command "C:\AzureData\Initialize.ps1 *> C:\AzureData\Initialize.log"</CommandLine>
9+
<Description>Execute Initialize.ps1 script</Description>
10+
<Order>4</Order>
11+
</SynchronousCommand>
12+
</FirstLogonCommands>
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# This script gets run once when the VM is first created.
2+
3+
# Initialize the data disk & home directory.
4+
$disk = Get-Disk -Number 2
5+
if ($disk.PartitionStyle -Eq 'RAW')
6+
{
7+
"Initializing data disk"
8+
$disk | Initialize-Disk
9+
} else {
10+
"data disk already initialized"
11+
}
12+
13+
$partitions = Get-Partition -DiskNumber $disk.Number | Where-Object Type -Ne 'Reserved'
14+
if ($partitions.Count -Eq 0) {
15+
"Creating partition on data disk"
16+
$partition = New-Partition -DiskNumber $disk.Number -UseMaximumSize
17+
} else {
18+
$partition = $partitions[0]
19+
$s = "data disk already has partition of size {0:n1} GiB" -f ($partition.Size / 1073741824)
20+
Write-Output $s
21+
}
22+
23+
$volume = Get-Volume -Partition $partition
24+
if ($volume.FileSystemType -Eq 'Unknown')
25+
{
26+
"Formatting data disk"
27+
Format-Volume -InputObject $volume -FileSystem NTFS -Confirm:$false
28+
} else {
29+
"data disk is already formatted"
30+
}
31+
32+
# Mount the partition
33+
Add-PartitionAccessPath -InputObject $partition -AccessPath "F:"
34+
35+
# Enable RDP
36+
Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server' -name "fDenyTSConnections" -value 0
37+
# Enable RDP through Windows Firewall
38+
Enable-NetFirewallRule -DisplayGroup "Remote Desktop"
39+
# Disable Network Level Authentication (NLA)
40+
# Clients will connect via Coder's tunnel
41+
(Get-WmiObject -class "Win32_TSGeneralSetting" -Namespace root\cimv2\terminalservices -ComputerName $env:COMPUTERNAME -Filter "TerminalName='RDP-tcp'").SetUserAuthenticationRequired(0)
42+
43+
# Install Chocolatey package manager
44+
Set-ExecutionPolicy Bypass -Scope Process -Force
45+
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
46+
iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
47+
# Reload path so sessions include "choco" and "refreshenv"
48+
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
49+
50+
# Install Git and reload path
51+
choco install -y git
52+
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
53+
54+
# Set protocol to TLS1.2 for agent download
55+
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
56+
57+
# Set Coder Agent to run immediately, and on each restart
58+
$init_script = @'
59+
${init_script}
60+
'@
61+
Out-File -FilePath "C:\AzureData\CoderAgent.ps1" -InputObject $init_script
62+
$task = @{
63+
TaskName = 'CoderAgent'
64+
Action = (New-ScheduledTaskAction -Execute 'powershell.exe' -Argument '-sta -ExecutionPolicy Unrestricted -Command "C:\AzureData\CoderAgent.ps1 *>> C:\AzureData\CoderAgent.log"')
65+
Trigger = (New-ScheduledTaskTrigger -AtStartup), (New-ScheduledTaskTrigger -Once -At (Get-Date).AddSeconds(15))
66+
Settings = (New-ScheduledTaskSettingsSet -DontStopOnIdleEnd -ExecutionTimeLimit ([TimeSpan]::FromDays(3650)) -Compatibility Win8)
67+
Principal = (New-ScheduledTaskPrincipal -UserId 'vm\coder' -RunLevel Highest -LogonType S4U)
68+
}
69+
Register-ScheduledTask @task -Force
70+
71+
# Additional Chocolatey package installs (optional, uncomment to enable)
72+
# choco feature enable -n=allowGlobalConfirmation
73+
# choco install visualstudio2022community --package-parameters "--add=Microsoft.VisualStudio.Workload.ManagedDesktop;includeRecommended --passive --locale en-US"
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
name: Develop in Windows on Azure
3+
description: Get started with Windows development on Microsoft Azure.
4+
tags: [cloud, azure, windows]
5+
icon: /icon/azure.png
6+
---
7+
8+
# azure-windows
9+
10+
To get started, run `coder templates init`. When prompted, select this template.
11+
Follow the on-screen instructions to proceed.
12+
13+
## Authentication
14+
15+
This template assumes that coderd is run in an environment that is authenticated
16+
with Azure. For example, run `az login` then `az account set --subscription=<id>`
17+
to import credentials on the system and user running coderd. For other ways to
18+
authenticate [consult the Terraform docs](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs#authenticating-to-azure).
19+
20+
## Dependencies
21+
22+
This template depends on the Azure CLI tool (`az`) to start and stop the Windows VM. Ensure this
23+
tool is installed and available in the path on the machine that runs coderd.
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
terraform {
2+
required_providers {
3+
coder = {
4+
source = "coder/coder"
5+
version = "0.7.0"
6+
}
7+
azurerm = {
8+
source = "hashicorp/azurerm"
9+
version = "=3.52.0"
10+
}
11+
}
12+
}
13+
14+
provider "azurerm" {
15+
features {}
16+
}
17+
18+
provider "coder" {
19+
}
20+
21+
data "coder_workspace" "me" {}
22+
23+
data "coder_parameter" "location" {
24+
description = "What location should your workspace live in?"
25+
display_name = "Location"
26+
name = "location"
27+
default = "eastus"
28+
mutable = false
29+
option {
30+
value = "eastus"
31+
name = "East US"
32+
}
33+
option {
34+
value = "centralus"
35+
name = "Central US"
36+
}
37+
option {
38+
value = "southcentralus"
39+
name = "South Central US"
40+
}
41+
option {
42+
value = "westus2"
43+
name = "West US 2"
44+
}
45+
}
46+
47+
data "coder_parameter" "data_disk_size" {
48+
description = "Size of your data (F:) drive in GB"
49+
display_name = "Data disk size"
50+
name = "data_disk_size"
51+
default = 20
52+
mutable = "false"
53+
type = "number"
54+
validation {
55+
min = 5
56+
max = 5000
57+
}
58+
}
59+
60+
resource "coder_agent" "main" {
61+
arch = "amd64"
62+
auth = "azure-instance-identity"
63+
os = "windows"
64+
login_before_ready = false
65+
}
66+
67+
resource "random_password" "admin_password" {
68+
length = 16
69+
special = true
70+
# https://docs.microsoft.com/en-us/windows/security/threat-protection/security-policy-settings/password-must-meet-complexity-requirements#reference
71+
# we remove characters that require special handling in XML, as this is how we pass it to the VM
72+
# namely: <>&'"
73+
override_special = "~!@#$%^*_-+=`|\\(){}[]:;,.?/"
74+
}
75+
76+
locals {
77+
prefix = "coder-win"
78+
admin_username = "coder"
79+
}
80+
81+
resource "azurerm_resource_group" "main" {
82+
name = "${local.prefix}-${data.coder_workspace.me.id}"
83+
location = data.coder_parameter.location.value
84+
tags = {
85+
Coder_Provisioned = "true"
86+
}
87+
}
88+
89+
// Uncomment here and in the azurerm_network_interface resource to obtain a public IP
90+
#resource "azurerm_public_ip" "main" {
91+
# name = "publicip"
92+
# resource_group_name = azurerm_resource_group.main.name
93+
# location = azurerm_resource_group.main.location
94+
# allocation_method = "Static"
95+
# tags = {
96+
# Coder_Provisioned = "true"
97+
# }
98+
#}
99+
resource "azurerm_virtual_network" "main" {
100+
name = "network"
101+
address_space = ["10.0.0.0/24"]
102+
location = azurerm_resource_group.main.location
103+
resource_group_name = azurerm_resource_group.main.name
104+
tags = {
105+
Coder_Provisioned = "true"
106+
}
107+
}
108+
resource "azurerm_subnet" "internal" {
109+
name = "internal"
110+
resource_group_name = azurerm_resource_group.main.name
111+
virtual_network_name = azurerm_virtual_network.main.name
112+
address_prefixes = ["10.0.0.0/29"]
113+
}
114+
resource "azurerm_network_interface" "main" {
115+
name = "nic"
116+
resource_group_name = azurerm_resource_group.main.name
117+
location = azurerm_resource_group.main.location
118+
ip_configuration {
119+
name = "internal"
120+
subnet_id = azurerm_subnet.internal.id
121+
private_ip_address_allocation = "Dynamic"
122+
// Uncomment for public IP address as well as azurerm_public_ip resource above
123+
# public_ip_address_id = azurerm_public_ip.main.id
124+
}
125+
tags = {
126+
Coder_Provisioned = "true"
127+
}
128+
}
129+
# Create storage account for boot diagnostics
130+
resource "azurerm_storage_account" "my_storage_account" {
131+
name = "diag${random_id.storage_id.hex}"
132+
location = azurerm_resource_group.main.location
133+
resource_group_name = azurerm_resource_group.main.name
134+
account_tier = "Standard"
135+
account_replication_type = "LRS"
136+
}
137+
# Generate random text for a unique storage account name
138+
resource "random_id" "storage_id" {
139+
keepers = {
140+
# Generate a new ID only when a new resource group is defined
141+
resource_group = azurerm_resource_group.main.name
142+
}
143+
byte_length = 8
144+
}
145+
146+
resource "azurerm_managed_disk" "data" {
147+
name = "data_disk"
148+
location = azurerm_resource_group.main.location
149+
resource_group_name = azurerm_resource_group.main.name
150+
storage_account_type = "Standard_LRS"
151+
create_option = "Empty"
152+
disk_size_gb = data.coder_parameter.data_disk_size.value
153+
}
154+
155+
# Create virtual machine
156+
resource "azurerm_windows_virtual_machine" "main" {
157+
name = "vm"
158+
admin_username = local.admin_username
159+
admin_password = random_password.admin_password.result
160+
location = azurerm_resource_group.main.location
161+
resource_group_name = azurerm_resource_group.main.name
162+
network_interface_ids = [azurerm_network_interface.main.id]
163+
size = "Standard_DS1_v2"
164+
custom_data = base64encode(
165+
templatefile("${path.module}/Initialize.ps1.tftpl", { init_script = coder_agent.main.init_script })
166+
)
167+
os_disk {
168+
name = "myOsDisk"
169+
caching = "ReadWrite"
170+
storage_account_type = "Premium_LRS"
171+
}
172+
source_image_reference {
173+
publisher = "MicrosoftWindowsServer"
174+
offer = "WindowsServer"
175+
sku = "2022-datacenter-azure-edition"
176+
version = "latest"
177+
}
178+
additional_unattend_content {
179+
content = "<AutoLogon><Password><Value>${random_password.admin_password.result}</Value></Password><Enabled>true</Enabled><LogonCount>1</LogonCount><Username>${local.admin_username}</Username></AutoLogon>"
180+
setting = "AutoLogon"
181+
}
182+
additional_unattend_content {
183+
content = file("${path.module}/FirstLogonCommands.xml")
184+
setting = "FirstLogonCommands"
185+
}
186+
boot_diagnostics {
187+
storage_account_uri = azurerm_storage_account.my_storage_account.primary_blob_endpoint
188+
}
189+
tags = {
190+
Coder_Provisioned = "true"
191+
}
192+
}
193+
194+
resource "coder_metadata" "rdp_login" {
195+
resource_id = azurerm_windows_virtual_machine.main.id
196+
item {
197+
key = "Username"
198+
value = local.admin_username
199+
}
200+
item {
201+
key = "Password"
202+
value = random_password.admin_password.result
203+
sensitive = true
204+
}
205+
}
206+
207+
resource "azurerm_virtual_machine_data_disk_attachment" "main_data" {
208+
managed_disk_id = azurerm_managed_disk.data.id
209+
virtual_machine_id = azurerm_windows_virtual_machine.main.id
210+
lun = "10"
211+
caching = "ReadWrite"
212+
}
213+
214+
# Stop the VM
215+
resource "null_resource" "stop_vm" {
216+
count = data.coder_workspace.me.transition == "stop" ? 1 : 0
217+
depends_on = [azurerm_windows_virtual_machine.main]
218+
provisioner "local-exec" {
219+
# Use deallocate so the VM is not charged
220+
command = "az vm deallocate --ids ${azurerm_windows_virtual_machine.main.id}"
221+
}
222+
}
223+
224+
# Start the VM
225+
resource "null_resource" "start" {
226+
count = data.coder_workspace.me.transition == "start" ? 1 : 0
227+
depends_on = [azurerm_windows_virtual_machine.main]
228+
provisioner "local-exec" {
229+
command = "az vm start --ids ${azurerm_windows_virtual_machine.main.id}"
230+
}
231+
}

0 commit comments

Comments
 (0)