skip to content

The `sideEffects` field is mandatory on every webhook entry in a Kubernetes MutatingWebhookConfiguration or ValidatingWebhookConfiguration. What does it declare, which values are allowed, and how does the API server use it when a client sends a request with `--dry-run=server`?

level: middleimportance: should knowfreq 30%

answer

  1. sideEffects = required field, no default
  2. v1 allows only None and NoneOnDryRun
  3. server dry-run runs full admission, skips only the etcd write
  4. AdmissionReview carries request.dryRun: true
  5. effects orphan when a later webhook denies — use a controller

basics

~20 s

sideEffects declares whether the webhook changes state outside the object under review. In admissionregistration.k8s.io/v1 it must be None or NoneOnDryRun. For a server dry-run request, the API server skips None-declared-unsafe webhooks and calls the others with dryRun: true set in the AdmissionReview.

solid answer

~60 s

A webhook is supposed to be a pure decision function, but some reach out and change things — write to an external inventory, allocate an IP from an IPAM system, create a companion object. That is a **side effect**, and `sideEffects` is how you declare it. In `admissionregistration.k8s.io/v1` the only valid values are: - **`None`** — the webhook never changes state outside the AdmissionReview response. - **`NoneOnDryRun`** — it does have side effects normally, but it inspects the `request.dryRun` flag in the AdmissionReview and performs none when it is true. The API server needs this because `kubectl apply --dry-run=server` runs the full admission chain to show what *would* happen, without persisting. It will only invoke webhooks that promise not to mutate the outside world; a webhook that cannot make that promise causes the dry-run request to be rejected rather than silently causing effects. Side effects also matter outside dry-run: admission can be retried or the request can fail later, so effects may be orphaned. Reconciling controllers are the right place for real state changes.

code

yaml · 21 lines
yaml
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
  name: ipam-allocator
webhooks:
  - name: ipam.example.com
    admissionReviewVersions: ["v1"]
    sideEffects: NoneOnDryRun   # required field; None would be a lie here
    failurePolicy: Fail
    timeoutSeconds: 5
    rules:
      - operations: ["CREATE"]
        apiGroups: [""]
        apiVersions: ["v1"]
        resources: ["pods"]
    clientConfig:
      service:
        name: ipam
        namespace: net-system
        path: /mutate
      caBundle: LS0tLS1CRUdJTi...

go deeper

for a junior

Know that sideEffects declares whether the webhook changes anything outside the reviewed object, and that it is a required field.

for a middle

Name both valid v1 values, explain that server dry-run runs the whole admission chain with request.dryRun true, and that the webhook must check that flag.

for a senior

Add why admission-time effects orphan on later rejection, reinvocation and retries, and argue for moving real state changes into a reconciling controller.

for a principal

Treat admission as a pure, low-latency decision layer in the platform contract; effects belong in level-triggered controllers with idempotency keys and garbage collection, and dry-run must remain a truthful preview for the whole organisation's tooling.

## What a side effect is The contract of an admission webhook is narrow: receive an `AdmissionReview`, return an `AdmissionReview` response containing a decision and optionally a patch. Anything *else* the webhook does — creating a Kubernetes object, calling an external API, writing a database row, reserving an address in an IPAM system, incrementing a license counter — is a **side effect**, because it changes state that is not the object under review and is not undone if the request is later rejected or fails. The `sideEffects` field is a machine-readable promise about that behavior. It is **required** on every webhook entry in `admissionregistration.k8s.io/v1`; there is no default, so a configuration without it is rejected at creation time. ## The allowed values In the GA `v1` API only two values are valid: - **`None`** — this webhook has no side effects at all. It reads the request and answers. Most policy and defaulting webhooks are genuinely `None`, and this is what you should aim for. - **`NoneOnDryRun`** — the webhook does have side effects in normal operation, but it honors the dry-run flag: when `request.dryRun` is `true` in the AdmissionReview it performs none of them and returns the decision it *would* have made. The older, removed `v1beta1` API also allowed `Unknown` and `Some`, which meant "this webhook may have side effects and cannot be dry-run". Those were dropped in `v1` precisely to push authors toward webhooks that are safe to dry-run. ## How the API server uses it When a client issues a server-side dry-run — `kubectl apply --dry-run=server`, `kubectl create --dry-run=server`, or any request with `?dryRun=All` — the API server runs the **entire** write path: authorization, mutating admission, schema validation, validating admission, quota checks. Everything happens except the final write to etcd. That is what makes server dry-run genuinely useful: it shows the object as it would be after every mutating webhook and defaulting rule, and it surfaces policy rejections. For that to be safe, the API server needs to know which webhooks it may call. Its rule is: - Webhooks declaring `None` or `NoneOnDryRun` are **called**, with `request.dryRun: true` set in the AdmissionReview payload. - A webhook that has not promised dry-run safety causes the **dry-run request itself to fail** with an error rather than being silently skipped — the API server will not pretend a policy was evaluated when it was not. (Under `v1` this situation only arises with configurations converted from the old beta API, since `v1` no longer accepts the unsafe values.) So the value is not just documentation: it is enforced behavior on a whole class of requests. ## Writing a webhook that honors dryRun If your webhook must do something external, the implementation is straightforward and must be explicit: ```go if req.DryRun != nil && *req.DryRun { // compute the decision/patch, but perform no external calls return decisionOnly(req) } ``` Declaring `None` while actually writing to an external system is a real bug, not a formality: the cluster will call you during dry-runs, and every `kubectl diff` (which uses server dry-run under the hood) will mutate production state. ## Why side effects are a design smell anyway Even with `NoneOnDryRun` correctly implemented, side effects in admission are fragile: - Admission runs **before** the object is persisted. If a later validating webhook denies the request, or the etcd write fails, or optimistic concurrency loses a conflict on an UPDATE, your external effect has already happened and nothing will roll it back. - Mutating webhooks can be **reinvoked** (`reinvocationPolicy: IfNeeded`), so the same request may trigger your code more than once. - The API server may retry the client's request; clients themselves retry on conflict. All three mean effects can be duplicated or orphaned. The Kubernetes-native answer is to keep admission pure and put real state changes in a **controller** that reconciles from the stored object: the object is durable, the controller is level-triggered, and it can retry until the external system agrees. If you truly cannot avoid an admission-time effect, make it idempotent, key it on something stable such as the object UID, and add a reconciler that garbage-collects effects whose object never materialised.

  • What is the difference between --dry-run=client and --dry-run=server for a webhook author?
    Client-side dry-run never contacts the API server's write path, so no webhook is invoked and no defaulting or policy is applied — it only validates and prints locally. Server-side dry-run executes authorization, mutating admission, schema validation and validating admission, skipping only the etcd write, so your webhook is called with request.dryRun set to true.
  • Your mutating webhook creates a companion ConfigMap for each Pod it admits. Why is that a poor design even if you set NoneOnDryRun correctly?
    Admission runs before persistence, so if a later validating webhook denies the Pod or the etcd write fails, the ConfigMap already exists with no owner and nothing cleans it up. Reinvocation and client retries can also create it more than once. The durable pattern is to admit the Pod and let a controller reconcile the ConfigMap from the stored object, where retries and ownerReferences give you idempotency and garbage collection.

saying these in an interview costs you the question

  • Declaring sideEffects: None while the webhook calls an external system, so every kubectl diff mutates production
  • Thinking sideEffects is optional or has a sensible default — it is required in v1
  • Believing server dry-run skips webhooks entirely
  • Confusing --dry-run=client (never reaches admission) with --dry-run=server
  • Assuming an admission-time external effect is rolled back if the request is later rejected

context