Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions docs/loadbalancer-annotations.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,3 +239,18 @@ This annotation is ignored when `service.beta.kubernetes.io/scw-loadbalancer-ext
The possible formats are:
- `<pn-id>`: will attach a single Private Network to the LB.
- `<pn-id>,<pn-id>`: will attach the two Private Networks to the LB.

### `service.beta.kubernetes.io/scw-loadbalancer-health-check-from-service`

This is the annotation to configure the load balancer backend to use the service's `healthCheckNodePort` for health checks.
When enabled for a port, the health check will use the `healthCheckNodePort` from the service specification instead of the regular `NodePort`.
This is particularly useful when the service has `externalTrafficPolicy: Local`, which automatically allocates a `healthCheckNodePort` for health checking.
The possible values are `false`, `true` or `*` for all ports or a comma delimited list of the service ports (for instance `80,443`).

**Important:** When this annotation is enabled, the health check configuration is overridden with the following settings:
- **Protocol:** HTTP
- **Method:** GET
- **URI:** `/healthz`
- **Expected Code:** 200

This configuration is specifically designed to work with Kubernetes' standard health check endpoint. All other health check type annotations (such as `service.beta.kubernetes.io/scw-loadbalancer-health-check-type`, `service.beta.kubernetes.io/scw-loadbalancer-health-check-http-uri`, etc.) are ignored for the ports where this annotation is enabled.
133 changes: 72 additions & 61 deletions scaleway/loadbalancers.go
Original file line number Diff line number Diff line change
Expand Up @@ -994,6 +994,10 @@ func isPortInRange(r string, p int32) (bool, error) {
if err != nil {
return false, err
}
// Validate port is within valid range (1-65535)
if intPort < 1 || intPort > 65535 {
return false, fmt.Errorf("port %d is outside valid range (1-65535)", intPort)
}
if int64(p) == intPort {
return true, nil
}
Expand Down Expand Up @@ -1158,8 +1162,74 @@ func servicePortToBackend(service *v1.Service, loadbalancer *scwlb.LB, port v1.S
return nil, err
}

healthCheck := &scwlb.HealthCheck{
Port: port.NodePort,
healthCheck, err := getNativeHealthCheck(service, port.Port)
if err != nil {
return nil, err
}

if healthCheck == nil {
healthCheck = &scwlb.HealthCheck{
Port: port.NodePort,
}

healthCheckType, err := getHealthCheckType(service, port.NodePort)
if err != nil {
return nil, err
}

switch healthCheckType {
case "mysql":
hc, err := getMysqlHealthCheck(service, port.NodePort)
if err != nil {
return nil, err
}
healthCheck.MysqlConfig = hc
case "ldap":
hc, err := getLdapHealthCheck(service, port.NodePort)
if err != nil {
return nil, err
}
healthCheck.LdapConfig = hc
case "redis":
hc, err := getRedisHealthCheck(service, port.NodePort)
if err != nil {
return nil, err
}
healthCheck.RedisConfig = hc
case "pgsql":
hc, err := getPgsqlHealthCheck(service, port.NodePort)
if err != nil {
return nil, err
}
healthCheck.PgsqlConfig = hc
case "tcp":
hc, err := getTCPHealthCheck(service, port.NodePort)
if err != nil {
return nil, err
}
healthCheck.TCPConfig = hc
case "http":
hc, err := getHTTPHealthCheck(service, port.NodePort)
if err != nil {
return nil, err
}
healthCheck.HTTPConfig = hc
case "https":
hc, err := getHTTPSHealthCheck(service, port.NodePort)
if err != nil {
return nil, err
}
healthCheck.HTTPSConfig = hc
default:
klog.Errorf("wrong value for healthCheckType")
return nil, errLoadBalancerInvalidAnnotation
}

healthCheckSendProxy, err := getHealthCheckSendProxy(service)
if err != nil {
return nil, err
}
healthCheck.CheckSendProxy = healthCheckSendProxy
}

healthCheckDelay, err := getHealthCheckDelay(service)
Expand All @@ -1186,65 +1256,6 @@ func servicePortToBackend(service *v1.Service, loadbalancer *scwlb.LB, port v1.S
}
healthCheck.TransientCheckDelay = healthCheckTransientCheckDelay

healthCheckSendProxy, err := getHealthCheckSendProxy(service)
if err != nil {
return nil, err
}
healthCheck.CheckSendProxy = healthCheckSendProxy

healthCheckType, err := getHealthCheckType(service, port.NodePort)
if err != nil {
return nil, err
}

switch healthCheckType {
case "mysql":
hc, err := getMysqlHealthCheck(service, port.NodePort)
if err != nil {
return nil, err
}
healthCheck.MysqlConfig = hc
case "ldap":
hc, err := getLdapHealthCheck(service, port.NodePort)
if err != nil {
return nil, err
}
healthCheck.LdapConfig = hc
case "redis":
hc, err := getRedisHealthCheck(service, port.NodePort)
if err != nil {
return nil, err
}
healthCheck.RedisConfig = hc
case "pgsql":
hc, err := getPgsqlHealthCheck(service, port.NodePort)
if err != nil {
return nil, err
}
healthCheck.PgsqlConfig = hc
case "tcp":
hc, err := getTCPHealthCheck(service, port.NodePort)
if err != nil {
return nil, err
}
healthCheck.TCPConfig = hc
case "http":
hc, err := getHTTPHealthCheck(service, port.NodePort)
if err != nil {
return nil, err
}
healthCheck.HTTPConfig = hc
case "https":
hc, err := getHTTPSHealthCheck(service, port.NodePort)
if err != nil {
return nil, err
}
healthCheck.HTTPSConfig = hc
default:
klog.Errorf("wrong value for healthCheckType")
return nil, errLoadBalancerInvalidAnnotation
}

backend := &scwlb.Backend{
Name: fmt.Sprintf("%s_tcp_%d", string(service.UID), port.NodePort),
Pool: nodeIPs,
Expand Down
44 changes: 44 additions & 0 deletions scaleway/loadbalancers_annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,12 @@ const (
// - "<pn-id>": will attach a single Private Network to the LB.
// - "<pn-id>,<pn-id>": will attach the two Private Networks to the LB.
serviceAnnotationPrivateNetworkIDs = "service.beta.kubernetes.io/scw-loadbalancer-pn-ids"

// serviceAnnotationLoadBalancerHealthCheckFromService is the annotation to use healthCheckNodePort from the service
// The possible values are "false", "true" or "*" for all ports or a comma delimited list of the service port
// (for instance "80,443"). When enabled for a port, the health check will use the service's healthCheckNodePort
// instead of the regular NodePort, overriding any other health check configuration.
serviceAnnotationLoadBalancerHealthCheckFromService = "service.beta.kubernetes.io/scw-loadbalancer-health-check-from-service"
)

func getLoadBalancerID(service *v1.Service) (scw.Zone, string, error) {
Expand Down Expand Up @@ -1057,3 +1063,41 @@ func getEnableHTTP3(service *v1.Service) (bool, error) {
}
return strconv.ParseBool(enableHTTP3)
}

// getNativeHealthCheck returns the healthCheckNodePort config if the feature is enabled.
// Returns nil if standard legacy logic should be used.
func getNativeHealthCheck(service *v1.Service, targetPort int32) (*scwlb.HealthCheck, error) {
annotationValue := service.Annotations[serviceAnnotationLoadBalancerHealthCheckFromService]
isEnabled, err := isPortInRange(annotationValue, targetPort)
if err != nil {
return nil, fmt.Errorf("invalid health check annotation: %w", err)
}

if !isEnabled {
return nil, nil
}

// If the user requested the feature but K8s hasn't assigned the port yet (usually means
// externalTrafficPolicy is not set to Local), we must fall back to avoid blackholing traffic.
hcNodePort := service.Spec.HealthCheckNodePort
if hcNodePort == 0 {
klog.Warningf("Annotation '%s' is active for %s/%s, but HealthCheckNodePort is 0. "+
"Ensure 'externalTrafficPolicy: Local'. Falling back to standard NodePort.",
serviceAnnotationLoadBalancerHealthCheckFromService, service.Namespace, service.Name)
return nil, nil
}

// Validate healthCheckNodePort is within valid range
if hcNodePort < 1 || hcNodePort > 65535 {
return nil, fmt.Errorf("invalid healthCheckNodePort %d: port must be in range 1-65535", hcNodePort)
}

return &scwlb.HealthCheck{
Port: hcNodePort,
HTTPConfig: &scwlb.HealthCheckHTTPConfig{
Method: "GET",
Code: scw.Int32Ptr(200),
URI: "/healthz",
},
}, nil
}
Loading
Loading