Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Server-Side Apply with –ssa Flag

The --ssa flag scaffolds APIs with controllers that use Server-Side Apply, enabling safer field management when resources are shared between your controller and users or other controllers.

By using this flag, you will get:

  • Standard controller scaffolding (same as go/v4)
  • Automatic generation of apply configuration types for type-safe Server-Side Apply
  • Makefile integration to generate apply configurations alongside DeepCopy methods
  • API markers (+genclient, +kubebuilder:ac:generate=true) for applyconfiguration generation

When to use it?

Use this flag when:

  • Multiple controllers manage the same resource: Your controller manages some fields while other controllers or users manage others
  • Users customize your CRs: Users add their own labels, annotations, or spec fields that your controller shouldn’t overwrite
  • Partial field management: You only want to manage specific fields and leave others alone
  • Avoiding conflicts: You want declarative field ownership tracking to prevent accidental overwrites

Don’t use it when:

  • Your controller is the sole owner of the resource (traditional Update/Patch is simpler)
  • You manage the entire object (no shared ownership)
  • Simple CRUD operations where you control everything

How does it work?

Traditional Update vs Server-Side Apply

Traditional approach (without this flag):

func (r *MyResourceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    var resource myv1.MyResource
    if err := r.Get(ctx, req.NamespacedName, &resource); err != nil {
        return ctrl.Result{}, client.IgnoreNotFound(err)
    }

    // Problem: This overwrites ALL fields, including user customizations
    resource.Spec.Replicas = 3
    resource.Labels["managed-by"] = "my-controller"

    if err := r.Update(ctx, &resource); err != nil {
        return ctrl.Result{}, err
    }

    return ctrl.Result{}, nil
}

Server-Side Apply approach (with this flag):

import (
    "context"

    apierrors "k8s.io/apimachinery/pkg/api/errors"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    ctrl "sigs.k8s.io/controller-runtime"
    "sigs.k8s.io/controller-runtime/pkg/client"
    logf "sigs.k8s.io/controller-runtime/pkg/log"

    myv1 "example.com/project/api/apps/v1"
)

func (r *MyResourceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    log := logf.FromContext(ctx)

    // Build desired state - only specify fields you want to manage
    resource := &myv1.MyResource{
        TypeMeta: metav1.TypeMeta{
            APIVersion: myv1.GroupVersion.String(),
            Kind:       "MyResource",
        },
        ObjectMeta: metav1.ObjectMeta{
            Name:      req.NamespacedName.Name,
            Namespace: req.NamespacedName.Namespace,
            Labels: map[string]string{
                "managed-by": "my-controller",
            },
        },
        Spec: myv1.MyResourceSpec{
            Replicas: 3,
        },
    }

    // Apply using SSA - only manages the fields specified above
    if err := r.Patch(ctx, resource, client.Apply,
        client.FieldOwner("my-controller")); err != nil {
        if apierrors.IsConflict(err) {
            log.Info("Field ownership conflict, will retry", "resource", req.NamespacedName)
            return ctrl.Result{Requeue: true}, nil
        }
        return ctrl.Result{}, err
    }

    return ctrl.Result{}, nil
}

What gets generated

  1. API types with applyconfiguration markers in api/<group>/<version>/:

    • +kubebuilder:ac:generate=true in groupversion_info.go (package-level)
    • +genclient marker on your CRD type in *_types.go
  2. Standard controller (same as go/v4):

    func (r *MyKindReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
        _ = logf.FromContext(ctx)
    
        // TODO(user): your logic here
    
        return ctrl.Result{}, nil
    }
    

    After running make generate, you can import and use the generated applyconfiguration types in your controller.

  3. Updated Makefile (first API only):

    .PHONY: generate
    generate: controller-gen
        "$(CONTROLLER_GEN)" object:headerFile="hack/boilerplate.go.txt" \
            applyconfiguration:headerFile="hack/boilerplate.go.txt" paths="./..."
    

    The applyconfiguration generation is added to the existing object generation command.

  4. Apply configuration types (generated by make generate):

    applyconfiguration/
    └── <group>/<version>/
        ├── mykind.go
        ├── mykindspec.go
        └── mykindstatus.go
    

How to use it?

1. Initialize your project

kubebuilder init --domain example.com --repo example.com/myproject

2. Create API with the flag

kubebuilder create api \
  --group apps \
  --version v1 \
  --kind Application \
  --ssa

3. Implement Server-Side Apply in your controller

After running make generate, you can use Server-Side Apply in your controller:

import (
    apierrors "k8s.io/apimachinery/pkg/api/errors"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    ctrl "sigs.k8s.io/controller-runtime"
    "sigs.k8s.io/controller-runtime/pkg/client"
    logf "sigs.k8s.io/controller-runtime/pkg/log"

    appsv1 "example.com/myproject/api/apps/v1"
)

