Aggregated APIs vs CRDs in Kubernetes
A plain-English guide to the two ways of teaching Kubernetes new tricks: how each one works inside, what each one costs, and how to choose the right door before you build.
Imagine a city hall. Citizens can file a handful of standard forms: a building permit, a business license, a marriage certificate. Over the years, new needs appear. Drone registration. Electric scooter parking permits. Rooftop garden approvals. The city can respond in two ways. It can design a new paper form and give it to the existing clerks, who store it in the same filing cabinet and follow the same stamping routine as every other form. Or it can open a brand-new office down the hall, with its own staff, its own filing system and its own rules, and simply tell the front desk, “send anything about drones to that office.”
Kubernetes faces the same choice. Its API understands pods, services and deployments out of the box. Platform teams constantly need new kinds of objects, such as a database, a certificate, a virtual machine or a pipeline. Kubernetes offers two official ways to add them. The first is the Custom Resource Definition, or CRD: a new form handled by the existing clerks. The second is the aggregated API: a new office with its own staff, plugged in behind the same front desk.
Both give you real Kubernetes objects that work with kubectl, permissions and watches. But they differ enormously in effort, power, risk and operating cost. This guide explains each one from the ground up, shows how they work internally, compares them fairly, and ends with a decision method you can use in a design review. No prior knowledge is assumed, and every new term is explained in plain words.
1Why Extend the Kubernetes API at All?
Before comparing the two doors, we need to understand why anyone wants to open a door. The answer lies in what Kubernetes really is.
Kubernetes is an API first
Most people meet Kubernetes as a tool that runs containers. But under the surface, Kubernetes is mainly a very well-designed API, a standard way for programs to ask for things and to learn what exists. Every action, from creating a pod to scaling a deployment, is just a request to this API. Every piece of cluster state is just an object stored behind it. Even the built-in components, such as the scheduler and the kubelet, are simply clients of the same API that you use.
This design has a powerful consequence. If you can teach the API a new kind of object, you gain a bundle of features for free: a consistent way to create, read, update and delete it, a way to watch for changes, permission control, audit logs, command-line tooling and an ecosystem of other tools that already know how to talk to the API. Instead of building a whole new management system, you extend the one that your teams already know.
Think of Kubernetes as a smartphone operating system. It ships with built-in apps, but its real value is that you can add new apps that behave like first-class citizens. They use the same notifications, the same permissions screen and the same app switcher. Extending the API is like installing a new app that feels native.
The declarative idea
Kubernetes objects are declarative. You do not give step-by-step instructions. You write down what you want, such as “I want a database with three replicas”, and store it as an object. A program called a controller then watches the object and works to make reality match the description. This split between a stored wish and a tireless worker is the foundation of the operator pattern, and both CRDs and aggregated APIs exist to support it.
What teams build on top
Databases as objects
A developer writes a short object that says “PostgreSQL, three replicas, daily backups”, and an operator creates the pods, storage and backup jobs.
Certificates as objects
An object describes a certificate that is needed for a domain, and a controller requests it and renews it before it expires.
Pipelines and workflows
A build or data pipeline is described as an object, and a controller runs its steps in order.
Virtual machines and clusters
Even whole virtual machines or entire Kubernetes clusters can be requested by creating an object.
Two official extension mechanisms
The Kubernetes project documents exactly two ways to add new API types that look and feel native. These are the subject of this guide.
The Kubernetes documentation advises choosing a CRD unless you need something that only an aggregated API can give. By the end of this guide, you will know exactly what those needs are.
2The Kubernetes API in Five Minutes
To understand how to extend the API, we need a clear picture of what the API looks like and how requests move through it.
Resources, groups, versions and kinds
Every object type in Kubernetes has a few names that identify it. Learning these words makes everything else easier.
- A resource is a type of object that the API can serve, such as pods or deployments. It appears in web addresses in its plural form.
- A kind is the singular, formal name of the type, such as Pod or Deployment. It is the word you write at the top of a manifest.
- A group is a family that the resource belongs to, such as the apps group or the batch group. The oldest types live in a nameless “core” group. Groups prevent name clashes, so two projects can both define something called “Backup” without confusion.
- A version marks the maturity and shape of the type, such as v1beta1 or v1. Types can have several versions at once, so that old clients keep working while new ones adopt the improved shape.
Together, group, version and resource form the web address of a type. A custom resource gets its own address in the same way as a built-in one, which is why tools treat them equally.
Verbs: what you can do
The API is organized around a small set of actions called verbs: get, list, watch, create, update, patch and delete. Permissions are expressed in these terms. A role might say “may list and watch databases, but not delete them”. Because every type uses the same verbs, one permission system covers everything.
The watch superpower
The watch verb is what makes Kubernetes feel alive. Instead of asking repeatedly “has anything changed?”, a client opens a long-lived connection and the server pushes every change as it happens. Controllers depend on this. An extension that cannot support watching efficiently is a poor citizen, so both extension mechanisms must provide it.
Where the data lives
Kubernetes stores its state in a small, consistent key-value database called etcd. The API server is the only component that talks to etcd directly. Everything else asks the API server. The API server validates each request, applies rules, and then saves the object. When you create a custom resource using a CRD, it is saved here, right beside pods and secrets. When you use an aggregated API, you decide where your data lives.
flowchart LR
U["kubectl, controllers, CI tools"] -->|"HTTPS requests"| API["Kubernetes API Server"]
API --> A["Authentication - who are you?"]
A --> Z["Authorization - are you allowed?"]
Z --> AD["Admission - rules and defaults"]
AD --> V["Validation of the object"]
V --> E["etcd - the cluster database"]
E -.->|"change events"| W["Watchers and controllers"]
Picture an airport check-in desk. The agent checks your passport (authentication), checks your ticket class (authorization), applies the airline’s rules to your bag (admission), makes sure the booking is complete (validation), and finally records your seat in the central system (etcd). Everyone else, from the gate staff to the baggage handlers, learns about you from that central record.
3Three Servers Wearing One Coat
The Kubernetes API server looks like one program. Inside, it is actually a chain of three, and that chain is the secret behind both extension mechanisms.
The delegation chain
The main Kubernetes API server binary is built from three cooperating servers stacked in a line. A request enters at the front and, if the first server does not own the answer, it is handed down the line.
The aggregator
This is the front desk. It knows about registered extension API servers. If a request is for a type owned by one of them, the aggregator forwards it there. Otherwise it hands the request down to the next server.
The core API server
This serves the built-in types such as pods, services, nodes and deployments. If the request is not for one of its types, it passes the request on.
The API extensions server
This serves everything defined through CRDs. It creates and removes web addresses on the fly whenever a CRD is added or deleted.
The not-found handler
If nobody claims the request, the answer is a simple “not found”.
flowchart TB
REQ["Incoming API request"] --> AGG["Aggregator - front desk"]
AGG -->|"type owned by an APIService"| EXT["External extension API server"]
AGG -->|"not owned - delegate"| CORE["Core API server - pods, services, nodes"]
CORE -->|"not a built-in type - delegate"| CRDS["API extensions server - custom resources from CRDs"]
CRDS -->|"unknown - delegate"| NF["Not found"]
CORE --> ETCD["etcd"]
CRDS --> ETCD
EXT --> OWN["Its own storage - your choice"]
What this tells us
This picture already reveals the heart of the comparison.
- With a CRD, your new type is served by the third link in the chain, a part of the Kubernetes API server itself. You write no server code. You only describe the type, and the existing machinery does the rest.
- With an aggregated API, you write and run a completely separate API server. You tell the aggregator at the front desk to forward certain requests to it. You gain full control, and you take on full responsibility.
A CRD is like adding a new menu item to a restaurant’s existing kitchen. The same cooks, the same stoves and the same pantry handle it. An aggregated API is like renting a food truck, parking it next to the restaurant and putting a sign on the front door. The truck has its own cooks and its own fridge, so it can do things the kitchen cannot, but it also needs its own staff, fuel and cleaning.
Because the aggregator sits at the front, both kinds of extension look identical to users. A person running kubectl get cannot tell, without checking, whether an object comes from a CRD or from an aggregated server.
4First, Do You Need an API at All?
Many teams reach for a new API type when something far simpler would do. A good architect asks this question first.
A ladder of options
Think of extension options as steps on a ladder, from lightest to heaviest. Always start at the bottom and climb only as far as you must.
ConfigMap or Secret
If you only need a place to keep some settings that your own program reads, a ConfigMap is enough. No new API is needed.
Annotations and labels
For small bits of extra information attached to existing objects, simple tags can be enough.
Custom resource with a CRD
When you want a first-class declarative object that users create and controllers act upon, a CRD is the usual answer.
Aggregated API
Only when a CRD cannot give you what you need, because of storage, behavior or protocol, do you run your own API server.
Signs you need a real API type
- You want users to manage the thing with the same tools as other Kubernetes objects.
- You want permissions to be controlled per object type, and per namespace.
- You want to watch for changes and react automatically.
- You want the object to appear in listings, with status that shows its health.
- You want a controller to turn declared intent into real-world actions.
Signs you do not
- Your program just reads a few settings at startup. Use a ConfigMap.
- The data is large, changes constantly or is queried in complex ways. A real database is better, and an API object can just point to it.
- Only one team and one tool will ever use the data, and nobody needs Kubernetes permissions or watches.
- You are building a general-purpose business application. Kubernetes objects are best for describing infrastructure and platform intent, not for storing customer records.
Do not turn the cluster’s database into your application’s database. etcd is small and precious. A flood of custom objects can slow down the whole cluster for everyone.
5CRDs: A New Form for the Existing Clerks
A Custom Resource Definition is the lightest and most popular way to extend Kubernetes. It is a description of a new type, written as a normal Kubernetes object.
The two words, carefully
People often mix up two similar terms. The CustomResourceDefinition, or CRD, is the blueprint: a Kubernetes object that describes a new type. A custom resource, or CR, is an actual instance of that type. The CRD is like the design of a form. A custom resource is a filled-in copy of that form. Once you apply a CRD, the API server immediately starts accepting custom resources of that type.
What a CRD describes
Group, kind and names
The group the type belongs to, its kind, its plural and singular names and optional short names. Together these decide the web address and how users type the name on the command line.
Namespaced or cluster-wide
Whether each object lives inside a namespace, like a pod, or sits at the cluster level, like a node. This choice shapes permissions and cannot easily be changed later.
Schema
A structured description of the allowed fields, their types and rules, written in the OpenAPI format. This is the contract between users and controllers.
Version list
The versions that exist, which ones are served to clients, which one is used for storage and how to convert between them.
Subresources and columns
Optional features such as a separate status section, a scale interface and extra columns shown in command-line listings.
Validation and defaults
Constraints such as minimum values, allowed choices, default values and custom expression rules that are checked on every write.
Why the schema matters so much
In the current stable API for CRDs, every custom resource type must have a structural schema. This is a strict, well-defined way of describing fields so that the API server can understand every part of the object. The benefits are large:
- Validation. A malformed object is rejected immediately with a clear message, instead of failing mysteriously inside a controller hours later.
- Pruning. Fields that are not in the schema are removed before saving, so typos cannot silently sneak into the database.
- Defaulting. Missing fields can be filled in automatically with sensible values.
- Documentation. Tools can read the schema to show help text and offer autocompletion.
A schema is like the printed boxes on a paper form. “Name: ____”, “Age: ____ (number only)”, “Phone: ____”. If someone writes a poem in the age box, the clerk hands the form back. Without the printed boxes, every form would be a free-for-all and the person processing it later would struggle.
Rules written as expressions
Beyond basic types, modern Kubernetes lets CRD authors write validation rules using the Common Expression Language, known as CEL. These are small, safe expressions that run inside the API server. A rule can say “the maximum must be greater than or equal to the minimum”, or “this field cannot be changed after creation”. Because these rules run inside the server, you often do not need to build and run a separate validation web service, which removes a major source of complexity and failure.
Built-in admission policies written in CEL can also target custom resources, so teams can add organization-wide rules without writing any code or running any webhook.
6How the API Server Serves Custom Resources
The magic of CRDs is that no restart and no new program is needed. Let us watch what happens inside the server when a CRD is applied.
From CRD to working API
You create the CRD
The CRD itself is an ordinary object in a built-in group. It goes through the usual checks and is saved to etcd.
The extensions server notices
A controller inside the API server watches for CRDs. It sees the new one and starts to build handlers for the new type.
Names are checked
The server makes sure the new names do not clash with anything existing. A condition on the CRD reports whether the names were accepted.
The new web address appears
The server registers REST handlers for create, read, update, delete, list and watch. A second condition, called Established, tells you when the type is ready for use.
Clients discover it
The new group and version show up in the list of available APIs, so command-line tools and controllers can find it.
Users create custom resources
Each custom resource is validated against the schema, defaulted, pruned and stored in etcd under its own key, next to every other object.
sequenceDiagram
participant U as User
participant A as API Server
participant X as Extensions server inside
participant E as etcd
U->>A: Create CRD
A->>E: Save CRD
E-->>X: CRD change event
X->>X: Build REST handlers and schema
X-->>A: Type is Established
U->>A: Create custom resource
A->>A: Authenticate, authorize, admit
A->>X: Validate, default and prune with schema
X->>E: Save custom resource
E-->>U: Success and watch events
Features you receive for free
- Status subresource. A separate section for what the controller observes, kept apart from what the user wants. Users edit the wish, the controller edits the status, and neither can overwrite the other by accident.
- Scale subresource. A standard interface that lets tools like the Horizontal Pod Autoscaler scale your custom type just like a deployment.
- Printer columns. Extra columns in listings, so a database object can show its version and readiness at a glance.
- Finalizers. Markers that stop an object from being fully deleted until a controller has cleaned up the outside resources it created.
- Owner references and garbage collection. A parent object can own children, so deleting the parent automatically cleans up everything it created.
- Watch with caching. The server supports efficient change streams out of the box.
- Permissions, auditing and tooling. Role-based access control, audit logs,
kubectland GitOps tools all work immediately.
Deleting a CRD deletes every custom resource of that type, because without the blueprint the instances have no meaning. In production, protect CRDs of important operators with tight permissions and with backups.
7CRD Versioning, Conversion and Migration
Every API changes over time. A CRD must support old and new shapes at the same time, and this is where real planning begins.
Why versions exist
Imagine your database type starts with a simple field for the size. Later you realize you need to specify the storage and the compute separately. You cannot just rename fields, because thousands of stored objects and many tools use the old shape. Instead, you add a new version of the type, and keep the old one working until everyone has moved.
Served versus stored
A CRD can list several versions. Two flags control their roles. A version marked served can be used by clients. Exactly one version is marked storage, and this is the shape in which objects are written to etcd. When a client uses a different version than the storage version, the API server must convert the object on the way in and out.
Two conversion strategies
None strategy
- The server only changes the version label and leaves all fields as they are.
- Works only when versions have identical shapes.
- No extra service to run.
- Good for simple promotion, such as beta to stable with no changes.
Webhook strategy
- The server calls your conversion service to transform objects between versions.
- Handles any change, such as renamed or restructured fields.
- The service becomes a critical dependency: if it is down, reads and writes can fail.
- Requires careful testing, since conversions must be loss-free in both directions.
Moving stored data to a new storage version
Changing the storage version does not automatically rewrite old objects. They stay in etcd in the old shape until they are next written. Before you can safely stop serving an old version, you must make sure no stored object still uses it. A storage version migration, which simply reads and re-writes every object, takes care of this. The project provides tooling for such migrations, and many operators run their own.
| Step | What you do | Why |
|---|---|---|
| 1 | Add the new version and serve both | Old clients keep working |
| 2 | Provide conversion between them | Objects look correct in either version |
| 3 | Switch the storage version to the new one | New writes use the new shape |
| 4 | Migrate all stored objects | No object remains in the old shape |
| 5 | Stop serving the old version | Retire it safely |
| 6 | Remove the old version from the list | Only after confirming nothing depends on it |
Plan versions from day one. Choose a stable group name that you own, start at an early version such as v1alpha1 to signal that things may change, and promote only when the shape is truly settled.
8Controllers and Operators: The Brain Behind the Form
A CRD on its own does nothing except store data. The real value appears when a controller reads the objects and acts on them.
What a controller does
A controller is a program, usually running as a pod in the cluster, that watches custom resources and makes the real world match what they describe. It follows the reconciliation loop you may know from other Kubernetes parts: observe the desired state, observe the actual state, take the smallest action that moves them closer, then repeat. A controller bundled with a custom resource type, designed to manage one application or system, is called an operator. The word suggests a human operator whose knowledge has been captured in software.
A CRD is the order form at a bakery. The controller is the baker. You fill in “one chocolate cake, birthday, for Saturday”. The baker reads the order, checks what is in the oven, bakes what is missing, and marks the order as ready. If someone eats the cake early, the baker notices and bakes another, because the order still stands.
The reconciliation cycle
flowchart TB
W["Watch custom resources"] --> Q["Work queue of changed objects"]
Q --> R["Reconcile one object"]
R --> O["Read desired spec and actual world"]
O --> D{"Do they match?"}
D -->|"yes"| S["Update status: healthy"]
D -->|"no"| A["Create, update or delete child resources"]
A --> S2["Update status: progressing"]
S --> W
S2 --> W
Good controller habits
- Idempotence. Running the same reconcile twice must be safe. Controllers can be restarted at any time and will see the same objects again.
- Level-based thinking. React to the current state, not to a list of past events. Events can be missed, but the state is always there to read.
- Owner references. Mark everything the controller creates as owned by the custom resource, so cleanup is automatic.
- Finalizers. Use them to clean up outside resources, such as cloud disks, before the object disappears.
- Status conditions. Report progress in a standard way that humans and tools can read.
- Leader election. Run several copies for availability, with only one active at a time.
Controllers work with both mechanisms
Notice that a controller is independent of how the API type is served. The same controller pattern works whether the objects come from a CRD or from an aggregated server, because it simply talks to the Kubernetes API. The choice between the two doors affects how the objects are stored and served, not how controllers behave. Many operator frameworks make building controllers much easier, and they are written mostly with CRDs in mind.
9Aggregated APIs: A Whole New Office Behind the Front Desk
The second door is heavier but much more flexible. Instead of describing a type to the existing server, you bring your own server.
The big idea
With API aggregation, you write and run your own API server, called an extension API server. It could be a pod inside the cluster or even something running elsewhere. You then register it with the main API server by creating a small object called an APIService. This object says, in effect: “Any request for group X and version Y should be forwarded to this service.”
From then on, the main API server acts as a front door and a bouncer. It checks who the caller is and whether they are allowed to proceed, and then passes the request along to your server. Your server answers, and the reply travels back through the front door to the client. To the user, it feels like one single Kubernetes API.
Think of a large company with a main reception desk. A new department opens in a separate building with its own staff and filing system. Reception is told: “Anything about the Drone Department goes to the other building.” Visitors still enter through the same lobby, show the same badge and use the same directory. Behind the lobby, the department runs things its own way.
What an APIService contains
Group and version
The exact address that this entry claims, such as a group name and version. Requests matching it are forwarded.
Service reference
Which Kubernetes service in which namespace, and which port, leads to your extension server. This is how the main API server finds it.
CA bundle
The certificate authority that the main API server uses to check that it is talking to the genuine extension server and not an impostor.
Priorities
Numbers that decide ordering when several groups or versions compete for display and preference in discovery.
Availability is reported
The APIService object also carries a status. The main API server regularly checks that the extension server is reachable and healthy, and records an Available condition. If it is not available, requests for that address fail with an error saying the service is unavailable. This honest reporting is helpful, but it also reveals a risk: the extension server is now on the critical path for its part of the API.
Real examples you may already use
- Metrics Server registers the metrics group so that
kubectl topand the Horizontal Pod Autoscaler can ask for live CPU and memory. The data comes from memory, not from the database. - Custom metrics adapters serve the custom and external metrics groups, translating questions into queries against monitoring systems.
- KubeVirt, which runs virtual machines on Kubernetes, uses CRDs for its main objects but also adds an aggregated API for special actions such as opening a console to a running machine.
- Service Catalog, an earlier project for provisioning external services, was built as an aggregated API server. It has since been retired, a reminder of the long-term cost of this approach.
Notice that the best-known aggregated APIs serve data that does not belong in etcd at all, such as live metrics. That is the classic reason to choose aggregation.
10Inside Aggregation: How a Request Travels
The trickiest part of aggregation is trust. The extension server must trust the main API server, and the main API server must trust the extension server. Let us see how this is arranged.
The journey of one request
The client sends a request
A user runs a command that touches the aggregated type. The request goes to the main API server like any other.
Authentication happens at the front
The main API server works out who the caller is, using certificates, tokens or whatever login method the cluster uses.
Authorization happens at the front
It checks whether this user may perform this action on this type, using the normal permission rules.
The aggregator matches an APIService
It sees that the requested group and version belong to an extension server, and prepares to proxy the request.
The request is forwarded with identity
The main API server connects to the extension server over a secure channel, using its own special client certificate, and passes the caller’s name and groups in trusted headers.
The extension server verifies and double-checks
It confirms that the connection really came from the main API server, reads the identity headers, and may ask the main API server whether this user is allowed to perform the action.
The extension server does the work
It validates the object, reads or writes its own storage, and builds the response, which flows back to the client.
sequenceDiagram
participant C as Client
participant M as Main API Server with aggregator
participant X as Extension API Server
participant S as Its own storage or data source
C->>M: Request for aggregated type
M->>M: Authenticate and authorize caller
M->>X: Proxy request with front-proxy certificate and identity headers
X->>X: Verify proxy certificate and read identity
X->>M: Ask permission check for this user
M-->>X: Allowed
X->>S: Read or write
S-->>X: Data
X-->>M: Response
M-->>C: Response
The trust machinery
- Front-proxy client certificate. The main API server presents a special certificate when it forwards requests. The extension server accepts requests only from a caller holding such a certificate signed by the expected authority.
- Request header authentication. The identity of the original caller is carried in headers, such as the user name and group names. Because only the trusted front door can set them, the extension server can believe them.
- Published settings. The main API server publishes the details that extension servers need, such as the certificate authority and the header names, in a well-known configuration object in the system namespace. Extension servers read it on startup.
- Delegated authorization. Rather than maintaining its own permission rules, the extension server asks the main API server to check whether this user may perform this action. This keeps all permission logic in one place, and means ordinary Kubernetes roles work for aggregated types too.
- CA bundle in the APIService. This lets the main API server verify the extension server’s own certificate in the other direction.
Picture a hotel concierge who phones a partner restaurant: “This guest is Ms. Rao, a verified resident, please seat her.” The restaurant recognizes the concierge’s voice and special code, so it trusts the guest’s identity. Even so, it may call the front desk to confirm that Ms. Rao has dining privileges. Two checks, one trusted channel.
The APIService has an option to skip checking the extension server’s certificate. Never use it in production. It removes the main guarantee that the front door is talking to the genuine server, and exposes you to impersonation inside the cluster network.
11What It Takes to Build an Extension Server
With a CRD, you write a description. With aggregation, you write a real server. It helps to see the long list of things that become your job.
Responsibilities you inherit
REST handlers
Every verb for every version of every resource: create, read, update, patch, delete, list and watch, all following Kubernetes conventions exactly.
Storage
Choose and operate a place for the data. It could be your own etcd, a relational database, a cache, or computed on the fly. You must make it durable and fast.
Watch support
Efficient change notifications with resource versions so controllers can resume cleanly after disconnects. This is subtle to get right.
Discovery and schema publishing
Describe your types, their versions and the shapes of objects so that tools can find and understand them.
Versioning and conversion
Write your own code to convert between versions, and your own strategy for storing and migrating data.
Validation and admission
Implement rules in code, and decide how admission policies and webhooks interact with your types.
Security plumbing
Certificates, delegated authentication and authorization, secure serving, and keeping those certificates rotated.
Operations
Deployment, scaling, high availability, upgrades, monitoring and the on-call rotation for yet another critical server.
Tools that help
You are not starting from nothing. The Kubernetes project publishes a generic API server library that contains most of the machinery, including request handling, authentication, authorization, watch and storage on etcd. A sample extension server in the project shows how to put the pieces together, and community frameworks exist to generate boilerplate. Still, even with these tools, you take on a project of significant size, written in the same language and style as Kubernetes itself, and you must keep it up to date with every Kubernetes release.
Effort at a glance
| Task | With a CRD | With an aggregated API |
|---|---|---|
| Define the type | Write a schema description | Write Go types and generate code |
| Serve it | Automatic, built in | Write and run a server |
| Store it | Automatic in cluster etcd | Your own design and operation |
| Validate it | Schema plus expression rules | Code in your server |
| Secure it | Inherited from the API server | Certificates and delegation you must wire up |
| Upgrade it | Apply a new CRD | Build, release and roll out a new server |
| Typical time to first working version | Hours to days | Weeks to months |
The ongoing cost matters more than the first build. Every Kubernetes upgrade may bring changes to the libraries you depend on. Plan for a team to own this server for as long as it exists.
12Side by Side: Aggregated APIs vs CRDs
Now that you know both mechanisms from the inside, here is a direct comparison across the dimensions that matter in practice.
| Dimension | CRD | Aggregated API |
|---|---|---|
| Who serves it | The built-in extensions server inside the API server | Your own separate API server |
| Code you write | None for the API. A controller if you want behavior | A full API server plus any controllers |
| Where data is stored | The cluster’s etcd | Anywhere you choose |
| Validation | Schema, defaults, pruning and CEL rules | Any logic in code |
| Versioning | Declared versions with none or webhook conversion | Your own conversion code |
| Custom actions beyond create, read, update and delete | Only status and scale subresources | Any subresource or special verb you design |
| Protocols | Standard REST and watch | Can also support streaming and special protocols on subresources |
| Extra network hop | None | Yes, through the aggregator |
| Failure impact | Tied to the main API server | Your server can fail independently, and affect discovery and cleanup |
| Permissions | Standard role-based access control | Standard role-based access control through delegation |
| Command-line and tool support | Full | Full, if you implement conventions correctly |
| Operational burden | Low | High |
| Typical use | Operators and platform APIs | Computed data, special storage or special verbs |
flowchart LR
subgraph CRD["CRD path"]
C1["Client"] --> C2["API Server"]
C2 --> C3["Built-in extensions server"]
C3 --> C4["Cluster etcd"]
end
subgraph AGG["Aggregated path"]
A1["Client"] --> A2["API Server with aggregator"]
A2 --> A3["Your extension API server"]
A3 --> A4["Your own storage or live data"]
end
The short version
A CRD gives you a free, fast, inherited API with limits on what it can do. An aggregated API gives you total freedom with total responsibility. In nearly every case, the free option is enough, and the freedom of the second option goes unused.
13What Only Aggregated APIs Can Do
There are real situations where a CRD simply cannot do the job. Knowing them helps you avoid both over-engineering and under-engineering.
1. Data that does not live in etcd
A CRD always stores objects in the cluster’s etcd. If your data is huge, changes many times per second, already lives in another system, or is computed from live sources, putting it into etcd is unwise or impossible. An aggregated API can read from a database, a monitoring system or even memory. Metrics Server is the classic case: every number is fresh, short-lived and never saved.
2. Custom actions on objects
Kubernetes pods have special actions like opening a shell, streaming logs and forwarding ports. These are not plain create-or-update operations. They are subresources with their own behavior. CRDs allow only two subresources, status and scale. If you need an action such as “open a console to this virtual machine” or “stream the output of this job”, an aggregated server can provide it. This is why KubeVirt, which mostly uses CRDs, still runs an aggregated server for its console and similar actions.
3. Different storage rules
Perhaps you need strong history, data in a relational database, special indexes, or storage with a different consistency model. Or perhaps you need an object that exists only virtually, without being saved at all. Aggregation lets you design this.
4. Complex or non-standard logic on writes
CRDs support schema rules, expression rules and, with extra webhooks, custom checks. An aggregated server can perform any logic inside the request itself: look up outside systems, generate values, or reject changes based on arbitrary conditions, without separate webhook calls.
5. Performance at a different scale
If you must serve a huge number of objects, or huge individual objects, you may need your own storage, caches and indexes tuned to your workload, rather than sharing the cluster database.
6. Managing a different system as if it were Kubernetes
Sometimes a team wants Kubernetes-style access, tooling and permissions in front of a completely different system. An aggregated server can present such systems through familiar API conventions, using its own backing data.
Live computed data
Metrics, status snapshots and anything calculated at request time.
Special verbs
Consoles, streaming, attach, port-forward-like features.
External stores
Data owned by a relational database, a cloud service or a large dataset.
Unusual semantics
Custom conversion, history and server-side logic not possible in schema rules.
A hybrid is often the best answer: use CRDs for the main declarative objects, which controllers act upon, and add a small aggregated API only for the special actions or computed views that CRDs cannot offer.
14What CRDs Give You for Free
The reason CRDs dominate real-world use is not only that they are simple. It is how much comes bundled with them without any effort.
The free bundle
Storage, durability and backups
Custom resources live in the same database as everything else. Whatever backup plan protects the cluster state also protects your objects.
High availability
Your API is as available as the main API server itself, whether that is a single node or a managed multi-zone control plane.
Security model
Authentication, authorization, audit logging and admission all apply automatically. There are no extra certificates to rotate.
Watch and caching
Efficient change streams and server-side caching are provided by the platform and well tested at scale.
Garbage collection
Owner references and finalizers behave exactly as they do for built-in types.
Upgrades
Nothing extra to build, release or roll out when Kubernetes itself upgrades. Your CRD just continues to work.
The ecosystem effect
Because custom resources look like ordinary Kubernetes objects, the entire world of Kubernetes tooling treats them as first-class. Configuration management and GitOps tools can apply them from a repository. Policy engines can validate them. Dashboards can display them. Backup tools can include them. Command-line completion can suggest their fields. A CRD plugs into all of this on day one.
This is why so many well-known projects are built primarily on CRDs.
- cert-manager defines certificates and issuers as custom resources, and a controller keeps certificates valid.
- Prometheus Operator defines monitoring servers and scrape rules as custom resources.
- Argo CD and Argo Workflows describe applications and pipelines as custom resources.
- Istio and the Gateway API describe traffic routing and gateways as custom resources.
- Cluster API describes entire Kubernetes clusters as custom resources, which controllers create and manage.
- Crossplane describes cloud resources and platform building blocks as custom resources.
- Knative describes serverless services and revisions as custom resources.
Choosing a CRD is like building a new room inside an existing house. The plumbing, wiring, heating, roof and insurance are already there. Choosing aggregation is like building a new detached house on the same street. You get more freedom in design, but you need your own plumbing, wiring, roof and insurance.
Limits you should expect
- You cannot choose where or how data is stored.
- You are limited to the standard set of actions, plus the status and scale subresources.
- Complex checks that need outside information require admission webhooks, which are separate services that add moving parts.
- Object size and count are bounded by what etcd and the API server comfortably handle.
- Very complex version changes require a conversion webhook, with its own care and risk.
15Performance and Scalability
Both mechanisms can scale well, but each has its own pressure points. Knowing them keeps surprises away.
The figures above are the default limits of a standard etcd setup, and managed Kubernetes services may configure them differently. Treat them as signs of scale, not guarantees.
Pressure points with CRDs
- Shared database. Custom resources share etcd with everything else. A controller that writes constantly, or objects that are large, can slow down or even endanger the entire cluster. If etcd hits its storage quota, writes stop for everyone.
- Object size. Very large objects, such as those carrying embedded files or long lists, are heavy to store, transfer and cache. Keep objects small and store bulk data elsewhere.
- Object count. Hundreds of thousands of objects of one type can strain listing and caching in the API server, which holds recent data in memory for fast watches.
- Number of CRDs. Each CRD adds handlers, schemas and cached discovery data to the API server. Very large numbers of CRDs can increase memory use and slow discovery for every client.
- Write frequency. Controllers that update status many times per second create heavy write load and heavy watch traffic. Batch updates and avoid pointless writes.
Pressure points with aggregated APIs
- The extra hop. Every request passes through the main API server and is then proxied to your server. This adds a little latency and some load to the front door, which is rarely a problem but should be measured for very busy APIs.
- Your own caching and watch. If your server does not cache and manage watches as efficiently as the main one, many controllers watching your types can overload it.
- Your own storage. You are now responsible for the scalability of whatever holds your data.
- Connection handling. Long-lived watch connections and streams all pass through the proxy and need to be handled carefully, including timeouts and reconnection.
flowchart TB
subgraph SHARED["CRD - shared resources"]
CR["Many custom resources"] --> ETCD["Cluster etcd"]
PODS["Pods, secrets, nodes"] --> ETCD
ETCD --> RISK["One busy neighbor can slow everyone"]
end
subgraph ISO["Aggregated - isolated resources"]
AR["Your objects"] --> OWN["Your own storage"]
OWN --> SAFE["Cluster etcd is protected, but you operate more systems"]
end
The usual recipe for healthy CRD use is simple: small objects, moderate counts, quiet controllers and large data kept elsewhere. If your design needs to break these rules, that is a strong hint that you may need a different storage, and therefore an aggregated API or no API at all.
16Availability and Failure Scenarios
Every design should be examined by asking one question: what breaks when this part fails? The two mechanisms fail in very different ways.
When an aggregated server goes down
If your extension server is unreachable, requests for its types fail. That is expected. But there are side effects that surprise many teams.
- Discovery errors. Tools that list all available APIs may print warnings or even fail, because one group could not be reached. Users see confusing messages about being unable to retrieve the complete list of server APIs.
- Stuck namespace deletion. When a namespace is deleted, Kubernetes must look through every type to remove objects in it. If one aggregated API is unavailable, the check cannot complete and the namespace may remain in a terminating state indefinitely.
- Controller trouble. Controllers and tools that scan all types, such as garbage collection and backup tools, may stall or log errors.
- Autoscaler impact. If the unavailable server is the metrics provider, the autoscalers that depend on it cannot make decisions.
When something fails with a CRD
- The CRD is deleted. All its custom resources are deleted with it. This is the most dangerous CRD mistake.
- The conversion webhook is down. Reads and writes that need conversion between versions can fail, which can break controllers and tools for that type.
- An admission webhook is down. Depending on its failure setting, writes are either blocked or silently unchecked.
- The controller is down. The API still works, and objects can still be created, but nothing acts on them. Status goes stale, and users are left waiting. This is a softer failure than a missing API.
- A bad schema change. A stricter schema can cause existing stored objects to become invalid, which makes updating them fail until they are fixed.
| Failure | CRD effect | Aggregated effect |
|---|---|---|
| The extension server is down | Not applicable | Type unavailable, discovery warnings, risk of stuck namespaces |
| The main API server is down | Everything is down | Everything is down |
| The controller is down | API works, no reconciliation | API works, no reconciliation |
| Certificate expires | Only affects webhooks, if any | Proxy connection to the extension server can fail |
| Storage is full | Cluster etcd writes stop for all | Only your own storage is affected |
| Bad release of the server | No server to release | The type could return errors until you roll back |
Protecting yourself
- Run extension servers with multiple replicas, spread across zones, with proper health checks and disruption budgets.
- Monitor APIService availability and alert quickly when it flips.
- Automate the rotation of certificates well before they expire.
- Keep conversion and admission webhooks as few and as simple as possible, and design their failure policy on purpose.
- Treat CRDs as precious data: limit who can delete them, and back up both CRDs and their instances.
- Test the failure of every component in a staging environment before relying on it.
17Security Considerations
Both mechanisms widen the cluster’s attack surface. The question is who carries the burden of protecting that surface.
Shared protections
- Role-based access control. Permissions work on custom types just as on built-in ones. You can grant read-only access to a type, restrict creation to a team, or limit access to specific namespaces.
- Audit logging. Both kinds of types appear in the cluster audit trail, so you can see who created or changed what.
- Admission policies. Cluster-wide policies, whether written in CEL or run by policy engines, can examine custom objects.
CRD-specific points
Who can create CRDs
Creating or editing a CRD changes the shape of the API. It should be a highly restricted permission, since it can affect every user of that type.
Cluster-scoped types
Objects with cluster scope cannot be separated by namespace, so permission mistakes have wider effects. Prefer namespaced types unless the thing is truly cluster-wide.
Controller privileges
Operators often hold powerful permissions to create pods, secrets or cloud resources. A bug or break-in in the operator is a path to those powers. Grant it only what it needs.
Strict schemas
A tight schema is a form of input validation. It rejects unexpected fields and values before they reach any controller.
Aggregation-specific points
Your server is new code
It has its own bugs and its own dependencies. A flaw in it is exposed to the cluster network and can be used to run attacks against all callers.
Certificate handling
Mistakes in verifying the front-proxy certificate could allow someone to impersonate any user by faking the identity headers.
Skipped TLS checks
Disabling verification of the extension server’s certificate lets any pod that can hijack its network address answer requests meant for the real server.
Own data store
The storage has its own credentials, network exposure, encryption and backup needs, none of which the cluster handles for you.
Practical hardening
- Use network policies so that only the main API server and the necessary clients can reach the extension server.
- Run it with the least privileges possible, as a non-root user, with a read-only file system where feasible.
- Always rely on delegated authorization so that permissions stay defined in one place.
- Never use the option to skip certificate checks.
- Scan and sign the images of extension servers and operators, and verify them at admission.
- Encrypt sensitive fields at rest. Remember that secrets must not be placed in plain custom resource fields; reference a Secret instead.
Never place passwords, tokens or keys inside the fields of a custom resource. Objects are widely readable, logged and copied by tools. Store secrets in proper secret objects or an external vault, and let your custom resource point to them by name.
18Monitoring and Troubleshooting
When an extension misbehaves, you need to know where to look. Both kinds expose clear signals if you know their names.
What to look at
CRD conditions
The CRD reports whether names were accepted and whether it is Established. A type that is not Established cannot be used yet.
APIService conditions
The Available condition and its message explain whether the extension server is reachable, and why not. Listing all APIServices shows the health of every extension at a glance.
API server metrics
Request counts, error rates and latencies per resource and verb show which types are slow or failing. A dedicated metric counts unavailable aggregated services.
Audit logs
Show who changed or deleted a type or an object, and when.
Controller metrics
Queue depth, reconcile durations and error counts reveal whether the operator is keeping up.
etcd health
Database size, write latency and object counts per type tell you whether CRD usage is straining the cluster.
A troubleshooting walk-through
The type does not exist
Check that the CRD exists and is Established, that the group and version are spelled correctly, and that the version is marked as served.
Creating an object is rejected
Read the error message. It usually names the exact field that failed the schema or expression rule. Also check for admission policies and webhooks.
Objects exist but nothing happens
This is a controller problem. Check that the controller pod is running, has permissions and is not stuck in a failing loop. Read its logs and its metrics.
Errors about unavailable API groups
List APIServices and look for ones that are not Available. Check the extension server’s pods, its service, its certificates and the network path from the API server.
A namespace is stuck terminating
Look for unavailable aggregated APIs and for custom resources with finalizers whose controllers are no longer running. Restore the controller or remove the dead API registration with care.
Reads or writes fail after a version change
Check the conversion webhook, the storage version and whether stored objects still match the schema.
Alert on two simple things above all: any APIService that stops being Available, and any CRD whose controller has gone quiet. Together they catch most extension failures before users report them.
19A Decision Guide You Can Use Tomorrow
Here is a simple method for choosing between the two doors. Ask the questions in order, and stop at the first clear answer.
flowchart TB
S["New API need"] --> Q1{"Is a ConfigMap or plain settings enough?"}
Q1 -->|"yes"| CM["Use a ConfigMap"]
Q1 -->|"no"| Q2{"Must data live outside etcd or be computed live?"}
Q2 -->|"yes"| AGG["Use an aggregated API"]
Q2 -->|"no"| Q3{"Need custom verbs or subresources beyond status and scale?"}
Q3 -->|"yes, essential"| HYB["CRD for objects plus aggregated API for the special actions"]
Q3 -->|"no"| Q4{"Can schema, CEL rules and webhooks express your validation?"}
Q4 -->|"no"| AGG
Q4 -->|"yes"| Q5{"Small objects, moderate counts, modest write rate?"}
Q5 -->|"no"| AGG
Q5 -->|"yes"| CRD["Use a CRD with a controller"]
The questions in words
- Is a plain setting enough? If yes, stop. Use a ConfigMap and avoid a new API.
- Must the data live outside etcd, or be computed live? Metrics, giant datasets and external records say yes. That points to aggregation.
- Do you truly need special actions? Consoles, streams and other custom verbs cannot be done by a CRD. A hybrid might serve you best.
- Can your validation rules be written as schema and expressions? If your rules need unusual logic inside the request, aggregation gives the freedom.
- Is the scale reasonable for shared storage? Huge or hot data harms the cluster database. If your numbers are high, move away from etcd.
- Do you have a team to run a server forever? If not, that alone is a reason to pick a CRD and live with its limits.
Scoring your situation
| Signal | Points toward CRD | Points toward aggregation |
|---|---|---|
| Main purpose | Declarative intent that a controller acts on | Serving computed or external data |
| Data size and rate | Small and slow-changing | Large or fast-changing |
| Special actions | None beyond status and scale | Consoles, streams, custom verbs |
| Team capacity | Limited operations time | Dedicated platform team |
| Time to market | Weeks | Months are acceptable |
| Risk appetite | Prefer inherited safety | Willing to own security and availability |
| Ecosystem needs | Want maximum compatibility with GitOps and policy tools | Willing to verify compatibility yourself |
Switching later is expensive. Moving from a CRD to an aggregated API changes where data lives and how it is served, and users must be migrated. If you are unsure, begin with a CRD and a clearly separated data layer so that a later move stays possible.
20Design Patterns and Anti-Patterns
Experienced platform teams reuse a small set of patterns. They also share a list of traps they once fell into.
Patterns that work
Spec and status split
Users write the spec, the controller writes the status. Always use the status subresource so the two never collide.
Reference, do not embed
Point to secrets, config and large data by name instead of copying them into the object. Keeps objects small and safe.
Small, focused types
Several small types with clear owners beat one giant type with dozens of options.
Hybrid API
CRDs for the declarative core, with a small aggregated server only for special actions or computed views.
Schema as the contract
Invest in a precise schema with defaults and expression rules. It is the cheapest and most reliable validation you can have.
Conventional conditions
Report health with standard condition lists so humans and automation read status the same way everywhere.
Anti-patterns to avoid
What it looks like
A team stores application records, user profiles or event streams as custom resources, because the API is convenient.
Why it hurts
The cluster database fills up, listings slow down and a problem in the application can threaten the whole platform. Kubernetes is not a general-purpose database.
Better approach
Keep bulk and business data in a real database. Use custom resources only to describe infrastructure intent, and have them point to the data.
What it looks like
A team builds a full aggregated API server for something a CRD could easily describe, because it sounds more powerful.
Why it hurts
They inherit months of build effort, a permanent on-call burden and a new security surface, without using a single capability that CRDs lack.
Better approach
Start with a CRD. Move to aggregation only when you can name the specific capability that a CRD cannot provide.
What it looks like
A CRD is published with a loose schema that accepts anything, or with no versioning plan, and the shape changes between releases.
Why it hurts
Bad data enters the cluster, controllers crash on unexpected input, and users cannot upgrade without breaking their existing objects.
Better approach
Define strict schemas, start with an early version name, add conversion before any breaking change and migrate stored objects before retiring old versions.
What it looks like
A critical aggregated API runs as a single replica, with a hand-managed certificate, and without alerts on its availability.
Why it hurts
One crash or one expired certificate makes the API unavailable, causes discovery errors across the cluster and can leave namespaces stuck during deletion.
Better approach
Run multiple replicas across zones, automate certificate rotation and alert on the APIService condition.
What it looks like
A controller writes the status of every object every few seconds, even when nothing has changed.
Why it hurts
It generates constant write traffic and watch events, wasting etcd capacity and waking every other controller that watches the type.
Better approach
Only update when something truly changed, and batch changes together.
21Best Practices and Common Mistakes
Here is a checklist to take into your next design review.
Best practices for CRDs
- Own your group name. Use a domain you control, so that names never clash with other projects.
- Write a strict structural schema with types, limits, defaults and descriptions for every field.
- Use expression rules for cross-field checks, rather than adding a webhook that needs to be run and kept alive.
- Enable the status subresource, and keep user intent and observed state apart.
- Add printer columns so that a simple listing tells users what they need to know.
- Plan versions early. Start with an alpha version, use conversion only when needed and migrate stored data before removing old versions.
- Protect the CRD. Tight permissions for create, edit and delete, and backups of both the CRD and its instances.
- Prefer namespaced scope unless the thing is genuinely cluster-wide.
Best practices for aggregated APIs
- Justify it in writing. Document the capability that a CRD cannot provide, and revisit it at each major Kubernetes release as CRDs gain features.
- Use the official libraries rather than inventing your own handling of watch, versions and authentication.
- Delegate authentication and authorization to the main API server.
- Verify certificates in both directions and automate their renewal.
- Run it highly available and monitor the APIService condition.
- Plan the data layer for backups, encryption, scaling and recovery.
- Keep up with upgrades. Assign a team to follow Kubernetes releases and update the server’s dependencies regularly.
Common mistakes
- Reaching for aggregation because it feels more serious, when a CRD would do.
- Treating the cluster database as an application database.
- Putting secrets directly into custom resource fields.
- Deleting a CRD without realizing that all its objects disappear.
- Forgetting finalizers, leaving outside resources such as cloud disks behind.
- Leaving finalizers on objects after the controller has been removed, which makes them impossible to delete.
- Building conversion that loses information, so objects change when read through another version.
- Skipping alerts on APIService availability and controller health.
- Making many tiny CRDs with overlapping meaning, instead of a clear small set.
22Real-World Usage
The balance of use in the wild shows how the guidance plays out in practice: a great many CRDs, and a small number of carefully justified aggregated servers.
The operator ecosystem: CRDs by default
Nearly all popular platform projects, including certificate management, monitoring operators, GitOps controllers, service meshes, cluster provisioning and cloud resource managers, are built on CRDs with controllers. Their authors chose the free storage, the inherited security and the broad tool compatibility over custom control.
Metrics: aggregation for live data
The resource metrics, custom metrics and external metrics interfaces used by autoscalers are served by aggregated API servers. The data is short-lived and computed from monitoring sources, so keeping it out of etcd is exactly the right call, and a standard group name lets autoscalers find it.
Virtual machines: a hybrid
KubeVirt describes virtual machines with CRDs, so that controllers and tools can manage them like any other object. For interactive actions such as console and display access, it adds an aggregated API that serves subresources, which CRDs cannot do. This shows the hybrid pattern working at scale.
Lessons from retired projects
Service Catalog was built as an aggregated API server when CRDs were less capable. As CRDs gained schemas, validation and versioning, the case for running a separate server weakened, and the project was eventually retired. The lesson: the capability gap between the two mechanisms shrinks over time, so recheck your reasons before committing to aggregation.
Platform teams building internal APIs
Many companies expose their own internal platform concepts, such as an application, a team environment or a database request, as custom resources, so developers use the same declarative tools for infrastructure as for code. Some go further and layer higher-level abstractions on top, composing many lower-level resources into a single simple request.
23Frequently Asked Questions
Quick answers to the questions that come up most often.
24Summary and Key Takeaways
What to remember
- Kubernetes is an extensible API. Adding a new type gives you permissions, watches, tooling and audit trails that your teams already know.
- There are two official doors. A CRD teaches the built-in extensions server a new type. An aggregated API registers your own separate server behind the front desk.
- CRDs are the default. You write a schema, and the API server handles storage, validation, watch, security and high availability. A controller adds behavior.
- Aggregation is for special needs. Choose it for data outside etcd, computed or live data, custom verbs and subresources, or logic that schemas cannot express, and accept the cost of running a server.
- The failure modes differ. A broken extension server can cause discovery errors and stuck namespaces. A deleted CRD removes all its objects. Plan monitoring and protection for both.
- Security is shared, but owned differently. CRDs inherit the cluster’s protections. Aggregated servers must correctly wire up certificates, delegated authorization and their own data security.
- Start simple and decide with a method. Ask whether a ConfigMap is enough, then whether any need truly rules out a CRD. Hybrids are valid, and the capability gap between the two keeps shrinking.