skip to content

CRD Versions & Conversion

A CRD may serve several versions but stores exactly one, so reads of an older object either pass through a conversion webhook or are relabelled by the None strategy. Interviewers ask how v1alpha1 objects survive the move to v1 without rewriting etcd.

part ofKubernetesoverview, primer and where to startread it →
on this pageshow

questions

4

When a Kubernetes CRD switches its storage version to v1, how are objects still stored as v1alpha1 served, and when is conversion strategy None enough?

level: middleimportance: must knowfreq 58%

answer

  1. storage flag governs writes only
  2. convert in memory, never write back
  3. None touches one field
  4. desiredAPIVersion plus objects list
  5. same order, only labels and annotations

basics

~20 s

A CRD stores exactly one version, but old objects stay in etcd as last written; the API server converts them in memory whenever a request needs another version. Strategy None only rewrites apiVersion, so it suits identical schemas only.

solid answer

~50 s

The `storage: true` flag decides how the API server encodes *new writes*; it never rewrites what is already in etcd, so a `SuggestIndex` created during the alpha is still stored as `v1alpha1` after the switch. On every get, list or watch the API server decodes the stored object and converts it to the version the client requested, and on every write it converts the request body to the storage version. Nothing is written back on a read. `spec.conversion.strategy: None` does that conversion by changing only `apiVersion`, so it is correct only when the versions share one schema. When a field is renamed or restructured, say `spec.shards` becoming `spec.sharding.count`, you need `strategy: Webhook`: the API server POSTs a `ConversionReview` with the objects and a `desiredAPIVersion`, and the webhook returns them converted, in the same order, changing no metadata except labels and annotations.

code

yaml · 58 lines
yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: suggestindexes.autocomplete.example.com
spec:
  group: autocomplete.example.com
  scope: Namespaced
  names:
    plural: suggestindexes
    singular: suggestindex
    kind: SuggestIndex
  versions:
  - name: v1alpha1
    served: true
    storage: false
    deprecated: true
    schema:
      openAPIV3Schema:
        type: object
        properties:
          spec:
            type: object
            properties:
              shards:
                type: integer
              modelRef:
                type: string
  - name: v1
    served: true
    storage: true
    schema:
      openAPIV3Schema:
        type: object
        properties:
          spec:
            type: object
            properties:
              sharding:
                type: object
                properties:
                  count:
                    type: integer
              model:
                type: object
                properties:
                  name:
                    type: string
  conversion:
    strategy: Webhook
    webhook:
      conversionReviewVersions: ["v1"]
      clientConfig:
        # caBundle: base64 PEM of the CA that signed the webhook's serving certificate
        service:
          namespace: search-system
          name: suggestindex-conversion
          path: /convert
          port: 443

go deeper

for a junior

Remember that a CRD serves several versions but stores exactly one, and that old objects keep their old encoding until they are written again.

for a middle

Explain the request path: decode stored bytes, convert to the requested version, convert writes to the storage version, and show what None does versus a ConversionReview call.

for a senior

Show you know the ConversionReview contract cold: same order and count, only labels and annotations mutable, any pair of versions, and that a read never migrates data.

for a principal

Frame the choice as operational cost: identical schemas keep None and no extra service, while a real schema change buys a webhook the API server depends on.

