Address plan
Every internal IP range we allocate, what owns it, and where it is declared.
Ranges must not overlap. VPC peering rejects an overlap outright. The tailnet silently routes the wrong way when two environments claim the same space.
Take a new range from here and record it here in the same change (INFRA-6).
Written after the fact
These were chosen ad hoc, one environment at a time. This page documents what exists, not a scheme designed up front. The patterns below are real but unplanned, so check "next free" before using it.
Main subnets
One per environment, europe-west3. Declared in infra/terraform/envs/common.env as
TF_VAR_gcp_main_subnet_ip_cidr_range_<env>, and again in each <env>.env unsuffixed.
| Environment | Range | Notes |
|---|---|---|
production |
10.0.0.0/20 |
|
staging |
10.0.16.0/20 |
Reserved, not allocated. The environment is not built |
| unassigned | 10.0.32.0/20 |
Free |
devel |
10.0.48.0/20 |
/20 on 16-block boundaries. Next free: 10.0.32.0/20, then 10.0.64.0/20.
Private service access
Where Google's managed services (Cloud SQL, Memorystore) are peered in. Declared as
TF_VAR_gcp_private_service_access_ip_cidr_range_<env>.
| Environment | Range |
|---|---|
devel |
10.220.124.0/22 |
| unassigned | 10.221.124.0/22 (free) |
staging |
10.222.124.0/22 (reserved) |
production |
10.223.124.0/22 |
Second octet increments from 220. Next free: 10.221.124.0/22, then 10.224.124.0/22.
Private Service Connect endpoints
One address per environment, not a range. Declared in infra/inventory/<env>/config.yml as
LYC_GCP_PRIVATE_SERVICE_CONNECT_IP_ADDRESS, mirrored per environment in
infra/inventory/common/config.yml.
| Environment | Address |
|---|---|
production |
10.3.0.5 |
| unassigned | 10.3.0.6 (free) |
devel |
10.3.0.7 |
staging |
10.3.0.8 (reserved) |
All in 10.3.0.0/24, reserved for this.
Inbound DNS resolver
The GCP inbound forwarding address the tailnet resolves internal names through. It comes out of the
environment's own main subnet, so it needs no separate reservation. Declared as
LYC_GCP_INBOUND_DNS_RESOLVE_ADDR, and for staging as
TF_VAR_gcp_inbound_dns_resolve_addr_staging in common.env.
| Environment | Address |
|---|---|
production |
10.0.0.23 |
devel |
10.0.48.92 |
staging |
10.0.16.2 |
k3s load-balancer VIP pool
The pool the k3s server hands to LoadBalancer services. Declared in
infra/inventory/<env>/config.yml as LYC_K3S_SERVER_LB_IPPOOL_CIDR.
| Environment | Range |
|---|---|
production |
10.41.0.0/24 |
| unassigned | 10.43.0.0/24 (free) |
devel |
10.45.0.0/24 |
k3s cluster and service CIDRs
The pod and service ranges the k3s server uses. Declared in infra/inventory/<env>/config.yml as
LYC_K3S_SERVER_CLUSTER_CIDR and LYC_K3S_SERVER_SERVICE_CIDR.
| Environment | Cluster | Service |
|---|---|---|
production |
10.42.0.0/16 |
10.43.0.0/16 |
| unassigned | 10.44.0.0/16 (free) |
10.45.0.0/16 (free) |
devel |
10.46.0.0/16 |
10.47.0.0/16 |
These interleave with the VIP pools above, so pools sit inside a /16 recorded here. The free
10.43.0.0/24 is inside production's service CIDR, and devel's 10.45.0.0/24 is inside the free
10.45.0.0/16. Both are cluster-internal and unrouted, so nothing conflicts today, but an
environment that takes the free pair would put its service CIDR around devel's VIP pool.
Next free pair: 10.48.0.0/16 and 10.49.0.0/16.
Cluster-internal, never routed onto the tailnet.
Tailscale
The tailnet uses Tailscale's CGNAT range, 100.64.0.0/10. A route for it is created in
infra/terraform/_modules/gcp/tailscale/routing.tf. We allocate nothing inside it.
Subnet routers advertise the environment's main subnet, its private-service-access range and its PSC
address as a /32. See infra/ansible/playbooks/tailscale-subnet-router.yml.
Every environment's routes go into one tailnet. Two environments with overlapping ranges therefore break each other's routing, not just their own. That is why the no-overlap rule is not negotiable.
Adding an environment
- Take the next free main subnet, private-service-access range and PSC address from above.
- Add them as
TF_VAR_gcp_*_<env>entries incommon.envand unsuffixed in<env>.env. Consumers reading the_${lyceum_env}form get an empty value until thecommon.enventries exist, which surfaces as unrelated DNS and PSC errors. - Record them here, in the same pull request.