Skip to content

Commit 7fdbbc8

Browse files
committed
feat(occm/lb): octavia prometheus listener's annotation
1 parent bcd8033 commit 7fdbbc8

File tree

6 files changed

+392
-15
lines changed

6 files changed

+392
-15
lines changed

docs/openstack-cloud-controller-manager/expose-applications-using-loadbalancer-type-service.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,22 @@ Request Body:
202202

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

205+
- `loadbalancer.openstack.org/metrics-enable`
206+
207+
If 'true', enable the Prometheus listener on the loadbalancer. (default: 'false')
208+
209+
The Kubernetes service must be the [owner of the LoadBalancer](#sharing-load-balancer-with-multiple-services)
210+
211+
Not supported when `lb-provider=ovn` is configured in openstack-cloud-controller-manager.
212+
213+
- `loadbalancer.openstack.org/metrics-port`
214+
215+
Defines the Prometheus listener's port. If `metric-enable` is 'true', the annotation is automatically added to the service. Default: `9100`
216+
217+
- `loadbalancer.openstack.org/metrics-allow-cidrs`
218+
219+
Defines the Prometheus listener's allowed cirds. __Warning__: [security recommendations](#metric-listener-allowed-cird-security-recommendation). Default: none
220+
205221
- `loadbalancer.openstack.org/flavor-id`
206222

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

259+
- `loadbalancer.openstack.org/load-balancer-vip-address`
260+
261+
This annotation is automatically added and it contains the Octavia's Virtual-IP (VIP).
262+
243263
- `loadbalancer.openstack.org/node-selector`
244264

245265
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.
@@ -636,3 +656,64 @@ is not yet supported by OCCM.
636656
Internally, OCCM would automatically look for IPv4 or IPv6 subnet to allocate the load balancer
637657
address from based on the service's address family preference. If the subnet with preferred
638658
address family is not available, load balancer can not be created.
659+
660+
### Metric endpoint configuration
661+
662+
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:
663+
664+
```yaml
665+
kind: Service
666+
apiVersion: v1
667+
metadata:
668+
name: service-with-metric
669+
namespace: default
670+
annotations:
671+
loadbalancer.openstack.org/metrics-enable: "true" # Enable the listener endpoint on the Octavia LoadBalancer (default false)
672+
loadbalancer.openstack.org/metrics-port: "9100" # Listener's port (default 9100)
673+
loadbalancer.openstack.org/metrics-allow-cidrs: "10.0.0.0/8, fe80::/10" # Listener's allowed cidrs (default none)
674+
spec:
675+
type: LoadBalancer
676+
```
677+
678+
Then, you can configure a Prometheus scrapper like to get metrics from the LoadBalancer.
679+
680+
e.g. Prometheus Operator configuration:
681+
682+
```yaml
683+
apiVersion: monitoring.coreos.com/v1alpha1
684+
kind: ScrapeConfig
685+
metadata:
686+
name: octavia-sd-config
687+
labels:
688+
release: prometheus # adapt it to your Prometheus deployment configuration
689+
spec:
690+
kubernetesSDConfigs:
691+
- role: Service
692+
relabelings:
693+
- sourceLabels: [__meta_kubernetes_namespace]
694+
targetLabel: namespace
695+
action: replace
696+
- sourceLabels: [__meta_kubernetes_service_name]
697+
targetLabel: job
698+
action: replace
699+
- sourceLabels:
700+
- __meta_kubernetes_service_annotation_loadbalancer_openstack_org_load_balancer_vip_address
701+
- __meta_kubernetes_service_annotation_loadbalancer_openstack_org_metrics_port
702+
separator: ":"
703+
targetLabel: __address__
704+
action: replace
705+
- sourceLabels:
706+
- __meta_kubernetes_service_annotation_loadbalancer_openstack_org_metrics_enable
707+
regex: "true"
708+
action: keep
709+
```
710+
711+
> 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.
712+
713+
For more information: https://docs.openstack.org/octavia/latest/user/guides/monitoring.html#monitoring-with-prometheus
714+
715+
Grafana dashboard for Octavia Amphora: https://grafana.com/grafana/dashboards/15828-openstack-octavia-amphora-load-balancer/
716+
717+
#### Metric listener allowed CIRD security recommendation
718+
719+
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`.

pkg/openstack/events.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,5 @@ const (
2323
eventLBAZIgnored = "LoadBalancerAvailabilityZonesIgnored"
2424
eventLBFloatingIPSkipped = "LoadBalancerFloatingIPSkipped"
2525
eventLBRename = "LoadBalancerRename"
26+
eventLBMetricListenerIgnored = "LoadBalancerMetricListenerIgnored"
2627
)

pkg/openstack/loadbalancer.go

Lines changed: 115 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ const (
7171
ServiceAnnotationLoadBalancerSubnetID = "loadbalancer.openstack.org/subnet-id"
7272
ServiceAnnotationLoadBalancerNetworkID = "loadbalancer.openstack.org/network-id"
7373
ServiceAnnotationLoadBalancerMemberSubnetID = "loadbalancer.openstack.org/member-subnet-id"
74+
ServiceAnnotationLoadBalancerMetricsEnabled = "loadbalancer.openstack.org/metrics-enable"
75+
ServiceAnnotationLoadBalancerMetricsPort = "loadbalancer.openstack.org/metrics-port"
76+
ServiceAnnotationLoadBalancerMetricsAllowCidrs = "loadbalancer.openstack.org/metrics-allow-cidrs"
7477
ServiceAnnotationLoadBalancerTimeoutClientData = "loadbalancer.openstack.org/timeout-client-data"
7578
ServiceAnnotationLoadBalancerTimeoutMemberConnect = "loadbalancer.openstack.org/timeout-member-connect"
7679
ServiceAnnotationLoadBalancerTimeoutMemberData = "loadbalancer.openstack.org/timeout-member-data"
@@ -87,6 +90,7 @@ const (
8790
ServiceAnnotationLoadBalancerHealthMonitorMaxRetriesDown = "loadbalancer.openstack.org/health-monitor-max-retries-down"
8891
ServiceAnnotationLoadBalancerLoadbalancerHostname = "loadbalancer.openstack.org/hostname"
8992
ServiceAnnotationLoadBalancerAddress = "loadbalancer.openstack.org/load-balancer-address"
93+
ServiceAnnotationLoadBalancerVIPAddress = "loadbalancer.openstack.org/load-balancer-vip-address"
9094
// revive:disable:var-naming
9195
ServiceAnnotationTlsContainerRef = "loadbalancer.openstack.org/default-tls-container-ref"
9296
// revive:enable:var-naming
@@ -95,14 +99,15 @@ const (
9599
ServiceAnnotationLoadBalancerID = "loadbalancer.openstack.org/load-balancer-id"
96100

97101
// Octavia resources name formats
98-
servicePrefix = "kube_service_"
99-
lbFormat = "%s%s_%s_%s"
100-
listenerPrefix = "listener_"
101-
listenerFormat = listenerPrefix + "%d_%s"
102-
poolPrefix = "pool_"
103-
poolFormat = poolPrefix + "%d_%s"
104-
monitorPrefix = "monitor_"
105-
monitorFormat = monitorPrefix + "%d_%s"
102+
servicePrefix = "kube_service_"
103+
lbFormat = "%s%s_%s_%s"
104+
listenerPrefix = "listener_"
105+
listenerFormat = listenerPrefix + "%d_%s"
106+
listenerFormatMetric = listenerPrefix + "metric_%s"
107+
poolPrefix = "pool_"
108+
poolFormat = poolPrefix + "%d_%s"
109+
monitorPrefix = "monitor_"
110+
monitorFormat = monitorPrefix + "%d_%s"
106111
)
107112

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

@@ -451,6 +459,32 @@ func getIntFromServiceAnnotation(service *corev1.Service, annotationKey string,
451459
return defaultSetting
452460
}
453461

462+
// getStringArrayFromServiceAnnotationSeparatedByComma searches a given v1.Service for a specific annotationKey
463+
// and either returns the annotation's string array value (using comma as separator), or the specified defaultSetting.
464+
// Each value of the array is TrimSpaced. After the trim, if the string is empty, remove it.
465+
func getStringArrayFromServiceAnnotationSeparatedByComma(service *corev1.Service, annotationKey string, defaultSetting []string) []string {
466+
klog.V(4).Infof("getStringArrayFromServiceAnnotationSeparatedByComma(%s/%s, %v, %q)", service.Namespace, service.Name, annotationKey, defaultSetting)
467+
if annotationValue, ok := service.Annotations[annotationKey]; ok {
468+
returnValue := []string{}
469+
splitAnnotation := strings.FieldsFunc( // avoid empty string by using this func instead of strings.split()
470+
annotationValue, func(r rune) bool {
471+
return r == ','
472+
},
473+
)
474+
for _, value := range splitAnnotation {
475+
trimmedValue := strings.TrimSpace(value)
476+
if len(trimmedValue) == 0 {
477+
continue
478+
}
479+
returnValue = append(returnValue, trimmedValue)
480+
}
481+
klog.V(4).Infof("Found a Service Annotation: %v = %q", annotationKey, returnValue)
482+
return returnValue
483+
}
484+
klog.V(4).Infof("Could not find a Service Annotation; falling back to default setting: %v = %q", annotationKey, defaultSetting)
485+
return defaultSetting
486+
}
487+
454488
// getBoolFromServiceAnnotation searches a given v1.Service for a specific annotationKey and either returns the annotation's boolean value or a specified defaultSetting
455489
// 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.
456490
func getBoolFromServiceAnnotation(service *corev1.Service, annotationKey string, defaultSetting bool) bool {
@@ -1137,6 +1171,58 @@ func (lbaas *LbaasV2) ensureOctaviaListener(lbID string, name string, curListene
11371171
return listener, nil
11381172
}
11391173

1174+
func (lbaas *LbaasV2) ensurePrometheusListener(lbID string, name string, curListenerMapping map[listenerKey]*listeners.Listener, _ corev1.ServicePort, svcConf *serviceConfig, _ *corev1.Service) (*listeners.Listener, error) {
1175+
listener, isPresent := curListenerMapping[listenerKey{
1176+
Protocol: listeners.ProtocolPrometheus,
1177+
Port: svcConf.metricPort,
1178+
}]
1179+
if !isPresent {
1180+
listenerCreateOpt := listeners.CreateOpts{
1181+
Name: name,
1182+
Protocol: listeners.ProtocolPrometheus,
1183+
ProtocolPort: svcConf.metricPort,
1184+
AllowedCIDRs: svcConf.metricAllowedCIDRs,
1185+
LoadbalancerID: lbID,
1186+
Tags: []string{svcConf.lbName},
1187+
}
1188+
1189+
var err error
1190+
listener, err = openstackutil.CreateListener(lbaas.lb, lbID, listenerCreateOpt)
1191+
if err != nil {
1192+
return nil, fmt.Errorf("failed to create metric listener for loadbalancer %s: %v", lbID, err)
1193+
}
1194+
1195+
klog.V(2).Infof("Metric listener %s created for loadbalancer %s", listener.ID, lbID)
1196+
} else {
1197+
listenerChanged := false
1198+
updateOpts := listeners.UpdateOpts{}
1199+
1200+
if svcConf.supportLBTags {
1201+
if !cpoutil.Contains(listener.Tags, svcConf.lbName) {
1202+
var newTags []string
1203+
copy(newTags, listener.Tags)
1204+
newTags = append(newTags, svcConf.lbName)
1205+
updateOpts.Tags = &newTags
1206+
listenerChanged = true
1207+
}
1208+
}
1209+
1210+
if !cpoutil.StringListEqual(svcConf.metricAllowedCIDRs, listener.AllowedCIDRs) {
1211+
updateOpts.AllowedCIDRs = &svcConf.metricAllowedCIDRs
1212+
listenerChanged = true
1213+
}
1214+
1215+
if listenerChanged {
1216+
klog.InfoS("Updating metric listener", "listenerID", listener.ID, "lbID", lbID, "updateOpts", updateOpts)
1217+
if err := openstackutil.UpdateListener(lbaas.lb, lbID, listener.ID, updateOpts); err != nil {
1218+
return nil, fmt.Errorf("failed to update metric listener %s of loadbalancer %s: %v", listener.ID, lbID, err)
1219+
}
1220+
klog.InfoS("Updated metric listener", "listenerID", listener.ID, "lbID", lbID)
1221+
}
1222+
}
1223+
return listener, nil
1224+
}
1225+
11401226
// buildListenerCreateOpt returns listeners.CreateOpts for a specific Service port and configuration
11411227
func (lbaas *LbaasV2) buildListenerCreateOpt(port corev1.ServicePort, svcConf *serviceConfig, name string) listeners.CreateOpts {
11421228
listenerCreateOpt := listeners.CreateOpts{
@@ -1800,6 +1886,25 @@ func (lbaas *LbaasV2) ensureOctaviaLoadBalancer(ctx context.Context, clusterName
18001886
curListeners = popListener(curListeners, listener.ID)
18011887
}
18021888

1889+
// Check if we need to expose the metric endpoint
1890+
svcConf.metricEnabled = getBoolFromServiceAnnotation(service, ServiceAnnotationLoadBalancerMetricsEnabled, false)
1891+
if svcConf.metricEnabled && openstackutil.IsOctaviaFeatureSupported(lbaas.lb, openstackutil.OctaviaFeaturePrometheusListener, lbaas.opts.LBProvider) {
1892+
// Only a LB owner can add the prometheus listener (to avoid conflict with a shared loadbalancer)
1893+
if isLBOwner {
1894+
svcConf.metricPort = getIntFromServiceAnnotation(service, ServiceAnnotationLoadBalancerMetricsPort, 9100)
1895+
lbaas.updateServiceAnnotation(service, ServiceAnnotationLoadBalancerMetricsPort, strconv.Itoa(svcConf.metricPort))
1896+
svcConf.metricAllowedCIDRs = getStringArrayFromServiceAnnotationSeparatedByComma(service, ServiceAnnotationLoadBalancerMetricsAllowCidrs, []string{})
1897+
listener, err := lbaas.ensurePrometheusListener(loadbalancer.ID, cpoutil.Sprintf255(listenerFormatMetric, lbName), curListenerMapping, corev1.ServicePort{}, svcConf, service)
1898+
if err != nil {
1899+
return nil, err
1900+
}
1901+
curListeners = popListener(curListeners, listener.ID)
1902+
} else {
1903+
msg := "Metric Listener cannot be deployed on Service %s, only owner Service can do that"
1904+
lbaas.eventRecorder.Eventf(service, corev1.EventTypeWarning, eventLBMetricListenerIgnored, msg, serviceName)
1905+
klog.Infof(msg, serviceName)
1906+
}
1907+
}
18031908
// Deal with the remaining listeners, delete the listener if it was created by this Service previously.
18041909
if err := lbaas.deleteOctaviaListeners(loadbalancer.ID, curListeners, isLBOwner, lbName); err != nil {
18051910
return nil, err
@@ -1819,8 +1924,9 @@ func (lbaas *LbaasV2) ensureOctaviaLoadBalancer(ctx context.Context, clusterName
18191924
}
18201925
}
18211926

1822-
// save address into the annotation
1927+
// save addresses into the annotations
18231928
lbaas.updateServiceAnnotation(service, ServiceAnnotationLoadBalancerAddress, addr)
1929+
lbaas.updateServiceAnnotation(service, ServiceAnnotationLoadBalancerVIPAddress, loadbalancer.VipAddress)
18241930

18251931
// add LB name to load balancer tags.
18261932
if svcConf.supportLBTags {

0 commit comments

Comments
 (0)