In a previous article, we learned that Kubernetes essentially is an API under-the-hood and every action you take within a Kubernetes cluster, be it the creation of pods or the monitoring of services, boils down to interactions with its API.
We learned that there are three different ways of interacting with the API:
via
kubectl
via simple HTTP requests with
curl
using Client Libraries
In our previous discussion, we particularly focused on accessing the Kubernetes API by making simple HTTP requests using the curl
command - which is a practical and a very beginner-friendly way of understanding the API mechanics.
Now, in this article we’ll be taking that concept to a next level and focus on accessing the Kubernetes API programmatically i.e via client libraries.
The aim here is to provide you a step-by-step guide on performing basic CRUD operations (create, read, update and delete) on a Kubernetes resource, using Go as the programming language.
Getting Started - Understanding the Basics
Before we dive into the demo itself, let’s ensure we have the basics firmly in place!
Familiarity with Kubernetes API Concepts
Throughout the guide, we'll use various terminologies associated with the core Kubernetes API concepts, which were, very conveniently covered in the previous blog(and that too, in-depth). If you haven't already, I highly recommend checking that out before moving on with this one.
Importance of Using Client Libraries
One may ask this question - Why make things more complicated? Why not just stick to the basic HTTP requests instead of using a client library?
Thats a valid question to consider and and there are several reasons to why learning and using a client library is essential:
Abstraction of Complexity - Client libraries abstract away the complexities of direct HTTP communication, offering a simpler way to interact with API.
Error Handling - These libraries typically come with built-in error handling mechanisms. Thus, simplifying the process of dealing with errors and failures when interacting with API (which is certainly important, right?).
Efficiency - These libraries often provide optimized methods for common tasks, reducing the amount of code needed and improving overall efficiency. (we’ll get a better idea of this in the upcoming sections)
Community Support - Popular client libraries have a strong community of users who contribute to documentation, provide support, and share best practices - which in turn makes it easier to learn and troubleshoot any issues that may arise during application development.
Exploring Client-go
As mentioned previously, we’ll be using the Go programming language to perform basic CRUD operations on a Kubernetes resource via the client library.
Now, the official Go client library used for interacting with Kubernetes clusters is called client-go. It provides a set of functions and structures needed to interact with Kubernetes API programmatically, allowing developers to manage resources such as pods, services, deployments, and much more.
The purpose is simple - to simplify the development of Kubernetes-related applications. It does this by abstracting away the complexities of working directly with Kubernetes API, providing a more user-friendly interface for Go developers.
One may ask this question here - “So, we don’t interact with the API when usingclient-go
?”
That’s partially correct! When using client-go
, we don't interact “directly” with the Kubernetes API endpoints. Instead, client-go
provides a layer of abstraction between the us (the developers) and the low-level details of making HTTP requests to those endpoints.
It provides all the necessary set of functions and data structures that developers can use to perform actions on Kubernetes resources (like pods, services, deployments, etc.) without needing to handle the HTTP communication themselves.
Demo - CRUD Operations on Pod
To keep things simple, we'll be performing the basic CRUD operations on a Pod:
Creating a Pod.
Retrieving all the current Pods in the cluster.
Updating an existing Pod.
Deleting an existing Pod.
Prerequisites
Before we begin with the development, here are a few things you’ll need:
kubectl installed
Go installed (latest version)
A Kubernetes cluster (we’ll be using minikube for this tutorial, but feel free to choose any other tool)
Step 1 - Creating a Kubernetes Cluster
Here, we’ll use minikube to bootstrap a single node Kubernetes cluster using the following command:
$ minikube start
😄 minikube v1.32.0 on Darwin 14.4 (arm64)
✨ Using the docker driver based on existing profile
👍 Starting control plane node minikube in cluster minikube
🚜 Pulling base image ...
🔥 Creating docker container (CPUs=2, Memory=7792MB) ...
🐳 Preparing Kubernetes v1.28.3 on Docker 24.0.7 ...
🔗 Configuring bridge CNI (Container Networking Interface) ...
🔎 Verifying Kubernetes components...
▪ Using image gcr.io/k8s-minikube/storage-provisioner:v5
🌟 Enabled addons: storage-provisioner, default-storageclass
...
As the cluster creation process finishes, use the following command to check the cluster information:
$ kubectl cluster-info
Kubernetes control plane is running at https://127.0.0.1:52016
CoreDNS is running at https://127.0.0.1:52016/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
Step 2 - Initial Project Setup
In this step, we’ll be doing the following things:
Set up the Go project environment
Install the
k8s.io/client-go
module
Let us start by creating a new directory for our project and initializing a new Go module (go.mod
):
$ mkdir k8s-crud-demo
$ cd k8s-crud-demo
$ go mod init github.com/USERNAME/k8s-crud
Now, we can install the k8s.io/client-go
module using the following command:
go get k8s.io/client-go@latest
This will install the latest version of the k8s.io/client-go
module, which includes all the necessary packages needed to interact with the Kubernetes API.
Step 3 - Create a new Kubernetes Client
Before we perform any operations on an existing Kubernetes cluster, we first need to create a new client.
Even if we are using a client library in this scenario, it all boils down to the basic client-server communication and we understood in the previous blog post that, only an authenticated client can make requests to the Kubernetes API.
Therefore, regardless of whether we’re using a client library like client-go
or not, we need to establish an authenticated connection with the Kubernetes API server.
Thankfully, the process here is much simpler than what we did while making HTTP requests, as we’ll directly be using the existing kubeconfig
file to get the cluster info and create a new client from that.
Get the location of the
kubeconfig
file from the system and store that in a variable:home, _ := os.UserHomeDir() kubeConfigPath := filepath.Join(home, ".kube/config")
Next, we’ll use the
BuildConfigFromFlags()
method from thek8s.io/client-go/tools/clientcmd
package to create a new client configuration based on the providedkubeconfig
file:config, err := clientcmd.BuildConfigFromFlags("", kubeConfigPath) if err != nil { panic(err.Error()) }
At last, we’ll use this configuration to create a new client, using the
k8s.io/client-go/kubernetes
package:client := kubernetes.NewForConfigOrDie(config)
Step 4 - Retrieving All the Current Pods
Let us start with the most basic operation - reading and listing down all the current running pods in our Kubernetes cluster.
📍 Note
Before building the logic for this operation, make sure you already have a few pods running in your newly created cluster to see some output in the end.
You can use
kubectl
in this case to do so:$ kubectl run demo --image=nginx pod/demo created
In this scenario, I have the following pods running in my cluster:
$ kubectl get pods NAME READY STATUS RESTARTS AGE demo-crud55wwk 1/1 Running 1 (6d ago) 6d1h demo-nginx 1/1 Running 1 (6d ago) 6d1h go-api-2mwpl 1/1 Running 2 (6d ago) 6d1h test-deploy-859f95ffcc-8p8t8 1/1 Running 8 (6d ago) 18d test-deploy-859f95ffcc-fcdld 1/1 Running 8 (6d ago) 18d
For the logic of retrieving all the running pods in a cluster, paste the following code snippet in your main.go
file:
// define the namespace
namespace := "default"
// get the Pod interface (easy for later use)
podsClient := client.CoreV1().Pods(namespace)
// read all pods
pods, err := podsClient.List(context.TODO(), metav1.ListOptions{})
if err != nil {
panic(err.Error())
}
fmt.Printf("There are %d pods in the cluster\n", len(pods.Items))
// loop through pod list to get names
for i, pod := range pods.Items {
fmt.Printf("Name of %dth pod: %s\n", i, pod.Name)
}
A breakdown of the core logic being used is as follows:
client.CoreV1().Pods(namespace)
- In the previous article, we covered that the Kubernetes API resources are divided into different API Groups and versions. Now, Pod (a K8s resource) is found under the core group, havingv1
as the version.So, in this line we are first calling the
CoreV1()
function from thek8s.io/client-go/kubernetes
package which returns aCoreV1Interface
interface, which is a collection of some embedded interfaces of all the Kubernetes resources that fall under thecore v1
API group:type CoreV1Interface interface { RESTClient() rest.Interface ComponentStatusesGetter ConfigMapsGetter EndpointsGetter EventsGetter LimitRangesGetter NamespacesGetter NodesGetter PersistentVolumesGetter PersistentVolumeClaimsGetter PodsGetter ... }
From here, we are then using the
Pods()
method included in thePodsGetter
interface, which in turn returns thePodInterface
interface, which is a collection of methods to work with the Pod resource:type PodInterface interface { Create(ctx context.Context, pod *v1.Pod, opts metav1.CreateOptions) (*v1.Pod, error) Update(ctx context.Context, pod *v1.Pod, opts metav1.UpdateOptions) (*v1.Pod, error) UpdateStatus(ctx context.Context, pod *v1.Pod, opts metav1.UpdateOptions) (*v1.Pod, error) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error Get(ctx context.Context, name string, opts metav1.GetOptions) (*v1.Pod, error) ... }
📍 Note
Going forward, we’ll be using the methods listed in the
PodInterface
interface to perform all the CRUD operations.podsClient.List(…)
- This one’s simple to understand! Here, we use theList()
method defined under thePodInterface
interface, which returns aPodList
struct. ThePodList
struct represents a list of Kubernetes pods.type PodList struct { metav1.TypeMeta `json:",inline"` // Standard list metadata. // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds // +optional metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` // List of pods. // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md Items []Pod `json:"items" protobuf:"bytes,2,rep,name=items"` }
The Items field represents a slice of
Pod
objects and that is what we accessed usingpods.Items
in the next set of lines.
Here’s the complete code for the read operation, along with the output after execution:
package main
import (
"context"
"fmt"
"os"
"path/filepath"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
)
func main() {
// get kubeconfig
home, _ := os.UserHomeDir()
kubeConfigPath := filepath.Join(home, ".kube/config")
// use the current context in kubeconfig
config, err := clientcmd.BuildConfigFromFlags("", kubeConfigPath)
if err != nil {
panic(err.Error())
}
// create a new client
client := kubernetes.NewForConfigOrDie(config)
// define the namespace
namespace := "default"
// define the pods client (easy for later use)
podsClient := client.CoreV1().Pods(namespace)
// read all pods
pods, err := podsClient.List(context.TODO(), metav1.ListOptions{})
if err != nil {
panic(err.Error())
}
fmt.Printf("There are %d pods in the cluster\n", len(pods.Items))
for i, pod := range pods.Items {
fmt.Printf("Name of %dth pod: %s\n", i, pod.Name)
}
}
💡 If you’re a beginner in the Go programming language, one thing you certainly might have noticed is, a lot of the parts of the
client-go
library that we’ll be using, are interconnected in some way.Interfaces contains some other interfaces, which may contain some different types or structs, which may contain some methods and so on.
A nice hack to see the interface/struct/method signatures in order to understand their connections, is to use
cmd + click
orwin + click
feature in editors like VSCode, which leads you to that specific interface/struct/method and then, you can understand how its all connected.
Step 5 - Create a Pod
Now, when it comes to creating a new Pod, there are two main things we need to define:
A pod definition - giving details such as Pod name, container name, container image etc.
Creating the Pod using the specified Pod definition.
Below is the code snippet to define a new pod definition:
podDefintion := &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "demo-k8s-",
Namespace: "default",
},
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Name: "nginx-container",
Image: "nginx:latest",
},
},
},
}
A breakdown of the core logic being used is as follows:
&v1.Pod{}
- here, we are accessing thePod
struct which is from thek8s.io/api/core/v1
package. The signature of the struct looks like this:type Pod struct { metav1.TypeMeta `json:",inline"` // Standard object's metadata. // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata // +optional metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` // Specification of the desired behavior of the pod. // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status // +optional Spec PodSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` // Most recently observed status of the pod. // This data may not be up to date. // Populated by the system. // Read-only. // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status // +optional Status PodStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"` }
Its important to note that, the fields mentioned in this struct are themselves structs, which have their own fields and those are the ones which we are actually using.
For instance, the
ObjectMeta
struct from themetav1
package contains the following fields, which we have used in our implementation:type ObjectMeta struct { GenerateName string `json:"generateName,omitempty" protobuf:"bytes,2,opt,name=generateName"` Namespace string `json:"namespace,omitempty" protobuf:"bytes,3,opt,name=namespace"` ... }
After defining the pod definition, we’ll use the Create()
method (from the PodInterface
- learned above) to create a new Pod, based on the pod definition:
newPod, err := podsClient.Create(context.TODO(), podDefintion, metav1.CreateOptions{})
if err != nil {
panic(err.Error())
}
fmt.Printf("Pod '%s' is created!", newPod.Name)
Here’s the complete code for the create operation, along with the output after execution:
package main
import (
"context"
"fmt"
"os"
"path/filepath"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
)
func main() {
// get kubeconfig
home, _ := os.UserHomeDir()
kubeConfigPath := filepath.Join(home, ".kube/config")
// use the current context in kubeconfig
config, err := clientcmd.BuildConfigFromFlags("", kubeConfigPath)
if err != nil {
panic(err.Error())
}
// create a new client
client := kubernetes.NewForConfigOrDie(config)
// define the namespace
namespace := "default"
// define the pods client (easy for later use)
podsClient := client.CoreV1().Pods(namespace)
// create a pod defintion
podDefintion := &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "demo-k8s-",
Namespace: "default",
},
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Name: "nginx-container",
Image: "nginx:latest",
},
},
},
}
// create a new pod
newPod, err := podsClient.Create(context.TODO(), podDefintion, metav1.CreateOptions{})
if err != nil {
panic(err.Error())
}
fmt.Printf("Pod '%s' is created!", newPod.Name)
}
Step 6 - Update an Existing Pod
Alright, let us say - we wish to change the container image version of the new pod we create in the above step i.e. demo-k8s-7p7w9
(in my case).
The current container image being used by the pod can be found using the following command:
$ kubectl describe pod demo-k8s-7p7w9
...
Containers:
nginx-container:
Container ID: docker://f6a20de83befe78916136b425b7354fcc09bc6436de06efb7abb9fa25b260998
Image: nginx:latest
Image ID: docker-pullable://nginx@sha256:6db391d1c0cfb30588ba0bf72ea999404f2764febf0f1f196acd5867ac7efa7e
Port: <none>
Host Port: <none>
State: Running
...
Below is the code snippet to update the image version of this specific pod:
fmt.Println("Updating pod...")
retryErr := retry.RetryOnConflict(retry.DefaultRetry, func() error {
// retrive the latest pod
currentPod, updateErr := podsClient.Get(context.TODO(), "demo-k8s-7p7w9", metav1.GetOptions{})
if updateErr != nil {
panic(updateErr.Error())
}
// change container image
currentPod.Spec.Containers[0].Image = "nginx:1.25.4"
// update pod
updatedPod, updateErr := podsClient.Update(context.TODO(), currentPod, metav1.UpdateOptions{})
fmt.Printf("Updated pod: %s", updatedPod.Name)
return updateErr
})
if retryErr != nil {
panic(retryErr.Error())
}
A breakdown of the core logic being used is as follows:
Here, we are mainly using the
Get()
and theUpdate()
method from thePodInterface
interface to first get the information about that specific pod, and then update the container image field with a new image version - in this case, fromnginx:latest
tonginx:1.25.4
.retry.RetryOnConflict()
- This one’s interesting, because we are enclosing the entire update operation inside this.Here, we are using the
RetryOnConflict()
method from thek8s.io/client-go/util/retry
package, which is designed to handle conflicts that may occur when attempting to update a Kubernetes resource.In a distributed system like Kubernetes, conflicts can arise when multiple clients attempt to modify the same resource simultaneously. In that case, the
RetryOnConflict()
function implements a retry mechanism that retries the provided operation (in this case, the pod update operation) if a conflict error occurs.You can find more details about the
retry
package in the documentation.
Here’s the complete code for the update operation, along with the output after execution:
package main
import (
"context"
"fmt"
"os"
"path/filepath"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/util/retry"
)
func main() {
// get kubeconfig
home, _ := os.UserHomeDir()
kubeConfigPath := filepath.Join(home, ".kube/config")
// use the current context in kubeconfig
config, err := clientcmd.BuildConfigFromFlags("", kubeConfigPath)
if err != nil {
panic(err.Error())
}
// create a new client
client := kubernetes.NewForConfigOrDie(config)
// define the namespace
namespace := "default"
// define the pods client (easy for later use)
podsClient := client.CoreV1().Pods(namespace)
// update a pod
fmt.Println("Updating pod...")
retryErr := retry.RetryOnConflict(retry.DefaultRetry, func() error {
// retrive the latest pod
currentPod, updateErr := podsClient.Get(context.TODO(), "demo-k8s-7p7w9", metav1.GetOptions{})
if updateErr != nil {
panic(updateErr.Error())
}
// change container image
currentPod.Spec.Containers[0].Image = "nginx:1.25.4"
// update pod
updatedPod, updateErr := podsClient.Update(context.TODO(), currentPod, metav1.UpdateOptions{})
fmt.Printf("Updated pod: %s", updatedPod.Name)
return updateErr
})
if retryErr != nil {
panic(retryErr.Error())
}
}
Step 7 - Delete an Existing Pod
It’s time for a cleanup and now we wish to delete the pod we created above i.e. demo-k8s-7p7w9
.
Below is the code snippet to delete the specified pod:
deleteErr := podsClient.Delete(context.TODO(), "demo-k8s-7p7w9", metav1.DeleteOptions{})
if deleteErr != nil {
panic(deleteErr.Error())
}
This one’s simple, as we are just using the Delete()
method from the PodInterface
interface to delete a specific pod.
Here’s the complete code for the delete operation, along with the output after execution:
package main
import (
"context"
"os"
"path/filepath"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
)
func main() {
// get kubeconfig
home, _ := os.UserHomeDir()
kubeConfigPath := filepath.Join(home, ".kube/config")
// use the current context in kubeconfig
config, err := clientcmd.BuildConfigFromFlags("", kubeConfigPath)
if err != nil {
panic(err.Error())
}
// create a new client
client := kubernetes.NewForConfigOrDie(config)
// define the namespace
namespace := "default"
// define the pods client (easy for later use)
podsClient := client.CoreV1().Pods(namespace)
// delete a pod
deleteErr := podsClient.Delete(context.TODO(), "demo-k8s-7p7w9", metav1.DeleteOptions{})
if deleteErr != nil {
panic(deleteErr.Error())
}
}
Additional Configurations Options In Client-go
Here are a few additional and useful configurations, which are good to know!
Alternate Way to Kubeconfig Setup
In the initial step, we set the kubeconfig
file location be the default location which is in - ${HOME}/.kube/config
.
There’s an alternate way of configuring this step, wherein we can use the flag —-kubeconfig
, to set a custom location for the kubeconfig
file to be used.
Below is the code snippet to set the —-kubeconfig
flag:
var kubeconfig *string
if home := homedir.HomeDir(); home != "" {
kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) absolute path to the kubeconfig file")
} else {
kubeconfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file")
}
flag.Parse()
config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)
if err != nil {
panic(err)
}
A breakdown of the important concepts is as follows:
homedir.HomeDir()
- Here, we are using theHomeDir()
method from thek8s.io/client-go/util/homedir
package to fetch the user’s home location.We are using the flag package to define a new flag
kubeconfig
, that takes in a string input.
Now, if you wish to give a custom location of the kubeconfig
file to use, it can be set as follows:
$ go run read.go --kubeconfig="/Users/kunalverma/Desktop/config"
There are 5 pods in the cluster
Name of 0th pod: demo-crud55wwk
Name of 1th pod: demo-nginx
Name of 2th pod: go-api-2mwpl
Name of 3th pod: test-deploy-859f95ffcc-8p8t8
Name of 4th pod: test-deploy-859f95ffcc-fcdld
Alternate Way to Create a New Client
When it comes to the creating a new client using the config, the client-go
module offers two ways to do so:
Using
kubernetes.NewForConfigOrDie(config)
- This is what we have used in the demo above.Using
kubernetes.NewForConfig(config)
The major difference between these two approaches is the way these handle errors.
NewForConfigOrDie()
automatically takes care of any errors by panicking if there is an error in the config. Whereas, in NewForConfig()
we need to handle the error explicitly, as shown below:
client, err := kubernetes.NewForConfig(config)
if err != nil{
errors.New("Error in Config")
}
Conclusion
In this practical guide, we covered the essentials of Kubernetes development using client-go
in Go. From setting up our environment to performing CRUD operations on Pod, we've certainly gained some valuable insights.
These fundamentals set the right stage for you to navigate Kubernetes development with confidence and build robust applications with ease.
We’ll certainly be building some cool projects using the client libraries in the near future, so be sure to follow Kubesimplify for more such content.
Here's a detailed video on this topic ⤵
Happy Learning!
Resources
Follow Kubesimplify on Hashnode, Twitter/X and LinkedIn. Join our Discord server to learn with us!