Skip to content

Commit 96c5f32

Browse files
ExcellencedevCopilotDevelopmentCats
authored
Add Hetzner Cloud server template example (#560)
## Description This PR adds a template example for the Hetzner Cloud Sever. This video shows the provisioning of multiple hetzner instances in coder with dynamic param enabled, running simultaneously and shown in the Hetzner console, checking labels on both the OS Filesystem and on the hetzner console and then shutting down through coder demonstrating clean up on hetzner and deleting workspaces without errors DEMO VIDEO: https://drive.google.com/file/d/1JkhjszCRM9K27XDlMi_2nXtJosoflns_/view?usp=sharing ## Type of Change - [ ] New module - [x] New template - [ ] Bug fix - [ ] Feature/enhancement - [ ] Documentation - [ ] Other ## Template Information **Path:** `registry/Excellencedev/templates/hetzner-linux` ## Testing & Validation - [x] Tests pass (`bun test`) - [x] Code formatted (`bun fmt`) - [x] Changes tested locally ## Related Issues /closes #209 /claim #209 --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: DevCats <christofer@coder.com>
1 parent 146540c commit 96c5f32

File tree

8 files changed

+317
-0
lines changed

8 files changed

+317
-0
lines changed

.github/typos.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ muc = "muc" # For Munich location code
33
tyo = "tyo" # For Tokyo location code
44
Hashi = "Hashi"
55
HashiCorp = "HashiCorp"
6+
hel = "hel" # For Helsinki location code
67
mavrickrishi = "mavrickrishi" # Username
78
mavrick = "mavrick" # Username
89
inh = "inh" # Option in setpriv command

.icons/hetzner.svg

Lines changed: 5 additions & 0 deletions
Loading
99.5 KB
Loading

registry/Excellencedev/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
display_name: "Excellencedev"
3+
bio: "Love to contribute"
4+
avatar: "./.images/avatar.png"
5+
support_email: "ademiluyisuccessandexcellence@gmail.com"
6+
status: "community"
7+
---
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
display_name: Hetzner Cloud Server
3+
description: Provision Hetzner Cloud servers as Coder workspaces
4+
icon: ../../../../.icons/hetzner.svg
5+
tags: [vm, linux, hetzner]
6+
---
7+
8+
# Remote Development on Hetzner Cloud (Linux)
9+
10+
Provision Hetzner Cloud servers as [Coder workspaces](https://coder.com/docs/workspaces) with this example template.
11+
12+
> [!WARNING]
13+
> **Workspace Storage Persistence:** When a workspace is stopped, the Hetzner Cloud server instance is stopped but your home volume and stored data persist. This means your files and data remain intact when you resume the workspace.
14+
15+
> [!IMPORTANT]
16+
> **Volume Management & Costs:** Hetzner Cloud volumes persist even when workspaces are stopped and will continue to incur storage costs (€0.0476/GB/month). Volumes are only automatically deleted when the workspace is completely deleted. Monitor your volumes in the [Hetzner Cloud Console](https://console.hetzner.cloud/) to manage costs effectively.
17+
18+
## Prerequisites
19+
20+
To deploy workspaces as Hetzner Cloud servers, you'll need:
21+
22+
- Hetzner Cloud [API token](https://console.hetzner.cloud/projects) (create under Security > API Tokens)
23+
24+
### Authentication
25+
26+
This template assumes that the Coder Provisioner is run in an environment that is authenticated with Hetzner Cloud.
27+
28+
Obtain a Hetzner Cloud API token from your [Hetzner Cloud Console](https://console.hetzner.cloud/projects) and provide it as the `hcloud_token` variable when creating a workspace.
29+
For more authentication options, see the [Terraform provider documentation](https://registry.terraform.io/providers/hetznercloud/hcloud/latest/docs#authentication).
30+
31+
> [!NOTE]
32+
> This template is designed to be a starting point. Edit the Terraform to extend the template to support your use case.
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
#cloud-config
2+
users:
3+
- name: ${username}
4+
sudo: ["ALL=(ALL) NOPASSWD:ALL"]
5+
groups: sudo
6+
shell: /bin/bash
7+
packages:
8+
- git
9+
%{ if home_volume_label != "" ~}
10+
fs_setup:
11+
- device: /dev/disk/by-id/scsi-0HC_Volume_${volume_id}
12+
filesystem: ext4
13+
label: ${home_volume_label}
14+
overwrite: false # This prevents reformatting the disk on every boot
15+
16+
mounts:
17+
- [
18+
"/dev/disk/by-id/scsi-0HC_Volume_${volume_id}",
19+
"/home/${username}",
20+
ext4,
21+
"defaults,uid=1000,gid=1000",
22+
]
23+
%{ endif ~}
24+
write_files:
25+
- path: /opt/coder/init
26+
permissions: "0755"
27+
encoding: b64
28+
content: ${init_script}
29+
- path: /etc/systemd/system/coder-agent.service
30+
permissions: "0644"
31+
content: |
32+
[Unit]
33+
Description=Coder Agent
34+
After=network-online.target
35+
Wants=network-online.target
36+
37+
[Service]
38+
User=${username}
39+
ExecStart=/opt/coder/init
40+
Environment=CODER_AGENT_TOKEN=${coder_agent_token}
41+
Restart=always
42+
RestartSec=10
43+
TimeoutStopSec=90
44+
KillMode=process
45+
46+
OOMScoreAdjust=-900
47+
SyslogIdentifier=coder-agent
48+
49+
[Install]
50+
WantedBy=multi-user.target
51+
runcmd:
52+
%{ if home_volume_label != "" ~}
53+
- |
54+
until [ -e /dev/disk/by-id/scsi-0HC_Volume_${volume_id} ]; do
55+
echo "Waiting for volume device..."
56+
sleep 2
57+
done
58+
%{ endif ~}
59+
- mount -a
60+
- chown ${username}:${username} /home/${username}
61+
- systemctl enable coder-agent
62+
- systemctl start coder-agent
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"type_meta": {
3+
"cx22": { "cores": 2, "memory_gb": 4, "disk_gb": 40 },
4+
"cx32": { "cores": 4, "memory_gb": 8, "disk_gb": 80 },
5+
"cx42": { "cores": 8, "memory_gb": 16, "disk_gb": 160 },
6+
"cx52": { "cores": 16, "memory_gb": 32, "disk_gb": 320 },
7+
"cpx11": { "cores": 2, "memory_gb": 2, "disk_gb": 40 },
8+
"cpx21": { "cores": 3, "memory_gb": 4, "disk_gb": 80 },
9+
"cpx31": { "cores": 4, "memory_gb": 8, "disk_gb": 160 },
10+
"cpx41": { "cores": 8, "memory_gb": 16, "disk_gb": 240 },
11+
"cpx51": { "cores": 16, "memory_gb": 32, "disk_gb": 360 },
12+
"ccx13": { "cores": 2, "memory_gb": 8, "disk_gb": 80 },
13+
"ccx23": { "cores": 4, "memory_gb": 16, "disk_gb": 160 },
14+
"ccx33": { "cores": 8, "memory_gb": 32, "disk_gb": 240 },
15+
"ccx43": { "cores": 16, "memory_gb": 64, "disk_gb": 360 },
16+
"ccx53": { "cores": 32, "memory_gb": 128, "disk_gb": 600 },
17+
"ccx63": { "cores": 48, "memory_gb": 192, "disk_gb": 960 }
18+
},
19+
"availability": {
20+
"fsn1": ["cpx11", "cpx21", "cpx31", "cpx41", "cpx51", "ccx13", "ccx23", "ccx33"],
21+
"ash": ["cpx11", "cpx21", "cpx31", "cpx41", "cpx51", "ccx13", "ccx23", "ccx33"],
22+
"hel1": ["cx22", "cpx11", "cpx21", "cpx31", "cpx41", "cpx51", "ccx13", "ccx23", "ccx33"],
23+
"hil": ["cpx11", "cpx21", "cpx31", "cpx41", "ccx13", "ccx23", "ccx33"],
24+
"nbg1": ["cx22", "cx32", "cx42", "cx52", "cpx11", "cpx21", "cpx31", "cpx41", "cpx51", "ccx13", "ccx23", "ccx33"],
25+
"sin": ["cpx11", "cpx21", "cpx31", "cpx41", "cpx51", "ccx13", "ccx23", "ccx33"]
26+
}
27+
}
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
terraform {
2+
required_providers {
3+
hcloud = {
4+
source = "hetznercloud/hcloud"
5+
}
6+
coder = {
7+
source = "coder/coder"
8+
}
9+
}
10+
}
11+
12+
variable "hcloud_token" {
13+
sensitive = true
14+
}
15+
16+
provider "hcloud" {
17+
token = var.hcloud_token
18+
}
19+
20+
# Available locations: https://docs.hetzner.com/cloud/general/locations/
21+
data "coder_parameter" "hcloud_location" {
22+
name = "hcloud_location"
23+
display_name = "Hetzner Location"
24+
description = "Select the Hetzner Cloud location for your workspace."
25+
type = "string"
26+
default = "fsn1"
27+
option {
28+
name = "DE Falkenstein"
29+
value = "fsn1"
30+
}
31+
option {
32+
name = "US Ashburn, VA"
33+
value = "ash"
34+
}
35+
option {
36+
name = "US Hillsboro, OR"
37+
value = "hil"
38+
}
39+
option {
40+
name = "SG Singapore"
41+
value = "sin"
42+
}
43+
option {
44+
name = "DE Nuremberg"
45+
value = "nbg1"
46+
}
47+
option {
48+
name = "FI Helsinki"
49+
value = "hel1"
50+
}
51+
}
52+
53+
# Available server types: https://docs.hetzner.com/cloud/servers/overview/
54+
data "coder_parameter" "hcloud_server_type" {
55+
name = "hcloud_server_type"
56+
display_name = "Hetzner Server Type"
57+
description = "Select the Hetzner Cloud server type for your workspace."
58+
type = "string"
59+
60+
dynamic "option" {
61+
for_each = local.hcloud_server_type_options_for_selected_location
62+
content {
63+
name = option.value.name
64+
value = option.value.value
65+
}
66+
}
67+
}
68+
69+
resource "hcloud_server" "dev" {
70+
count = data.coder_workspace.me.start_count
71+
name = "coder-${data.coder_workspace.me.name}-dev"
72+
image = "ubuntu-24.04"
73+
server_type = data.coder_parameter.hcloud_server_type.value
74+
location = data.coder_parameter.hcloud_location.value
75+
public_net {
76+
ipv4_enabled = true
77+
ipv6_enabled = true
78+
}
79+
user_data = templatefile("cloud-config.yaml.tftpl", {
80+
username = lower(data.coder_workspace_owner.me.name)
81+
home_volume_label = "coder-${data.coder_workspace.me.id}-home"
82+
volume_id = hcloud_volume.home_volume.id
83+
init_script = base64encode(coder_agent.main.init_script)
84+
coder_agent_token = coder_agent.main.token
85+
})
86+
labels = {
87+
"coder_workspace_name" = data.coder_workspace.me.name,
88+
"coder_workspace_owner" = data.coder_workspace_owner.me.name,
89+
}
90+
}
91+
92+
resource "hcloud_volume" "home_volume" {
93+
name = "coder-${data.coder_workspace.me.id}-home"
94+
size = data.coder_parameter.home_volume_size.value
95+
location = data.coder_parameter.hcloud_location.value
96+
labels = {
97+
"coder_workspace_name" = data.coder_workspace.me.name,
98+
"coder_workspace_owner" = data.coder_workspace_owner.me.name,
99+
}
100+
}
101+
102+
resource "hcloud_volume_attachment" "home_volume_attachment" {
103+
count = data.coder_workspace.me.start_count
104+
volume_id = hcloud_volume.home_volume.id
105+
server_id = hcloud_server.dev[count.index].id
106+
automount = false
107+
}
108+
109+
locals {
110+
username = lower(data.coder_workspace_owner.me.name)
111+
112+
# Data source: local JSON file under the module directory
113+
# Check API for latest server types & availability: https://docs.hetzner.cloud/reference/cloud#server-types
114+
hcloud_server_types_data = jsondecode(file("${path.module}/hetzner_server_types.json"))
115+
hcloud_server_type_meta = local.hcloud_server_types_data.type_meta
116+
hcloud_server_types_by_location = local.hcloud_server_types_data.availability
117+
118+
hcloud_server_type_options_for_selected_location = [
119+
for type_name in lookup(local.hcloud_server_types_by_location, data.coder_parameter.hcloud_location.value, []) : {
120+
name = format("%s (%d vCPU, %dGB RAM, %dGB)", upper(type_name), local.hcloud_server_type_meta[type_name].cores, local.hcloud_server_type_meta[type_name].memory_gb, local.hcloud_server_type_meta[type_name].disk_gb)
121+
value = type_name
122+
}
123+
]
124+
}
125+
126+
data "coder_provisioner" "me" {}
127+
128+
provider "coder" {}
129+
130+
data "coder_workspace" "me" {}
131+
132+
data "coder_workspace_owner" "me" {}
133+
134+
data "coder_parameter" "home_volume_size" {
135+
name = "home_volume_size"
136+
display_name = "Home volume size"
137+
description = "How large would you like your home volume to be (in GB)?"
138+
type = "number"
139+
default = "20"
140+
mutable = false
141+
validation {
142+
min = 1
143+
max = 100 # Adjust the max size as needed
144+
}
145+
}
146+
147+
resource "coder_agent" "main" {
148+
os = "linux"
149+
arch = "amd64"
150+
151+
metadata {
152+
key = "cpu"
153+
display_name = "CPU Usage"
154+
interval = 5
155+
timeout = 5
156+
script = "coder stat cpu"
157+
}
158+
metadata {
159+
key = "memory"
160+
display_name = "Memory Usage"
161+
interval = 5
162+
timeout = 5
163+
script = "coder stat mem"
164+
}
165+
metadata {
166+
key = "home"
167+
display_name = "Home Usage"
168+
interval = 600 # every 10 minutes
169+
timeout = 30 # df can take a while on large filesystems
170+
script = "coder stat disk --path /home/${local.username}"
171+
}
172+
}
173+
174+
module "code-server" {
175+
count = data.coder_workspace.me.start_count
176+
source = "registry.coder.com/coder/code-server/coder"
177+
178+
# This ensures that the latest non-breaking version of the module gets downloaded, you can also pin the module version to prevent breaking changes in production.
179+
version = "~> 1.0"
180+
181+
agent_id = coder_agent.main.id
182+
order = 1
183+
}

0 commit comments

Comments
 (0)