Turn a single 15-line AppService CRD into a fully deployed, autoscaled, secured, and observable deployment without writing 200 lines of brittle YAML.
Table of Contents
Deploying a microservice to Kubernetes means hand-writing 6-10 YAML files (Deployment, Service, Ingress, HPA, PDB, NetworkPolicy...) and getting several subtly wrong.
Write this:
apiVersion: platform.mydomain.dev/v1alpha1
kind: AppService
metadata:
name: my-api
spec:
app:
image: my-api:1.0.0
replicas: 3
port: 8080
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 70The operator reconciles it into 9 production-ready resources — Deployment, Service, HPA, Ingress, PVC, PDB, NetworkPolicy, canary rollout, and a Grafana dashboard — automatically.
| Without KubeLoom | With KubeLoom | |
|---|---|---|
| Files to write | 6-10 separate YAML manifests | 1 AppService CRD (~15 lines) |
| Canary rollout | Manual second Deployment + Istio config | canary.enabled: true |
| Observability | Hand-code ServiceMonitor + Grafana JSON | observability.enabled: true |
| Network policy | Write CIDR rules from memory | networkPolicy.enabled: true |
| Autoscaling | Separate HPA manifest, easy to drift | autoscaling.enabled: true |
| Toggle a resource off | Delete the YAML file, hope nothing breaks | enabled: false (operator deletes it) |
┌─────────────────────────────┐
│ Developer writes │
│ 1 AppService CRD │
└──────────────┬──────────────┘
│
▼
┌─────────────────────────────┐
│ KubeLoom Operator │
│ (controller-runtime) │
└──────────────┬──────────────┘
│
┌────────────────────┼────────────────────┐
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────────┐
│ Core │ │ Scaling & │ | Networking & |
│ Workload │ │ Resilience │ | Observability |
├──────────────┤ ├──────────────┤ ├──────────────────┤
│ Deployment │ │ HPA │ │ Ingress │
│ Service │ │ PDB │ │ NetworkPolicy │
│ PVC │ │ Canary │ │ Grafana Dashboard│
│ Env/Probes │ │ Strategy │ │ Prometheus │
│ Security │ │ Lifecycle │ │ annotations │
└──────────────┘ └──────────────┘ └──────────────────┘
AppService CR
│
▼
┌─────────────┐
│ reconcileDeployment ──→ Deployment (full pod spec)
│ reconcileService ──→ Service
│ reconcileHPA ──→ HPA (or delete if disabled)
│ reconcileIngress ──→ Ingress (or delete if disabled)
│ reconcileStorage ──→ PVC (or delete if disabled)
│ reconcileDisruption ──→ PDB (or delete if disabled)
│ reconcileNetworkPolicy ─→ NetworkPolicy (or delete if disabled)
│ reconcileCanary ──→ Canary Deployment (or delete if disabled)
│ reconcileObservability ─→ ConfigMap (Grafana dashboard)
└─────────────┘
│
▼
status.conditions[Available=True]
- One CRD, nine resources — single
AppServicegenerates Deployment, Service, HPA, Ingress, PVC, PDB, NetworkPolicy, canary Deployment, and Grafana dashboard ConfigMap - Toggle-driven — every optional resource has an
enabledflag. Flip it on/off without removing code - Idempotent reconciliation — safe to run repeatedly. Creates or updates with
CreateOrUpdate, deletes when disabled - Full pod spec control — env vars, probes (HTTP/TCP), lifecycle hooks, security contexts, DNS, image pull, logging, sidecars, init containers, rolling update strategy
- Canary deployments — 1-replica canary with
track=canarylabel for traffic differentiation - Observability — auto-generated Grafana dashboard ConfigMaps + Prometheus scrape annotations
- Resilience labels — Istio retry and circuit-breaker annotations on pod labels
- Real test coverage — 12 unit test files, envtest integration tests, e2e in isolated Kind clusters via GitHub Actions
# Install CRDs and deploy the controller
make install
export IMG=<your-registry>/kubeloom:latest
make docker-build docker-push IMG=$IMG
make deploy IMG=$IMG
# Apply a sample AppService
kubectl apply -k config/samples/The operator immediately reconciles:
demo-app ← your AppService
├── Deployment/demo-app ← pod spec with all mutations
├── Deployment/demo-app-canary ← 1-replica canary with track label
├── Service/demo-app ← ClusterIP with observability annotations
├── HPA/demo-app ← CPU-based autoscaling (2-8 replicas)
├── PDB/demo-app ← minAvailable: 1
├── NetworkPolicy/demo-app ← CIDR-based ingress/egress rules
├── Ingress/demo-app ← (disabled in sample, host routing with TLS)
└── ConfigMap/grafana-dashboard-* ← auto-generated Grafana dashboard
The report script (scripts/e2e-report.sh) validates every reconciled resource:
- Go 1.26+
- Docker (or Podman) for building the controller image
- kubectl configured for a Kubernetes cluster
- Kind (for local development and e2e tests)
git clone https://github.com/Kritagya123611/Kubeloom.git
cd Kubeloom
export IMG=<your-registry>/kubeloom:latest
make docker-build docker-push IMG=$IMG
make install
make deploy IMG=$IMG
kubectl apply -k config/samples/
kubectl logs -n kubeloom-system deployment/kubeloom-controller-manager -c manager -fmake run # run controller locally
kubectl apply -k config/samples/ # apply sample in another terminalmake test # unit + integration (envtest, no cluster needed)
make lint # golangci-lint
make test-e2e # e2e in isolated Kind cluster (auto-cleanup)The controller accepts these flags at startup:
| Flag | Default | Description |
|---|---|---|
--metrics-bind-address |
:8443 |
Metrics server listen address |
--health-probe-bind-address |
:8081 |
Health/readiness probe address |
--leader-elect |
false |
Enable leader election for HA |
--metrics-secure |
true |
Require TLS for metrics endpoint |
--enable-http2 |
false |
Enable HTTP/2 (disabled to mitigate CVE-2023-44488) |
These flags control how the operator itself runs. For the workload it
manages, everything is driven by the AppService spec fields below.
The AppService CRD at platform.mydomain.dev/v1alpha1 supports 24 spec
fields. Each field maps to specific Kubernetes resources:
| Field | Description | Reconciles To |
|---|---|---|
app |
Image, replicas, port, command, args, workingDir, imagePullPolicy | Deployment, Pod |
env |
Environment variables, envFrom (ConfigMap/Secret refs) | Pod env vars |
resources |
CPU/memory requests and limits | Container resources |
probes |
Readiness, liveness, startup (HTTP GET or TCP) | Pod probes |
service |
Service type, port, targetPort | Service |
ingress |
Host, path, TLS, annotations | Ingress |
autoscaling |
Min/max replicas, CPU/memory utilization targets | HPA |
scheduling |
nodeSelector, tolerations, affinity, topologySpreadConstraints | Pod scheduling |
security |
runAsNonRoot, readOnlyRootFilesystem, seccompProfile, etc. | SecurityContext |
storage |
PVC size, storageClass, mountPath | PersistentVolumeClaim |
canary |
Enabled, weight, stableImage | Canary Deployment |
resilience |
Retry attempts, circuit-breaker thresholds | Pod labels |
disruption |
minAvailable, maxUnavailable | PDB |
networkPolicy |
CIDR-based ingress/egress allow lists | NetworkPolicy |
observability |
ServiceMonitor CR, Grafana dashboard, metrics config | ServiceMonitor, ConfigMap, Service annotations |
logging |
Log level, format, sidecar image | Env vars, sidecar container |
lifecycle |
PreStop, PostStart hooks (exec, HTTP, TCP) | Pod lifecycle |
strategy |
RollingUpdate maxSurge/maxUnavailable | Deployment strategy |
dns |
Pod hostname | Pod hostname |
imagePull |
Registry prefix, imagePullSecrets | Pod imagePullSecrets |
sidecars |
Additional sidecar containers | Pod containers |
initContainers |
Init containers | Pod init containers |
serviceAccountName |
Custom service account name | Pod serviceAccountName (SA created if rbac.createServiceAccount: true) |
rbac |
ServiceAccount creation, roles, clusterRoles | ServiceAccount, RoleBinding, ClusterRoleBinding |
├── cmd/main.go Manager entry point
├── api/v1alpha1/
│ ├── appservice_types.go CRD type definitions (24 fields)
│ ├── groupversion_info.go Group/version registration
│ └── zz_generated.deepcopy.go Auto-generated DeepCopy
├── internal/controller/
│ ├── appservice_controller.go Main reconciler (9 sub-reconcilers)
│ ├── deployment.go Deployment reconciliation + pod spec
│ ├── service.go Service reconciliation
│ ├── hpa.go HPA (create/delete)
│ ├── ingress.go Ingress (create/delete)
│ ├── storage.go PVC (create/delete)
│ ├── disruption.go PDB (create/delete)
│ ├── networkpolicy.go NetworkPolicy (create/delete)
│ ├── canary.go Canary Deployment
│ ├── observability.go Grafana dashboard ConfigMap
│ ├── env.go Env var mutation
│ ├── probes.go Health probe mutation
│ ├── lifecycle.go Lifecycle hook mutation
│ ├── scheduling.go NodeSelector mutation
│ ├── security.go SecurityContext mutation
│ ├── dns.go DNS hostname mutation
│ ├── imagepull.go Image registry/pull secret mutation
│ ├── logging.go Log level/format + resilience labels
│ ├── sidecars.go Sidecar + init container mutation
│ ├── strategy.go Deployment strategy mutation
│ ├── *_test.go 12 unit test files (envtest)
│ └── suite_test.go Ginkgo test suite setup
├── config/
│ ├── crd/bases/ Generated CRD YAML
│ ├── rbac/ Generated RBAC manifests
│ ├── samples/ Example AppService CRs
│ └── manager/ Controller Deployment manifest
├── test/e2e/ End-to-end tests (Kind cluster)
├── scripts/e2e-report.sh Visual e2e report script
├── Makefile Build, test, deploy commands
├── Dockerfile Multi-stage (golang:1.26 → distroless)
└── PROJECT Kubebuilder metadata
# Install tooling (downloads controller-gen, kustomize, envtest to ./bin/)
make manifests generate
# Run tests
make test
# Lint and auto-fix
make lint-fix- Edit
api/v1alpha1/*_types.gofor CRD changes → runmake manifests generate - Edit
internal/controller/*.gofor reconciliation logic → runmake test - Run
make lint-fixbefore committing - CI validates everything (lint, unit tests, e2e on Kind)
git checkout -b feature/my-change
make test lint-fix
git add . && git commit -m "Add feature description"
git push origin feature/my-change
# Open a PR — CI runs automatically| Component | Technology |
|---|---|
| Language | Go 1.26 |
| Framework | Kubebuilder v4.15.0 / controller-runtime v0.24.1 |
| CRD Version | API v1 (CustomResourceDefinition v1) |
| Kubernetes | v1.36 client libraries |
| Testing | Ginkgo + Gomega, envtest (real API + etcd) |
| E2E | Kind clusters, Cert-Manager |
| CI | GitHub Actions (lint, test, e2e) |
| Linting | golangci-lint v2.12.2 (20+ analyzers) |
| Runtime Image | distroless/static:nonroot (UID 65532) |
Apache License 2.0 — Copyright 2026. See LICENSE for details.
- Kubebuilder — the scaffolding framework this project is built on
- controller-runtime — the reconciliation library
- controller-tools — CRD and RBAC generation


