ipset-load-balancing · diff
git:20260612.b890dfa to git:20260612.0083049
69 added, 186 removed. Audit A to A.
---
name: ipset-load-balancing
- description: "Reserves static IP addresses and configures load balancers on Control Plane. Use when the user asks about IP sets, static or fixed IPs, dedicated or direct load balancers, IP whitelisting, or egress IPs."
+ description: "Static IPs and load balancers on Control Plane. Use when the user asks about IP sets, fixed IPs, direct or dedicated load balancers, exposing raw TCP/UDP ports, IP allowlisting, geo headers, or egress IPs."
---
# IP Sets & Load Balancing
> **Tool availability:** some MCP tools named here live in the `full` toolset profile — if one is not advertised on this connection, tell the user to reconnect the MCP server with `?toolsets=full` (or use the `cpln` CLI fallback). Reads and deletes work on every profile via the generic `list_resources` / `get_resource` / `delete_resource` tools.
- Deep detail for IP sets plus direct and dedicated load balancers. For the LB-type picker basics and routing, see the `workload` skill — this skill carries the full configuration.
-
- ## Load Balancer Types
-
- | Type | Scope | Custom Ports | Static IPs | Wildcard Hosts | Protocols | Extra Cost |
- |---|---|---|---|---|---|---|
- | Default (shared) | All workloads | No | No | No | HTTP/HTTPS only | No |
- | Direct | Per-workload | Yes (22-32768) | Via IP Set | No | TCP, UDP, HTTP, HTTPS, WS, WSS | Yes |
- | Dedicated | Per-GVC | Yes | Via IP Set | Yes | All (via Domains) | Yes |
-
- - **Default (shared):** all workloads, no config, HTTP/HTTPS on 80/443 only.
- - **Direct:** per-workload, custom port mappings, Geo DNS routing, optional static IPs; you manage TLS certs. Set with `mcp__cpln__configure_workload_load_balancer`.
- - **Dedicated:** per-GVC, enables custom ports on domains, wildcard hostnames, accept-all-hosts, and redirect rules. A GVC setting — enable `spec.loadBalancer.dedicated` with `mcp__cpln__update_gvc`.
-
- ## IP Sets
-
- An IP set reserves a **static public IP address in each location** of a GVC. Use them when external partners need to allowlist your IPs, or compliance requires fixed egress addresses.
-
- ### How IP Sets Work
-
- 1. Create an IP set with target locations and a link to a workload or GVC.
- 2. The target workload/GVC must link back to the IP set (**bidirectional**).
- 3. Control Plane allocates one public IP per location.
- 4. IPs persist across deployments as long as `retentionPolicy: keep`.
+ An IP set reserves one static public IPv4 address per location and attaches it to a **direct** (per-workload) or **dedicated** (per-GVC) load balancer. The linking is bidirectional, and the recurring failure is configuring only one side: the IP set's `spec.link` must point at the workload/GVC AND that target's load balancer must reference the IP set back — otherwise addresses sit `unbound` and the IP set carries `status.warning: Cross-link misconfiguration`. The `workload` skill is primary for the LB-type picker and routing basics; this skill carries the full configuration.
- ### IP Set Spec
+ ## Load balancer types
- | Field | Type | Required | Description |
+ | Type | Scope | What it adds | Cost |
|---|---|---|---|
- | `spec.link` | string | No | Link to workload (`//gvc/NAME/workload/NAME`) or GVC (`//gvc/NAME`) |
- | `spec.locations[].name` | string | Yes | Location reference (e.g. `//location/aws-us-west-2`) |
- | `spec.locations[].retentionPolicy` | string | Yes | `keep` (retain the IP — reserved IPs bill while kept) or `free` (release the IP and stop charges) |
-
- **Status fields:** `status.ipAddresses[]` contains `name` (location), `ip` (public IP), `id` (cloud allocation ID), `state` (`bound` or `unbound`), and `created` (ISO 8601).
+ | Default (shared) | every workload | HTTP/HTTPS on 80/443, nothing to configure | included |
+ | Direct | one workload | raw TCP/UDP on external ports 22-32768, static IPs, TLS passthrough | charged while enabled |
+ | Dedicated | whole GVC | domain custom ports and TCP routing, wildcard and accept-all hosts, redirects, trusted proxies, static IPs | per location (multiZone adds cross-zone charges) |
- ### Create an IP Set
+ Toggling the `direct` block (workload) or `dedicated` flag (GVC) requires the **`configureLoadBalancer` permission** on that resource — `edit` does not imply it (403 "Not allowed to change loadBalancer configuration"); `manage` covers it.
- Prefer `mcp__cpln__create_ipset` — pass `name`, optional `link` (workload/GVC), and `locations[]`. Read with `mcp__cpln__list_resources` (kind="ipset") / `mcp__cpln__get_resource` (kind="ipset"). Fall back to `cpln apply -f ipset.yaml --org MY_ORG` when MCP is unavailable, or in CI/CD.
+ ## IP sets
```yaml
kind: ipset
- name: my-static-ips
- description: Static IPs for partner allowlisting
+ name: partner-ips
spec:
- link: //gvc/my-gvc
+ link: //gvc/GVC/workload/WORKLOAD # or //gvc/GVC for a dedicated LB
locations:
- name: //location/aws-us-west-2
- retentionPolicy: keep
- - name: //location/aws-eu-west-1
- retentionPolicy: keep
+ retentionPolicy: keep # keep | free
```
- ### Release an IP
-
- Set `retentionPolicy: free` to release an allocated IP and stop charges. Use `mcp__cpln__add_ipset_location` with `retentionPolicy: free` (it overwrites an already-configured location's policy). An IP is not released until it is no longer in use (no linked workload, GVC location not active).
-
- ### Manage an IP Set
-
- Typed MCP tools map one-to-one to these operations — see the **MCP Tools** table below (`create_ipset`, `get_resource`/`list_resources` (kind="ipset"), `add_ipset_location`, `remove_ipset_location`, `update_ipset`, `delete_resource` (kind="ipset")).
-
- CLI fallback when MCP is unavailable or unauthenticated:
-
- ```bash
- cpln ipset create --name my-ips --link //gvc/my-gvc \
- --location aws-us-west-2,keep --location aws-eu-west-1,keep --org MY_ORG
- cpln ipset get my-ips --org MY_ORG -o yaml
- cpln ipset add-location my-ips --location aws-ap-southeast-1,keep --org MY_ORG
- cpln ipset update-location my-ips --location aws-us-west-2,free --org MY_ORG # release an IP
- cpln ipset remove-location my-ips --location aws-ap-southeast-1 --org MY_ORG
- cpln ipset delete my-ips --org MY_ORG
- ```
+ How allocation actually works:
- **Location format:** `location-name,retention-policy` (default retention: `keep`).
+ - **IPs are allocated in the locations of the linked GVC** (for a workload link, the workload's GVC). No `spec.link`, no allocation — `spec.locations` alone does nothing.
+ - `spec.locations` pins a per-location `retentionPolicy`; unlisted locations behave as `keep` while in the GVC. Workload links require the GVC segment — `//workload/WORKLOAD` without it is rejected.
+ - `keep` (default) allocates eagerly and holds the IP through unlinking, GVC location removal, and target deletion (state drops to `unbound`, billing continues until the IP set is deleted). `free` allocates only while bound and releases once the location leaves the GVC or the link/target goes away.
+ - **Flipping `keep` to `free` does not release an IP whose location is still active in the GVC.** To stop charges: detach the binding (`update_ipset` with `removeLink: true`) so `free` locations release, then delete the IP set to release the rest.
+ - `state: bound` means both sides point at each other; `unbound` means allocated but unused. Delete is **blocked with 400 while any address is bound** — remove the back-link first. Re-adding a location later does NOT return the same IP.
+ - Supported on AWS (Elastic IP), GCP (static external address, STANDARD network tier), and Azure (static public IPv4), including BYOK on those clouds. Other providers fail with `status.error` "provider not configured to use IpSets"; cloud IP-quota errors also land in `status.error`.
- ## Direct Load Balancer (Per-Workload)
+ ## Direct load balancer (per workload)
- Creates a load balancer in each location where the workload runs, with Geo DNS latency-based routing across locations. No domain registration required.
+ One cloud L4 load balancer per location running the workload, with `externalTrafficPolicy: Local` so the client IP reaches the workload. No TLS termination — the workload owns its certificates. No domain registration needed: each location's address is published on the workload's canonical endpoint DNS with latency-based geo routing, and `status.canonicalEndpoint` switches to the **first** port's `scheme://HOST:externalPort`. Custom hostnames can CNAME to that endpoint. Inbound firewall CIDRs still apply — they become cloud-level source ranges on the LB.
```yaml
spec:
loadBalancer:
direct:
enabled: true
+ ipSet: //ipset/partner-ips # optional static IPs; that IP set must link back to this workload
ports:
+ - externalPort: 5432 # 22-32768
+ protocol: TCP # TCP or UDP
+ containerPort: 5432 # plain number 80-65535; reserved: 8012, 8022, 9090, 9091, 15000, 15001, 15006, 15020, 15021, 15090, 41000
- externalPort: 443
protocol: TCP
- scheme: https
- containerPort: 8443
- - externalPort: 9000
- protocol: UDP
- containerPort: 9000
+ scheme: https # display-only (http|tcp|https|ws|wss): sets the URL scheme shown in UI/status
```
- ### Port Configuration
-
- | Field | Type | Required | Constraints | Description |
- |---|---|---|---|---|
- | `externalPort` | integer | Yes | 22-32768 | Publicly exposed port |
- | `protocol` | string | Yes | `TCP` or `UDP` | Transport protocol |
- | `scheme` | string | No | `http`, `tcp`, `https`, `ws`, `wss` | Overrides default `https` for UI/status links |
- | `containerPort` | integer | No | 80-65535 | Container listening port |
-
- **Reserved container ports** (cannot be used): 8012, 8022, 9090, 9091, 15000, 15001, 15006, 15020, 15021, 15090, 41000.
-
- ### Attach Static IPs via IP Set
-
- Both the workload and the IP set must reference each other (bidirectional):
-
- **Workload:** add `ipSet: //ipset/my-static-ips` under `spec.loadBalancer.direct` (alongside `enabled` and `ports`).
-
- **IP Set:** set `spec.link: //gvc/my-gvc/workload/my-workload` with the matching `locations[]`.
-
- ### Geo Location Headers
-
- Inject client geo data into HTTP request headers using MaxMind GeoLite2:
-
- ```yaml
- spec:
- loadBalancer:
- geoLocation:
- enabled: true
- headers:
- asn: X-GeoIP-ASN
- city: X-GeoIP-City
- country: X-GeoIP-Country
- region: X-GeoIP-Region
- ```
+ Set with `mcp__cpln__configure_workload_load_balancer` — it replaces the whole `spec.loadBalancer` block (`remove: true` clears it) and rolls a new deployment (about a minute).
- - Requires at least one header when enabled; all header names must be unique.
- - Only works on workloads exposing an HTTP port. Header name max length: 128 characters.
+ ### Geo location headers (`spec.loadBalancer.geoLocation`)
- ### Replica Direct (Stateful Workloads)
+ Injects MaxMind GeoLite2 client-location headers on inbound HTTP requests — works with any LB type, no effect on non-HTTP ports. Set `enabled: true` plus `headers` naming at least one of `asn`/`city`/`country`/`region` (names unique, max 128 chars each). Matching client-sent headers are replaced, so apps can trust the values; the country header carries the two-letter ISO code. Filtering on these headers (geo blocking) lives in the `firewall-networking` skill.
- Address individual replicas via subdomain `replica-<index>`. Only valid for `type: stateful` workloads.
+ ### Replica direct (`spec.loadBalancer.replicaDirect: true`)
- ```yaml
- spec:
- type: stateful
- loadBalancer:
- replicaDirect: true
- direct:
- enabled: true
- ports:
- - externalPort: 5432
- protocol: TCP
- containerPort: 5432
- ```
+ Stateful workloads only (rejected for other types, including `vm`), capped by a separate quota of **6 replicas per workload**. Each replica becomes addressable as `replica-INDEX.` on the workload's endpoints; internal names appear in `status.replicaInternalNames`. Per-replica custom-domain routing is in the `domain` skill; replica identities and database patterns in `stateful-storage`.
- ## Dedicated Load Balancer (Per-GVC)
+ ## Dedicated load balancer (per GVC)
- Creates a dedicated load balancer in each GVC location, enabling features unavailable with the shared LB: custom ports on domains, wildcard hostnames, accept-all-hosts, and redirect rules.
+ A GVC setting — set with `mcp__cpln__update_gvc` (the `loadBalancer` object is replaced wholesale):
```yaml
spec:
loadBalancer:
dedicated: true
- trustedProxies: 1
- multiZone:
- enabled: true
+ ipSet: //ipset/gvc-ips # optional; that IP set must link back to //gvc/GVC
+ trustedProxies: 0 # 0 (default) source client IP | 1 last X-Forwarded-For address | 2 second-to-last; sets the logged IP and X-Envoy-External-Address
+ multiZone: { enabled: false } # cross-zone load balancing, extra charges
redirect:
class:
- status5xx: https://error.example.com
- status401: https://auth.example.com/login?return_to=%REQ(:path)%
- ipSet: //ipset/my-gvc-ips
+ status5xx: https://errors.example.com # any 500-level response (must be a valid URI)
+ status401: https://auth.example.com/login?return_to=%REQ(:path)% # supports Envoy format strings
```
- ### Fields
+ Required before domains can use custom ports or the TCP protocol (without it those deploy as warnings and never route) and for wildcard / accept-all hosts — details in the `domain` skill. Enabling or disabling it can cause a brief connectivity blip while DNS propagates. Its access logs are queryable as `{gvc="GVC", workload="_loadbalancer"}`.
- | Field | Type | Default | Valid Values | Description |
- |---|---|---|---|---|
- | `dedicated` | boolean | `false` | `true`, `false` | Enable dedicated LB per location |
- | `trustedProxies` | integer | `0` | `0`, `1`, `2` | How to determine client IP (see below) |
- | `multiZone.enabled` | boolean | `false` | `true`, `false` | Cross-zone load balancing (extra charges) |
- | `ipSet` | string | - | IP set link | Reference to IP set for static IPs |
- | `redirect.class.status5xx` | string | - | Valid URI | Redirect URL for 500-level errors |
- | `redirect.class.status401` | string | - | String | Redirect for 401; supports Envoy format strings |
+ ## Verify
- ### Trusted Proxies
+ - `mcp__cpln__get_resource` (kind="ipset") — every `status.ipAddresses[].state` is `bound`, and no `status.warning` (cross-link) or `status.error` (provider/quota). Share the `ip` values only once bound.
+ - `mcp__cpln__list_deployments` — all locations ready after an LB change; the workload's `status.canonicalEndpoint` reflects the direct-LB scheme and port.
+ - CLI fallback (CI/CD): `CPLN_TOKEN` + `cpln ipset get NAME --org ORG -o yaml`.
- Controls the IP used for request logging and the `X-Envoy-External-Address` header.
+ ## Troubleshooting
- | Value | Behavior |
+ | Symptom | Cause and fix |
|---|---|
- | `0` | Use source client IP address (default) |
- | `1` | Use last address in `X-Forwarded-For` header |
- | `2` | Use second-to-last address in `X-Forwarded-For` header |
-
- ### Attach Static IPs via IP Set
-
- Both the GVC and IP set must reference each other (bidirectional):
-
- **GVC:** set `spec.loadBalancer.dedicated: true` and `spec.loadBalancer.ipSet: //ipset/my-gvc-ips`.
-
- **IP Set:** set `spec.link: //gvc/my-gvc` with the matching `locations[]`.
-
- ## Common Patterns
-
- - **Static IPs for partner allowlisting:** create an IP set linked to the workload or GVC → configure the direct or dedicated LB → poll `get_resource` (kind="ipset") until each `status.ipAddresses[].state` is `bound` → share those IPs → keep `retentionPolicy: keep` so IPs don't change.
- - **Non-HTTP service exposure** (databases, game servers, custom protocols): a direct LB with `protocol: TCP`/`UDP` ports (see the direct LB example above).
- - **Consistent IPs across a GVC:** a dedicated LB with an IP set at the GVC level — all workloads share the same static IPs per location.
-
- ## Quick Reference
+ | IPs stay `unbound`, warning `Cross-link misconfiguration: /org/...` | Only one side is linked — the object named in the warning points here without a matching `spec.link` (or vice versa); configure both sides |
+ | No IPs allocated at all | `spec.link` missing (locations alone allocate nothing), or the linked GVC has no locations |
+ | Delete fails 400 "one or more ip addresses are bound" | Remove the workload/GVC back-link or pass `removeLink: true` to `update_ipset`, wait for `unbound`, delete again |
+ | Still billed after setting `free` | The location is still active in the GVC — `free` releases only when it leaves the GVC or the IP set is unlinked |
+ | `status.error` "provider not configured to use IpSets" | That location's cloud has no IP-set support (AWS, GCP, Azure only — including BYOK on them) |
+ | `status.error` AddressLimitExceeded / QUOTA_EXCEEDED / PublicIPCountLimitReached | Cloud-account IP quota exhausted in that region — request an increase from the provider |
+ | 403 "not granted [configureLoadBalancer]" | Toggling direct/dedicated needs that permission — `edit` alone is not enough |
+ | API rejects `containerPort` | It is a plain number (80-65535 minus reserved ports); the docs' `containerPort: {port: N}` object form is wrong |
+ | Deploy warning "TCP access can only be restricted to specific ip addresses when using a custom domain and the GVC has dedicated loadBalancer enabled" | Inbound CIDR rules on a TCP port need the dedicated LB (custom domain) or a direct LB — the shared LB cannot enforce them |
- ### MCP Tools
+ ## Quick reference
| Tool | Purpose |
|---|---|
- | `mcp__cpln__create_ipset` | Create an IP set with optional `link` (workload/GVC) and `locations[]` (`name`, `retentionPolicy`). |
- | `mcp__cpln__list_resources` (kind="ipset") | List all IP sets with locations, retention policies, and allocated IPs (read-only). |
- | `mcp__cpln__get_resource` (kind="ipset") | Inspect one IP set: bound link, locations, retention, allocated IPs (`bound`/`unbound`), status. |
- | `mcp__cpln__add_ipset_location` | Add new locations or overwrite the retentionPolicy of existing ones (use `free` to release an IP). |
- | `mcp__cpln__remove_ipset_location` | Drop one or more locations from the IP set entirely (DESTRUCTIVE). |
- | `mcp__cpln__update_ipset` | Edit description, tags, or bound workload/GVC link; pass `removeLink: true` to detach. |
- | `mcp__cpln__delete_resource` (kind="ipset") | Delete an IP set, releasing every reserved IP (DESTRUCTIVE — confirm blast radius first). |
-
- Location names accept friendly names (`"frankfurt"`, `"tel aviv"`), location IDs (`"aws-us-west-2"`), or full links (`"//location/aws-us-west-2"`) — the MCP server resolves them automatically.
-
- Configure a **workload's** load balancer (direct, geo headers, replicaDirect) with `mcp__cpln__configure_workload_load_balancer`. A **GVC-level dedicated** LB is a GVC setting — enable `spec.loadBalancer.dedicated` with `mcp__cpln__update_gvc` (or `cpln apply`).
-
- ### Example: create an IP set bound to a workload
+ | `mcp__cpln__create_ipset` | Create with optional `link` and `locations[]` (`retentionPolicy` defaults to `keep`); friendly location names resolve server-side |
+ | `mcp__cpln__update_ipset` | Description, tags, replace `link`, or `removeLink: true` to detach |
+ | `mcp__cpln__add_ipset_location` | Add locations or overwrite an existing location's `retentionPolicy` |
+ | `mcp__cpln__remove_ipset_location` | Drop location entries (two-phase confirm; releases only IPs whose location is no longer active in the GVC) |
+ | `mcp__cpln__list_resources` / `mcp__cpln__get_resource` / `mcp__cpln__delete_resource` (kind="ipset") | Read, and delete (releases every IP; blocked while bound) |
+ | `mcp__cpln__configure_workload_load_balancer` | Workload side: `direct`, `geoLocation`, `replicaDirect` (`remove: true` clears) |
+ | `mcp__cpln__update_gvc` | GVC side: `loadBalancer` (dedicated, ipSet, trustedProxies, multiZone, redirect) |
- ```json
- {
- "name": "my-static-ips",
- "link": "//gvc/my-gvc/workload/my-workload",
- "locations": [
- { "name": "aws-us-west-2", "retentionPolicy": "keep" },
- { "name": "frankfurt", "retentionPolicy": "keep" }
- ]
- }
- ```
+ CLI fallback: `cpln ipset create --name NAME --link LINK --location LOC,POLICY`, plus `add-location` / `update-location` / `remove-location REF --location ...` and `get` / `delete`. `cpln gvc update --set` cannot reach `spec.loadBalancer` — use `cpln gvc edit` or `cpln apply`.
- ### Related Skills
+ ### Related skills
- - **cpln-workload** — Start here: the primary workload skill (types, defaults, spec shape) that routes here for load balancers & static IPs.
- - **cpln-firewall-networking** — Firewall rules, CIDR filtering, and load balancer type overview
- - **cpln-workload-security** — Direct load balancer security, JWT auth, mTLS
- - **cpln-native-networking** — PrivateLink, Private Service Connect, agent-based connectivity
- - **cpln-stateful-storage** — Replica direct addressing for stateful workloads
+ - **workload** — the primary skill: LB-type picker, container ports, endpoints, the `configure_workload_*` tools.
+ - **domain** — custom domains, custom ports and TCP routes on the dedicated LB, per-replica routing.
+ - **firewall-networking** — inbound/outbound CIDR rules, header filtering on geo headers.
+ - **workload-security** — TLS on the workload behind a direct LB, JWT auth, mTLS.
+ - **stateful-storage** — replica identities and replica-direct with databases.
## Documentation
- [IP Set Reference](https://docs.controlplane.com/reference/ipset.md)
- [Load Balancing Reference](https://docs.controlplane.com/reference/workload/load-balancing.md)
- [GVC Reference (Dedicated LB)](https://docs.controlplane.com/reference/gvc.md)
- [Domain Reference](https://docs.controlplane.com/reference/domain.md)