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:
Constructing the
v1.Servicestruct: Define the desired Service in Go code, filling in themetadata(name, namespace) andspec(selector, ports, type).Getting the Service client: Access
clientset.CoreV1().Services(namespace).Calling the
Createmethod: Pass the context and the constructed Service struct toservicesClient.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.ServiceStruct: We build the Go struct mirroring the desired YAML definition.Selector: TheSpec.Selectormap connects the Service to Pods labeledapp=my-web-app.Ports: We define the mapping from the Service's port (80) to the Pods'targetPort(8080). We useintstr.FromInt()to create theIntOrStringtype needed forTargetPort. If targeting a named port on the Pod, you'd useintstr.FromString("port-name").CreateCall:servicesClient.Create(...)sends the request to the API server.IsAlreadyExistsCheck: 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 toGetthe Service first, modify the retrieved object, and then pass that modified object toUpdate. Kubernetes uses theresourceVersionfield (managed internally) for optimistic concurrency control – trying to update using an object with an outdatedresourceVersionwill 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:
Change
Spec.Typetov1.ServiceTypeNodePort.Optionally, specify a
NodePortvalue in theServicePortdefinition (e.g.,port.NodePort = 30080). If you don't specify it, Kubernetes will allocate one automatically.
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?