Skip to content

Commit

Permalink
feat(occm/lb): octavia prometheus listener's annotation
Browse files Browse the repository at this point in the history
  • Loading branch information
Lucasgranet committed Aug 26, 2024
1 parent 75b1fbb commit 1219946
Show file tree
Hide file tree
Showing 7 changed files with 390 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,22 @@ Request Body:

Defines the health monitor retry count for the loadbalancer pool members to be marked down.

- `loadbalancer.openstack.org/metrics-enable`

If 'true', enable the Prometheus listener on the loadbalancer. (default: 'false')

The Kubernetes service must be the [owner of the LoadBalancer](#sharing-load-balancer-with-multiple-services)

Not supported when `lb-provider=ovn` is configured in openstack-cloud-controller-manager.

- `loadbalancer.openstack.org/metrics-port`

Defines the Prometheus listener's port. If `metric-enable` is 'true', the annotation is automatically added to the service. Default: `9100`

- `loadbalancer.openstack.org/metrics-allow-cidrs`

Defines the Prometheus listener's allowed cirds. __Warning__: [security recommendations](#metric-listener-allowed-cird-security-recommendation). Default: none

- `loadbalancer.openstack.org/flavor-id`

The id of the flavor that is used for creating the loadbalancer.
Expand Down Expand Up @@ -236,6 +252,10 @@ Request Body:
This annotation is automatically added and it contains the floating ip address of the load balancer service.
When using `loadbalancer.openstack.org/hostname` annotation it is the only place to see the real address of the load balancer.

- `loadbalancer.openstack.org/load-balancer-vip-address`

This annotation is automatically added and it contains the Octavia's Virtual-IP (VIP).

- `loadbalancer.openstack.org/node-selector`

A set of key=value annotations used to filter nodes for targeting by the load balancer. When defined, only nodes that match all the specified key=value annotations will be targeted. If an annotation includes only a key without a value, the filter will check only for the existence of the key on the node. If the value is not set, the `node-selector` value defined in the OCCM configuration is applied.
Expand Down Expand Up @@ -628,3 +648,66 @@ is not yet supported by OCCM.
Internally, OCCM would automatically look for IPv4 or IPv6 subnet to allocate the load balancer
address from based on the service's address family preference. If the subnet with preferred
address family is not available, load balancer can not be created.

### Metric endpoint configuration

Since Octavia v2.25, Octavia proposes to expose an HTTP Prometheus endpoint. Using the annotation `loadbalancer.openstack.org/metrics-enable`, you will be able to configure this endpoint on the LoadBalancer:

```yaml
kind: Service
apiVersion: v1
metadata:
name: service-with-metric
namespace: default
annotations:
loadbalancer.openstack.org/metrics-enable: "true" # Enable the listener endpoint on the Octavia LoadBalancer (default false)
loadbalancer.openstack.org/metrics-port: "9100" # Listener's port (default 9100)
loadbalancer.openstack.org/metrics-allow-cidrs: "10.0.0.0/8, fe80::/10" # Listener's allowed cidrs (default none)
spec:
type: LoadBalancer
```

Then, you can configure a Prometheus scrapper like to get metrics from the LoadBalancer.

e.g. Prometheus Operator configuration:

```yaml
apiVersion: monitoring.coreos.com/v1alpha1
kind: ScrapeConfig
metadata:
name: octavia-sd-config
labels:
release: prometheus # adapt it to your Prometheus deployment configuration
spec:
kubernetesSDConfigs:
- role: Service
relabelings:
- sourceLabels: [__meta_kubernetes_namespace]
targetLabel: namespace
action: replace
- sourceLabels: [__meta_kubernetes_service_name]
targetLabel: job
action: replace
- sourceLabels:
- __meta_kubernetes_service_annotation_loadbalancer_openstack_org_load_balancer_vip_address
- __meta_kubernetes_service_annotation_loadbalancer_openstack_org_metrics_port
separator: ":"
targetLabel: __address__
action: replace
- sourceLabels:
- __meta_kubernetes_service_annotation_loadbalancer_openstack_org_metrics_enable
- __meta_kubernetes_service_annotationpresent_loadbalancer_openstack_org_load_balancer_vip_address
separator: ;
regex: "true;true"
action: keep
```

> This configuration use the `loadbalancer.openstack.org/load-balancer-vip-address` annotation that will use the Octavia's VIP to fetch the metric endpoint. Adapt it to your Octavia deployment.

For more information: https://docs.openstack.org/octavia/latest/user/guides/monitoring.html#monitoring-with-prometheus

Grafana dashboard for Octavia Amphora: https://grafana.com/grafana/dashboards/15828-openstack-octavia-amphora-load-balancer/

#### Metric listener allowed CIRD security recommendation

If the Octavia LoadBalancer is exposed with a public IP, the Prometheus listener is also exposed (at least for Amphora). Even if no critical data are exposed by this endpoint, __it's strongly recommended to apply an allowed cidrs on the listener__ via the annotation `loadbalancer.openstack.org/metrics-allow-cidrs`.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ require (
github.com/spf13/viper v1.15.0
github.com/stretchr/testify v1.8.4
go.uber.org/goleak v1.3.0
golang.org/x/exp v0.0.0-20230321023759-10a507213a29
golang.org/x/sys v0.21.0
golang.org/x/term v0.21.0
google.golang.org/grpc v1.58.3
Expand Down Expand Up @@ -138,7 +139,6 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.26.0 // indirect
golang.org/x/crypto v0.24.0 // indirect
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/oauth2 v0.10.0 // indirect
golang.org/x/sync v0.7.0 // indirect
Expand Down
1 change: 1 addition & 0 deletions pkg/openstack/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ const (
eventLBAZIgnored = "LoadBalancerAvailabilityZonesIgnored"
eventLBFloatingIPSkipped = "LoadBalancerFloatingIPSkipped"
eventLBRename = "LoadBalancerRename"
eventLBMetricListenerIgnored = "LoadBalancerMetricListenerIgnored"
)
124 changes: 115 additions & 9 deletions pkg/openstack/loadbalancer.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ const (
ServiceAnnotationLoadBalancerSubnetID = "loadbalancer.openstack.org/subnet-id"
ServiceAnnotationLoadBalancerNetworkID = "loadbalancer.openstack.org/network-id"
ServiceAnnotationLoadBalancerMemberSubnetID = "loadbalancer.openstack.org/member-subnet-id"
ServiceAnnotationLoadBalancerMetricsEnabled = "loadbalancer.openstack.org/metrics-enable"
ServiceAnnotationLoadBalancerMetricsPort = "loadbalancer.openstack.org/metrics-port"
ServiceAnnotationLoadBalancerMetricsAllowCidrs = "loadbalancer.openstack.org/metrics-allow-cidrs"
ServiceAnnotationLoadBalancerTimeoutClientData = "loadbalancer.openstack.org/timeout-client-data"
ServiceAnnotationLoadBalancerTimeoutMemberConnect = "loadbalancer.openstack.org/timeout-member-connect"
ServiceAnnotationLoadBalancerTimeoutMemberData = "loadbalancer.openstack.org/timeout-member-data"
Expand All @@ -87,6 +90,7 @@ const (
ServiceAnnotationLoadBalancerHealthMonitorMaxRetriesDown = "loadbalancer.openstack.org/health-monitor-max-retries-down"
ServiceAnnotationLoadBalancerLoadbalancerHostname = "loadbalancer.openstack.org/hostname"
ServiceAnnotationLoadBalancerAddress = "loadbalancer.openstack.org/load-balancer-address"
ServiceAnnotationLoadBalancerVIPAddress = "loadbalancer.openstack.org/load-balancer-vip-address"
// revive:disable:var-naming
ServiceAnnotationTlsContainerRef = "loadbalancer.openstack.org/default-tls-container-ref"
// revive:enable:var-naming
Expand All @@ -95,14 +99,15 @@ const (
ServiceAnnotationLoadBalancerID = "loadbalancer.openstack.org/load-balancer-id"

// Octavia resources name formats
servicePrefix = "kube_service_"
lbFormat = "%s%s_%s_%s"
listenerPrefix = "listener_"
listenerFormat = listenerPrefix + "%d_%s"
poolPrefix = "pool_"
poolFormat = poolPrefix + "%d_%s"
monitorPrefix = "monitor_"
monitorFormat = monitorPrefix + "%d_%s"
servicePrefix = "kube_service_"
lbFormat = "%s%s_%s_%s"
listenerPrefix = "listener_"
listenerFormat = listenerPrefix + "%d_%s"
listenerFormatMetric = listenerPrefix + "metric_%s"
poolPrefix = "pool_"
poolFormat = poolPrefix + "%d_%s"
monitorPrefix = "monitor_"
monitorFormat = monitorPrefix + "%d_%s"
)

// LbaasV2 is a LoadBalancer implementation based on Octavia
Expand Down Expand Up @@ -142,6 +147,9 @@ type serviceConfig struct {
healthMonitorTimeout int
healthMonitorMaxRetries int
healthMonitorMaxRetriesDown int
metricAllowedCIDRs []string
metricEnabled bool
metricPort int
preferredIPFamily corev1.IPFamily // preferred (the first) IP family indicated in service's `spec.ipFamilies`
}

Expand Down Expand Up @@ -451,6 +459,32 @@ func getIntFromServiceAnnotation(service *corev1.Service, annotationKey string,
return defaultSetting
}

// getStringArrayFromServiceAnnotationSeparatedByComma searches a given v1.Service for a specific annotationKey
// and either returns the annotation's string array value (using comma as separator), or the specified defaultSetting.
// Each value of the array is TrimSpaced. After the trim, if the string is empty, remove it.
func getStringArrayFromServiceAnnotationSeparatedByComma(service *corev1.Service, annotationKey string, defaultSetting []string) []string {
klog.V(4).Infof("getStringArrayFromServiceAnnotationSeparatedByComma(%s/%s, %v, %q)", service.Namespace, service.Name, annotationKey, defaultSetting)
if annotationValue, ok := service.Annotations[annotationKey]; ok {
returnValue := []string{}
splitAnnotation := strings.FieldsFunc( // avoid empty string by using this func
annotationValue, func(r rune) bool {
return r == ','
},
)
for _, value := range splitAnnotation {
trimmedValue := strings.TrimSpace(value)
if len(trimmedValue) == 0 {
continue
}
returnValue = append(returnValue, trimmedValue)
}
klog.V(4).Infof("Found a Service Annotation: %v = %q", annotationKey, returnValue)
return returnValue
}
klog.V(4).Infof("Could not find a Service Annotation; falling back to default setting: %v = %q", annotationKey, defaultSetting)
return defaultSetting
}

// getBoolFromServiceAnnotation searches a given v1.Service for a specific annotationKey and either returns the annotation's boolean value or a specified defaultSetting
// If the annotation is not found or is not a valid boolean ("true" or "false"), it falls back to the defaultSetting and logs a message accordingly.
func getBoolFromServiceAnnotation(service *corev1.Service, annotationKey string, defaultSetting bool) bool {
Expand Down Expand Up @@ -1122,6 +1156,58 @@ func (lbaas *LbaasV2) ensureOctaviaListener(lbID string, name string, curListene
return listener, nil
}

func (lbaas *LbaasV2) ensurePrometheusListener(lbID string, name string, curListenerMapping map[listenerKey]*listeners.Listener, _ corev1.ServicePort, svcConf *serviceConfig, _ *corev1.Service) (*listeners.Listener, error) {
listener, isPresent := curListenerMapping[listenerKey{
Protocol: listeners.ProtocolPrometheus,
Port: svcConf.metricPort,
}]
if !isPresent {
listenerCreateOpt := listeners.CreateOpts{
Name: name,
Protocol: listeners.ProtocolPrometheus,
ProtocolPort: svcConf.metricPort,
AllowedCIDRs: svcConf.metricAllowedCIDRs,
LoadbalancerID: lbID,
Tags: []string{svcConf.lbName},
}

var err error
listener, err = openstackutil.CreateListener(lbaas.lb, lbID, listenerCreateOpt)
if err != nil {
return nil, fmt.Errorf("failed to create metric listener for loadbalancer %s: %v", lbID, err)
}

klog.V(2).Infof("Metric listener %s created for loadbalancer %s", listener.ID, lbID)
} else {
listenerChanged := false
updateOpts := listeners.UpdateOpts{}

if svcConf.supportLBTags {
if !cpoutil.Contains(listener.Tags, svcConf.lbName) {
var newTags []string
copy(newTags, listener.Tags)
newTags = append(newTags, svcConf.lbName)
updateOpts.Tags = &newTags
listenerChanged = true
}
}

if !cpoutil.StringListEqual(svcConf.metricAllowedCIDRs, listener.AllowedCIDRs) {
updateOpts.AllowedCIDRs = &svcConf.metricAllowedCIDRs
listenerChanged = true
}

if listenerChanged {
klog.InfoS("Updating metric listener", "listenerID", listener.ID, "lbID", lbID, "updateOpts", updateOpts)
if err := openstackutil.UpdateListener(lbaas.lb, lbID, listener.ID, updateOpts); err != nil {
return nil, fmt.Errorf("failed to update metric listener %s of loadbalancer %s: %v", listener.ID, lbID, err)
}
klog.InfoS("Updated metric listener", "listenerID", listener.ID, "lbID", lbID)
}
}
return listener, nil
}

// buildListenerCreateOpt returns listeners.CreateOpts for a specific Service port and configuration
func (lbaas *LbaasV2) buildListenerCreateOpt(port corev1.ServicePort, svcConf *serviceConfig, name string) listeners.CreateOpts {
listenerCreateOpt := listeners.CreateOpts{
Expand Down Expand Up @@ -1787,6 +1873,25 @@ func (lbaas *LbaasV2) ensureOctaviaLoadBalancer(ctx context.Context, clusterName
curListeners = popListener(curListeners, listener.ID)
}

// Check if we need to expose the metric endpoint
svcConf.metricEnabled = getBoolFromServiceAnnotation(service, ServiceAnnotationLoadBalancerMetricsEnabled, false)
if svcConf.metricEnabled && openstackutil.IsOctaviaFeatureSupported(lbaas.lb, openstackutil.OctaviaFeaturePrometheusListener, lbaas.opts.LBProvider) {
// Only a LB owner can add the prometheus listener (to avoid conflict with a shared loadbalancer)
if isLBOwner {
svcConf.metricPort = getIntFromServiceAnnotation(service, ServiceAnnotationLoadBalancerMetricsPort, 9100)
lbaas.updateServiceAnnotation(service, ServiceAnnotationLoadBalancerMetricsPort, strconv.Itoa(svcConf.metricPort))
svcConf.metricAllowedCIDRs = getStringArrayFromServiceAnnotationSeparatedByComma(service, ServiceAnnotationLoadBalancerMetricsAllowCidrs, []string{})
listener, err := lbaas.ensurePrometheusListener(loadbalancer.ID, cpoutil.Sprintf255(listenerFormatMetric, lbName), curListenerMapping, corev1.ServicePort{}, svcConf, service)
if err != nil {
return nil, err
}
curListeners = popListener(curListeners, listener.ID)
} else {
msg := "Metric Listener cannot be deployed on Service %s, only owner Service can do that"
lbaas.eventRecorder.Eventf(service, corev1.EventTypeWarning, eventLBMetricListenerIgnored, msg, serviceName)
klog.Infof(msg, serviceName)
}
}
// Deal with the remaining listeners, delete the listener if it was created by this Service previously.
if err := lbaas.deleteOctaviaListeners(loadbalancer.ID, curListeners, isLBOwner, lbName); err != nil {
return nil, err
Expand All @@ -1806,8 +1911,9 @@ func (lbaas *LbaasV2) ensureOctaviaLoadBalancer(ctx context.Context, clusterName
}
}

// save address into the annotation
// save addresses into the annotations
lbaas.updateServiceAnnotation(service, ServiceAnnotationLoadBalancerAddress, addr)
lbaas.updateServiceAnnotation(service, ServiceAnnotationLoadBalancerVIPAddress, loadbalancer.VipAddress)

// add LB name to load balancer tags.
if svcConf.supportLBTags {
Expand Down
Loading

0 comments on commit 1219946

Please sign in to comment.