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
-
API types with applyconfiguration markers in
api/<group>/<version>/:+kubebuilder:ac:generate=trueingroupversion_info.go(package-level)+genclientmarker on your CRD type in*_types.go
-
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. -
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
applyconfigurationgeneration is added to the existing object generation command. -
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:
ConfigandStatuscontrollers use traditional UpdateWorkloadcontroller uses Server-Side Apply- Only
Workloadhas 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 Applyconfig/crd/bases/*: Scaffolds CRD (same as standard)config/samples/*: Scaffolds sample CR (same as standard)Makefile: Adds apply configuration generation for this APIcmd/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
api/v1/navigator_types.go- API with+genclientmarkerapi/v1/groupversion_info.go- Package with+kubebuilder:ac:generate=trueinternal/controller/navigator_controller.go- Standard controller templateapi/v1/applyconfiguration/- Generated apply configurations
Multi-group project: testdata/project-v4-multigroup
api/sea-creatures/v1/prawn_types.go- API with+genclientmarkerapi/sea-creatures/v1/groupversion_info.go- Package with+kubebuilder:ac:generate=trueinternal/controller/sea-creatures/prawn_controller.go- Standard controller templateapi/sea-creatures/v1/applyconfiguration/- Generated apply configurations