Terraform module that provisions and manages Hetzner Cloud networks, subnets, and routes.
- Optional creation of a new Hetzner Cloud network or reuse of an existing one
- Ability to reference existing networks by either ID or name when reusing infrastructure
- Support for multiple subnets across network zones and subnet types (
server,cloud, orvswitch) - Optional custom routes for advanced scenarios (VPN gateways, proxy servers)
- Optional delete protection and vSwitch route exposure on the managed network
- Helper outputs with aggregated subnet IDs (per zone and for vSwitch subnets) to simplify downstream wiring
- Consistent outputs that expose identifiers and computed attributes for downstream modules
Network Zone Constraint: All subnets within a single Hetzner Cloud network must be in the same network zone (e.g., eu-central, us-east, us-west, ap-southeast).
What you CAN do:
- ✅ Create network with subnets in any single zone (
eu-central,us-east,us-west,ap-southeast) - ✅ Connect servers from different locations within that zone (e.g., fsn1, nbg1, hel1 all in
eu-central) - ✅ All servers in the network can communicate via private IPs regardless of their specific location
What you CANNOT do:
- ❌ Mix network zones in one network (e.g.,
eu-central+us-eastsubnets in same network) - ❌ Create a single network spanning Europe and USA
If you need resources in multiple network zones: Create separate networks for each zone:
- Network A in
eu-centralfor European resources - Network B in
us-eastfor USA resources - Servers in different networks cannot communicate via private IPs
This is documented in Hetzner Cloud Networks FAQ:
"all locations within a Network have to be from the same network zone as the subnet. All subnets within a Network also have to be from the same network zone."
vSwitch Limitation: vSwitch subnets (for connecting dedicated servers) only work in eu-central network zone. You must create vSwitches in Hetzner Robot first, then reference their IDs in your configuration.
module "hcloud_network" {
source = "danylomikula/network/hcloud"
version = "~> 2.1"
create_network = true
name = "lab-core"
ip_range = "10.10.0.0/16"
labels = {
environment = "lab"
owner = "platform"
}
subnets = {
edge = {
type = "cloud"
network_zone = "eu-central"
ip_range = "10.10.10.0/24"
}
# vSwitch subnet for connecting dedicated servers
# Note: vSwitch ID must be created in Hetzner Robot first
# Only works in eu-central network zone
compute = {
type = "vswitch"
network_zone = "eu-central"
ip_range = "10.10.20.0/24"
vswitch_id = 12345 # from Robot console
}
}
}To reuse an existing network by name and let the module manage only subnets/routes:
module "hcloud_network_existing" {
source = "danylomikula/network/hcloud"
version = "~> 2.0"
create_network = false
existing_network_name = "lab-core-shared"
subnets = {
workers = {
type = "server"
network_zone = "eu-central"
ip_range = "10.10.30.0/24"
}
}
}Review the examples directory for a fully working configuration that you can copy into your environment.
| Directory | Description |
|---|---|
examples/basic |
Creates a new network with both standard and vSwitch subnets. |
examples/reuse-existing |
Reuses an existing network by name and only adds new subnets. Outputs aggregated subnet IDs for quick consumption. |
examples/vswitch-subnets |
Focuses on vSwitch-backed subnets that attach to pre-existing vSwitch IDs supplied via variables. |
examples/routes-only |
Demonstrates managing custom routes on an existing network (e.g., for VPN gateway or proxy server scenarios). |
Networks created outside Terraform (e.g. via the Hetzner Cloud Console) can be imported into this module with an import block. If the network itself should stay unmanaged, skip the import entirely and set create_network = false with existing_network_id or existing_network_name — the module then only manages the subnets and routes it creates.
To adopt the network fully, inspect it with hcloud network describe <network-id> -o json, mirror it in the module input, then verify the first plan shows no changes:
module "network" {
source = "danylomikula/network/hcloud"
version = "~> 2.1"
name = "legacy-core"
ip_range = "10.0.0.0/16"
# Match the live protection flag — otherwise the first plan
# wants to disable it.
delete_protection = true
subnets = {
web = {
type = "cloud"
network_zone = "eu-central"
ip_range = "10.0.1.0/24"
}
}
routes = {
vpn-backhaul = {
destination = "172.16.0.0/12"
gateway = "10.0.1.10"
}
}
}
# Import the network by its ID.
import {
to = module.network.hcloud_network.this[0]
id = "1234567"
}
# Subnets import with "<network_id>-<ip_range>".
import {
to = module.network.hcloud_network_subnet.this["web"]
id = "1234567-10.0.1.0/24"
}
# Routes import with "<network_id>-<destination>" — the gateway
# is not part of the import ID.
import {
to = module.network.hcloud_network_route.this["vpn-backhaul"]
id = "1234567-172.16.0.0/12"
}Import checklist:
- Match
ip_rangeexactly — changing it replaces the network. Verify the first plan shows no destroy/replace actions. - Match
delete_protectionandexpose_routes_to_vswitchto the live flags. Both default tofalse, so importing a protected network withoutdelete_protection = trueleaves a plan diff that wants to disable the protection. - Subnet and route map keys are free-form Terraform addresses — resource identity comes from the import ID, so pick keys that read well and keep them stable.
- Match
labelsto the live network — a mismatch is only a harmless in-place update, but it keeps the first plan from being clean. delete_protectiononly guards against deletion outside of Terraform (Console/API): when Terraform itself destroys the network, the provider lifts the lock first. To protect the network fromterraform destroyas well, use theprevent_destroylifecycle attribute.
| Name | Version |
|---|---|
| terraform | >= 1.14.0 |
| hcloud | >= 1.66.0 |
| Name | Version |
|---|---|
| hcloud | >= 1.66.0 |
No modules.
| Name | Type |
|---|---|
| hcloud_network.this | resource |
| hcloud_network_route.this | resource |
| hcloud_network_subnet.this | resource |
| hcloud_network.existing_by_id | data source |
| hcloud_network.existing_by_name | data source |
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
| create_network | When true, the module creates the Hetzner Cloud network; when false, an existing network ID must be provided. | bool |
true |
no |
| delete_protection | Enable delete protection on the managed network. Has no effect when create_network is false (the module does not modify protection on networks it merely references). | bool |
false |
no |
| existing_network_id | ID of an existing Hetzner Cloud network to reuse when create_network is false. | number |
null |
no |
| existing_network_name | Name of an existing Hetzner Cloud network to reuse when create_network is false. | string |
null |
no |
| expose_routes_to_vswitch | Expose the network routes to the vSwitch connection. Only takes effect when a vSwitch connection is active. Has no effect when create_network is false. | bool |
false |
no |
| ip_range | CIDR of the Hetzner Cloud network (for example 10.0.0.0/16). Required when create_network is true. | string |
null |
no |
| labels | Labels applied to the network and subnets. | map(string) |
{} |
no |
| name | Name for the Hetzner Cloud network. Required when create_network is true. | string |
null |
no |
| routes | Optional map of custom routes to configure inside the network. | map(object({ |
{} |
no |
| subnets | Map of subnet definitions keyed by an arbitrary name. | map(object({ |
{} |
no |
| Name | Description |
|---|---|
| network_id | ID of the Hetzner Cloud network managed (or referenced) by this module. |
| network_ip_range | CIDR of the Hetzner Cloud network. |
| network_labels | Labels currently set on the network object. |
| network_name | Name of the Hetzner Cloud network. |
| routes | Routes configured in the network through this module. |
| subnet_ids_by_zone | Map of network zones to the IDs of subnets created in each zone. |
| subnets | Details about the subnets created by this module. |
| vswitch_subnet_ids | List of subnet IDs that are associated with vSwitch subnets. |
| Module | Description | GitHub | Terraform Registry |
|---|---|---|---|
| terraform-hcloud-server | Manage Hetzner Cloud servers | GitHub | Registry |
| terraform-hcloud-firewall | Manage Hetzner Cloud firewalls | GitHub | Registry |
| terraform-hcloud-ssh-key | Manage Hetzner Cloud SSH keys | GitHub | Registry |
Module managed by Danylo Mikula.
Contributions are welcome! Please read the Contributing Guide for details on the process and commit conventions.
Apache 2.0 Licensed. See LICENSE for full details.