Summary
When using a private Load Balancer in a Scaleway Kapsule cluster (via the service.beta.kubernetes.io/scw-loadbalancer-private: "true" annotation), there is currently no way to assign a fixed private IP address to the Load Balancer's Private Network interface. The CCM always lets Scaleway allocate a random IP from the Private Network's DHCP pool.
This is a blocking limitation for hybrid connectivity scenarios where an on-premises datacenter (connected via Scaleway InterLink or VPN site-to-site) needs to reach services inside the Kapsule cluster through a stable, predictable private IP — for example, to configure static routes, firewall rules, or DNS records on the datacenter side.
Context
Use case
In a typical hybrid architecture:
- Kapsule → Datacenter: straightforward — CoreDNS forward zones can resolve datacenter hostnames.
- Datacenter → Kapsule: requires a private Load Balancer with a stable IP that the datacenter can target. A DHCP-assigned IP that may change when the LB is recreated (e.g. during a Helm chart upgrade or a service re-creation) is not acceptable in production.
What the Scaleway API already supports
The Scaleway LB API already supports assigning a pre-reserved IPAM IP to a Load Balancer's Private Network attachment via the ipam_ids field of the AttachPrivateNetwork endpoint:
IPAM ID of a pre-reserved IP address to assign to the Load Balancer on this Private Network.
When null, a new private IP address is created for the Load Balancer on this Private Network.
This means no API-side changes are required — the capability exists today. Only the CCM needs to be updated to expose and use it.
Industry context
For reference, all three major cloud providers natively support fixed private IPs on internal/private load balancers from Kubernetes service annotations, with full dynamic backend management:
| Provider |
Mechanism |
| AWS (EKS) |
service.beta.kubernetes.io/aws-load-balancer-private-ipv4-addresses |
| GCP (GKE) |
spec.loadBalancerIP or networking.gke.io/load-balancer-ip-addresses |
| Azure (AKS) |
service.beta.kubernetes.io/azure-load-balancer-ipv4 or spec.loadBalancerIP |
Current behavior
In EnsureLoadBalancer() (scaleway/loadbalancers.go), the following guard clause explicitly rejects any combination of private LB + static IP:
if lbPrivate && hasLoadBalancerStaticIPs(service) {
return nil, fmt.Errorf("scaleway-cloud-controller-manager can only handle static IPs for public load balancers. Unsetting the static IP can result in the loss of the IP")
}
Additionally, in createLoadBalancer(), the call to getLoadBalancerStaticIPIDs() is gated behind if !lbPrivate, and the attachPrivateNetworks() function never populates the IpamIds field of ZonedAPIAttachPrivateNetworkRequest.
Proposed solution
Introduce a new annotation, for example:
service.beta.kubernetes.io/scw-loadbalancer-private-ipam-id: "<ipam-ip-uuid>"
This annotation would accept the UUID of an IP pre-reserved in Scaleway IPAM (from the Private Network's subnet).
Required changes in loadbalancers.go
The changes are minimal and localized to a small number of well-identified locations:
| # |
Location |
Change |
| 1 |
EnsureLoadBalancer() — guard clause |
Replace hard rejection with a branch: if the annotation is a private IPAM ID → allow, otherwise keep existing error for public flexible IPs on private LBs |
| 2 |
createLoadBalancer() |
Add a branch for lbPrivate case to read the new annotation and propagate the IPAM ID to the next step |
| 3 |
attachPrivateNetworks() |
Populate IpamIds: []string{ipamID} in ZonedAPIAttachPrivateNetworkRequest when the annotation is set |
| 4 |
deleteLoadBalancer() |
Extend ReleaseIP logic: do not release the IPAM IP when the new annotation is present (mirrors existing behavior for public flex IPs) |
| 5 |
EnsureLoadBalancer() — drift detection |
Add a privateIPAMMismatch check analogous to reservedIPMismatch, to trigger LB recreation if the IPAM IP has changed |
| 6 |
Annotations |
Add the new constant and document it in docs/loadbalancer-annotations.md |
Notes
- The
spec.loadBalancerIP field is not a viable alternative here: the CCM source code explicitly restricts it to public flexible IPs only (see guard clause above).
- The proposed annotation follows the existing naming convention used by other
scw-loadbalancer-* annotations.
- The Scaleway API already handles IPAM IP assignment at
AttachPrivateNetwork time — this is not a new API capability, just an unexposed one.
I'm raising this as a feature request based on a concrete production need in a Kapsule-based hybrid infrastructure project. I don't have deep expertise in this codebase, so I may have missed edge cases or implementation subtleties — I hope this analysis is useful as a starting point for the maintainers.
Summary
When using a private Load Balancer in a Scaleway Kapsule cluster (via the
service.beta.kubernetes.io/scw-loadbalancer-private: "true"annotation), there is currently no way to assign a fixed private IP address to the Load Balancer's Private Network interface. The CCM always lets Scaleway allocate a random IP from the Private Network's DHCP pool.This is a blocking limitation for hybrid connectivity scenarios where an on-premises datacenter (connected via Scaleway InterLink or VPN site-to-site) needs to reach services inside the Kapsule cluster through a stable, predictable private IP — for example, to configure static routes, firewall rules, or DNS records on the datacenter side.
Context
Use case
In a typical hybrid architecture:
What the Scaleway API already supports
The Scaleway LB API already supports assigning a pre-reserved IPAM IP to a Load Balancer's Private Network attachment via the
ipam_idsfield of theAttachPrivateNetworkendpoint:This means no API-side changes are required — the capability exists today. Only the CCM needs to be updated to expose and use it.
Industry context
For reference, all three major cloud providers natively support fixed private IPs on internal/private load balancers from Kubernetes service annotations, with full dynamic backend management:
service.beta.kubernetes.io/aws-load-balancer-private-ipv4-addressesspec.loadBalancerIPornetworking.gke.io/load-balancer-ip-addressesservice.beta.kubernetes.io/azure-load-balancer-ipv4orspec.loadBalancerIPCurrent behavior
In
EnsureLoadBalancer()(scaleway/loadbalancers.go), the following guard clause explicitly rejects any combination of private LB + static IP:Additionally, in
createLoadBalancer(), the call togetLoadBalancerStaticIPIDs()is gated behindif !lbPrivate, and theattachPrivateNetworks()function never populates theIpamIdsfield ofZonedAPIAttachPrivateNetworkRequest.Proposed solution
Introduce a new annotation, for example:
This annotation would accept the UUID of an IP pre-reserved in Scaleway IPAM (from the Private Network's subnet).
Required changes in
loadbalancers.goThe changes are minimal and localized to a small number of well-identified locations:
EnsureLoadBalancer()— guard clausecreateLoadBalancer()lbPrivatecase to read the new annotation and propagate the IPAM ID to the next stepattachPrivateNetworks()IpamIds: []string{ipamID}inZonedAPIAttachPrivateNetworkRequestwhen the annotation is setdeleteLoadBalancer()ReleaseIPlogic: do not release the IPAM IP when the new annotation is present (mirrors existing behavior for public flex IPs)EnsureLoadBalancer()— drift detectionprivateIPAMMismatchcheck analogous toreservedIPMismatch, to trigger LB recreation if the IPAM IP has changeddocs/loadbalancer-annotations.mdNotes
spec.loadBalancerIPfield is not a viable alternative here: the CCM source code explicitly restricts it to public flexible IPs only (see guard clause above).scw-loadbalancer-*annotations.AttachPrivateNetworktime — this is not a new API capability, just an unexposed one.I'm raising this as a feature request based on a concrete production need in a Kapsule-based hybrid infrastructure project. I don't have deep expertise in this codebase, so I may have missed edge cases or implementation subtleties — I hope this analysis is useful as a starting point for the maintainers.