Introduction
This post is just a simple exploratory post to record my learning process of building a Kubernetes operator. We will build a simple Kubernetes operator using operator-sdk and go.
Prerequisites
- Go
- Kubernetes cluster (e.g. minikube, KinD, or real cluster)
- kubectl
- Operator-sdk cli
- kubebuilder (for generating Kubernetes APIs)
Create a new operator project
operator-sdk init --domain=ngublag.com --repo=github.com/ardfard/k8s-operator-showcase
This will scaffold Go project for the operator.
Define a Custom Resource Definition (CRD)
Now, create an API for our custom resource App:
operator-sdk create api --group apps --version v1alpha1 --kind App --resource --controller
This command generates:
- A CRD definition (api/v1alpha1/app_types.go)
- A controller (controllers/app_controller.go)
- Define the CRD Schema
Edit api/v1alpha1/app_types.go
to define the spec and status of our App resource:
package v1alpha1
import (
"k8s.io/apimachinery/pkg/apis/meta/v1"
metav1 )
// AppSpec defines the desired state
type AppSpec struct {
int32 `json:"replicas"`
Replicas string `json:"image"`
Image }
// AppStatus defines the observed state
type AppStatus struct {
int32 `json:"availableReplicas"`
AvailableReplicas }
//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
type App struct {
.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
metav1
`json:"spec,omitempty"`
Spec AppSpec `json:"status,omitempty"`
Status AppStatus }
//+kubebuilder:object:root=true
type AppList struct {
.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
metav1[]App `json:"items"`
Items }
func init() {
.Register(&App{}, &AppList{})
SchemeBuilder}
This defines an App resource with:
- A spec field containing replicas and image.
- A status field tracking availableReplicas.
- Init function to register the App resource with the SchemeBuilder.
Generate the CRD YAML:
make generate && make manifests
Implement the Controller Logic
Edit controllers/app_controller.go to manage the App resource. The controller will ensure that a corresponding Deployment exists.
Replace the Reconcile function with:
func (r *AppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
var app appsv1alpha1.App
if err := r.Get(ctx, req.NamespacedName, &app); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// Define deployment for the app
:= &appsv1.Deployment{
deployment : metav1.ObjectMeta{
ObjectMeta: app.Name,
Name: app.Namespace,
Namespace},
: appsv1.DeploymentSpec{
Spec: &app.Spec.Replicas,
Replicas: &metav1.LabelSelector{
Selector: map[string]string{"app": app.Name},
MatchLabels},
: corev1.PodTemplateSpec{
Template: metav1.ObjectMeta{Labels: map[string]string{"app": app.Name}},
ObjectMeta: corev1.PodSpec{
Spec: []corev1.Container{
Containers{
: "app",
Name: app.Spec.Image,
Image},
},
},
},
},
}
// Create or update the deployment
if err := r.Client.Create(ctx, deployment); err != nil {
return ctrl.Result{}, err
}
// Update the status of the app
.Status.AvailableReplicas = deployment.Status.AvailableReplicas
app
if err := r.Client.Status().Update(ctx, &app); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
Deploy the Operator
Build and Push the Operator Image
make docker-build IMG=example.com/app-operator:v1
make docker-push IMG=example.com/app-operator:v1
Deploy to Kubernetes
make deploy IMG=example.com/app-operator:v1
This installs the CRD and the Operator into the cluster.
Create a Custom Resource
Define an App instance in app-sample.yaml
:
apiVersion: apps.example.com/v1alpha1
kind: App
metadata:
name: my-app
spec:
replicas: 3
image: nginx:latest
Apply it:
kubectl apply -f app-sample.yaml
Check the created Deployment:
kubectl get deployments
Verify the Operator
List the running pods:
kubectl get pods
Check the App resource:
kubectl get app my-app -o yaml
You should see the status.availableReplicas field reflecting the deployment state.
Clean Up the Operator
make undeploy
Conclusion
This post demonstrated how to build a simple Kubernetes operator using operator-sdk and go. You can find the complete code in this repository.