Client-go; kubernetes deployment,service and ingress

Tue, Apr 16, 2024 5-minute read

Client-go; kubernetes deployment,service and ingress

How to create a simple deployment exposed with an ingress in Kubernetes using the client-go SDK.

Preconditions:

  • k3s
  • traefik
  • echo.k3s.lcl mapped to local IP in /etc/hosts (if demo purposes only)
package main

import (
	"context"
	"fmt"
	"log"
	"log/slog"
	"os"

	"k8s.io/apimachinery/pkg/util/intstr"

	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	networkingv1 "k8s.io/api/networking/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/tools/clientcmd"
)

func main() {
	client := k8sClient()

	err := createDeployment(
		client,
		"default",
		"echo",
		2,
		"ealen/echo-server",
	)
	if err != nil {
		log.Fatal(err)
	}
	err = createService(
		client,
		"default",
		"echo-svc",
		3000,
	)
	if err != nil {
		log.Fatal(err)
	}
	err = createIngress(
		client,
		"default",
		"echo-ingress",
		3000,
	)
	if err != nil {
		log.Fatal(err)
	}
}

func createDeployment(
	client *kubernetes.Clientset,
	ns string,
	deploymentName string,
	replicas int32,
	image string,
) error {
	dc := client.AppsV1().Deployments(ns)

	dp := &appsv1.Deployment{
		ObjectMeta: metav1.ObjectMeta{
			Name: deploymentName,
		},
		Spec: appsv1.DeploymentSpec{
			Replicas: &replicas,
			Selector: &metav1.LabelSelector{
				MatchLabels: map[string]string{
					"app": deploymentName,
				},
			},
			Template: corev1.PodTemplateSpec{
				ObjectMeta: metav1.ObjectMeta{
					Labels: map[string]string{
						"app": deploymentName,
					},
				},
				Spec: corev1.PodSpec{
					Containers: []corev1.Container{
						{
							Name:  deploymentName,
							Image: image,
							Ports: []corev1.ContainerPort{
								{
									Name:          deploymentName,
									Protocol:      corev1.ProtocolTCP,
									ContainerPort: 8888,
								},
							},
						},
					},
				},
			},
		},
	}

	slog.Info("deployment", "deployment", deploymentName, "status", "creating")
	result, err := dc.Create(context.Background(), dp, metav1.CreateOptions{})
	if err != nil {
		return err
	}
	slog.Info("deployment", "deployment", result.GetObjectMeta().GetName(), "status", "created")
	return nil
}

func createService(client *kubernetes.Clientset, ns string, serviceName string, port int32) error {
	svc := &corev1.Service{
		ObjectMeta: metav1.ObjectMeta{
			Name: serviceName,
			Labels: map[string]string{
				"app": "echo",
			},
		},
		Spec: corev1.ServiceSpec{
			Ports: []corev1.ServicePort{
				{
					Port:       port,
					TargetPort: intstr.IntOrString{Type: intstr.Int, IntVal: 80},
				},
			},
			Selector: map[string]string{
				"app": "echo",
			},
		},
	}

	slog.Info("service", "service", serviceName, "status", "creating")
	service, err := client.CoreV1().Services(ns).Create(context.TODO(), svc, metav1.CreateOptions{})
	if err != nil {
		panic(err)
	}
	slog.Info("service", "service", service.GetObjectMeta().GetName(), "status", "created")
	return nil
}
func Ptr[T any](v T) *T {
	return &v
}

