CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/golang-k8s-io--client-go

Official Go client library for Kubernetes API - typed clients, controllers, and cluster interaction tools

Overview
Eval results
Files

apply-configurations.mddocs/reference/

Server-Side Apply - Apply Configurations

Back to Index

Server-Side Apply (SSA) is a declarative object management mechanism that enables multiple controllers to manage different fields of the same resource without conflicts. Apply configurations provide a builder pattern for constructing objects for SSA.

Package Information

  • Core Package: k8s.io/client-go/applyconfigurations
  • Sub-packages: Generated for all API groups (53+ packages)
  • Meta: k8s.io/client-go/applyconfigurations/meta/v1

Core Imports

import (
    corev1ac "k8s.io/client-go/applyconfigurations/core/v1"
    appsv1ac "k8s.io/client-go/applyconfigurations/apps/v1"
    metav1ac "k8s.io/client-go/applyconfigurations/meta/v1"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

What is Server-Side Apply?

Server-Side Apply differs from traditional Update operations:

Traditional Update:

  • Replaces entire object
  • Last write wins (overwrites other controllers' changes)
  • Requires reading before updating
  • Conflict on resource version mismatch

Server-Side Apply:

  • Declares intent for specific fields only
  • Tracks field ownership per field manager
  • Automatic three-way merge on server
  • Detects and resolves conflicts
  • Multiple controllers can manage different fields

Apply vs Update

// Traditional Update (NOT recommended for shared objects)
deployment, _ := clientset.AppsV1().Deployments("default").Get(ctx, "myapp", metav1.GetOptions{})
deployment.Spec.Replicas = pointer.Int32(3)
_, err := clientset.AppsV1().Deployments("default").Update(ctx, deployment, metav1.UpdateOptions{})

// Server-Side Apply (recommended)
deploymentApply := appsv1ac.Deployment("myapp", "default").
    WithSpec(appsv1ac.DeploymentSpec().
        WithReplicas(3))

_, err := clientset.AppsV1().Deployments("default").Apply(
    ctx,
    deploymentApply,
    metav1.ApplyOptions{
        FieldManager: "my-controller",
    })

Creating Apply Configurations

Basic Pod Example

import (
    corev1ac "k8s.io/client-go/applyconfigurations/core/v1"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// Create pod apply configuration
podApply := corev1ac.Pod("my-pod", "default").
    WithLabels(map[string]string{"app": "myapp"}).
    WithSpec(corev1ac.PodSpec().
        WithContainers(
            corev1ac.Container().
                WithName("nginx").
                WithImage("nginx:1.21").
                WithPorts(corev1ac.ContainerPort().
                    WithContainerPort(80).
                    WithProtocol(corev1.ProtocolTCP))))

// Apply to cluster
result, err := clientset.CoreV1().Pods("default").Apply(
    context.TODO(),
    podApply,
    metav1.ApplyOptions{
        FieldManager: "my-controller",
        Force:        false,
    })

Deployment Example

import (
    appsv1ac "k8s.io/client-go/applyconfigurations/apps/v1"
    corev1ac "k8s.io/client-go/applyconfigurations/core/v1"
    metav1ac "k8s.io/client-go/applyconfigurations/meta/v1"
)

deploymentApply := appsv1ac.Deployment("myapp", "default").
    WithLabels(map[string]string{"app": "myapp"}).
    WithSpec(appsv1ac.DeploymentSpec().
        WithReplicas(3).
        WithSelector(metav1ac.LabelSelector().
            WithMatchLabels(map[string]string{"app": "myapp"})).
        WithTemplate(corev1ac.PodTemplateSpec().
            WithLabels(map[string]string{"app": "myapp"}).
            WithSpec(corev1ac.PodSpec().
                WithContainers(corev1ac.Container().
                    WithName("myapp").
                    WithImage("myapp:v1.0.0").
                    WithPorts(corev1ac.ContainerPort().
                        WithContainerPort(8080)).
                    WithEnv(
                        corev1ac.EnvVar().
                            WithName("LOG_LEVEL").
                            WithValue("info"),
                        corev1ac.EnvVar().
                            WithName("PORT").
                            WithValue("8080")).
                    WithResources(corev1ac.ResourceRequirements().
                        WithRequests(corev1.ResourceList{
                            corev1.ResourceCPU:    resource.MustParse("100m"),
                            corev1.ResourceMemory: resource.MustParse("128Mi"),
                        }).
                        WithLimits(corev1.ResourceList{
                            corev1.ResourceCPU:    resource.MustParse("500m"),
                            corev1.ResourceMemory: resource.MustParse("512Mi"),
                        }))))))

result, err := clientset.AppsV1().Deployments("default").Apply(
    context.TODO(),
    deploymentApply,
    metav1.ApplyOptions{
        FieldManager: "my-controller",
    })

Service Example

serviceApply := corev1ac.Service("myapp", "default").
    WithLabels(map[string]string{"app": "myapp"}).
    WithSpec(corev1ac.ServiceSpec().
        WithSelector(map[string]string{"app": "myapp"}).
        WithType(corev1.ServiceTypeLoadBalancer).
        WithPorts(
            corev1ac.ServicePort().
                WithName("http").
                WithPort(80).
                WithTargetPort(intstr.FromInt(8080)).
                WithProtocol(corev1.ProtocolTCP)))

result, err := clientset.CoreV1().Services("default").Apply(
    context.TODO(),
    serviceApply,
    metav1.ApplyOptions{
        FieldManager: "my-controller",
    })

Apply Options

type ApplyOptions struct {
    // TypeMeta allows you to set the APIVersion and Kind
    TypeMeta

    // DryRun controls whether the request is executed
    DryRun []string

    // Force overrides conflicts with other field managers
    Force bool

    // FieldManager is the name of the actor applying the configuration
    FieldManager string

    // FieldValidation determines server-side validation
    FieldValidation string
}

// Apply with options
result, err := clientset.CoreV1().Pods("default").Apply(
    context.TODO(),
    podApply,
    metav1.ApplyOptions{
        FieldManager: "my-controller",
        Force:        true,  // Force take ownership of conflicting fields
        DryRun:       []string{metav1.DryRunAll},  // Dry run only
    })

Field Ownership

Understanding Field Managers

Each field in an object can have one or more field managers:

// Controller A applies replicas
deploymentApply := appsv1ac.Deployment("myapp", "default").
    WithSpec(appsv1ac.DeploymentSpec().
        WithReplicas(3))

_, err := clientset.AppsV1().Deployments("default").Apply(
    ctx, deploymentApply,
    metav1.ApplyOptions{FieldManager: "controller-a"})

// Controller B applies image (no conflict)
deploymentApply := appsv1ac.Deployment("myapp", "default").
    WithSpec(appsv1ac.DeploymentSpec().
        WithTemplate(corev1ac.PodTemplateSpec().
            WithSpec(corev1ac.PodSpec().
                WithContainers(corev1ac.Container().
                    WithName("myapp").
                    WithImage("myapp:v2.0.0")))))

_, err := clientset.AppsV1().Deployments("default").Apply(
    ctx, deploymentApply,
    metav1.ApplyOptions{FieldManager: "controller-b"})
// Success - different fields, no conflict

Handling Conflicts

// This will conflict if another manager owns the replicas field
deploymentApply := appsv1ac.Deployment("myapp", "default").
    WithSpec(appsv1ac.DeploymentSpec().
        WithReplicas(5))

_, err := clientset.AppsV1().Deployments("default").Apply(
    ctx, deploymentApply,
    metav1.ApplyOptions{FieldManager: "controller-c"})

if apierrors.IsConflict(err) {
    // Conflict detected - another manager owns this field
    fmt.Println("Conflict:", err)

    // Option 1: Force take ownership
    _, err = clientset.AppsV1().Deployments("default").Apply(
        ctx, deploymentApply,
        metav1.ApplyOptions{
            FieldManager: "controller-c",
            Force:        true,  // Force take ownership
        })

    // Option 2: Don't apply this field, let other manager keep it
    // Just remove it from your apply configuration
}

Applying Status Subresource

// Apply status separately
podApply := corev1ac.Pod("my-pod", "default").
    WithStatus(corev1ac.PodStatus().
        WithPhase(corev1.PodRunning).
        WithConditions(corev1ac.PodCondition().
            WithType(corev1.PodReady).
            WithStatus(corev1.ConditionTrue)))

result, err := clientset.CoreV1().Pods("default").ApplyStatus(
    context.TODO(),
    podApply,
    metav1.ApplyOptions{
        FieldManager: "my-controller",
    })

Complete Controller Example

package main

import (
    "context"
    "fmt"

    appsv1 "k8s.io/api/apps/v1"
    corev1 "k8s.io/api/core/v1"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    appsv1ac "k8s.io/client-go/applyconfigurations/apps/v1"
    corev1ac "k8s.io/client-go/applyconfigurations/core/v1"
    metav1ac "k8s.io/client-go/applyconfigurations/meta/v1"
    "k8s.io/client-go/kubernetes"
    "k8s.io/client-go/tools/clientcmd"
)

const fieldManager = "example-controller"

func reconcileDeployment(ctx context.Context, clientset kubernetes.Interface) error {
    // Build desired state declaratively
    deploymentApply := appsv1ac.Deployment("myapp", "default").
        WithLabels(map[string]string{
            "app":        "myapp",
            "managed-by": "example-controller",
        }).
        WithSpec(appsv1ac.DeploymentSpec().
            WithReplicas(3).
            WithSelector(metav1ac.LabelSelector().
                WithMatchLabels(map[string]string{"app": "myapp"})).
            WithTemplate(corev1ac.PodTemplateSpec().
                WithLabels(map[string]string{"app": "myapp"}).
                WithSpec(corev1ac.PodSpec().
                    WithContainers(corev1ac.Container().
                        WithName("myapp").
                        WithImage("myapp:v1.0.0").
                        WithPorts(corev1ac.ContainerPort().
                            WithContainerPort(8080))))))

    // Apply to cluster
    result, err := clientset.AppsV1().Deployments("default").Apply(
        ctx,
        deploymentApply,
        metav1.ApplyOptions{
            FieldManager: fieldManager,
        })

    if err != nil {
        return fmt.Errorf("failed to apply deployment: %w", err)
    }

    fmt.Printf("Applied deployment: %s (replicas: %d)\n",
        result.Name, *result.Spec.Replicas)
    return nil
}

func main() {
    config, _ := clientcmd.BuildConfigFromFlags("", clientcmd.RecommendedHomeFile)
    clientset, _ := kubernetes.NewForConfig(config)

    ctx := context.Background()
    if err := reconcileDeployment(ctx, clientset); err != nil {
        panic(err)
    }
}

Building Apply Configurations from Existing Objects

import (
    appsv1 "k8s.io/api/apps/v1"
    appsv1ac "k8s.io/client-go/applyconfigurations/apps/v1"
)

// Get existing deployment
deployment, err := clientset.AppsV1().Deployments("default").Get(
    ctx, "myapp", metav1.GetOptions{})

// Extract apply configuration
extractedApply, err := appsv1ac.ExtractDeployment(deployment, fieldManager)

// Modify and reapply
extractedApply.Spec.Replicas = pointer.Int32(5)

result, err := clientset.AppsV1().Deployments("default").Apply(
    ctx,
    extractedApply,
    metav1.ApplyOptions{
        FieldManager: fieldManager,
    })

Advantages of Server-Side Apply

  1. Declarative: Declare desired state, not operations
  2. Conflict Detection: Automatic conflict detection and resolution
  3. Multi-Actor: Multiple controllers can safely manage same object
  4. Field Ownership: Clear ownership of each field
  5. Atomic: All changes applied atomically
  6. Efficient: Only specified fields are updated
  7. Merge Strategy: Server handles merge logic

Common Patterns

Patching Single Field

// Only update replicas, leave everything else alone
deploymentApply := appsv1ac.Deployment("myapp", "default").
    WithSpec(appsv1ac.DeploymentSpec().
        WithReplicas(5))

clientset.AppsV1().Deployments("default").Apply(
    ctx, deploymentApply,
    metav1.ApplyOptions{FieldManager: "autoscaler"})

Adding Labels

// Add labels without affecting other fields
podApply := corev1ac.Pod("my-pod", "default").
    WithLabels(map[string]string{
        "environment": "production",
        "team":        "platform",
    })

clientset.CoreV1().Pods("default").Apply(
    ctx, podApply,
    metav1.ApplyOptions{FieldManager: "labeler"})

Conditional Apply

// Only apply if certain conditions are met
deployment, err := clientset.AppsV1().Deployments("default").Get(
    ctx, "myapp", metav1.GetOptions{})

if deployment.Status.AvailableReplicas == *deployment.Spec.Replicas {
    // Only update image if all replicas are available
    deploymentApply := appsv1ac.Deployment("myapp", "default").
        WithSpec(appsv1ac.DeploymentSpec().
            WithTemplate(corev1ac.PodTemplateSpec().
                WithSpec(corev1ac.PodSpec().
                    WithContainers(corev1ac.Container().
                        WithName("myapp").
                        WithImage("myapp:v2.0.0")))))

    clientset.AppsV1().Deployments("default").Apply(
        ctx, deploymentApply,
        metav1.ApplyOptions{FieldManager: "roller"})
}

Best Practices

  1. Use Meaningful Field Managers: Use descriptive names like "my-controller" not "default"
  2. Avoid Force Unless Necessary: Only use Force when you know you should take ownership
  3. Partial Updates: Only specify fields you manage
  4. Dry Run First: Use DryRun to test changes
  5. Handle Conflicts: Implement proper conflict resolution
  6. One Manager Per Field: Don't have multiple managers updating same field
  7. Document Ownership: Document which controller owns which fields
  8. Status Separately: Apply status subresource separately from spec
  9. Idempotent: Make your apply operations idempotent
  10. Error Handling: Handle conflicts and other errors gracefully

Comparison with Other Methods

MethodUse CaseProsCons
CreateNew resourcesSimpleFails if exists
UpdateFull replacementComplete controlOverwrites others
Patch (JSON)Specific changesPreciseComplex syntax
Patch (Merge)Partial updatesSimpler than JSONCan't remove fields
Patch (Strategic)K8s-aware mergeSmart mergingK8s types only
ApplyDeclarative mgmtMulti-actor safeRequires SSA support

Related Documentation

Back to Index

Install with Tessl CLI

npx tessl i tessl/golang-k8s-io--client-go

docs

index.md

tile.json