Components & reuse
Optional, composable feature sets.
A stand mixer ships with a bowl and a motor. The pasta roller and the meat grinder are sold separately, and each attachment comes with two things: the new part, and the collar that clamps it onto the machine you already own. You fit the ones you want and leave the rest in the drawer. A Kustomize component is that attachment. Kustomize (the built-in, template-free tool that layers edits onto plain Kubernetes YAML, the text format that describes cluster objects) already gives you a base (the shared manifests everyone starts from) and overlays (the per-environment tweaks stacked on top). A component is the optional attachment that a base and an overlay cannot express cleanly on their own: a feature that carries its own objects and the patches that wire them into whatever you already built.
You reach for one the moment several environments all want the same optional feature. A monitoring sidecar (an extra container that rides alongside your app). A secret pulled in from an outside vault. A hardened securityContext (the block of settings that decides what a container is allowed to do). Copy that YAML into every overlay by hand and you get drift: prod gets the fix, staging keeps a stale copy, dev runs whatever version someone last remembered. A component captures the feature once, and each environment opts in by name.
What a component actually is
A component file looks almost identical to a kustomization.yaml, with two changes. The apiVersion is kustomize.config.k8s.io/v1alpha1, and the kind is Component instead of Kustomization. Inside, it carries the same building blocks a normal kustomization can: resources, patches, generators, and transformers. The difference is not what it holds, it is when it runs. A base you list under resources is rendered on its own, and the finished output is merged into yours. A component is poured onto the caller's running pile of objects, the pile Kustomize builds up as it reads your overlay from top to bottom. Because it runs against that pile, its patch can reach a Deployment (a Kubernetes object that runs and keeps a set of identical pods alive, a pod being one or more containers scheduled together) that the base declared, not only the ones the component shipped itself. That single fact is the whole reason components exist.
The v1alpha1 label looks scary for something you would run in prod. It has been stable for years and is exactly what current Kustomize expects here, so do not talk yourself into changing it. The one real requirement is version: the components field needs Kustomize v3.7 or newer, and the copy embedded in kubectl has been new enough since kubectl 1.21. Check yours with kubectl version, which prints the Kustomize build it carries.
# the shape of a project that uses componentsfind . -type f | sort
./base/deployment.yaml./base/kustomization.yaml./components/hardening/harden.yaml./components/hardening/kustomization.yaml./components/vault-agent/external-secret.yaml./components/vault-agent/inject-agent.yaml./components/vault-agent/kustomization.yaml./overlays/dev/kustomization.yaml./overlays/prod/kustomization.yaml./overlays/staging/kustomization.yaml
Writing a component
Here is a component that turns on Vault-backed secrets. It brings one object of its own, an ExternalSecret (a custom resource that pulls a value out of an external vault and lands it in the cluster as a normal Secret), and it patches the workload so the Vault agent knows to inject into it.
apiVersion: kustomize.config.k8s.io/v1alpha1kind: Component # NOT Kustomizationresources:- external-secret.yaml # the feature's own objectpatches:- path: inject-agent.yamltarget:kind: Deployment # match by kind, not by name
apiVersion: apps/v1kind: Deploymentmetadata:name: web # required to make a valid patch document;spec: # the target above is what actually selectstemplate:metadata:annotations:vault.hashicorp.com/agent-inject: "true"vault.hashicorp.com/role: web
Read the patch carefully, because the selection rule trips people up. This is a strategic merge patch (a partial copy of the object that Kustomize folds field-by-field into the real one). The target block says kind: Deployment, so this patch lands on every Deployment in the pile, whatever it is named. The metadata.name: web in the body is not doing the choosing. It is there only because a strategic merge document has to be a valid partial object, and Kustomize ignores that name when a target is present. Drop the name entirely and the build fails to parse the patch, so keep it, but understand that target is the selector. That is what lets one component patch a base it has never seen: it matches by kind, not by the app's specific name.
Turning it on, per environment
An overlay stays an ordinary Kustomization. It pulls the base in under resources, then lists the features it wants under components, which are applied after everything in resources and in the order you write them. Prod wants Vault secrets and hardening. Staging wants Vault secrets only. Dev wants neither. All three read from one base and one shared set of component directories, with zero copies of the feature YAML.
apiVersion: kustomize.config.k8s.io/v1beta1kind: Kustomizationresources:- ../../basecomponents:- ../../components/vault-agent # applied in this order,- ../../components/hardening # each sees what came before
kubectl kustomize overlays/prod
apiVersion: apps/v1kind: Deploymentmetadata:name: webspec:replicas: 2selector:matchLabels:app: webtemplate:metadata:annotations:vault.hashicorp.com/agent-inject: "true"vault.hashicorp.com/role: weblabels:app: webspec:containers:- image: registry.example.com/web:1.4.2name: webports:- containerPort: 8080securityContext:runAsNonRoot: truerunAsUser: 10001seccompProfile:type: RuntimeDefault---apiVersion: external-secrets.io/v1beta1kind: ExternalSecretmetadata:name: web-dbspec:data:- remoteRef:key: kv/data/webproperty: db_passwordsecretKey: passwordsecretStoreRef:kind: ClusterSecretStorename: vault-backendtarget:name: web-db
One base Deployment came out carrying both features. The Vault annotations sit on the pod template, the securityContext from the hardening component sits below it, and the ExternalSecret rides along as its own object. Nothing in the base file mentions Vault or securityContext. In CI (the pipeline that checks a change before it merges), you do not trust that by eye. You grep the rendered output for the pieces that were supposed to land.
kubectl kustomize overlays/prod | grep -E 'runAsNonRoot|agent-inject|ExternalSecret'
vault.hashicorp.com/agent-inject: "true"runAsNonRoot: truekind: ExternalSecret
Three matches, one per feature. That grep is your proof the layers wired in. Run it against the render before the manifests reach the cluster, not after an incident when you are trying to work out why a pod came up as root.
Where a base falls short
The line between a component and a base is thin and load-bearing. Anything under resources is rendered as its own independent Kustomize target, and the result is merged into yours, which means a resources entry can only patch objects it declared inside itself. It never sees your base's Deployment. A component is the opposite. It is applied straight into the caller's pile, so it can patch the base's objects, and it can react to whatever earlier components already added. Order therefore matters. Components apply top to bottom, each one sees everything above it, and when two of them write the same field the lower one wins. Reach for a component whenever a feature has to modify a resource it does not own; reach for a base or a plain resources entry when the feature is fully self-contained.
Order is significant, and the last patch wins without a sound. When two components patch the same field, say both set the pod securityContext, the one listed lower overwrites the one above it. Reorder the components list and your rendered output changes with no error and nothing to flag it. Any time you move entries around, or add a component that touches a field an existing one already sets, rebuild and diff the output before you ship.
Reuse as a control you can prove
The payoff for a defender is that a security baseline becomes a control written once and switched on wherever you name it. Here is the hardening component. It sets runAsNonRoot (the pod refuses to start if its container would run as the root user), a fixed non-root user ID, and a RuntimeDefault seccomp profile. Seccomp, short for secure computing mode, is a kernel feature that filters which system calls a container is allowed to make; RuntimeDefault means the container runtime's own curated blocklist, which shrinks the attack surface a compromised process has to work with.
apiVersion: kustomize.config.k8s.io/v1alpha1kind: Componentpatches:- path: harden.yamltarget:kind: Deployment
apiVersion: apps/v1kind: Deploymentmetadata:name: webspec:template:spec:securityContext:runAsNonRoot: truerunAsUser: 10001seccompProfile:type: RuntimeDefault
Because switching it on is a single line in an overlay, coverage is greppable. You can ask, in one command, which environments are missing the control. The flag -rL prints the files that do not contain the pattern, so this list is every overlay that forgot to opt in.
grep -rL 'components/hardening' overlays/*/kustomization.yaml
overlays/dev/kustomization.yamloverlays/staging/kustomization.yaml
That is a coverage report you can generate on demand and wire into a pipeline gate. An attacker who lands inside a pod cares a great deal whether runAsNonRoot and a RuntimeDefault seccomp profile are set, because those are a big part of what stands between a process that stays boxed inside its container and one with a straight path to the node. This tells you, in plain text, exactly where they are not. One caveat on the patch itself: pod-level fields like these need no container name, so the component stays reusable. Container-level fields, such as readOnlyRootFilesystem or dropping Linux capabilities, merge by container name, so for those you either standardise the container name across your bases or target the patch more narrowly.
Make kubectl kustomize (or kustomize build) part of the merge, and diff its output against the last known-good render. Components move real behavior, a sidecar, a secret, a securityContext, so a one-line edit to a components list can add or quietly remove a control. The rendered manifests are the only honest record of what you actually shipped.
Try this
Run find . -type f | sort on a scratch host or disposable cluster and read the output against what this lesson described. Then change one input so it fails, and re-run: the error you get is the one you will meet in production.
Takeaway
The trap worth remembering here: list it under components:, never resources. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.