## One stored version, several served versions A **CustomResourceDefinition** (CRD) lists its API versions in `spec.versions`. Each entry carries two flags that answer different questions: - **`served`** — whether clients can use the version at `/apis/<group>/<version>/...`. - **`storage`** — whether the API server encodes objects in this version when it **writes** them to etcd. Exactly one entry may have `storage: true`; a CRD with zero or two is rejected with "must have exactly one version marked as storage version". The important word is *writes*. Moving `storage: true` from `v1alpha1` to `v1` changes how the **next** write is encoded. It does not touch anything already in etcd. On a 12-node GPU cluster serving the search-autocomplete models, the 4,318 `SuggestIndex` objects created during the alpha stay encoded as `v1alpha1` until something writes each of them again. ## What the API server does on each request 1. **Read, list or watch.** The API server loads the stored bytes, looks at their `apiVersion`, and if it differs from the version the client asked for, converts the object in memory before responding. One list can hold objects stored in several versions; each is converted as needed. 2. **Create, update or patch.** The request body arrives in the client's version and is converted to the storage version before it is persisted. 3. **Nothing is written back.** A read converts in memory only. That is how old objects survive a storage switch without an etcd rewrite — and why they stay old until a write or a deliberate storage migration touches them. Which converter runs is decided per CRD by **`spec.conversion.strategy`**, which defaults to `None`. ## Strategy `None` The `None` converter **changes only `apiVersion`** and leaves every other field as it was. That is correct when the versions share one schema — promoting `v1beta1` to `v1` with no field changes is the typical case. It is wrong as soon as the schemas differ. Suppose `v1alpha1` had `spec.shards` and `v1` renamed it to `spec.sharding.count`. A `v1` read of an alpha object under `None` returns an object labelled `v1` with no `spec.sharding` at all: the value is not moved, because `None` never looks at the schemas. A `v1` client or controller then treats the field as unset. `None` is also the only strategy allowed while the legacy `spec.preserveUnknownFields` is `true`. ## Strategy `Webhook` and the ConversionReview With `strategy: Webhook`, the API server calls an HTTPS endpoint described by `spec.conversion.webhook.clientConfig`: either a `url`, or a `service` reference (`namespace`, `name`, optional `path`, and `port`, which defaults to 443), plus a `caBundle` used to trust the server's certificate. The request body is a **`ConversionReview`**; the API server uses the first entry of `conversionReviewVersions` it supports (`v1` or `v1beta1`). The request carries: - `request.uid` — identifies this round trip; the response must echo it as `response.uid`. - `request.desiredAPIVersion` — the target, for example `autocomplete.example.com/v1`. - `request.objects` — the objects to convert, which may come from different source versions in one call. The response must carry: - `response.convertedObjects` — the same number of objects, **in the same order**, each with `apiVersion` set to the desired version and unchanged kind, `metadata.uid`, `metadata.name` and `metadata.namespace`. - `response.result.status` — `Success`, or `Failure` with a `message`, sent with HTTP 200 either way. The webhook may change **labels and annotations**; every other metadata change is silently discarded, because the API server copies the original metadata back. Webhook conversion also requires `spec.preserveUnknownFields: false`. Since the API server may ask for **any** source/target pair among the listed versions, the webhook must convert between every pair. A common design routes all conversions through one **hub** version, so each extra version needs one converter to and from the hub instead of one per pair. ## Choosing between them | Situation | Strategy | |---|---| | Versions differ only in name and stability level | `None` | | A field is renamed, moved, split or changes type | `Webhook` | | Old objects must expose new fields derived from old ones | `Webhook` | | You cannot run a highly available HTTPS service for the CRD | Keep the schemas identical and stay on `None` | ## A worked shape ```yaml spec: conversion: strategy: Webhook webhook: conversionReviewVersions: ["v1"] clientConfig: service: namespace: search-system name: suggestindex-conversion path: /convert ``` The interview takeaway: the storage version controls how **new writes** are encoded, conversion happens in memory on every request that crosses versions, and `None` is a **relabel**, not a translation.

  • Why does moving storage: true to v1 in a Kubernetes CRD not rewrite the objects already in etcd?
    The storage flag is consulted only when the API server encodes an object for a write. Reads decode whatever bytes are in etcd and convert in memory, and they never persist the result. Rewriting thousands of objects inside a CRD update would also be slow and unsafe. Old objects are re-encoded only when something writes them: a normal update, a no-op write, or a storage version migration.
  • In a Kubernetes CRD conversion webhook, which parts of an object may the webhook change besides the spec?
    It must set `apiVersion` to `desiredAPIVersion` and may change labels and annotations. Any other metadata change is discarded because the API server restores the original metadata, and the list must keep the same length, order, kind, `metadata.uid`, `metadata.name` and `metadata.namespace`, or the whole conversion fails.
  • Why must a Kubernetes CRD conversion webhook convert v1alpha1 to v1beta1 even if every client uses v1?
    The API server can request any pair of versions listed in `spec.versions`: a client, a controller or kubectl may ask for `v1beta1`, and stored objects may be in either older version. A webhook that implements only the path clients use today fails the first request that crosses another pair. Routing every conversion through one hub version keeps the number of converters linear.

A library shelves each book in the edition it was bought in and translates on request; buying new books in the new edition does not re-print the old ones. A None strategy just swaps the cover label.

saying these in an interview costs you the question

  • Moving storage: true to v1 re-encodes every existing object in etcd.
  • Strategy None maps renamed fields by comparing the two schemas.
  • A read of an old object writes the converted form back to etcd.
  • A CRD may mark two versions as storage while it migrates.
  • The conversion webhook runs only for clients that request the old version.
  • The webhook can rename or re-namespace an object while converting it.
open as a page

Why does the Kubernetes API server refuse to remove v1alpha1 from a CRD's spec.versions, and how do you migrate stored objects so the removal succeeds?

level: seniorimportance: should knowfreq 42%

basics

~20 s

The CRD's status.storedVersions still lists v1alpha1, so etcd may hold objects in it. Re-encode every object in the storage version with a StorageVersionMigration or no-op writes, trim storedVersions to v1, then delete the version entry.

open as a page

A Kubernetes CRD's conversion webhook becomes slow or unreachable; what breaks across the cluster, and how would you run that webhook so it cannot stall the API?

level: seniorimportance: should knowfreq 36%

basics

~20 s

Every request needing conversion fails or slows: lists, watches, controllers' informers, writes, namespace deletion and migrations. There is no failurePolicy to skip it, so run the webhook redundantly with a PodDisruptionBudget, pure fast code and rotated TLS.

open as a page

In a Kubernetes CRD, what does setting deprecated: true on a version entry do for clients, and how is it different from served: false?

level: juniorimportance: nice to knowfreq 27%

basics

~20 s

Setting deprecated: true keeps the CRD version fully working but adds a warning to every response at that version, optionally replaced by deprecationWarning. Setting served: false removes the version's endpoints, so requests get 404 while stored objects stay readable.

open as a page