Most explanations of Kubernetes start with a diagram full of boxes and arrows, and by the third box you have stopped following. So let us do the opposite. Let us take one small web app, the kind you would happily push to a hosting platform in an afternoon, and deploy it on raw Kubernetes by hand. Every term you have heard and never quite pinned down shows up naturally when you do this, and I will explain each one plainly as it arrives. By the end you will understand what Kubernetes actually asks of you, why it is genuinely powerful, and why that power has a cost that lands squarely on the smallest teams.
The app is nothing special: a container image that listens on port 8080 and needs one setting, a greeting message, injected from outside. That is it. Watch how much surface area that tiny thing touches.
First, what Kubernetes even is
Kubernetes is a system for running containers across a set of machines. You give it a picture of what you want running, and it works to make reality match that picture and keep it matching. If a container dies, it starts another. If a machine goes away, it moves the work elsewhere. The catch is that you describe that "picture of what you want" in files, and those files are where the whole story lives.
The machines Kubernetes runs your containers on are called a cluster. The files you hand it are almost always YAML, a plain text format for structured data. You will write a lot of YAML. Each YAML file describes an object, a thing Kubernetes should track and reconcile. Our little app needs several of these objects working together, so let us build them one at a time.
The Deployment: what should be running, and how many
The first object is a Deployment. A Deployment answers a simple question: what container should be running, and how many copies of it. Those copies are called replicas. If you ask for three replicas, Kubernetes keeps three identical copies of your container alive, spread across the cluster, and restarts any that fall over. When you push a new version of your image, the Deployment rolls the change out gradually, replacing old copies with new ones so the app never fully goes dark.
Here is the YAML for our app:
apiVersion: apps/v1
kind: Deployment
metadata:
name: greeter
spec:
replicas: 2
selector:
matchLabels:
app: greeter
template:
metadata:
labels:
app: greeter
spec:
containers:
- name: greeter
image: ghcr.io/you/greeter:1.0.0
ports:
- containerPort: 8080
Read it top to bottom. kind: Deployment says what sort of object this is. replicas: 2 asks for two copies. The selector and the labels under template are how the Deployment knows which running copies belong to it: it stamps every copy with the label app: greeter and then watches everything carrying that label. The template section is the recipe for one copy, a pod, which is Kubernetes' name for one or more containers scheduled together as a unit. Inside it we name the image and say the container listens on port 8080.
Already there is a fair amount to hold in your head, and we have not injected the one setting the app needs, exposed it to anyone, or made it reachable at a URL. The Deployment only describes the process. Everything else is still ahead.
The ConfigMap: getting settings into the container
Our app needs a greeting message, and we do not want to bake that into the image, because then changing it means rebuilding. Kubernetes has an object for exactly this, called a ConfigMap. A ConfigMap is a named bag of key value settings that lives in the cluster, separate from any container. You define the values once, then wire them into a container as environment variables (or as files, but environment variables are the common case).
apiVersion: v1
kind: ConfigMap
metadata:
name: greeter-config
data:
GREETING: "Hello from Kubernetes"
That object exists on its own now, but nothing uses it yet. Settings do not flow anywhere by magic. You have to go back into the Deployment and connect them, which means editing the container spec to pull the value in:
containers:
- name: greeter
image: ghcr.io/you/greeter:1.0.0
ports:
- containerPort: 8080
env:
- name: GREETING
valueFrom:
configMapKeyRef:
name: greeter-config
key: GREETING
Now the container starts with a GREETING environment variable whose value comes from the ConfigMap. Notice the shape of the work: two separate objects, defined in two places, joined by names that have to match exactly. Get the name wrong, and the container starts with no greeting and you find out at runtime. This pattern, small pieces referring to each other by name, is the texture of Kubernetes everywhere. It is flexible, and it is also a lot of careful bookkeeping.
kubectl: the tool you drive it all with
So far we have written files describing what we want. Nothing is running, because we have not told the cluster about any of it. The tool that talks to a cluster is kubectl, the Kubernetes command line client. You will live in this tool.
To send your objects to the cluster, you apply them:
kubectl apply -f deployment.yaml
kubectl apply -f configmap.yaml
kubectl apply means "make the cluster match these files." Run it, and Kubernetes reads your Deployment, creates the pods, pulls the image, and starts your two replicas. To see what happened, you ask:
kubectl get pods
which lists the running pods and whether they are healthy. When something is wrong, and on your first few tries something usually is, you read the logs a pod is printing:
kubectl logs greeter-7c9f8b6d4-x2k5p
That long name is the actual pod name Kubernetes generated, which you copy out of the get pods output. When you ship a new image version, you update the image in your YAML, apply again, and then watch the rollout replace the old copies with new ones:
kubectl rollout status deployment/greeter
kubectl rollout also lets you undo a bad release by rolling back to the previous version. None of these commands is hard on its own. The point is the sheer number of them, and that you need to know which to reach for, and how to read what each prints, before the cluster tells you anything useful. This is the day to day of operating raw Kubernetes: a steady stream of kubectl verbs against objects you are keeping straight in your head.
Helm: because writing all this YAML by hand gets old
Once you have written a Deployment, a ConfigMap, and the few other objects a real app needs, and then copied them for a second app with slightly different values, you feel the repetition immediately. The common answer is Helm, a package manager for Kubernetes.
Helm's core idea is the chart: a bundle of templated YAML for an application, with the changeable bits pulled out into a separate values file. Instead of hand editing raw YAML, you set values and Helm renders the full objects for you.
helm install greeter ./greeter-chart --values values.yaml
Your values.yaml might just say replicas: 2 and greeting: "Hello from Kubernetes", and the chart's templates expand that into the complete Deployment and ConfigMap. This is genuinely helpful. It is also another tool with its own concepts (charts, values, releases, templating syntax) layered on top of the concepts you already had to learn. Helm makes the YAML easier to manage. It does not make the underlying pile of objects smaller or simpler to understand. You are now fluent in Kubernetes objects and in Helm's way of generating them.
Ingress: making it reachable from the internet
Here is the part that surprises people. Everything we have built so far runs your app inside the cluster, but nobody on the internet can reach it. A Deployment does not give you a URL. To expose an app to the outside world by hostname and path, you use an object called an Ingress.
An Ingress is a set of routing rules: requests for this hostname and this path go to this app inside the cluster. It looks like this:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: greeter
spec:
rules:
- host: greeter.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: greeter
port:
number: 80
(That backend points at a Service, yet another object, that gives your pods a stable internal address. We skipped writing it out, but you need one too.) And here is the catch that trips up almost everyone the first time: an Ingress object on its own does nothing. It is only a set of rules. Something has to actually read those rules and route traffic accordingly, and that something is a separate piece of software called an ingress controller.
The ingress controller does not come with a cluster by default. You install and run it yourself. It is the real proxy sitting at the edge of your cluster, watching every Ingress object and turning those rules into live routing. So to make one small app reachable, you have to stand up and maintain a whole networking component that has nothing to do with your app, and then keep it running, because if it goes down, every app behind it goes dark at once.
TLS: and now the certificates
You are not done, because greeter.example.com should be served over HTTPS, and HTTPS needs a TLS certificate. TLS is the encryption that puts the padlock in the browser and keeps traffic private. Certificates expire, usually every ninety days, so this is not a one time task, it is an ongoing one.
The usual approach is to install yet another component, commonly cert-manager, which requests certificates from a certificate authority and renews them automatically before they lapse. You configure it, you point your Ingress at the certificate it manages, and you make sure your DNS actually points at your ingress controller so the whole chain lines up. Miss the renewal wiring, and some quiet Sunday a certificate expires and your site starts throwing security warnings to every visitor.
Step back and count what you are holding
Look at what it took to run one small app that listens on a port and needs one setting:
- A Deployment, with replicas, selectors, labels, and a pod template.
- A ConfigMap, wired into the container by exactly matching names.
- A Service to give the pods a stable address.
- An Ingress, which is only rules.
- An ingress controller you install and keep running, or nothing routes.
- TLS certificates and a component to issue and renew them.
- DNS pointed correctly at the edge.
- And
kubectl, with its dozen verbs, plus maybe Helm and its own concepts on top.
Here is the honest turn. Every one of those pieces exists for a good reason. Kubernetes is built to run enormous, varied systems, so it breaks everything into small, composable objects you can arrange in almost any shape. That flexibility is real, and at large scale it is exactly what you want. But flexibility and complexity are the same coin. The reason you can arrange it any way is that you have to arrange it every way. There are no defaults doing the obvious thing for you, because Kubernetes refuses to assume what the obvious thing is.
For a big platform team, that tradeoff is worth it. For a solo developer or a small agency shipping a handful of apps, it is a heavy tax. Every object above is something you have to learn, write, wire up correctly, and then hold in your head on day two when a certificate needs renewing or a rollout stalls. None of it is the thing you set out to do, which was: run my app.
Where Burrow comes in
This is the exact gap Burrow closes, and it closes it without taking your infrastructure away from you.
Here is a fair concession first. If all you have is one small app, a hosting platform is genuinely simpler and cheaper today: you push your code, they run it, you never see a single object above. The tradeoff is that they run your app on their platform, on machines you do not control, under limits and pricing they set. Burrow is for the case where you want that same ease but on a cluster you own.
You point your own AI coding agent at your own cluster and say, in plain words, "deploy the greeter app to prod." Under the hood, Burrow does the work this whole post walked through: it creates the Deployment with the right replicas, sets up the ConfigMap and injects your settings, stands up the Ingress, and handles TLS with automatic certificate issuance and renewal so your app comes up reachable at a real URL with the padlock. You did not write a line of YAML or learn a single kubectl verb. Underneath, all of it is still real, standard Kubernetes, so nothing is faked and nothing is proprietary.
The plainest version is one command:
burrow app deploy greeter --image ghcr.io/you/greeter:1.0.0 --env prod
And the ownership stays with you, all of it. You install Burrow onto your own cluster with burrow install. There is no rented brain running your infrastructure from somewhere else, and no lock in: it is open source, Apache-2.0, and self hosted. You hold root. You keep the keys.
Because pointing an agent at production makes anyone nervous, and it should, Burrow puts you in charge of what the agent may do on its own. You, the human operator, set the policy:
burrow guard set --env prod deploy confirm
That makes any production deploy stop and wait for your sign off. The agent proposes the change, and you decide whether it happens. Riskier actions can be set to deny outright, routine ones to allow. The agent does the tedious assembly this post spent two thousand words describing. You keep the final say on anything that touches prod.
The real lesson
Kubernetes is not bad, and it is not magic. It is a genuinely powerful system that expresses your app as a set of small pieces you compose yourself: the Deployment, the ConfigMap, the Ingress, the controller, the certificates, all driven through kubectl and maybe Helm. That composability is its strength. For the smallest teams, it is also its weight, because the versatility that makes it powerful is the same versatility you personally have to understand and maintain.
Burrow's whole reason to exist is to carry that weight for you while leaving the cluster in your hands. Same real Kubernetes underneath, same infrastructure you own, minus the pile of objects you would otherwise be holding in your head. You describe what you want in plain words. The tedious, exacting assembly happens for you, on machines that are yours.