Skip to content

frostmoln_vpc_route (Resource) ​

Manages one static route on a VPC in the Frostmoln Cloud Platform.

ONE RESOURCE IS ONE ROUTE, deliberately. A whole-table resource would be authoritative over a set that also contains platform-owned routes the tenant can neither see nor delete, so it would fight the platform and show a diff that never converges.

A route sends traffic for destination to next_hop. The next hop is an address on a subnet attached to the VPC, or the reserved token internet, which means "out this VPC's own internet gateway". next_hop is therefore NOT validated as an IP address.

A route is matched on its DESTINATION ONLY — a VPC router has no per-source routing — so a route applies to every instance in the VPC, not to one of them. That is what makes the forced-tunnel pattern (0.0.0.0/0 to an appliance, plus <peer>/32 to internet so the appliance's own tunnel still works) take every instance off the tunnel for the peer's address, not just the appliance.

Platform-owned routes are invisible here: they are not listed, cannot be created, and cannot be imported.

Every attribute forces replacement. A route has no server-side identity beyond its destination and there is no in-place update for one, so a change destroys and re-creates the route.

Authoritative scope — who owns what on this resource, declared in internal/scopedecl and machine-checked against the schema.

Create-immutable — destination, next_hop, vpc_id: a route has no server-side identity beyond its destination and there is no in-place update for one, so a change destroys and re-creates the route.

Platform-invented default — platform-owned VPC routes (the system routes DNS and managed services ride): keep-with-docs. Invisible by design — not listed, cannot be created, cannot be imported; the frostmoln_vpc_routes data source sees exactly the tenant-visible table.

Example Usage ​

terraform
resource "frostmoln_vpc" "main" {
  name = "my-vpc"
  cidr = "10.0.0.0/16"
}

resource "frostmoln_subnet" "app" {
  name   = "app"
  cidr   = "10.0.1.0/24"
  vpc_id = frostmoln_vpc.main.id
  zone   = "falkenberg"
}

# The appliance the routes below point at.
resource "frostmoln_instance" "appliance" {
  name      = "appliance"
  flavor_id = data.frostmoln_flavor.medium.id
  image_id  = data.frostmoln_image.ubuntu.id
  vpc_id    = frostmoln_vpc.main.id
  subnet_id = frostmoln_subnet.app.id
}

# Reach a partner network through the appliance.
#
# The next hop is the appliance's COMPUTED private_ip, never a literal
# address: the reference is what lets the graph see that the route needs the
# instance (and, through it, the subnet) — a literal "10.0.1.10" plus
# depends_on says the same thing by hand, goes stale when the appliance is
# replaced, and can tie the instance and the route into a dependency cycle
# when the instance's own configuration references the route's VPC.
resource "frostmoln_vpc_route" "partner" {
  vpc_id      = frostmoln_vpc.main.id
  destination = "203.0.113.0/24"
  next_hop    = frostmoln_instance.appliance.private_ip
}

# Route the whole VPC through a VPN appliance, and keep the appliance's own
# tunnel working.
#
# Without the second route the appliance's encrypted packets to its remote peer
# match the default route and loop back to it. A route is matched on its
# DESTINATION only — a VPC router has no per-source routing — so the exception
# takes every instance in the VPC off the tunnel for that peer's address, not
# just the appliance.
#
# A default route captures ALL egress from the VPC. Public IPs on instances here
# stop serving while it exists: the reply is routed by this same table, and its
# destination is the internet client. DNS and managed services keep working.
resource "frostmoln_gateway" "main" {
  vpc_id = frostmoln_vpc.main.id
  mode   = "public_ip"
}

resource "frostmoln_vpc_route" "forced_tunnel" {
  vpc_id      = frostmoln_vpc.main.id
  destination = "0.0.0.0/0"
  next_hop    = frostmoln_instance.appliance.private_ip
}

resource "frostmoln_vpc_route" "tunnel_peer_exception" {
  vpc_id      = frostmoln_vpc.main.id
  destination = "198.51.100.7/32"

  # `internet` is a reserved token, not an address: "out this VPC's own internet
  # gateway". It is the one place a literal next_hop is still correct — no
  # attribute can express the route, because the platform's own default gateway
  # has no address a customer could name. It needs the gateway to exist, hence
  # the dependency — without it the write is refused with
  # ROUTE_NO_INTERNET_GATEWAY.
  next_hop = "internet"

  depends_on = [frostmoln_gateway.main]
}

Schema ​

Required ​

  • destination (String) Destination CIDR, in canonical (masked) form — 203.0.113.0/24, not 203.0.113.5/24. An IPv4 default route (0.0.0.0/0) is permitted; an IPv6 one (::/0) is NOT offered and is refused. A destination that falls inside a platform route, or inside a subnet attached to this VPC, is refused.

    A DEFAULT ROUTE (0.0.0.0/0) CAPTURES ALL EGRESS from the VPC, and two consequences follow. Public IPs on instances in this VPC stop serving while it exists — the reply is routed by the same table, and its destination is the internet client — so plan it alongside anything that depends on inbound traffic. DNS and managed services keep working: they ride platform routes that out-specify a default route. And it counts as TWO against the VPC's route limit, because the platform stores it as two equivalent halves and reports it back as the single 0.0.0.0/0 you wrote.

  • next_hop (String) Next hop: an address on a subnet attached to this VPC, of the same address family as destination — or the reserved token internet.

    internet is resolved by the platform when the route is written and is read back as the token, never as the address it resolved to, so a gateway rebuild cannot leave a stale literal in state. It requires the VPC to have an internet gateway; add a frostmoln_gateway and let this route depend on it.

    Traffic sent via internet leaves through the platform's own gateway and is source-NAT'd to an address the platform does not publish. If the far end pins or allow-lists your source address, attach a public IP to the instance instead.

  • vpc_id (String) The ID of the VPC this route belongs to.

Read-Only ​

  • id (String) The identifier of the route, {vpc_id}/{destination}. A route has no server-side id: a destination is unique within a VPC and identifies it.

Import ​

Import is supported using the following syntax:

The terraform import command can be used, for example:

shell
# Import a VPC route by {vpc_id}/{destination}.
terraform import frostmoln_vpc_route.partner "vpc-abc123/203.0.113.0/24"

# The destination is a CIDR and contains a slash of its own; only the FIRST
# slash separates the VPC id from the destination.
#
# This is how a route Terraform does not already own is brought under
# management — one added through the portal, the fm CLI or the API. Platform-owned
# routes cannot be imported: they are not part of the tenant's route set, a read
# does not list them, and a delete for one answers ROUTE_NOT_FOUND.