05 - Kubernetes Client-Go
📋 Jump to Takeaways🎁 What if you could build your own kubectl, Helm, or ArgoCD? They all share one library, and once you learn it, you can build anything they can.
Imports in code snippets are trimmed for brevity. See the full example linked at the end for complete, compilable source.
Client-Go Library Overview
client-go is the official Go client library for Kubernetes. Every tool that talks to a K8s cluster (kubectl, Helm, ArgoCD, custom controllers) uses it internally. The library provides typed clients for all built-in resources, dynamic clients for CRDs, and an informer framework for efficient event-driven watching.
go get k8s.io/client-go@latest
go get k8s.io/apimachinery@latestKey packages:
| Package | Purpose |
|---|---|
kubernetes |
Typed clientset for built-in resources |
rest |
REST client config and transport |
tools/clientcmd |
Load kubeconfig files |
informers |
Cached, event-driven resource watching |
dynamic |
Untyped client for any resource (CRDs) |
Loading Kubeconfig
Your tool needs to authenticate to the API server. There are two modes:
Out-of-Cluster Configuration
For CLI tools, local development, or anything running outside the cluster:
package main
import (
"path/filepath"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/util/homedir"
)
func outOfClusterClient() (*kubernetes.Clientset, error) {
kubeconfig := filepath.Join(homedir.HomeDir(), ".kube", "config")
config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
if err != nil {
return nil, err
}
return kubernetes.NewForConfig(config)
}In-Cluster Configuration
For pods running inside the cluster (controllers, operators, jobs):
import "k8s.io/client-go/rest"
func inClusterClient() (*kubernetes.Clientset, error) {
config, err := rest.InClusterConfig()
if err != nil {
return nil, err
}
return kubernetes.NewForConfig(config)
}Unified Pattern
Most tools combine both approaches:
func getClient() (*kubernetes.Clientset, error) {
// Try in-cluster first
config, err := rest.InClusterConfig()
if err != nil {
// Fall back to kubeconfig
kubeconfig := filepath.Join(homedir.HomeDir(), ".kube", "config")
config, err = clientcmd.BuildConfigFromFlags("", kubeconfig)
if err != nil {
return nil, err
}
}
return kubernetes.NewForConfig(config)
}Creating Clientsets
The Clientset groups typed clients by API group and version. You access resources through a hierarchy:
clientset, _ := kubernetes.NewForConfig(config)
// Core v1 resources
clientset.CoreV1().Pods("default")
clientset.CoreV1().Services("default")
clientset.CoreV1().ConfigMaps("my-namespace")
// Apps v1 resources
clientset.AppsV1().Deployments("default")
clientset.AppsV1().StatefulSets("default")
// All namespaces
clientset.CoreV1().Pods("") // empty string = all namespacesListing Pods, Deployments, and Services
Each resource client exposes standard CRUD operations:
import (
"context"
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func listResources(clientset *kubernetes.Clientset) error {
ctx := context.Background()
// List pods with label selector
pods, err := clientset.CoreV1().Pods("default").List(ctx, metav1.ListOptions{
LabelSelector: "app=web",
})
if err != nil {
return err
}
for _, pod := range pods.Items {
fmt.Printf("Pod: %s | Phase: %s | IP: %s\n",
pod.Name, pod.Status.Phase, pod.Status.PodIP)
// Output: Pod: web-abc123 | Phase: Running | IP: 10.244.0.12
}
// List deployments
deployments, err := clientset.AppsV1().Deployments("").List(ctx, metav1.ListOptions{})
if err != nil {
return err
}
for _, d := range deployments.Items {
fmt.Printf("Deployment: %s/%s | Replicas: %d/%d\n",
d.Namespace, d.Name, d.Status.ReadyReplicas, *d.Spec.Replicas)
// Output: Deployment: default/web-app | Replicas: 3/3
}
return nil
}Watch and Informers for Real-Time Updates
Polling the API server is wasteful. Informers maintain a local cache and deliver events when resources change:
import (
"time"
"k8s.io/client-go/informers"
"k8s.io/client-go/tools/cache"
)
func watchPods(clientset *kubernetes.Clientset) {
// Create a shared informer factory (resync every 30s)
factory := informers.NewSharedInformerFactory(clientset, 30*time.Second)
podInformer := factory.Core().V1().Pods().Informer()
podInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
pod := obj.(*corev1.Pod)
fmt.Printf("[ADD] %s/%s\n", pod.Namespace, pod.Name)
// Output: [ADD] default/web-abc123
},
UpdateFunc: func(oldObj, newObj interface{}) {
pod := newObj.(*corev1.Pod)
fmt.Printf("[UPDATE] %s/%s phase=%s\n",
pod.Namespace, pod.Name, pod.Status.Phase)
// Output: [UPDATE] default/web-abc123 phase=Running
},
DeleteFunc: func(obj interface{}) {
pod := obj.(*corev1.Pod)
fmt.Printf("[DELETE] %s/%s\n", pod.Namespace, pod.Name)
// Output: [DELETE] default/web-abc123
},
})
// Start all informers
stopCh := make(chan struct{})
factory.Start(stopCh)
factory.WaitForCacheSync(stopCh)
// Block forever (or until signal)
<-stopCh
}Informers use a single watch connection per resource type and share the cache across your application. Always prefer informers over raw Watch calls.
Creating and Patching Resources
Create resources by constructing typed objects:
import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func createConfigMap(clientset *kubernetes.Clientset) error {
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "app-config",
Namespace: "default",
Labels: map[string]string{
"managed-by": "go-devops-tool",
},
},
Data: map[string]string{
"database_url": "postgres://db:5432/app",
"log_level": "info",
"feature_flags": `{"new_ui": true, "beta_api": false}`,
},
}
created, err := clientset.CoreV1().ConfigMaps("default").Create(
context.Background(), cm, metav1.CreateOptions{},
)
if err != nil {
return err
}
fmt.Printf("Created ConfigMap: %s (uid: %s)\n", created.Name, created.UID)
// Output: Created ConfigMap: app-config (uid: 7a3f2c91-4e8b-11ee-be56-0242ac120002)
return nil
}For partial updates, use strategic merge patches:
import (
"encoding/json"
"k8s.io/apimachinery/pkg/types"
)
func patchDeploymentReplicas(clientset *kubernetes.Clientset, name string, replicas int32) error {
patch := map[string]interface{}{
"spec": map[string]interface{}{
"replicas": replicas,
},
}
patchBytes, _ := json.Marshal(patch)
_, err := clientset.AppsV1().Deployments("default").Patch(
context.Background(),
name,
types.StrategicMergePatchType,
patchBytes,
metav1.PatchOptions{},
)
return err
}Dynamic Client for CRDs
The typed clientset only knows built-in resources. For Custom Resource Definitions, use the dynamic client:
import (
"k8s.io/client-go/dynamic"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
func listCustomResources(config *rest.Config) error {
dynClient, err := dynamic.NewForConfig(config)
if err != nil {
return err
}
// Define the GVR (Group/Version/Resource) for your CRD
gvr := schema.GroupVersionResource{
Group: "apps.example.com",
Version: "v1",
Resource: "webapps",
}
// List all instances
list, err := dynClient.Resource(gvr).Namespace("default").List(
context.Background(), metav1.ListOptions{},
)
if err != nil {
return err
}
for _, item := range list.Items {
name := item.GetName()
spec, _, _ := unstructured.NestedMap(item.Object, "spec")
fmt.Printf("WebApp: %s | Spec: %v\n", name, spec)
// Output: WebApp: my-service | Spec: map[image:nginx:1.25 port:8080 replicas:3]
}
return nil
}Cluster Resource Watcher
A Kubernetes client tool demonstrating three core client-go patterns: listing pods across all namespaces, creating a ConfigMap, and watching for real-time pod changes using a shared informer. It auto-detects in-cluster vs local kubeconfig and handles graceful shutdown on Ctrl+C.
Input: A valid kubeconfig at ~/.kube/config (or in-cluster service account when running inside a pod)
Output: A list of all pods with their status, ConfigMap creation confirmation, and a live stream of pod add/delete events
Full source: examples/cluster-resource-watcher
Key Takeaways
client-gois the foundation for all Go-based Kubernetes tooling. Learn it and you can build anything from CLI tools to operators- Always use the unified in-cluster/out-of-cluster config pattern so your tool works everywhere
- Informers are far more efficient than polling; they maintain a local cache and deliver granular add/update/delete events
- The dynamic client handles any resource type including CRDs without compile-time type information
- Strategic merge patches allow surgical updates without fetching and resending the entire resource
- All operations are context-aware. Pass cancellable contexts for proper shutdown handling
🎁 What if your code could watch for changes and automatically reconcile the cluster to match a desired state, no human intervention needed?