ClusterIP and NodePort Services

Now that we understand the v1.Service structure, let's use client-go to create and manage these resources programmatically. We'll focus on the two most common types for internal and testing/external access: ClusterIP and NodePort.

Creating a Service involves:

  1. Constructing the v1.Service struct: Define the desired Service in Go code, filling in the metadata (name, namespace) and spec (selector, ports, type).

  2. Getting the Service client: Access clientset.CoreV1().Services(namespace).

  3. Calling the Create method: Pass the context and the constructed Service struct to servicesClient.Create(...).

Let's build a Go program that creates a simple ClusterIP Service targeting Pods with a specific label.

Creating a ClusterIP Service

Imagine we have Pods running with the label app=my-web-app and they expose port 8080. We want to create a ClusterIP Service named my-webapp-svc that listens on port 80 and forwards traffic to the Pods' port 8080.

// examples/chapter-3/create-service/main.go
package main

import (
	"context"
	"flag"
	"fmt"
	"log"
	"path/filepath"

	// Kubernetes API imports
	v1 "k8s.io/api/core/v1"
	k8serrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/util/intstr" // Required for IntOrString type

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

func main() {
	// --- Setup Kubeconfig and Flags ---
	var kubeconfig *string
	if home := homedir.HomeDir(); home != "" {
		kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) kubeconfig path")
	} else {
		kubeconfig = flag.String("kubeconfig", "", "kubeconfig path")
	}
	namespace := flag.String("namespace", "default", "namespace to create the service in")
	serviceName := flag.String("service-name", "my-webapp-svc", "name for the new service")
	appLabel := flag.String("app-label", "my-web-app", "value of the 'app' label to select pods")
	servicePort := flag.Int("service-port", 80, "port the service will listen on")
	targetPort := flag.Int("target-port", 8080, "target port on the pods")

	flag.Parse()

	// --- Load Config and Create Clientset ---
	config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)
	if err != nil {
		log.Fatalf("Error building kubeconfig: %s", err.Error())
	}
	clientset, err := kubernetes.NewForConfig(config)
	if err != nil {
		log.Fatalf("Error creating clientset: %s", err.Error())
	}

	// --- Define the Service Object ---
	serviceSpec := &v1.Service{
		ObjectMeta: metav1.ObjectMeta{
			Name:      *serviceName,
			Namespace: *namespace,
			// Optional: Add labels to the service itself
			Labels: map[string]string{
				"created-by": "client-go-example",
			},
		},
		Spec: v1.ServiceSpec{
			// Selector targets pods with 'app=<appLabel value>'
			Selector: map[string]string{
				"app": *appLabel,
			},
			Ports: []v1.ServicePort{
				{
					Name:       "http", // Optional name for the port
					Protocol:   v1.ProtocolTCP,
					Port:       int32(*servicePort),              // Service listens on this port
					TargetPort: intstr.FromInt(*targetPort), // Pods' target port (can also be string name)
				},
				// Add more ports here if needed
			},
			Type: v1.ServiceTypeClusterIP, // Explicitly set type (though it's the default)
		},
	}

	// --- Create the Service ---
	fmt.Printf("Creating Service '%s' in namespace '%s'...\n", *serviceName, *namespace)
	servicesClient := clientset.CoreV1().Services(*namespace)

	createdService, err := servicesClient.Create(context.TODO(), serviceSpec, metav1.CreateOptions{})

	// --- Handle Errors (especially AlreadyExists) ---
	if err != nil {
		if k8serrors.IsAlreadyExists(err) {
			log.Printf("Service '%s' already exists in namespace '%s'. Fetching existing.\n", *serviceName, *namespace)
			// Optionally, get the existing service instead of failing
			createdService, err = servicesClient.Get(context.TODO(), *serviceName, metav1.GetOptions{})
			if err != nil {
				log.Fatalf("Failed to get existing service '%s': %s\n", *serviceName, err.Error())
			}
		} else {
			log.Fatalf("Error creating service '%s': %s\n", *serviceName, err.Error())
		}
	}

	fmt.Printf("Successfully ensured Service '%s' exists.\n", createdService.Name)
	fmt.Printf("  Type:       %s\n", createdService.Spec.Type)
	fmt.Printf("  ClusterIP:  %s\n", createdService.Spec.ClusterIP) // Note: May take a moment to be assigned
	fmt.Printf("  Selector:   app=%s\n", createdService.Spec.Selector["app"])
	fmt.Println("  Ports:")
	for _, port := range createdService.Spec.Ports {
		fmt.Printf("    - Port: %d, TargetPort: %s, Protocol: %s\n",
			port.Port, port.TargetPort.String(), port.Protocol)
	}
	fmt.Println("---------------")

	// You can now access the application within the cluster via:
	// <service-name>.<namespace>.svc.cluster.local:<service-port>
	// e.g., my-webapp-svc.default.svc.cluster.local:80
	// Or directly via the ClusterIP: <cluster-ip>:<service-port>
}

Key Points:

  • v1.Service Struct: We build the Go struct mirroring the desired YAML definition.

  • Selector: The Spec.Selector map connects the Service to Pods labeled app=my-web-app.

  • Ports: We define the mapping from the Service's port (80) to the Pods' targetPort (8080). We use intstr.FromInt() to create the IntOrString type needed for TargetPort. If targeting a named port on the Pod, you'd use intstr.FromString("port-name").

  • Create Call: servicesClient.Create(...) sends the request to the API server.

  • IsAlreadyExists Check: We specifically check if the Service already exists. In a real application, you might want to retrieve and potentially update the existing Service instead of just logging a message.

Managing Services (Get, Update, Delete)

Managing existing Services uses similar patterns:

  • Get(ctx, name, opts): Retrieves a specific Service by name.

  • Update(ctx, service, opts): Updates an existing Service. Important: You usually need to Get the Service first, modify the retrieved object, and then pass that modified object to Update. Kubernetes uses the resourceVersion field (managed internally) for optimistic concurrency control – trying to update using an object with an outdated resourceVersion will result in a conflict error (errors.IsConflict).

  • Delete(ctx, name, opts): Deletes a Service by name.

Modifying to NodePort

Changing the Service type to NodePort is straightforward programmatically. You would modify the serviceSpec before creating or updating:

  1. Change Spec.Type to v1.ServiceTypeNodePort.

  2. Optionally, specify a NodePort value in the ServicePort definition (e.g., port.NodePort = 30080). If you don't specify it, Kubernetes will allocate one automatically.

    // --- Snippet: Modifying Spec for NodePort ---
    serviceSpec.Spec.Type = v1.ServiceTypeNodePort
    // Optionally assign a specific NodePort (if desired and available)
    // serviceSpec.Spec.Ports[0].NodePort = 30080 // Example fixed nodePort

When you create or update the Service with Type: NodePort, Kubernetes will allocate a port on each node (either the one you specified or an automatic one) and configure kube-proxy (or equivalent) to forward traffic arriving on that NodeIP:NodePort combination to the Service's ClusterIP, which then forwards it to the backend Pods.

Being able to create and configure Services programmatically opens up possibilities for automation, custom controllers that manage service exposure based on specific logic, or tools that simplify service management for developers. Next, we'll examine the Endpoints object, the crucial link that dynamically maps a Service to its ready backend Pod IPs.

Last updated

Was this helpful?