Daniel's Tech Blog

Cloud Computing, Cloud Native & Kubernetes

How to send Cilium metrics to Azure Managed Prometheus

In today’s blog post, I walk you through the setup on how to send Cilium metrics to Azure Managed Prometheus.

Our setup covers two scenarios. The first one is an Azure Kubernetes Service cluster using Cilium via the BYOCNI (Bring Your Own CNI) option, and the second one is a K3s single node cluster running on a Raspberry PI 5 using Cilium.

In both scenarios, we utilize Grafana Alloy to scrape and ingest the Prometheus metrics as we do not want to use Azure’s integrated toolchain.

Prerequisites

As prerequisites, we need, in general, an Azure Managed Prometheus and an Azure Managed Grafana workspace.

-> https://learn.microsoft.com/en-us/azure/azure-monitor/metrics/azure-monitor-workspace-manage?tabs=azure-portal&WT.mc_id=AZ-MVP-5000119#create-an-azure-monitor-workspace
-> https://learn.microsoft.com/en-us/azure/managed-grafana/quickstart-managed-grafana-portal?WT.mc_id=AZ-MVP-5000119

Additionally, for the Kubernetes cluster setup, an Azure Service Principal is required.

-> https://learn.microsoft.com/en-us/cli/azure/azure-cli-sp-tutorial-1?view=azure-cli-latest&tabs=bash&WT.mc_id=AZ-MVP-5000119

For the Azure Kubernetes Service cluster setup, we use the Azure Managed Identity of the Azure Kubernetes Service cluster node pool.

We assign both identities the Monitoring Metrics Publisher role on the data collection rule of our Azure Managed Prometheus workspace. This enables the metrics ingestion for both identities.

-> https://learn.microsoft.com/en-us/azure/azure-monitor/metrics/prometheus-remote-write-virtual-machines?WT.mc_id=AZ-MVP-5000119&tabs=managed-identity%2Cprom-vm#assign-the-monitoring-metrics-publisher-role-to-the-application
-> https://learn.microsoft.com/en-us/azure/azure-monitor/metrics/prometheus-remote-write-virtual-machines?WT.mc_id=AZ-MVP-5000119&tabs=entra-application%2Cprom-vm#assign-the-monitoring-metrics-publisher-role-to-the-application-1

The last prerequisite is to ensure that the Cilium metrics for the Cilium agent and operator are enabled.

-> https://docs.cilium.io/en/stable/observability/metrics/#installation

Grafana Alloy Configuration

The Grafana Alloy deployment is done via the Helm Chart provided by Grafana. Hence, we use the following values in a YAML file to configure the Alloy installation.

alloy:
  configMap:
    create: false
    name: alloy-config
    key: config.alloy
  envFrom:
    - secretRef:
        name: alloy-azure-config
  extraEnv:
    - name: CLUSTER
      value: k8s
    - name: REGEX_METRICS_FILTER
      value: ".+"

The YAML file contains three configuration parameters: configMap, envFrom, and extraEnv.

In the configMap parameter, we instruct the Helm chart not to create the config map in the Kubernetes namespace. Within the envFrom parameter, we reference the Kubernetes secret that contains either the Azure Service Principal credentials or the Azure Managed Identity information, depending on our setup. The last parameter provides two additional environment variables for our Alloy setup. First, the environment variable CLUSTER adds the Kubernetes cluster name as an additional Prometheus label to each Prometheus metric. The second one, REGEX_METRICS_FILTER, provides the ability to only ingest Prometheus metrics to Azure Managed Prometheus that match the defined regular expression.

Before we dive into the config map let us have a look into the Kubernetes secret. For the Azure Kubernetes Service cluster setup, the secret contains the client ID of the managed identity, and the endpoint URL of the Azure Managed Prometheus workspace.

apiVersion: v1
kind: Secret
metadata:
  name: alloy-azure-config
  namespace: grafana-alloy
  labels:
    app: grafana-alloy
data:
  CLIENT_ID: <BASE64_VALUE>
  ENDPOINT_URL: <BASE64_VALUE>

The Kubernetes cluster setup with the Azure Service Principal requires different values. Besides the endpoint URL, it is the client ID and client secret of the Azure Service Principal, as well as the Entra tenant ID.

apiVersion: v1
kind: Secret
metadata:
  name: alloy-azure-config
  namespace: grafana-alloy
  labels:
    app: grafana-alloy
data:
  CLIENT_ID: <BASE64_VALUE>
  CLIENT_SECRET: <BASE64_VALUE>
  ENDPOINT_URL: <BASE64_VALUE>
  TENANT_ID: <BASE64_VALUE>

