{"id":53838,"date":"2025-10-09T05:10:50","date_gmt":"2025-10-09T05:10:50","guid":{"rendered":"https:\/\/www.devopsschool.com\/blog\/?p=53838"},"modified":"2025-10-09T05:10:50","modified_gmt":"2025-10-09T05:10:50","slug":"kubernetes-how-to-develop-kubernetes-operators","status":"publish","type":"post","link":"https:\/\/www.devopsschool.com\/blog\/kubernetes-how-to-develop-kubernetes-operators\/","title":{"rendered":"Kubernetes: How to Develop Kubernetes Operators"},"content":{"rendered":"\n<p>Let\u2019s build a tiny but <em>real<\/em> Kubernetes Operator end-to-end for <strong>Kubernetes 1.29<\/strong>. We\u2019ll do it the \u201cstandard\u201d way (Go + controller-runtime) using <strong>Operator SDK<\/strong> (which wraps Kubebuilder) and pin versions that are known to work well with v1.29. I\u2019ll show you how to <strong>develop<\/strong>, <strong>build<\/strong>, and <strong>deploy<\/strong>, with complete example code.<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>Heads-up: Kubernetes v1.29 is now in extended\/legacy support across major managed K8s (upstream support window has passed). The guide below is tested\/compatible with 1.29, but plan to upgrade soon. (<a href=\"https:\/\/docs.aws.amazon.com\/eks\/latest\/userguide\/kubernetes-versions.html?utm_source=chatgpt.com\" target=\"_blank\" rel=\"noopener\">AWS Documentation<\/a>)<\/p>\n<\/blockquote>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">0) What we\u2019re building<\/h1>\n\n\n\n<p>A CRD called <strong>Hello<\/strong> with fields:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>spec.message<\/code> (string) \u2013 the text to print<\/li>\n\n\n\n<li><code>spec.replicas<\/code> (int) \u2013 how many Pods<\/li>\n<\/ul>\n\n\n\n<p>The <strong>controller<\/strong> reconciles each <code>Hello<\/code> into a <strong>Deployment<\/strong> named <code>hello-&lt;cr-name&gt;<\/code> that runs a tiny container which prints the message forever. It also writes <code>status.readyReplicas<\/code>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">1) Prerequisites (for K8s 1.29)<\/h1>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Go <strong>1.22+<\/strong><\/li>\n\n\n\n<li>Docker (or another container builder)<\/li>\n\n\n\n<li>kubectl<\/li>\n\n\n\n<li>A Kubernetes <strong>1.29<\/strong> cluster (kind\/minikube\/EKS etc.)<\/li>\n\n\n\n<li>Operator SDK pinned to a release that targets K8s <strong>1.29<\/strong> (we\u2019ll use <code>v1.36.0<\/code>) (<a href=\"https:\/\/sdk.operatorframework.io\/docs\/upgrading-sdk-version\/v1.36.0\/?utm_source=chatgpt.com\" target=\"_blank\" rel=\"noopener\">sdk.operatorframework.io<\/a>)<\/li>\n<\/ul>\n\n\n\n<p>Install the SDK:<\/p>\n\n\n<pre class=\"wp-block-code\"><span><code class=\"hljs\">go install github.com\/operator-framework\/operator-sdk\/v3\/cmd\/operator-sdk@v1.36.0\n<\/code><\/span><\/pre>\n\n\n<p><em>(Kubebuilder\/Operator SDK projects use the same controller-runtime stack; commands like <code>make docker-build<\/code>\/<code>make deploy<\/code> come from the standard scaffolding.)<\/em> (<a href=\"https:\/\/book.kubebuilder.io\/quick-start.html?utm_source=chatgpt.com\" target=\"_blank\" rel=\"noopener\">book.kubebuilder.io<\/a>)<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">2) Scaffold the project<\/h1>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\">mkdir hello-operator &amp;&amp; cd hello-operator\n\n<span class=\"hljs-comment\"># Initialize a Go\/v4 project (the modern plugin line)<\/span>\noperator-sdk init \\\n  --domain=example.com \\\n  --owner <span class=\"hljs-string\">\"You\"<\/span> \\\n  --plugins go\/v4 \\\n  --project-name=hello-operator\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Create the API &amp; controller:<\/p>\n\n\n<pre class=\"wp-block-code\"><span><code class=\"hljs\">operator-sdk create api \\\n  --group=demo \\\n  --version=v1 \\\n  --kind=Hello \\\n  --resource --controller\n<\/code><\/span><\/pre>\n\n\n<p>This creates:<\/p>\n\n\n<pre class=\"wp-block-code\"><span><code class=\"hljs\">api\/v1\/hello_types.go\ncontrollers\/hello_controller.go\nconfig\/...\nMakefile, go.mod, main.go, etc.\n<\/code><\/span><\/pre>\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">3) Define the CRD types (api\/v1\/hello_types.go)<\/h1>\n\n\n\n<p>Replace the generated file with:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">package v1\n\n<span class=\"hljs-keyword\">import<\/span> (\n    metav1 <span class=\"hljs-string\">\"k8s.io\/apimachinery\/pkg\/apis\/meta\/v1\"<\/span>\n)\n\n<span class=\"hljs-comment\">\/\/ HelloSpec defines the desired state of Hello<\/span>\ntype HelloSpec struct {\n    <span class=\"hljs-comment\">\/\/ +kubebuilder:validation:MinLength=1<\/span>\n    Message string <span class=\"hljs-string\">`json:\"message\"`<\/span>\n\n    <span class=\"hljs-comment\">\/\/ +kubebuilder:validation:Minimum=1<\/span>\n    <span class=\"hljs-comment\">\/\/ +kubebuilder:default=1<\/span>\n    Replicas *int32 <span class=\"hljs-string\">`json:\"replicas,omitempty\"`<\/span>\n}\n\n<span class=\"hljs-comment\">\/\/ HelloStatus defines the observed state of Hello<\/span>\ntype HelloStatus struct {\n    <span class=\"hljs-comment\">\/\/ Ready replicas from the managed Deployment<\/span>\n    ReadyReplicas int32 <span class=\"hljs-string\">`json:\"readyReplicas,omitempty\"`<\/span>\n}\n\n<span class=\"hljs-comment\">\/\/+kubebuilder:object:root=true<\/span>\n<span class=\"hljs-comment\">\/\/+kubebuilder:subresource:status<\/span>\ntype Hello struct {\n    metav1.TypeMeta   <span class=\"hljs-string\">`json:\",inline\"`<\/span>\n    metav1.ObjectMeta <span class=\"hljs-string\">`json:\"metadata,omitempty\"`<\/span>\n\n    Spec   HelloSpec   <span class=\"hljs-string\">`json:\"spec,omitempty\"`<\/span>\n    Status HelloStatus <span class=\"hljs-string\">`json:\"status,omitempty\"`<\/span>\n}\n\n<span class=\"hljs-comment\">\/\/+kubebuilder:object:root=true<\/span>\ntype HelloList struct {\n    metav1.TypeMeta <span class=\"hljs-string\">`json:\",inline\"`<\/span>\n    metav1.ListMeta <span class=\"hljs-string\">`json:\"metadata,omitempty\"`<\/span>\n    Items           &#91;]Hello <span class=\"hljs-string\">`json:\"items\"`<\/span>\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Generate CRDs &amp; manifests:<\/p>\n\n\n<pre class=\"wp-block-code\"><span><code class=\"hljs\">make generate\nmake manifests\n<\/code><\/span><\/pre>\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">4) Implement the controller (controllers\/hello_controller.go)<\/h1>\n\n\n\n<p>Paste this full controller (it \u201ccreate-or-updates\u201d a Deployment and tracks status):<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">package controllers\n\n<span class=\"hljs-keyword\">import<\/span> (\n    <span class=\"hljs-string\">\"context\"<\/span>\n    <span class=\"hljs-string\">\"fmt\"<\/span>\n\n    appsv1 <span class=\"hljs-string\">\"k8s.io\/api\/apps\/v1\"<\/span>\n    corev1 <span class=\"hljs-string\">\"k8s.io\/api\/core\/v1\"<\/span>\n    <span class=\"hljs-string\">\"k8s.io\/apimachinery\/pkg\/api\/errors\"<\/span>\n    metav1 <span class=\"hljs-string\">\"k8s.io\/apimachinery\/pkg\/apis\/meta\/v1\"<\/span>\n    <span class=\"hljs-string\">\"k8s.io\/apimachinery\/pkg\/types\"<\/span>\n    <span class=\"hljs-string\">\"k8s.io\/utils\/ptr\"<\/span>\n\n    ctrl <span class=\"hljs-string\">\"sigs.k8s.io\/controller-runtime\"<\/span>\n    <span class=\"hljs-string\">\"sigs.k8s.io\/controller-runtime\/pkg\/client\"<\/span>\n    <span class=\"hljs-string\">\"sigs.k8s.io\/controller-runtime\/pkg\/controller\/controllerutil\"<\/span>\n    <span class=\"hljs-string\">\"sigs.k8s.io\/controller-runtime\/pkg\/log\"<\/span>\n\n    demov1 <span class=\"hljs-string\">\"github.com\/your-repo\/hello-operator\/api\/v1\"<\/span>\n)\n\n<span class=\"hljs-comment\">\/\/ +kubebuilder:rbac:groups=demo.example.com,resources=hellos,verbs=get;list;watch;create;update;patch;delete<\/span>\n<span class=\"hljs-comment\">\/\/ +kubebuilder:rbac:groups=demo.example.com,resources=hellos\/status,verbs=get;update;patch<\/span>\n<span class=\"hljs-comment\">\/\/ +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete<\/span>\n<span class=\"hljs-comment\">\/\/ +kubebuilder:rbac:groups=\"\",resources=events,verbs=create;patch<\/span>\n\ntype HelloReconciler struct {\n    client.Client\n}\n\n<span class=\"hljs-comment\">\/\/ Reconcile ensures a Deployment exists per Hello, then updates status.<\/span>\nfunc (r *HelloReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {\n    <span class=\"hljs-attr\">logger<\/span> := log.FromContext(ctx)\n\n    <span class=\"hljs-comment\">\/\/ 1) Get the Hello resource<\/span>\n    <span class=\"hljs-keyword\">var<\/span> hello demov1.Hello\n    <span class=\"hljs-keyword\">if<\/span> err := r.Get(ctx, req.NamespacedName, &amp;hello); err != nil {\n        <span class=\"hljs-keyword\">if<\/span> errors.IsNotFound(err) {\n            <span class=\"hljs-comment\">\/\/ CR deleted<\/span>\n            <span class=\"hljs-keyword\">return<\/span> ctrl.Result{}, nil\n        }\n        <span class=\"hljs-keyword\">return<\/span> ctrl.Result{}, err\n    }\n\n    <span class=\"hljs-comment\">\/\/ Defaults<\/span>\n    <span class=\"hljs-attr\">replicas<\/span> := int32(<span class=\"hljs-number\">1<\/span>)\n    <span class=\"hljs-keyword\">if<\/span> hello.Spec.Replicas != nil {\n        replicas = *hello.Spec.Replicas\n    }\n    <span class=\"hljs-attr\">msg<\/span> := hello.Spec.Message\n    <span class=\"hljs-keyword\">if<\/span> msg == <span class=\"hljs-string\">\"\"<\/span> {\n        msg = <span class=\"hljs-string\">\"Hello from \"<\/span> + hello.Name\n    }\n\n    <span class=\"hljs-comment\">\/\/ 2) Desired Deployment<\/span>\n    <span class=\"hljs-attr\">depName<\/span> := fmt.Sprintf(<span class=\"hljs-string\">\"hello-%s\"<\/span>, hello.Name)\n    <span class=\"hljs-attr\">labels<\/span> := map&#91;string]string{\n        <span class=\"hljs-string\">\"app.kubernetes.io\/name\"<\/span>:       <span class=\"hljs-string\">\"hello\"<\/span>,\n        <span class=\"hljs-string\">\"app.kubernetes.io\/instance\"<\/span>:   hello.Name,\n        <span class=\"hljs-string\">\"app.kubernetes.io\/managed-by\"<\/span>: <span class=\"hljs-string\">\"hello-operator\"<\/span>,\n    }\n\n    <span class=\"hljs-keyword\">var<\/span> dep appsv1.Deployment\n    <span class=\"hljs-attr\">depKey<\/span> := types.NamespacedName{<span class=\"hljs-attr\">Name<\/span>: depName, <span class=\"hljs-attr\">Namespace<\/span>: hello.Namespace}\n    <span class=\"hljs-keyword\">if<\/span> err := r.Get(ctx, depKey, &amp;dep); err != nil &amp;&amp; !errors.IsNotFound(err) {\n        <span class=\"hljs-keyword\">return<\/span> ctrl.Result{}, err\n    }\n\n    <span class=\"hljs-comment\">\/\/ create or update the Deployment<\/span>\n    op, <span class=\"hljs-attr\">err<\/span> := controllerutil.CreateOrUpdate(ctx, r.Client, &amp;dep, func() error {\n        dep.ObjectMeta.Name = depName\n        dep.ObjectMeta.Namespace = hello.Namespace\n        <span class=\"hljs-keyword\">if<\/span> dep.ObjectMeta.Labels == nil {\n            dep.ObjectMeta.Labels = map&#91;string]string{}\n        }\n        <span class=\"hljs-keyword\">for<\/span> k, <span class=\"hljs-attr\">v<\/span> := range labels {\n            dep.ObjectMeta.Labels&#91;k] = v\n        }\n\n        <span class=\"hljs-comment\">\/\/ OwnerRef so it gets garbage-collected with the CR<\/span>\n        <span class=\"hljs-keyword\">if<\/span> err := controllerutil.SetControllerReference(&amp;hello, &amp;dep, r.Scheme()); err != nil {\n            <span class=\"hljs-keyword\">return<\/span> err\n        }\n\n        dep.Spec.Selector = &amp;metav1.LabelSelector{<span class=\"hljs-attr\">MatchLabels<\/span>: labels}\n        dep.Spec.Replicas = ptr.To(replicas)\n        dep.Spec.Template.ObjectMeta.Labels = labels\n        dep.Spec.Template.Spec.Containers = &#91;]corev1.Container{\n            {\n                <span class=\"hljs-attr\">Name<\/span>:  <span class=\"hljs-string\">\"hello\"<\/span>,\n                <span class=\"hljs-attr\">Image<\/span>: <span class=\"hljs-string\">\"busybox:1.36\"<\/span>, <span class=\"hljs-comment\">\/\/ tiny &amp; works on 1.29<\/span>\n                <span class=\"hljs-attr\">Command<\/span>: &#91;]string{<span class=\"hljs-string\">\"\/bin\/sh\"<\/span>, <span class=\"hljs-string\">\"-c\"<\/span>},\n                <span class=\"hljs-attr\">Args<\/span>: &#91;]string{\n                    <span class=\"hljs-string\">`while true; do echo \"$(date) `<\/span> + msg + <span class=\"hljs-string\">`\"; sleep 5; done`<\/span>,\n                },\n                <span class=\"hljs-comment\">\/\/ Optional: expose the message as an env var instead<\/span>\n                <span class=\"hljs-attr\">Env<\/span>: &#91;]corev1.EnvVar{{<span class=\"hljs-attr\">Name<\/span>: <span class=\"hljs-string\">\"MESSAGE\"<\/span>, <span class=\"hljs-attr\">Value<\/span>: msg}},\n            },\n        }\n        <span class=\"hljs-keyword\">return<\/span> nil\n    })\n    <span class=\"hljs-keyword\">if<\/span> err != nil {\n        <span class=\"hljs-keyword\">return<\/span> ctrl.Result{}, err\n    }\n    <span class=\"hljs-keyword\">if<\/span> op != controllerutil.OperationResultNone {\n        logger.Info(<span class=\"hljs-string\">\"deployment reconciled\"<\/span>, <span class=\"hljs-string\">\"op\"<\/span>, op, <span class=\"hljs-string\">\"name\"<\/span>, depName)\n    }\n\n    <span class=\"hljs-comment\">\/\/ 3) Update status<\/span>\n    <span class=\"hljs-attr\">ready<\/span> := int32(<span class=\"hljs-number\">0<\/span>)\n    <span class=\"hljs-keyword\">if<\/span> dep.Status.ReadyReplicas &gt; <span class=\"hljs-number\">0<\/span> {\n        ready = dep.Status.ReadyReplicas\n    }\n    <span class=\"hljs-keyword\">if<\/span> hello.Status.ReadyReplicas != ready {\n        hello.Status.ReadyReplicas = ready\n        <span class=\"hljs-keyword\">if<\/span> err := r.Status().Update(ctx, &amp;hello); err != nil {\n            <span class=\"hljs-keyword\">return<\/span> ctrl.Result{}, err\n        }\n    }\n\n    <span class=\"hljs-comment\">\/\/ Requeue on changes automatically via watch; no explicit requeue needed<\/span>\n    <span class=\"hljs-keyword\">return<\/span> ctrl.Result{}, nil\n}\n\nfunc (r *HelloReconciler) SetupWithManager(mgr ctrl.Manager) error {\n    <span class=\"hljs-keyword\">return<\/span> ctrl.NewControllerManagedBy(mgr).\n        For(&amp;demov1.Hello{}).\n        Owns(&amp;appsv1.Deployment{}).\n        Complete(r)\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>Note the <code>+kubebuilder:rbac<\/code> lines\u2014these generate RBAC in <code>config\/rbac\/role.yaml<\/code> during <code>make manifests<\/code>.<\/p>\n<\/blockquote>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">5) Build &amp; run locally against your cluster<\/h1>\n\n\n\n<p>Apply CRDs:<\/p>\n\n\n<pre class=\"wp-block-code\"><span><code class=\"hljs\">make install\n<\/code><\/span><\/pre>\n\n\n<p>Run the controller <strong>locally<\/strong>:<\/p>\n\n\n<pre class=\"wp-block-code\"><span><code class=\"hljs\">make run\n<\/code><\/span><\/pre>\n\n\n<p>Create a sample CR:<\/p>\n\n\n<pre class=\"wp-block-code\"><span><code class=\"hljs\"># config\/samples\/demo_v1_hello.yaml\napiVersion: demo.example.com\/v1\nkind: Hello\nmetadata:\n  name: hello-sample\n  namespace: default\nspec:\n  message: \"Hello from Operator on K8s 1.29 \ud83d\udc4b\"\n  replicas: 2\n<\/code><\/span><\/pre>\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">kubectl apply -f config\/samples\/demo_v1_hello.yaml\nkubectl <span class=\"hljs-keyword\">get<\/span> hello -A\nkubectl <span class=\"hljs-keyword\">get<\/span> deploy -l app.kubernetes.io\/name=hello -n default\nkubectl logs -f deploy\/hello-hello-sample -n default\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>(Those standard <code>make<\/code> targets and flow are from the Kubebuilder\/Operator SDK quick start.) (<a href=\"https:\/\/book.kubebuilder.io\/quick-start.html?utm_source=chatgpt.com\" target=\"_blank\" rel=\"noopener\">book.kubebuilder.io<\/a>)<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">6) Containerize &amp; deploy the operator in-cluster<\/h1>\n\n\n\n<p>Build and push the manager image:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">export<\/span> IMG=<span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">your-registry<\/span>&gt;<\/span>\/hello-operator:v0.1.0\nmake docker-build docker-push IMG=$IMG\n<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Deploy RBAC\/manager\/CRDs with that image:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\">make deploy IMG=$IMG\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Now the operator runs as a Deployment in <code>hello-operator-system<\/code>. Create CRs and watch it manage Deployments.<\/p>\n\n\n\n<p>Cleanup:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">make undeploy\nkubectl <span class=\"hljs-keyword\">delete<\/span> -f config\/samples\/demo_v1_hello.yaml\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p><em>(These targets and sequence are the canonical way to package and deploy operators.)<\/em> (<a href=\"https:\/\/book.kubebuilder.io\/quick-start.html?utm_source=chatgpt.com\" target=\"_blank\" rel=\"noopener\">book.kubebuilder.io<\/a>)<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">7) Example <code>go.mod<\/code> (excerpt)<\/h1>\n\n\n\n<p>Let the scaffolding pin versions, but you\u2019ll see something like:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-built_in\">module<\/span> github.com\/your-repo\/hello-operator\n\ngo <span class=\"hljs-number\">1.22<\/span>\n\n<span class=\"hljs-built_in\">require<\/span> (\n    sigs.k8s.io\/controller-runtime v0.x.y\n    k8s.io\/api v0<span class=\"hljs-number\">.29<\/span>.x\n    k8s.io\/apimachinery v0<span class=\"hljs-number\">.29<\/span>.x\n    k8s.io\/client-go v0<span class=\"hljs-number\">.29<\/span>.x\n)\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>Controller-runtime &amp; client-go versions are what tie to Kubernetes 1.29; Operator SDK v1.36 aligns those dependencies to K8s v1.29. (<a href=\"https:\/\/sdk.operatorframework.io\/docs\/upgrading-sdk-version\/v1.36.0\/?utm_source=chatgpt.com\" target=\"_blank\" rel=\"noopener\">sdk.operatorframework.io<\/a>)<\/p>\n<\/blockquote>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">8) Sample CRs to try<\/h1>\n\n\n\n<p><strong>Scale &amp; message change<\/strong>:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">apiVersion: demo.example.com\/v1\n<span class=\"hljs-attr\">kind<\/span>: Hello\n<span class=\"hljs-attr\">metadata<\/span>:\n  name: hello-scale\n<span class=\"hljs-attr\">spec<\/span>:\n  message: <span class=\"hljs-string\">\"Namaste from 1.29!\"<\/span>\n  <span class=\"hljs-attr\">replicas<\/span>: <span class=\"hljs-number\">3<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Apply, then modify <code>replicas<\/code> or <code>message<\/code> to see the Deployment update.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">9) Namespacing \/ watch scope (optional)<\/h1>\n\n\n\n<p>By default the operator is cluster-scoped. To scope to a namespace, set <code>WATCH_NAMESPACE<\/code> in <code>config\/manager\/manager.yaml<\/code> (env var) before <code>make deploy<\/code>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">10) Testing notes for 1.29 (optional)<\/h1>\n\n\n\n<p>If you later add unit\/envtest tests (<code>make test<\/code>), <strong>envtest<\/strong> binary locations changed after K8s <strong>1.29.3<\/strong>\u2014Kubebuilder docs explain how to fetch the right envtest assets in <code>ENVTEST_KUBERNETES_VERSION<\/code>. (<a href=\"https:\/\/book.kubebuilder.io\/reference\/artifacts?utm_source=chatgpt.com\" target=\"_blank\" rel=\"noopener\">book.kubebuilder.io<\/a>)<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">11) Alternative: pure Kubebuilder<\/h1>\n\n\n\n<p>You can build the exact same operator using Kubebuilder directly:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-10\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\">go install sigs.k8s.io\/kubebuilder\/v4@latest\nkubebuilder init --domain=example.com --plugins go\/v4 --repo=github.com\/your-repo\/hello-operator\nkubebuilder create api --group=demo --version=v1 --kind=Hello\n<span class=\"hljs-comment\"># (same code as above, same make targets)<\/span>\nmake docker-build docker-push IMG=$IMG\nmake deploy IMG=$IMG\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-10\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Kubebuilder\u2019s book documents the same build\/deploy flow and targets. (<a href=\"https:\/\/book.kubebuilder.io\/quick-start.html?utm_source=chatgpt.com\" target=\"_blank\" rel=\"noopener\">book.kubebuilder.io<\/a>)<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Troubleshooting (1.29-specific gotchas)<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Beta APIs removed<\/strong>: If any third-party manifests in your cluster use removed beta API versions, they can fail on 1.29; check release notes and update them. (<a href=\"https:\/\/kubernetes.io\/blog\/2023\/11\/16\/kubernetes-1-29-upcoming-changes\/?utm_source=chatgpt.com\" target=\"_blank\" rel=\"noopener\">Kubernetes<\/a>)<\/li>\n\n\n\n<li><strong>Managed K8s<\/strong>: EKS keeps 1.29 under extended support (extra cost\/time-bound). Plan a minor upgrade path when possible. (<a href=\"https:\/\/docs.aws.amazon.com\/eks\/latest\/userguide\/kubernetes-versions.html?utm_source=chatgpt.com\" target=\"_blank\" rel=\"noopener\">AWS Documentation<\/a>)<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">You now have:<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>A CRD (<code>Hello<\/code>)<\/li>\n\n\n\n<li>A controller that <strong>creates\/updates<\/strong> a Deployment and <strong>updates status<\/strong><\/li>\n\n\n\n<li>A container image and a <strong>fully deployed<\/strong> operator<\/li>\n<\/ul>\n\n\n\n<p>If you want, I can tailor this for <strong>EKS\/ECR<\/strong> pushes (login\/tag commands), add a <strong>Helm chart<\/strong> for CRs, or wire in <strong>Prometheus metrics<\/strong>\/<strong>health probes<\/strong> (controller-runtime exposes them out-of-the-box). (<a href=\"https:\/\/book.kubebuilder.io\/reference\/metrics?utm_source=chatgpt.com\" target=\"_blank\" rel=\"noopener\">book.kubebuilder.io<\/a>)<\/p>\n\n\n\n<p>Happy operating! \ud83e\uddd1\u200d\u2708\ufe0f<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Let\u2019s build a tiny but real Kubernetes Operator end-to-end for Kubernetes 1.29. We\u2019ll do it the \u201cstandard\u201d way (Go + controller-runtime) using Operator SDK (which wraps Kubebuilder) and pin versions&#8230; <\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"_joinchat":[],"footnotes":""},"categories":[11138],"tags":[],"class_list":["post-53838","post","type-post","status-publish","format-standard","hentry","category-best-tools"],"_links":{"self":[{"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/posts\/53838","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/comments?post=53838"}],"version-history":[{"count":1,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/posts\/53838\/revisions"}],"predecessor-version":[{"id":53839,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/posts\/53838\/revisions\/53839"}],"wp:attachment":[{"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/media?parent=53838"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/categories?post=53838"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/tags?post=53838"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}