diff --git a/.github/typos.toml b/.github/typos.toml index fdb747483..aefbbfd43 100644 --- a/.github/typos.toml +++ b/.github/typos.toml @@ -5,6 +5,13 @@ Hashi = "Hashi" HashiCorp = "HashiCorp" mavrickrishi = "mavrickrishi" # Username mavrick = "mavrick" # Username +melmathari = "melmathari" # Username +fsn1 = "fsn1" # Hetzner Falkenstein datacenter code +nbg1 = "nbg1" # Hetzner Nuremberg datacenter code +hel1 = "hel1" # Hetzner Helsinki datacenter code +hel = "hel" # Hetzner Helsinki short code +hcloud = "hcloud" # Hetzner Cloud CLI/API +vcpus = "vcpus" # Virtual CPUs [files] extend-exclude = ["registry/coder/templates/aws-devcontainer/architecture.svg"] #False positive \ No newline at end of file diff --git a/.icons/hetzner.svg b/.icons/hetzner.svg new file mode 100644 index 000000000..74bb87c1a --- /dev/null +++ b/.icons/hetzner.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/registry/melmathari/.images/melmathari.jpeg b/registry/melmathari/.images/melmathari.jpeg new file mode 100644 index 000000000..b50b558d4 Binary files /dev/null and b/registry/melmathari/.images/melmathari.jpeg differ diff --git a/registry/melmathari/README.md b/registry/melmathari/README.md new file mode 100644 index 000000000..92faa46de --- /dev/null +++ b/registry/melmathari/README.md @@ -0,0 +1,16 @@ +--- +display_name: "Mohamed El Mathari" +bio: "Software engineer, no-code nerd, teacher, and always ready to try something new" +avatar: "./.images/melmathari.jpeg" +github: "melmathari" +linkedin: "https://www.linkedin.com/in/melmathari/" +website: "https://melmathari.dev" +support_email: "info@nocodeventure.com" +status: community +--- + +# About Me + +👨‍💻 Learning by contributing in Open Source. + +Software engineer, no-code nerd, teacher, and always ready to try something new. Based in the Netherlands, I'm a no-code advocate who loves building tools and launching products that make a difference. Whether I'm coding, mentoring, or experimenting, I aim to keep things simple and impactful. I'm a big fan of breaking down complex problems into straightforward solutions—one idea at a time. diff --git a/registry/melmathari/templates/hetzner-cloud/README.md b/registry/melmathari/templates/hetzner-cloud/README.md new file mode 100644 index 000000000..b1e7e1fac --- /dev/null +++ b/registry/melmathari/templates/hetzner-cloud/README.md @@ -0,0 +1,263 @@ +--- +display_name: Hetzner Cloud Server (Linux) +description: Provision Hetzner Cloud servers as Coder workspaces with networking and volumes +icon: ../../../../.icons/hetzner.svg +verified: false +tags: [vm, linux, hetzner, cloud, germany] +--- + +# Remote Development on Hetzner Cloud + +Provision Hetzner Cloud servers as [Coder workspaces](https://coder.com/docs/workspaces) with this template. + +This template provides a comprehensive Hetzner Cloud setup with: + +- **Dynamic Configuration**: Server types, locations, and images loaded from JSON +- **Location-Aware Filtering**: Available server types automatically filter based on selected location +- **Multiple Server Types**: ARM, Intel, AMD shared, and dedicated instances +- **Global Locations**: Europe, USA, and Asia datacenters +- **Persistent Storage**: Home volumes that survive workspace restarts +- **Secure Networking**: Private networks with firewall rules +- **Clean Architecture**: Region-based availability in JSON for easy maintenance + +## Prerequisites + +To deploy workspaces as Hetzner Cloud servers, you'll need: + +- Hetzner Cloud [API token](https://docs.hetzner.cloud/#authentication) +- Hetzner Cloud project (create one in the [Hetzner Cloud Console](https://console.hetzner.cloud/)) +- **SSH Keys**: Upload your SSH public keys to your Hetzner Cloud account (the template will use all available keys) + +### Authentication + +This template assumes that the Coder Provisioner is run in an environment that is authenticated with Hetzner Cloud. + +Set the `HCLOUD_TOKEN` environment variable to your Hetzner Cloud API token, or provide it via the `hcloud_token` variable in your `terraform.tfvars` file. + +For other authentication methods, consult the [Hetzner Cloud Terraform provider documentation](https://registry.terraform.io/providers/hetznercloud/hcloud/latest/docs). + +### Image Name Verification + +The template uses Hetzner's official image names. To verify current available images: + +```bash +# Set your API token +export HCLOUD_TOKEN="your-hetzner-cloud-api-token" + +# List all available images +curl -s -H "Authorization: Bearer $HCLOUD_TOKEN" \ + "https://api.hetzner.cloud/v1/images" \ + | jq '.images[] | select(.type=="system") | .name' +``` + +If you encounter image-related errors, check that the image names in `hetzner-config.json` match the official names exactly (some may include architecture suffixes like `-amd64`). + +## Architecture + +This template provisions the following resources: + +- **Hetzner Cloud server** (ephemeral, deleted on workspace stop) +- **Persistent volume** (mounted to `/home/`, survives workspace restarts) +- **Private network** with subnet for secure communication +- **Firewall** with rules for SSH, HTTP, HTTPS, and development ports +- **SSH keys** automatically loaded from your Hetzner Cloud account + +### Lifecycle Management + +- **Workspace start**: Server and volume are created, volume is attached +- **Workspace stop**: Server is destroyed, but volume persists +- **Workspace restart**: New server is created and existing volume is reattached + +This means that when the workspace restarts, any tools or files outside of the home directory are not persisted. To pre-bake tools into the workspace, modify the server image or use a [startup script](https://registry.terraform.io/providers/coder/coder/latest/docs/resources/script). + +## Server Types + +The template supports multiple Hetzner Cloud server types across four categories: + +- **ARM-based (CAX)**: Energy-efficient ARM architecture instances +- **Intel CPU-Optimized (CPX)**: High-performance Intel processors +- **AMD Shared (CX)**: Cost-effective AMD shared instances +- **Dedicated vCPU (CCX)**: Dedicated CPU resources for consistent performance + +Server types are automatically filtered based on your selected location. The specific availability is managed in `hetzner-config.json`. + +## Locations + +Available locations: + +- **Falkenstein, Germany** (fsn1) - Europe +- **Nuremberg, Germany** (nbg1) - Europe +- **Helsinki, Finland** (hel1) - Europe +- **Ashburn, Virginia, USA** (ash) - US East Coast +- **Hillsboro, Oregon, USA** (hil) - US West Coast +- **Singapore** (sin) - Asia Pacific + +## Supported Operating Systems + +- Ubuntu 24.04 LTS +- Ubuntu 22.04 LTS (default) +- Ubuntu 20.04 LTS +- Debian 12 +- Debian 11 +- CentOS Stream 9 +- Fedora 39 +- Rocky Linux 9 +- AlmaLinux 9 + +## Configuration + +### Required Variables + +```hcl +# terraform.tfvars +hcloud_token = "your-hetzner-cloud-api-token" +``` + +### Maintaining Configuration + +The template uses `hetzner-config.json` for dynamic configuration: + +- **Server Types**: Add new server types with their specifications +- **Locations**: Add new Hetzner datacenters as they become available +- **Images**: Update with current Hetzner image names (verify with API) +- **Availability**: Map server type restrictions per location (only shown server types that are available) + +**How it works**: When a user selects a location, the template automatically filters the server type dropdown to only show instances available in that location. This prevents configuration errors by design. + +**Example**: Adding a new server type: + +```json +"cx62": { "name": "CX62 (16 vCPU, 64 GB RAM, AMD)", "vcpus": 16, "memory": 64 } +``` + +The `availability_by_location` section maps which server types are available in each region: + +```json +"availability_by_location": { + "fsn1": ["cax11", "cpx11", "cx22", "ccx13", ...], // Europe: All types + "ash": ["cpx11", "ccx13", ...], // USA: Intel + Dedicated only + "sin": ["cpx11", "ccx13", ...] // Asia: Intel + Dedicated only +} +``` + +**Important**: Always verify image names match Hetzner's official names exactly to avoid provisioning errors. + +### Optional Variables + +All other parameters can be configured through the Coder workspace creation interface: + +- **Location**: Choose the datacenter location +- **Server Type**: Select from available server configurations +- **Operating System**: Choose your preferred Linux distribution from the curated list +- **Custom Image Override**: Optionally specify a custom Hetzner Cloud image name (overrides the OS selection) +- **Home Volume Size**: Set the size of persistent storage (10-1000 GB) + +### Custom Images + +You can use custom images in two ways: + +1. **Override Field**: Leave the "Custom Image Override" field empty to use the selected OS, or enter a custom image name to override it +2. **Examples**: + - `my-custom-snapshot` - Your own Hetzner Cloud snapshot + - `debian-12-amd64` - Specific architecture variant + - `ubuntu-24.04` - Newer image not yet in the dropdown list + +The custom override takes precedence over the dropdown selection, allowing you to use any valid Hetzner Cloud image name. + +## Security + +The template includes: + +- Private networking for secure inter-service communication +- Firewall rules allowing only necessary ports (22, 80, 443, 8080) +- SSH key authentication +- User isolation through cloud-init configuration + +## Cost Optimization + +- Servers are destroyed when workspaces stop, minimizing compute costs +- Volumes persist but are only charged for storage when servers are stopped +- Choose appropriate server types based on workload requirements +- Consider using shared vCPU instances for development workloads + +## Troubleshooting + +### Server Type Options Change When Selecting Location + +This is expected behavior! The template dynamically filters server types based on regional availability: + +- **Europe (fsn1, nbg1, hel1)**: Shows all server types including ARM (CAX) and AMD (CX) +- **USA/Asia (ash, hil, sin)**: Shows only Intel (CPX) and Dedicated (CCX) servers + +This prevents configuration errors by only showing what's actually available in your selected region. + +### Image Not Found Errors + +If you get errors like "image not found" or "invalid image name": + +1. **Verify Image Names**: Check current available images using the API: + + ```bash + curl -s -H "Authorization: Bearer $HCLOUD_TOKEN" \ + "https://api.hetzner.cloud/v1/images" \ + | jq '.images[] | select(.type=="system") | .name' | sort + ``` + +2. **Update JSON Configuration**: Edit `hetzner-config.json` to match exact image names from Hetzner +3. **Common Issues**: + - Some images may have architecture suffixes (e.g., `debian-12` vs `debian-12-amd64`) + - Image names may change over time as new versions are released + - Deprecated images are removed from the available list + +4. **Test Locally**: Before using in Coder, test image names with basic Terraform: + ```hcl + resource "hcloud_server" "test" { + name = "test" + server_type = "cx11" + image = "ubuntu-22.04" # Test this image name + location = "fsn1" + } + ``` + +### Volume Mount Issues + +If the home directory doesn't mount properly: + +1. Check that the volume is attached to the server +2. Verify the cloud-init configuration is applied correctly +3. Ensure the filesystem is formatted as ext4 + +### Network Connectivity Issues + +If you can't connect to development servers: + +1. Verify firewall rules allow the required ports +2. Check that the private network is configured correctly +3. Ensure the server has a public IP address + +## Local Testing + +To test this template locally, create a `terraform.tfvars` file with: + +```hcl +hcloud_token = "your-hetzner-cloud-api-token" +``` + +Then run: + +```bash +terraform init +terraform validate +terraform plan +``` + +## Notes + +> [!NOTE] +> This template is designed to be a starting point! Edit the Terraform configuration to extend the template to support your specific use case. + +> [!IMPORTANT] +> The SSH key parameter defaults to 0 (no SSH key). To enable SSH access, set `ssh_key_id` to your actual SSH key ID from Hetzner Cloud. + +> [!WARNING] +> Server types are automatically filtered based on location availability. If you don't see a specific server type in the dropdown, it's not available in your selected location. diff --git a/registry/melmathari/templates/hetzner-cloud/cloud-config.yaml.tftpl b/registry/melmathari/templates/hetzner-cloud/cloud-config.yaml.tftpl new file mode 100644 index 000000000..eb14bb537 --- /dev/null +++ b/registry/melmathari/templates/hetzner-cloud/cloud-config.yaml.tftpl @@ -0,0 +1,57 @@ +#cloud-config +hostname: ${hostname} +users: + - name: ${username} + sudo: ["ALL=(ALL) NOPASSWD:ALL"] + groups: sudo + shell: /bin/bash +packages: + - git + - curl + - wget + - unzip + - htop +disk_setup: + ${volume_device}: + table_type: "gpt" + layout: true + overwrite: false +fs_setup: + - label: coder-home + filesystem: ext4 + device: ${volume_device} + partition: auto +mounts: + - ["${volume_device}", "/home/${username}", "ext4", "defaults", "0", "2"] +write_files: + - path: /opt/coder/init + permissions: "0755" + encoding: b64 + content: ${init_script} + - path: /etc/systemd/system/coder-agent.service + permissions: "0644" + content: | + [Unit] + Description=Coder Agent + After=network-online.target + Wants=network-online.target + + [Service] + User=${username} + ExecStart=/opt/coder/init + Environment=CODER_AGENT_TOKEN=${coder_agent_token} + Restart=always + RestartSec=10 + TimeoutStopSec=90 + KillMode=process + + OOMScoreAdjust=-1000 + SyslogIdentifier=coder-agent + + [Install] + WantedBy=multi-user.target +runcmd: + - mkdir -p /home/${username} + - chown ${username}:${username} /home/${username} + - systemctl enable coder-agent + - systemctl start coder-agent diff --git a/registry/melmathari/templates/hetzner-cloud/hetzner-config.json b/registry/melmathari/templates/hetzner-cloud/hetzner-config.json new file mode 100644 index 000000000..5bbd38624 --- /dev/null +++ b/registry/melmathari/templates/hetzner-cloud/hetzner-config.json @@ -0,0 +1,226 @@ +{ + "_comment": "Image names should match Hetzner's official Terraform provider names exactly. Verify with: curl -H 'Authorization: Bearer $HCLOUD_TOKEN' 'https://api.hetzner.cloud/v1/images' | jq '.images[] | .name'", + "type_meta": { + "locations": { + "fsn1": { "name": "Falkenstein, Germany", "zone": "eu-central" }, + "nbg1": { "name": "Nuremberg, Germany", "zone": "eu-central" }, + "hel1": { "name": "Helsinki, Finland", "zone": "eu-central" }, + "ash": { "name": "Ashburn, VA, USA", "zone": "us-east" }, + "hil": { "name": "Hillsboro, OR, USA", "zone": "us-west" }, + "sin": { "name": "Singapore", "zone": "ap-southeast" } + }, + "server_types": { + "cax11": { + "name": "CAX11 (2 vCPU, 4 GB RAM, ARM)", + "vcpus": 2, + "memory": 4 + }, + "cax21": { + "name": "CAX21 (4 vCPU, 8 GB RAM, ARM)", + "vcpus": 4, + "memory": 8 + }, + "cax31": { + "name": "CAX31 (8 vCPU, 16 GB RAM, ARM)", + "vcpus": 8, + "memory": 16 + }, + "cax41": { + "name": "CAX41 (16 vCPU, 32 GB RAM, ARM)", + "vcpus": 16, + "memory": 32 + }, + "cpx11": { "name": "CPX11 (2 vCPU, 2 GB RAM)", "vcpus": 2, "memory": 2 }, + "cpx21": { "name": "CPX21 (3 vCPU, 4 GB RAM)", "vcpus": 3, "memory": 4 }, + "cpx31": { "name": "CPX31 (4 vCPU, 8 GB RAM)", "vcpus": 4, "memory": 8 }, + "cpx41": { + "name": "CPX41 (8 vCPU, 16 GB RAM)", + "vcpus": 8, + "memory": 16 + }, + "cpx51": { + "name": "CPX51 (16 vCPU, 32 GB RAM)", + "vcpus": 16, + "memory": 32 + }, + "cx22": { + "name": "CX22 (2 vCPU, 4 GB RAM, AMD)", + "vcpus": 2, + "memory": 4 + }, + "cx32": { + "name": "CX32 (4 vCPU, 8 GB RAM, AMD)", + "vcpus": 4, + "memory": 8 + }, + "cx42": { + "name": "CX42 (8 vCPU, 16 GB RAM, AMD)", + "vcpus": 8, + "memory": 16 + }, + "cx52": { + "name": "CX52 (16 vCPU, 32 GB RAM, AMD)", + "vcpus": 16, + "memory": 32 + }, + "ccx13": { + "name": "CCX13 (2 vCPU, 8 GB RAM, Dedicated)", + "vcpus": 2, + "memory": 8 + }, + "ccx23": { + "name": "CCX23 (4 vCPU, 16 GB RAM, Dedicated)", + "vcpus": 4, + "memory": 16 + }, + "ccx33": { + "name": "CCX33 (8 vCPU, 32 GB RAM, Dedicated)", + "vcpus": 8, + "memory": 32 + }, + "ccx43": { + "name": "CCX43 (16 vCPU, 64 GB RAM, Dedicated)", + "vcpus": 16, + "memory": 64 + }, + "ccx53": { + "name": "CCX53 (32 vCPU, 128 GB RAM, Dedicated)", + "vcpus": 32, + "memory": 128 + }, + "ccx63": { + "name": "CCX63 (48 vCPU, 192 GB RAM, Dedicated)", + "vcpus": 48, + "memory": 192 + } + }, + "images": { + "ubuntu-24.04": { + "name": "Ubuntu 24.04 LTS", + "icon": "/icon/ubuntu.svg" + }, + "ubuntu-22.04": { + "name": "Ubuntu 22.04 LTS", + "icon": "/icon/ubuntu.svg" + }, + "ubuntu-20.04": { + "name": "Ubuntu 20.04 LTS", + "icon": "/icon/ubuntu.svg" + }, + "debian-12": { "name": "Debian 12", "icon": "/icon/debian.svg" }, + "debian-11": { "name": "Debian 11", "icon": "/icon/debian.svg" }, + "centos-stream-9": { + "name": "CentOS Stream 9", + "icon": "/icon/centos.svg" + }, + "fedora-39": { "name": "Fedora 39", "icon": "/icon/fedora.svg" }, + "rocky-9": { "name": "Rocky Linux 9", "icon": "/icon/rockylinux.svg" }, + "alma-9": { "name": "AlmaLinux 9", "icon": "/icon/almalinux.svg" } + } + }, + "availability_by_location": { + "_comment": "Europe has ARM (CAX) + AMD (CX). USA/Asia only have Intel (CPX) + Dedicated (CCX).", + "fsn1": [ + "cax11", + "cax21", + "cax31", + "cax41", + "cpx11", + "cpx21", + "cpx31", + "cpx41", + "cpx51", + "cx22", + "cx32", + "cx42", + "cx52", + "ccx13", + "ccx23", + "ccx33", + "ccx43", + "ccx53", + "ccx63" + ], + "nbg1": [ + "cax11", + "cax21", + "cax31", + "cax41", + "cpx11", + "cpx21", + "cpx31", + "cpx41", + "cpx51", + "cx22", + "cx32", + "cx42", + "cx52", + "ccx13", + "ccx23", + "ccx33", + "ccx43", + "ccx53", + "ccx63" + ], + "hel1": [ + "cax11", + "cax21", + "cax31", + "cax41", + "cpx11", + "cpx21", + "cpx31", + "cpx41", + "cpx51", + "cx22", + "cx32", + "cx42", + "cx52", + "ccx13", + "ccx23", + "ccx33", + "ccx43", + "ccx53", + "ccx63" + ], + "ash": [ + "cpx11", + "cpx21", + "cpx31", + "cpx41", + "cpx51", + "ccx13", + "ccx23", + "ccx33", + "ccx43", + "ccx53", + "ccx63" + ], + "hil": [ + "cpx11", + "cpx21", + "cpx31", + "cpx41", + "cpx51", + "ccx13", + "ccx23", + "ccx33", + "ccx43", + "ccx53", + "ccx63" + ], + "sin": [ + "cpx11", + "cpx21", + "cpx31", + "cpx41", + "cpx51", + "ccx13", + "ccx23", + "ccx33", + "ccx43", + "ccx53", + "ccx63" + ] + } +} diff --git a/registry/melmathari/templates/hetzner-cloud/main.tf b/registry/melmathari/templates/hetzner-cloud/main.tf new file mode 100644 index 000000000..a9e5d0390 --- /dev/null +++ b/registry/melmathari/templates/hetzner-cloud/main.tf @@ -0,0 +1,383 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + } + hcloud = { + source = "hetznercloud/hcloud" + } + } +} + +provider "coder" {} + +# Variable for Hetzner Cloud API token +variable "hcloud_token" { + description = "Hetzner Cloud API token for authentication" + type = string + sensitive = true +} + +# Configure the Hetzner Cloud Provider +provider "hcloud" { + token = var.hcloud_token +} + +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} + +# Load Hetzner Cloud configuration from JSON +locals { + hetzner_config = jsondecode(file("${path.module}/hetzner-config.json")) + + # Generate server type options filtered by selected location + server_type_options_for_selected_location = [ + for type_key in lookup(local.hetzner_config.availability_by_location, data.coder_parameter.location.value, []) : { + name = local.hetzner_config.type_meta.server_types[type_key].name + value = type_key + } + ] +} + +# Hetzner Cloud locations parameter (dynamically generated from JSON) +data "coder_parameter" "location" { + name = "location" + display_name = "Location" + description = "This is the location where your workspace will be created." + icon = "/emojis/1f30e.png" + type = "string" + default = "fsn1" + mutable = false + + dynamic "option" { + for_each = local.hetzner_config.type_meta.locations + content { + name = option.value.name + value = option.key + icon = "/emojis/1f30e.png" + } + } +} + + +# Hetzner Cloud server types parameter (dynamically filtered based on selected location) +data "coder_parameter" "server_type" { + name = "server_type" + display_name = "Server Type" + description = "Which Hetzner Cloud server type would you like to use?" + default = "cx22" + type = "string" + icon = "/icon/memory.svg" + mutable = false + + # Filter server types based on the selected location + dynamic "option" { + for_each = local.server_type_options_for_selected_location + content { + name = option.value.name + value = option.value.value + } + } +} + +# Server image parameter (dynamically generated from JSON) +data "coder_parameter" "server_image" { + name = "server_image" + display_name = "Server Image" + description = "Which operating system image would you like to use?" + default = "ubuntu-22.04" + type = "string" + mutable = false + + dynamic "option" { + for_each = local.hetzner_config.type_meta.images + content { + name = option.value.name + value = option.key + icon = option.value.icon + } + } + +} + +# Optional custom image override +data "coder_parameter" "custom_image_override" { + name = "custom_image_override" + display_name = "Custom Image Override (optional)" + description = "Leave empty to use the selected image above, or enter a custom Hetzner Cloud image name to override (e.g., 'my-custom-snapshot', 'debian-12-amd64')" + type = "string" + default = "" + mutable = false +} + +# Determine which image to use - custom override takes precedence +locals { + final_image = data.coder_parameter.custom_image_override.value != "" ? data.coder_parameter.custom_image_override.value : data.coder_parameter.server_image.value +} + +# Home volume size parameter +data "coder_parameter" "volume_size" { + name = "volume_size" + display_name = "Home Volume Size (GB)" + description = "How large would you like your home volume to be (in GB)?" + type = "number" + default = 20 + mutable = true + + validation { + min = 10 + max = 1000 + monotonic = "increasing" + } +} + +locals { + # Ensure unique names by including workspace ID + server_name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}-${substr(data.coder_workspace.me.id, 0, 8)}" + volume_name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}-${substr(data.coder_workspace.me.id, 0, 8)}-home" + network_name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}-${substr(data.coder_workspace.me.id, 0, 8)}-net" + firewall_name = "coder-${lower(data.coder_workspace_owner.me.name)}-${lower(data.coder_workspace.me.name)}-${substr(data.coder_workspace.me.id, 0, 8)}-fw" + + # Get selected server type and location configuration + selected_server_type = local.hetzner_config.type_meta.server_types[data.coder_parameter.server_type.value] + selected_location = local.hetzner_config.type_meta.locations[data.coder_parameter.location.value] + network_zone = local.selected_location.zone +} + +resource "coder_agent" "main" { + os = "linux" + arch = "amd64" + + metadata { + key = "cpu" + display_name = "CPU Usage" + interval = 5 + timeout = 5 + script = "coder stat cpu" + } + metadata { + key = "memory" + display_name = "Memory Usage" + interval = 5 + timeout = 5 + script = "coder stat mem" + } + metadata { + key = "home" + display_name = "Home Usage" + interval = 600 # every 10 minutes + timeout = 30 # df can take a while on large filesystems + script = "coder stat disk --path /home/${lower(data.coder_workspace_owner.me.name)}" + } +} + +# See https://registry.coder.com/modules/coder/code-server +module "code-server" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/code-server/coder" + + # 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. + version = "~> 1.0" + + agent_id = coder_agent.main.id + order = 1 +} + +# See https://registry.coder.com/modules/coder/jetbrains +module "jetbrains" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/jetbrains/coder" + version = "~> 1.0" + agent_id = coder_agent.main.id + folder = "/home/coder" +} + +variable "ssh_key_id" { + type = number + description = <<-EOF + Hetzner Cloud SSH key ID (obtain via the Hetzner Cloud Console or CLI): + + Can be set to "0" for no SSH key. + + $ hcloud ssh-key list + EOF + sensitive = true + + validation { + condition = var.ssh_key_id >= 0 + error_message = "Invalid Hetzner Cloud SSH key ID, a number is required." + } +} + +# Create private network +resource "hcloud_network" "workspace" { + name = local.network_name + ip_range = "10.0.0.0/16" + + labels = { + "coder.workspace" = data.coder_workspace.me.name + "coder.owner" = data.coder_workspace_owner.me.name + "coder.resource" = "network" + } +} + +# Create network subnet +resource "hcloud_network_subnet" "workspace" { + network_id = hcloud_network.workspace.id + type = "cloud" + network_zone = local.network_zone + ip_range = "10.0.1.0/24" +} + +# Create firewall +resource "hcloud_firewall" "workspace" { + name = local.firewall_name + + labels = { + "coder.workspace" = data.coder_workspace.me.name + "coder.owner" = data.coder_workspace_owner.me.name + "coder.resource" = "firewall" + } + + rule { + direction = "in" + port = "22" + protocol = "tcp" + source_ips = ["0.0.0.0/0", "::/0"] + } + + rule { + direction = "in" + port = "80" + protocol = "tcp" + source_ips = ["0.0.0.0/0", "::/0"] + } + + rule { + direction = "in" + port = "443" + protocol = "tcp" + source_ips = ["0.0.0.0/0", "::/0"] + } + + rule { + direction = "in" + port = "8080" + protocol = "tcp" + source_ips = ["0.0.0.0/0", "::/0"] + } +} + +# Create volume for home directory +resource "hcloud_volume" "home_volume" { + name = local.volume_name + size = data.coder_parameter.volume_size.value + location = data.coder_parameter.location.value + format = "ext4" + + labels = { + "coder.workspace" = data.coder_workspace.me.name + "coder.owner" = data.coder_workspace_owner.me.name + "coder.resource" = "home-volume" + } + + # Protect the volume from being deleted due to changes in attributes + lifecycle { + ignore_changes = all + } +} + +# Create the server +resource "hcloud_server" "workspace" { + count = data.coder_workspace.me.start_count + name = local.server_name + server_type = data.coder_parameter.server_type.value + image = local.final_image + location = data.coder_parameter.location.value + ssh_keys = var.ssh_key_id > 0 ? [var.ssh_key_id] : [] + firewall_ids = [hcloud_firewall.workspace.id] + + labels = { + "coder.workspace" = data.coder_workspace.me.name + "coder.owner" = data.coder_workspace_owner.me.name + "coder.resource" = "workspace-server" + } + + public_net { + ipv4_enabled = true + ipv6_enabled = true + } + + network { + network_id = hcloud_network.workspace.id + ip = "10.0.1.5" + } + + user_data = templatefile("${path.module}/cloud-config.yaml.tftpl", { + hostname = local.server_name + username = lower(data.coder_workspace_owner.me.name) + volume_device = "/dev/sdb" + init_script = base64encode(coder_agent.main.init_script) + coder_agent_token = coder_agent.main.token + }) + + depends_on = [ + hcloud_network_subnet.workspace + ] + + # Proper lifecycle: server is destroyed when workspace stops, but volume persists + lifecycle { + ignore_changes = [ssh_keys, user_data] + } +} + +# Attach volume to server +resource "hcloud_volume_attachment" "home_volume" { + count = data.coder_workspace.me.start_count + volume_id = hcloud_volume.home_volume.id + server_id = hcloud_server.workspace[0].id + automount = true +} + +resource "coder_metadata" "workspace_info" { + count = data.coder_workspace.me.start_count + resource_id = hcloud_server.workspace[0].id + + item { + key = "location" + value = "${local.selected_location.name} (${hcloud_server.workspace[0].location})" + } + item { + key = "server_type" + value = "${local.selected_server_type.name} (${hcloud_server.workspace[0].server_type})" + } + item { + key = "vcpus" + value = local.selected_server_type.vcpus + } + item { + key = "memory" + value = "${local.selected_server_type.memory} GB" + } + item { + key = "image" + value = data.coder_parameter.custom_image_override.value != "" ? data.coder_parameter.custom_image_override.value : local.hetzner_config.type_meta.images[data.coder_parameter.server_image.value].name + } + item { + key = "public_ipv4" + value = hcloud_server.workspace[0].ipv4_address + } +} + +resource "coder_metadata" "volume_info" { + resource_id = hcloud_volume.home_volume.id + + item { + key = "size" + value = "${hcloud_volume.home_volume.size} GB" + } + item { + key = "location" + value = hcloud_volume.home_volume.location + } +}