Deployment¶
Kopf can be run outside the cluster, as long as the environment is authenticated to access the Kubernetes API. Normally, however, operators are deployed directly into the cluster.
Docker image¶
First, the operator must be packaged as a Docker image with Python 3.10 or newer:
FROM python:3.14
RUN pip install kopf
ADD . /src
CMD kopf run /src/handlers.py --verbose
Build and push it to a repository of your choice. Here, we use DockerHub (with the personal account “nolar” — replace it with your own name or namespace; you may also want to add version tags instead of the implied “latest”):
docker build -t nolar/kopf-operator .
docker push nolar/kopf-operator
See also
Read the DockerHub documentation for instructions on pushing and pulling Docker images.
Cluster deployment¶
The best way to deploy the operator to the cluster is via the Deployment object: it will be kept alive automatically, and upgrades will be applied properly on redeployment.
For this, create the deployment file:
apiVersion: apps/v1
kind: Deployment
metadata:
name: kopfexample-operator
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
application: kopfexample-operator
template:
metadata:
labels:
application: kopfexample-operator
spec:
serviceAccountName: kopfexample-account
containers:
- name: the-only-one
image: nolar/kopf-operator
Note that there is only one replica. Keep it that way. If two or more operators
run in the cluster for the same objects, they will collide with each other
and the consequences are unpredictable.
During pod restarts, only one pod should be running at a time as well:
use .spec.strategy.type=Recreate (see the documentation).
Deploy it to the cluster:
kubectl apply -f deployment.yaml
No services or ingresses are needed (unlike in typical web application examples), since the operator does not listen for incoming connections but only makes outgoing calls to the Kubernetes API.
RBAC¶
The pod where the operator runs must have permissions to access and manipulate objects, both domain-specific and built-in ones. For the example operator, those are:
kind: ClusterKopfPeeringfor the cross-operator awareness (cluster-wide).kind: KopfPeeringfor the cross-operator awareness (namespace-wide).kind: KopfExamplefor the example operator objects.kind: Pod/Job/PersistentVolumeClaimas the children objects.And others as needed.
For this, RBAC__ (Role-Based Access Control) can be used and attached to the operator’s pod via a service account.
__: https://kubernetes.io/docs/reference/access-authn-authz/rbac/
Here is an example of what an RBAC config should look like (remove the parts that are not needed: e.g. the cluster roles and bindings for a strictly namespace-bound operator):
---
apiVersion: v1
kind: ServiceAccount
metadata:
namespace: "{{NAMESPACE}}"
name: kopfexample-account
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: kopfexample-role-cluster
rules:
# Framework: knowing which other operators are running (i.e. peering).
- apiGroups: [kopf.dev]
resources: [clusterkopfpeerings]
verbs: [list, watch, patch, get]
# Framework: runtime observation of namespaces & CRDs (addition/deletion).
- apiGroups: [apiextensions.k8s.io]
resources: [customresourcedefinitions]
verbs: [list, watch]
- apiGroups: [""]
resources: [namespaces]
verbs: [list, watch]
# Framework: admission webhook configuration management.
- apiGroups: [admissionregistration.k8s.io/v1, admissionregistration.k8s.io/v1beta1]
resources: [validatingwebhookconfigurations, mutatingwebhookconfigurations]
verbs: [create, patch]
# Application: read-only access for watching cluster-wide.
- apiGroups: [kopf.dev]
resources: [kopfexamples]
verbs: [list, watch]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: "{{NAMESPACE}}"
name: kopfexample-role-namespaced
rules:
# Framework: knowing which other operators are running (i.e. peering).
- apiGroups: [kopf.dev]
resources: [kopfpeerings]
verbs: [list, watch, patch, get]
# Framework: posting the events about the handlers progress/errors.
- apiGroups: [""]
resources: [events]
verbs: [create]
# Application: watching & handling for the custom resource we declare.
- apiGroups: [kopf.dev]
resources: [kopfexamples]
verbs: [list, watch, patch]
# Application: other resources it produces and manipulates.
# Here, we create Jobs+PVCs+Pods, but we do not patch/update/delete them ever.
- apiGroups: [batch, extensions]
resources: [jobs]
verbs: [create]
- apiGroups: [""]
resources: [pods, persistentvolumeclaims]
verbs: [create]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: kopfexample-rolebinding-cluster
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: kopfexample-role-cluster
subjects:
- kind: ServiceAccount
name: kopfexample-account
namespace: "{{NAMESPACE}}"
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
namespace: "{{NAMESPACE}}"
name: kopfexample-rolebinding-namespaced
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: kopfexample-role-namespaced
subjects:
- kind: ServiceAccount
name: kopfexample-account
And the created service account is attached to the pods as follows:
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
serviceAccountName: kopfexample-account
containers:
- name: the-only-one
image: nolar/kopf-operator
Note that service accounts are always namespace-scoped. There are no cluster-wide service accounts. They must be created in the same namespace where the operator will run (even if it is going to serve the whole cluster).