Now, we can dive into the config map for our Grafana Alloy installation, which consists of six parts: logging, discovery.kubernetes, discovery.relabel, prometheus.scrape, prometheus.relabel, and prometheus.remote_write.

logging {
  level = "info"
  format = "json"
}

The log level can be set later to warn or error, but to the beginning, info is helpful for troubleshooting purposes. Instead of using the default logfmt format, we switch to json as a structured log format.

discovery.kubernetes "pods" {
  role = "pod"

  namespaces {
    own_namespace = false

    names = ["kube-system"]
  }

  selectors {
    role  = "pod"
    field = "spec.nodeName=" + coalesce(sys.env("HOSTNAME"), constants.hostname)
  }
}

We configure our Grafana Alloy installation to discover only pods for metrics scraping in the kube-system namespace. Additionally, we instruct Grafana Alloy to restrict the pod discovery to the same Kubernetes node as we run Grafana Alloy in the default daemon set setup.

For the discovery.relabel part we will focus only on the additional rules that have been added to the default scraping rules for pods mentioned in the Cilium example configuration.

-> https://github.com/cilium/cilium/blob/v1.17.4/examples/kubernetes/addons/prometheus/files/prometheus/prometheus.yaml#L45-L68

...
      rule {
        source_labels = ["__meta_kubernetes_pod_label_app_kubernetes_io_part_of"]
        action = "keep"
        regex = `cilium`
      }
...

Ensuring that we only keep Cilium agent and operator pods in the scraping target list, we check if the Kubernetes label app.kubernetes.io/part-of contains cilium as a value. These scraping targets are kept in the list, and everything else is removed. When we run “kubectl get pods -n kube-system -l ‘app.kubernetes.io/part-of=cilium’” upfront, we get the potential targets for a quick cross-check already.

❯ kubectl get pods -n kube-system -l 'app.kubernetes.io/part-of=cilium'
NAME                               READY   STATUS    RESTARTS         AGE
cilium-envoy-5qfs7                 1/1     Running   8 (3d10h ago)    30d
cilium-node-init-8kdjx             1/1     Running   17 (3d10h ago)   96d
cilium-operator-659bffc68c-vcnhl   1/1     Running   9 (3d10h ago)    30d
cilium-q4rkw                       1/1     Running   8 (3d10h ago)    30d
hubble-relay-5d5965474b-hxbxj      1/1     Running   8 (3d10h ago)    30d
hubble-ui-7fd6bc845b-l6hz9         2/2     Running   16 (3d10h ago)   30d

Besides this rule, we add a rule that adds the cluster name as a label to every scraping target.

...
      rule {
        replacement = sys.env("CLUSTER")
        target_label = "cluster"
      }
...

In the Prometheus scraping part, we set three specific configurations.

prometheus.scrape "pods" {
  job_name = "kubernetes-pods"
  honor_labels = true

  targets    = discovery.relabel.pods.output
  forward_to = [prometheus.relabel.pods.receiver]

  scrape_interval = "30s"
  scheme = "http"
}

First, we configure Grafana Alloy to honor labels and then set the scraping scheme to http. Furthermore, we increase the scraping interval from 60 to 30 seconds.

After the scraping part comes the prometheus.relabel part for filtering which Prometheus metrics we want to ingest into the Azure Managed Prometheus workspace.

prometheus.relabel "pods" {
  forward_to = [prometheus.remote_write.azure_managed_prometheus.receiver]

  rule {
    source_labels = ["__name__"]
    action = "keep"
    regex = sys.env("REGEX_METRICS_FILTER")
  }
}

Eventually, we reached the final part prometheus.remote_write for the metrics ingestion. Depending on whether we run Grafana Alloy on Azure Kubernetes Service or on another Kubernetes distribution, the configuration differs.

The Azure Kubernetes Service configuration uses the azuread block for the managed identity authentication.

prometheus.remote_write "azure_managed_prometheus" {
  endpoint {
    url = sys.env("ENDPOINT_URL")

    azuread {
      cloud = "AzurePublic"
      managed_identity {
        client_id = sys.env("CLIENT_ID")
      }
    }
  }
}

Whereas the Kubernetes configuration uses the oauth2 block for the Azure Service Principal authentication.

prometheus.remote_write "azure_managed_prometheus" {
  endpoint {
    url = sys.env("ENDPOINT_URL")

    oauth2 {
      client_id = sys.env("CLIENT_ID")
      client_secret = sys.env("CLIENT_SECRET")
      token_url = "https://login.microsoftonline.com/" + sys.env("TENANT_ID") + "/oauth2/v2.0/token"
      scopes    = ["https://monitor.azure.com/.default"]
    }
  }
}