func createIngress(client *kubernetes.Clientset, ns string, ingressName string, port int32) error {
	ingress := &networkingv1.Ingress{
		ObjectMeta: metav1.ObjectMeta{
			Name:      ingressName,
			Namespace: ns,
		},
		Spec: networkingv1.IngressSpec{
			IngressClassName: Ptr("traefik"),
			Rules: []networkingv1.IngressRule{
				{
					Host: "echo.k3s.lcl",
					IngressRuleValue: networkingv1.IngressRuleValue{
						HTTP: &networkingv1.HTTPIngressRuleValue{
							Paths: []networkingv1.HTTPIngressPath{
								{
									Path:     "/",
									PathType: Ptr(networkingv1.PathTypePrefix),
									Backend: networkingv1.IngressBackend{
										Service: &networkingv1.IngressServiceBackend{
											Name: "echo-svc",
											Port: networkingv1.ServiceBackendPort{
												Number: port,
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
	}
	slog.Info("ingress", "ingress", ingressName, "status", "creating")
	result, err := client.NetworkingV1().
		Ingresses(ingress.Namespace).
		Create(context.TODO(), ingress, metav1.CreateOptions{})
	if err != nil {
		panic(err)
	}
	slog.Info("ingress", "ingress", result.GetObjectMeta().GetName(), "status", "created")
	return nil
}

func k8sClient() *kubernetes.Clientset {
  // If you are using $HOME/.kube/config uncomment this and remove the
  // os.Getenv("KUBECONFIG") line
	//userHomeDir, err := os.UserHomeDir()
	//if err != nil {
	//	fmt.Printf("error getting user home dir: %v\n", err)
	//	os.Exit(1)
	//}
	//kubeConfigPath := filepath.Join(userHomeDir, ".kube", "config")
	kubeConfigPath := os.Getenv("KUBECONFIG")
	fmt.Printf("Using kubeconfig: %s\n", kubeConfigPath)

	kubeConfig, err := clientcmd.BuildConfigFromFlags("", kubeConfigPath)
	if err != nil {
		fmt.Printf("Error getting kubernetes config: %v\n", err)
		os.Exit(1)
	}

	clientset, err := kubernetes.NewForConfig(kubeConfig)
	if err != nil {
		fmt.Printf("error getting kubernetes config: %v\n", err)
		os.Exit(1)
	}
	return clientset
}

Run with go run . and if you have a connection to the cluster, it should output:

Using kubeconfig: /etc/rancher/k3s/k3s.yaml
2024/04/16 22:07:07 INFO deployment deployment=echo status=creating
2024/04/16 22:07:07 INFO deployment deployment=echo status=created
2024/04/16 22:07:07 INFO service service=echo-svc status=creating
2024/04/16 22:07:07 INFO service service=echo-svc status=created
2024/04/16 22:07:07 INFO ingress ingress=echo-ingress status=creating
2024/04/16 22:07:07 INFO ingress ingress=echo-ingress status=created

If your /etc/hosts file has an entry that has your local IP pointing at echo.k3s.lcl then you should be able to curl the pod.

# example of what /etc/hosts should look like on your host machine
192.168.0.1 echo.k3s.lcl 

Output from curl’ing echo.k3s.lcl:

# curl echo.k3s.lcl | jq .
{
  "host": {
    "hostname": "echo.k3s.lcl",
    "ip": "::ffff:10.42.0.1",
    "ips": []
  },
  "http": {
    "method": "GET",
    "baseUrl": "",
    "originalUrl": "/",
    "protocol": "http"
  },
  "request": {
    "params": {
      "0": "/"
    },
    "query": {},
    "cookies": {},
    "body": {},
    "headers": {
      "host": "echo.k3s.lcl",
      "user-agent": "curl/7.81.0",
      "accept": "*/*",
      "x-forwarded-for": "10.42.0.1",
      "x-forwarded-host": "echo.k3s.lcl",
      "x-forwarded-port": "80",
      "x-forwarded-proto": "http",
      "x-forwarded-server": "traefik-5df5cdc88d-b9tsn",
      "x-real-ip": "10.42.0.1",
      "accept-encoding": "gzip"
    }
  },
  "environment": {
    "PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
    "HOSTNAME": "echo-68cd9fb7bd-zxjmd",
    "NODE_VERSION": "20.11.0",
    "YARN_VERSION": "1.22.19",
    "ECHO_SVC_PORT_3000_TCP": "tcp://10.43.41.1:3000",
    "ECHO_SVC_PORT_3000_TCP_PORT": "3000",
    "KUBERNETES_PORT": "tcp://10.43.0.1:443",
    "KUBERNETES_SERVICE_PORT_HTTPS": "443",
    "KUBERNETES_PORT_443_TCP_PROTO": "tcp",
    "KUBERNETES_PORT_443_TCP_PORT": "443",
    "KUBERNETES_PORT_443_TCP_ADDR": "10.43.0.1",
    "ECHO_SVC_PORT_3000_TCP_PROTO": "tcp",
    "KUBERNETES_SERVICE_HOST": "10.43.0.1",
    "ECHO_SVC_SERVICE_HOST": "10.43.41.1",
    "ECHO_SVC_PORT_3000_TCP_ADDR": "10.43.41.1",
    "KUBERNETES_PORT_443_TCP": "tcp://10.43.0.1:443",
    "ECHO_SVC_SERVICE_PORT": "3000",
    "ECHO_SVC_PORT": "tcp://10.43.41.1:3000",
    "KUBERNETES_SERVICE_PORT": "443",
    "HOME": "/root"
  }
}

Updated

For resources which are not kubernetes primitives such as Traefik, you can use a Dynamic Client to create unstructured.Unstructired{}.

In this example we’re replacing the networking.k8s.io Ingress with a traefik.io/v1alpha1 IngressRoute.

func k8sDynClient() *dynamic.DynamicClient {
	kubeConfigPath := os.Getenv("KUBECONFIG")
	fmt.Printf("Using kubeconfig: %s\n", kubeConfigPath)

	kubeConfig, err := clientcmd.BuildConfigFromFlags("", kubeConfigPath)
	if err != nil {
		fmt.Printf("Error getting kubernetes config: %v\n", err)
		os.Exit(1)
	}

  // Everything is the same as k8sClient except this line.
	clientset, err := dynamic.NewForConfig(kubeConfig)
	if err != nil {
		fmt.Printf("error getting kubernetes config: %v\n", err)
		os.Exit(1)
	}
	return clientset
}


func createDynamicIngressRoute(client *dynamic.DynamicClient, ns string, ingressName string, port int32) error {
	ingress := &unstructured.Unstructured{
		Object: map[string]interface{}{
			"apiVersion": "traefik.io/v1alpha1",
			"kind":       "IngressRoute",
			"metadata": map[string]interface{}{
				"name":      ingressName,
				"namespace": ns,
			},
			"spec": map[string]interface{}{
				"entryPoints": []interface{}{
					"web",
				},
				"routes": []interface{}{
					map[string]interface{}{
						"match": "Host(`echo.k3s.lcl`) && PathPrefix(`/`)",
						"kind":  "Rule",
						"services": []interface{}{
							map[string]interface{}{
								"name":      "echo-svc",
								"port":      port,
								"namespace": "default",
								"kind":      "Service",
							},
						},
					},
				},
			},
		},
	}
	slog.Info("ingress", "ingress", ingressName, "status", "creating")
	result, err := client.Resource(schema.GroupVersionResource{
		Group:    "traefik.io",
		Version:  "v1alpha1",
		Resource: "ingressroutes",
	}).Namespace(ns).Create(context.TODO(), ingress, metav1.CreateOptions{})
	if err != nil {
		return err
	}
	slog.Info("ingress", "ingress", result.GetName(), "status", "created")
	return nil
}

Tags:

#kubernetes #go