Deploying the bpfman-operator
The bpfman-operator
repository exists in order to deploy and manage bpfman within a Kubernetes cluster.
This operator was built utilizing some great tooling provided by the
operator-sdk library.
A great first step in understanding some of the functionality can be to just run make help
.
Deploy bpfman Operator
The bpfman-operator
is running as a Deployment with a ReplicaSet of one.
It runs on the control plane and is composed of a single container.
The operator is responsible for launching the bpfman-daemon
DaemonSet and the bpfman-metrics-proxy
DaemonSet, which run on every node.
The bpfman-daemon
DaemonSet is composed of the containers bpfman
, bpfman-agent
, and node-driver-registrar
.
Described below are two ways to deploy bpfman in a Kubernetes cluster:
- Deploy Locally via KIND: Easiest way to deploy bpfman in a Kubernetes cluster and great for testing.
- Deploy To Openshift Cluster: Special steps are needed to deploy on an Openshift cluster because SELinux is enabled.
Deploy Locally via KIND
After reviewing the possible make targets it's quick and easy to get bpfman deployed locally on your system via a KIND cluster with:
Note
By default, bpfman-operator deploys bpfman with CSI enabled. CSI requires Kubernetes v1.26 due to a PR (kubernetes/kubernetes#112597) that addresses a gRPC Protocol Error that was seen in the CSI client code and it doesn't appear to have been backported. It is recommended to install kind v0.20.0 or later.
Deploy To Openshift Cluster
The recommended way of deploying bpfman to an OpenShift cluster is via the OpenShift Console and using Operator Hub. This is described in OperatorHub via OpenShift Console. For other options, see Deploy To Existing Cluster.
API Types Overview
Refer to api-spec.md for a detailed description of all the bpfman Kubernetes API types.
Cluster Scoped Versus Namespaced Scoped CRDs
For security reasons, cluster admins may want to limit certain applications to only loading eBPF programs
within a given namespace. However, not all eBPF programs make sense to be namespaced scoped.
To provide these controls for eBPF program loading, the bpfman operator includes both cluster-scoped CRDs (identified by the
Cluster
prefix) and namespace-scoped CRDs.
Deploy an eBPF Program to the cluster
There are sample yamls for a number of support program types in the bpfman-operator/config/samples directory.
Deploy Cluster Scoped Sample
Any of the cluster scoped samples can be applied as is.
To test the deployment simply deploy one of the sample xdpPrograms
:
If loading of the XDP Program to the selected nodes was successful it will be reported
back to the user via the ClusterBpfApplication
's status.
To see information in listing form simply run:
$ kubectl get ClusterBpfApplication xdp-pass-all-nodes
NAME NODESELECTOR STATUS AGE
xdp-pass-all-nodes Success 92s
For view full information, run:
$ kubectl get ClusterBpfApplication xdp-pass-all-nodes -o yaml
apiVersion: bpfman.io/v1alpha1
kind: ClusterBpfApplication
metadata:
annotations:
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"bpfman.io/v1alpha1","kind":"ClusterBpfApplication","metadata":{"annotations":{},"labels":{"app.kubernetes.io/name":"clusterbpfapplication"},"name":"xdp-pass-all-nodes"},"spec":{"byteCode":{"image":{"url":"quay.io/bpfman-bytecode/xdp_pass:latest"}},"nodeSelector":{},"programs":[{"name":"pass","type":"XDP","xdp":{"links":[{"interfaceSelector":{"primaryNodeInterface":true},"priority":55}]}}]}}
creationTimestamp: "2025-07-01T21:46:01Z"
finalizers:
- bpfman.io.operator/finalizer
generation: 1
labels:
app.kubernetes.io/name: clusterbpfapplication
name: xdp-pass-all-nodes
resourceVersion: "69629"
uid: 389cdde5-5916-4198-8ce8-2f7409d6c3f8
spec:
byteCode:
image:
imagePullPolicy: IfNotPresent
url: quay.io/bpfman-bytecode/xdp_pass:latest
nodeSelector: {}
programs:
- name: pass
type: XDP
xdp:
links:
- interfaceSelector:
primaryNodeInterface: true
priority: 55
proceedOn:
- Pass
- DispatcherReturn
status:
conditions:
- lastTransitionTime: "2025-07-01T21:46:10Z"
message: BPF application configuration successfully applied on all nodes
reason: Success
status: "True"
type: Success
To view each attachment point on each node, use the ClusterBpfApplicationState
object:
$ kubectl get clusterbpfapplicationstates -l bpfman.io/ownedByProgram=xdp-pass-all-nodes -o yaml
apiVersion: bpfman.io/v1alpha1
kind: ClusterBpfApplicationState
metadata:
creationTimestamp: "2025-07-01T21:46:01Z"
finalizers:
- bpfman.io.clbpfapplicationcontroller/finalizer
generation: 1
labels:
bpfman.io/ownedByProgram: xdp-pass-all-nodes
kubernetes.io/hostname: bpfman-deployment-control-plane
name: xdp-pass-all-nodes-cb774e3c
ownerReferences:
- apiVersion: bpfman.io/v1alpha1
blockOwnerDeletion: true
controller: true
kind: ClusterBpfApplication
name: xdp-pass-all-nodes
uid: 389cdde5-5916-4198-8ce8-2f7409d6c3f8
resourceVersion: "69628"
uid: b1436771-8591-490b-acaf-f229d079c336
status:
appLoadStatus: LoadSuccess
conditions:
- lastTransitionTime: "2025-07-01T21:46:10Z"
message: The BPF application has been successfully loaded and attached
reason: Success
status: "True"
type: Success
node: bpfman-deployment-control-plane
programs:
- name: pass
programId: 841
programLinkStatus: Success
type: XDP
xdp:
links:
- interfaceName: eth0
linkId: 1718587848
linkStatus: Attached
priority: 55
proceedOn:
- Pass
- DispatcherReturn
shouldAttach: true
uuid: c36d23bd-8ac2-4579-9e89-f745fd361fdd
updateCount: 2
Deploy Namespace Scoped Sample
The namespace scoped samples need a namespace and pods to attach to.
A yaml has been created that will create a Namespace
called "acme" (see
bpfman-operator/hack/namespace_scoped.yaml).
The reason for namespace scoped CRDs is to limit an application or user to a namespace.
To this end, this yaml also creates a limited ServiceAccount
, Role
, RoleBinding
and Secret
.
$ cd bpfman-operator
$ kubectl apply -f hack/namespace_scoped.yaml
namespace/acme created
serviceaccount/test-account created
role.rbac.authorization.k8s.io/test-account created
rolebinding.rbac.authorization.k8s.io/test-account created
secret/test-account-token created
To create a kubeconfig
file that limits access to the created namespace, use the script
bpfman-operator/hack/namespace_scoped.sh.
The script needs to know the name of the Cluster
, Namespace
, Service Account
and Secret
.
The script defaults these fields to what is currently in
bpfman-operator/hack/namespace_scoped.yaml.
However, if a file is passed to the script, it will look for the Secret
object and attempt to extract the values.
This can be used if the names are changed or a different yaml file is used.
The output of the script is the contents of a kubeconfig
.
This can be printed to the console or redirected to a file.
To use the kubeconfig
file, select the session to limit access in and run:
From within this limited access session, a sample nginx
deployment can be created in the same namespace.
Finally, load any of the namespaced samples from bpfman-operator/config/samples.
The status for each namespaced program is reported via the BpfApplication status field and further information can be seen in the BpfApplicationState CRDs. As an example, the following commands display the information of the program loaded in the acme namespace with the command above.
$ kubectl get bpfapplications -n acme
NAME NODESELECTOR STATUS AGE
bpfapplication-sample Success 2d6h
$ kubectl get bpfapplications bpfapplication-sample -n acme -o yaml
apiVersion: bpfman.io/v1alpha1
kind: BpfApplication
metadata:
annotations:
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"bpfman.io/v1alpha1","kind":"BpfApplication","metadata":{"annotations":{},"labels":{"app.kubernetes.io/name":"bpfapplication"},"name":"bpfapplication-sample","namespace":"acme"},"spec":{"byteCode":{"image":{"url":"quay.io/bpfman-bytecode/app-test:latest"}},"globalData":{"GLOBAL_u32":[13,12,11,10],"GLOBAL_u8":[1]},"nodeSelector":{},"programs":[{"name":"tc_pass_test","tc":{"links":[{"direction":"Ingress","interfaceSelector":{"interfaces":["eth0"]},"networkNamespaces":{"pods":{"matchLabels":{"app":"test-target"}}},"priority":55}]},"type":"TC"},{"name":"tcx_next_test","tcx":{"links":[{"direction":"Egress","interfaceSelector":{"interfaces":["eth0"]},"networkNamespaces":{"pods":{"matchLabels":{"app":"test-target"}}},"priority":100}]},"type":"TCX"},{"name":"uprobe_test","type":"UProbe","uprobe":{"links":[{"containers":{"pods":{"matchLabels":{"app":"test-target"}}},"function":"malloc","target":"libc"}]}},{"name":"uretprobe_test","type":"URetProbe","uretprobe":{"links":[{"containers":{"pods":{"matchLabels":{"app":"test-target"}}},"function":"malloc","target":"libc"}]}},{"name":"xdp_pass_test","type":"XDP","xdp":{"links":[{"interfaceSelector":{"interfaces":["eth0"]},"networkNamespaces":{"pods":{"matchLabels":{"app":"test-target"}}},"priority":100}]}}]}}
creationTimestamp: "2025-07-02T08:58:02Z"
finalizers:
- bpfman.io.operator/finalizer
generation: 2
labels:
app.kubernetes.io/name: bpfapplication
name: bpfapplication-sample
namespace: acme
resourceVersion: "74053"
uid: 0a2dd439-577f-49ae-81ae-54a9a2265ddb
spec:
byteCode:
image:
imagePullPolicy: IfNotPresent
url: quay.io/bpfman-bytecode/app-test:latest
globalData:
GLOBAL_u8: AQ==
GLOBAL_u32: DQwLCg==
nodeSelector: {}
programs:
- name: tc_pass_test
tc:
links:
- direction: Ingress
interfaceSelector:
interfaces:
- eth0
networkNamespaces:
pods:
matchLabels:
app: test-target
priority: 55
proceedOn:
- Pipe
- DispatcherReturn
type: TC
- name: tcx_next_test
tcx:
links:
- direction: Egress
interfaceSelector:
interfaces:
- eth0
networkNamespaces:
pods:
matchLabels:
app: test-target
priority: 100
type: TCX
- name: uprobe_test
type: UProbe
uprobe:
links:
- containers:
pods:
matchLabels:
app: test-target
function: malloc
offset: 0
target: libc
- name: uretprobe_test
type: URetProbe
uretprobe:
links:
- containers:
pods:
matchLabels:
app: test-target
function: malloc
offset: 0
target: libc
- name: xdp_pass_test
type: XDP
xdp:
links:
- interfaceSelector:
interfaces:
- eth0
networkNamespaces:
pods:
matchLabels:
app: test-target
priority: 100
proceedOn:
- Pass
- DispatcherReturn
status:
conditions:
- lastTransitionTime: "2025-07-02T08:58:19Z"
message: BPF application configuration successfully applied on all nodes
reason: Success
status: "True"
type: Success
To view each attachment point on each node, use the BpfApplicationState
object:
$ kubectl get bpfapplicationstate -n acme -l bpfman.io/ownedByProgram=bpfapplication-sample
NAME NODE STATUS AGE
bpfapplication-sample-6eab3078 bpfman-deployment-control-plane Success 2d7h
$ kubectl get bpfapplicationstate -n acme -l bpfman.io/ownedByProgram=bpfapplication-sample -o yaml
apiVersion: bpfman.io/v1alpha1
kind: BpfApplicationState
metadata:
creationTimestamp: "2025-07-02T08:58:02Z"
finalizers:
- bpfman.io.nsbpfapplicationcontroller/finalizer
generation: 1
labels:
bpfman.io/ownedByProgram: bpfapplication-sample
kubernetes.io/hostname: bpfman-deployment-control-plane
name: bpfapplication-sample-6eab3078
namespace: acme
ownerReferences:
- apiVersion: bpfman.io/v1alpha1
blockOwnerDeletion: true
controller: true
kind: BpfApplication
name: bpfapplication-sample
uid: 0a2dd439-577f-49ae-81ae-54a9a2265ddb
resourceVersion: "74052"
uid: 71499d5d-e248-404f-b432-112b395754dd
status:
appLoadStatus: LoadSuccess
conditions:
- lastTransitionTime: "2025-07-02T08:58:19Z"
message: The BPF application has been successfully loaded and attached
reason: Success
status: "True"
type: Success
node: bpfman-deployment-control-plane
programs:
- name: tc_pass_test
programId: 851
programLinkStatus: Success
tc: {}
type: TC
- name: tcx_next_test
programId: 852
programLinkStatus: Success
tcx: {}
type: TCX
- name: uprobe_test
programId: 853
programLinkStatus: Success
type: UProbe
uprobe: {}
- name: uretprobe_test
programId: 854
programLinkStatus: Success
type: URetProbe
uretprobe: {}
- name: xdp_pass_test
programId: 855
programLinkStatus: Success
type: XDP
xdp: {}
updateCount: 2