After walking you through the Grafana Alloy configuration, it is now time to deploy Grafana Alloy to our two Kubernetes clusters.

Rollout and Metrics Ingestion

The aforementioned configuration files can be found on my GitHub repository.

-> https://github.com/neumanndaniel/kubernetes/tree/master/cilium/prometheus-metrics

Before we install Grafana Alloy via its Helm Chart, we create a namespace, called grafana-alloy, the config map, and the secret containing either the managed identity or service principal information.

For the Azure Kubernetes Service cluster, we run the following commands

kubectl create namespace grafana-alloy
kubectl apply -f "./aks/config-map.yaml"
kubectl create secret generic alloy-azure-config -n grafana-alloy --from-literal=ENDPOINT_URL="<ENDPOINT_URL>" --from-literal=CLIENT_ID="<MANAGED_IDENTITY_CLIENT_ID>"

For the K3s cluster, we run a slightly different set of commands as we cannot use an Azure Managed Identity.

kubectl create namespace grafana-alloy
kubectl apply -f "./k8s/config-map.yaml"
kubectl create secret generic alloy-azure-config -n grafana-alloy --from-literal=ENDPOINT_URL="<ENDPOINT_URL>" --from-literal=CLIENT_ID="<SPN_CLIENT_ID>" --from-literal=CLIENT_SECRET="<SPN_CLIENT_SECRET>" --from-literal=TENANT_ID="<ENTRA_TENANT_ID>"

Now, we are ready to deploy Grafana Alloy to the Kubernetes clusters.

AKS:

helm repo add grafana https://grafana.github.io/helm-charts
helm repo update
helm upgrade --install grafana-alloy grafana/alloy --version "1.0.3" \
  --wait \
  --namespace "grafana-alloy" \
  -f "./aks/grafana-alloy.yaml"

K3s:

helm repo add grafana https://grafana.github.io/helm-charts
helm repo update
helm upgrade --install grafana-alloy grafana/alloy --version "1.0.3" \
  --wait \
  --namespace "grafana-alloy" \
  -f "./k8s/grafana-alloy.yaml"

Afterward, we check if the Grafana Alloy pods are up and running as seen in the screenshots below.

Terminal output - Kubernetes nodes and Grafana Alloy pods Terminal output - Kubernetes nodes and Grafana Alloy pods

The next step is the verification of a working metrics ingestion within the Azure Managed Prometheus workspace. This can either happen via the Prometheus explorer or the Metrics tab.

Azure portal - Azure Managed Prometheus - Prometheus explorer Azure portal - Azure Managed Prometheus - Metrics tab

Within the Prometheus explorer’s grid view, we see that the custom cluster label is added correctly to every scraping target.

In the case that no metrics arrive in the Azure Managed Prometheus workspace, Grafana Alloy provides a debug UI to examine the ingestion pipeline. All you need is a port forward to one of the Grafana Alloy pods.

❯ kubectl port-forward grafana-alloy-fr6ms  --address localhost 12345:12345
❯ open http://localhost:12345/graph

Grafana Alloy - Debug UI

The last step is logging in to the Azure Managed Grafana workspace to import the Cilium dashboard for presenting the various Cilium metrics.

-> https://github.com/cilium/cilium/tree/v1.17.4/install/kubernetes/cilium/files/cilium-agent/dashboards
-> https://github.com/cilium/cilium/tree/v1.17.4/install/kubernetes/cilium/files/cilium-operator/dashboards

Once the Cilium dashboards have been imported successfully, we look at the metrics of our Cilium installations.

Azure Managed Grafana - Cilium dashboard

Summary

Sending Cilium metrics to an Azure Managed Prometheus workspace is a bit more work than using the Azure Monitor integration for it. However, it provides two tremendous advantages from my point of view, using Grafana Alloy, as in this example, or the Prometheus Operator. First, we have full control over the entire configuration to customize it to our needs. Second, we can easily exchange the Prometheus backend.

Do not get me wrong, the entire integrated Azure toolchain for monitoring an Azure Kubernetes Service or an Azure Arc-enabled Kubernetes cluster is great and might fit your needs. However, when you want to customize or fine-tune the out of box solutions, it gets complicated, and you might be better served using self-managed solutions to accomplish your goals.

The example configurations can be found on my GitHub repository.

-> https://github.com/neumanndaniel/kubernetes/tree/master/cilium/prometheus-metrics


Posted

in

WordPress Cookie Notice by Real Cookie Banner