func (r *ApplicationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    log := logf.FromContext(ctx)

    // Build desired status - only specify fields you want to manage
    app := &appsv1.Application{
        TypeMeta: metav1.TypeMeta{
            APIVersion: appsv1.GroupVersion.String(),
            Kind:       "Application",
        },
        ObjectMeta: metav1.ObjectMeta{
            Name:      req.NamespacedName.Name,
            Namespace: req.NamespacedName.Namespace,
        },
        Status: appsv1.ApplicationStatus{
            Conditions: []metav1.Condition{
                {
                    Type:               "Available",
                    Status:             metav1.ConditionTrue,
                    Reason:             "Reconciled",
                    LastTransitionTime: metav1.Now(),
                },
            },
        },
    }

    // Apply status using SSA
    if err := r.Status().Patch(ctx, app, client.Apply,
        client.FieldOwner("application-controller")); err != nil {
        if apierrors.IsConflict(err) {
            log.Info("Field ownership conflict, will retry", "resource", req.NamespacedName)
            return ctrl.Result{Requeue: true}, nil
        }
        return ctrl.Result{}, err
    }

    return ctrl.Result{}, nil
}

4. Generate and run

make manifests generate
make test
make run

Mixing with traditional APIs

You can use this flag for specific APIs only. Other APIs in the same project can use traditional Update:

# API A - traditional approach (no flag)
kubebuilder create api --group core --version v1 --kind Config

# API B - with Server-Side Apply flag
kubebuilder create api --group apps --version v1 --kind Workload --ssa

# API C - traditional approach (no flag)
kubebuilder create api --group core --version v1 --kind Status

Result:

  • Config and Status controllers use traditional Update
  • Workload controller uses Server-Side Apply
  • Only Workload has apply configurations generated
  • All make targets (build-installer, test, etc.) work unchanged

Affected files

When using the create api command with this flag, the following files are affected:

  • api/<group>/<version>/*_types.go: Scaffolds the API types (same as standard)
  • internal/controller/*_controller.go: Scaffolds controller using Server-Side Apply
  • config/crd/bases/*: Scaffolds CRD (same as standard)
  • config/samples/*: Scaffolds sample CR (same as standard)
  • Makefile: Adds apply configuration generation for this API
  • cmd/main.go: Registers the controller (same as standard)

Generated file structure

After creating an API with this flag:

api/
└── apps/v1/
    ├── application_types.go
    ├── zz_generated.deepcopy.go
    └── applyconfiguration/             # Generated by 'make generate'
        └── apps/v1/
            ├── application.go
            ├── applicationspec.go
            └── applicationstatus.go

internal/controller/
└── application_controller.go      # Uses Server-Side Apply

Makefile                            # Updated with applyconfiguration target

Best practices

1. Always specify FieldOwner

client.FieldOwner("my-controller-name")

This identifies your controller in the managed fields and enables proper ownership tracking.

2. Handle conflicts gracefully

Conflicts occur when another manager owns a field you’re trying to manage. This is expected behavior in SSA:

err := r.Patch(ctx, obj, client.Apply, client.FieldOwner("my-controller"))
if err != nil {
    if apierrors.IsConflict(err) {
        // Conflict detected - another manager owns the same field
        // Best practice: Log and requeue to retry
        log.Info("Field ownership conflict detected, will retry",
            "resource", req.NamespacedName)
        return ctrl.Result{Requeue: true}, nil
    }
    // Handle other errors
    return ctrl.Result{}, err
}

3. Only manage what you need

// Good - only manage specific fields
appApply := appsv1apply.Application(name, ns).
    WithSpec(appsv1apply.ApplicationSpec().
        WithReplicas(3))

// Avoid - managing entire spec might conflict with users
appApply := appsv1apply.Application(name, ns).
    WithSpec(spec) // Don't set the entire spec

4. Use ForceOwnership sparingly

client.ForceOwnership  // Takes ownership even if another manager owns the field

Only use when your controller is the authoritative source for specific fields.

ForceOwnership defeats the purpose of Server-Side Apply by forcefully taking ownership from other managers. Reserve it for cases where your controller must have final say over certain fields (e.g., a parent controller that should always override child controllers).

Example when ForceOwnership is appropriate:

// A cluster-wide policy controller that must enforce security settings
if err := r.Apply(ctx, deployment, client.Apply,
    client.ForceOwnership,  // Policy controller has final authority
    client.FieldOwner("security-policy-controller")); err != nil {
    return ctrl.Result{}, err
}

Additional resources

For more details on Server-Side Apply concepts and patterns, see:

Example in Testdata

To see examples of SSA API scaffolds, check the testdata samples:

Single-group project: testdata/project-v4

Multi-group project: testdata/project-v4-multigroup