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
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, not203.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 single0.0.0.0/0you wrote.next_hop(String) Next hop: an address on a subnet attached to this VPC, of the same address family asdestination— or the reserved tokeninternet.internetis 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 afrostmoln_gatewayand let this route depend on it.Traffic sent via
internetleaves 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:
# 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.