Skip to content

Example — Refactoring homepage to use Kustomize Components ​

This guide walks through the before and after of adopting reusable Kustomize components to remove duplicated YAML boilerplate from an application's manifests, using the homepage app as a worked example.

Overview ​

FilesApprox. linesSecurity context
Beforedeployment.yaml, service.yaml, ingress.yaml, middleware.yaml, configmap.yaml, serviceaccount.yaml, clusterrole.yaml, kustomization.yaml~350Repeated in every app
Afterkustomization.yaml, deployment.yaml, configmap.yaml, ingress-patch.yaml, deployment-patch.yaml~100 (+ shared components)Defined once in app-base

Before, homepage carried a full set of manifests with all the security context, service account, networking and RBAC boilerplate inlined.

After, homepage references shared components and only keeps the app-specific spec plus a couple of small patches.

kustomization.yaml ​

Before — lots of boilerplate ​

yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: homepage
resources:
  - deployment.yaml
  - service.yaml
  - ingress.yaml
  - configmap.yaml
  - clusterrole.yaml
  - serviceaccount.yaml
  - middleware.yaml

commonLabels:
  app.kubernetes.io/name: homepage
commonAnnotations:
  description: "Dashboard UI"

After — clean, components provide the boilerplate ​

yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: homepage

# Use reusable components
components:
  - ../../../../vendor/k3s-lab/kubernetes/components/app-base
  - ../../../../vendor/k3s-lab/kubernetes/components/traefik-ingress
  - ../../../../vendor/k3s-lab/kubernetes/components/network-policies

# Only app-specific resources
resources:
  - deployment.yaml
  - configmap.yaml

# Customize component templates for this app
patches:
  - path: ingress-patch.yaml
  - path: deployment-patch.yaml

# Standard labels (app-base provides the managed-by label)
commonLabels:
  app.kubernetes.io/name: homepage

NOTE

patchesStrategicMerge was deprecated in Kustomize v5.0.0. Use the patches field with path: instead — strategic merge semantics still apply, so existing patch files keep working. Run kustomize edit fix to migrate automatically.

deployment.yaml ​

Before — full boilerplate ​

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: homepage
  namespace: apps # OLD: namespace set here
  labels:
    app.kubernetes.io/name: homepage
spec:
  revisionHistoryLimit: 3
  replicas: 1
  strategy:
    type: RollingUpdate
  selector:
    matchLabels:
      app.kubernetes.io/name: homepage
  template:
    metadata:
      labels:
        app.kubernetes.io/name: homepage
    spec:
      serviceAccountName: homepage
      automountServiceAccountToken: true
      dnsPolicy: ClusterFirst
      enableServiceLinks: true
      containers:
        - name: homepage
          image: "ghcr.io/gethomepage/homepage:v1.12.3@sha256:abc123..."
          imagePullPolicy: IfNotPresent
          securityContext: # ← boilerplate
            allowPrivilegeEscalation: false
            capabilities:
              drop:
                - ALL
            readOnlyRootFilesystem: true
            runAsNonRoot: true
            runAsUser: 1000
            runAsGroup: 1000
            seccompProfile:
              type: RuntimeDefault
          ports:
            - name: http
              containerPort: 8080
          env:
            - name: MY_POD_IP
              valueFrom:
                fieldRef:
                  fieldPath: status.podIP
          volumeMounts: # ← boilerplate (tmp/var-run)
            - name: tmp
              mountPath: /tmp
            - name: var-run
              mountPath: /var/run
          resources: # ← often identical across apps
            requests:
              cpu: "10m"
              memory: "32Mi"
            limits:
              cpu: "500m"
              memory: "256Mi"
      volumes:
        - name: tmp
          emptyDir: {}
        - name: var-run
          emptyDir: {}

After — clean ​

Comments mark what is now provided by a component or a patch instead of being declared inline.

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: homepage
  # namespace: ← provided by kustomization.yaml
spec:
  revisionHistoryLimit: 3
  replicas: 1
  strategy:
    type: RollingUpdate
  selector:
    matchLabels:
      app.kubernetes.io/name: homepage
  template:
    metadata:
      labels:
        app.kubernetes.io/name: homepage
    spec:
      # serviceAccountName: ← patched by deployment-patch.yaml
      # automountServiceAccountToken: ← provided by app-base
      # dnsPolicy: ← provided by app-base
      # enableServiceLinks: ← provided by app-base
      # securityContext: ← provided by app-base component
      # volumes: ← patched by deployment-patch.yaml
      containers:
        - name: homepage
          image: "ghcr.io/gethomepage/homepage:v1.12.3@sha256:abc123..."
          # imagePullPolicy: ← provided by app-base
          # securityContext: ← provided by app-base
          ports:
            - name: http
              containerPort: 8080
          env:
            - name: MY_POD_IP
              valueFrom:
                fieldRef:
                  fieldPath: status.podIP
          # volumeMounts: ← patched by deployment-patch.yaml
          # resources: ← patched by deployment-patch.yaml

ingress-patch.yaml — customize the domain ​

Patch the IngressRoute provided by the traefik-ingress component.

yaml
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
  name: app-https
spec:
  routes:
    - match: "Host(`homepage.kevindb.dev`)" # ← override domain
      kind: Rule
      services:
        - name: homepage # ← match service name
          port: 80

deployment-patch.yaml — override resources and volumes ​

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: homepage
spec:
  template:
    spec:
      serviceAccountName: homepage
      containers:
        - name: homepage
          runAsUser: 1000
          runAsGroup: 1000
          resources:
            requests:
              cpu: "10m"
              memory: "32Mi"
            limits:
              cpu: "500m"
              memory: "256Mi"
          volumeMounts:
            - name: tmp
              mountPath: /tmp
            - name: var-run
              mountPath: /var/run
      volumes:
        - name: tmp
          emptyDir: {}
        - name: var-run
          emptyDir: {}

Files that disappear ​

These files are no longer needed because the components provide them:

FileStatusProvided by
service.yamlDeletedapp-base
ingress.yamlDeletedtraefik-ingress (use ingress-patch.yaml)
middleware.yamlDeletedtraefik-ingress
serviceaccount.yamlDeletedapp-base
clusterrole.yamlDeletednetwork-policies or base
configmap.yamlKeptApp-specific

Benefits summary ​

Before

  • 8 files (deployment, service, ingress, middleware, configmap, clusterrole, serviceaccount, kustomization).
  • ~350 lines of YAML.
  • Boilerplate security context in every app.
  • Updating the security policy means editing 8+ apps.

After

  • 4 files (deployment, configmap, kustomization, plus 2 patches).
  • ~100 lines of YAML (+ ~300 lines in components, reused by all apps).
  • Security context defined once in app-base.
  • Updating the security policy means editing 1 file (the app-base component).
  • ~70% less duplicated YAML per app.
  • Much easier to onboard new apps.