A whole app in one file
Six microservices, twelve objects, one all.yaml. This chapter reads a real file line by line: a blog app with posts, comments, query, moderation, an event bus and a React client, all deployed with one kubectl apply.
The app
Services never call each other directly. Each one sends events to the event bus, which forwards every event to every service. Click a box to see its part of the file:
event-bus · port 4005
Receives an event at POST /events and forwards it to every other service. It needs to know their addresses, so its Deployment gets four env vars: POSTS_URL=http://posts-srv:4000, COMMENTS_URL, QUERY_URL, MODERATION_URL. Its Service has no type, so it is ClusterIP: only other Pods can reach it.
posts · port 4000
Creates posts. The browser calls it directly, so posts-srv is a LoadBalancer. After saving a post it sends a PostCreated event to EVENT_BUS_URL=http://event-bus-srv:4005.
comments · port 4001
Creates comments with status pending and emits CommentCreated. Listens for CommentModerated to update the status. LoadBalancer, because the browser posts comments to it.
query · port 4002
Builds a combined "posts with their comments" view from events, so the browser can load everything in one request. LoadBalancer, because the browser reads from it.
moderation · port 4003
Checks each new comment and emits CommentModerated. Only the event bus talks to it, so its Service is ClusterIP. The browser cannot reach it, and does not need to.
client · port 3000
The React dev server. Your browser loads the app from client-srv, then the JavaScript, now running in your browser, calls posts, comments and query. It never talks to the event bus.
The shape of the file
The file is the same block repeated six times: a Deployment and a Service per microservice, every object separated by ---.
# --- event-bus ---
Deployment event-bus-depl
---
Service event-bus-srv
---
# --- posts ---
Deployment posts-depl
---
Service posts-srv
---
… comments, query, moderation, client …---starts a new YAML document, so one file holds many objects.- Lines starting with
#are comments. Kubernetes ignores them. - Naming convention:
<name>-deplfor the Deployment,<name>-srvfor the Service, labelapp: <name>. kubectl apply -f infra/k8s/all.yamlcreates all 12 objects in one go.
One pair, line by line
Here is the posts pair, correctly indented. Indentation is the structure in YAML: two spaces per level, never tabs. Click the highlighted parts:
apiVersion: apps/v1
kind: Deployment
metadata:
name: posts-depl
spec:
replicas: 1
selector:
matchLabels:
app: posts
template:
metadata:
labels:
app: posts
spec:
containers:
- name: posts
image: micro-posts:latest
imagePullPolicy: Never
ports:
- containerPort: 4000
env:
- name: EVENT_BUS_URL
value: http://event-bus-srv:4005
---
apiVersion: v1
kind: Service
metadata:
name: posts-srv
spec:
type: LoadBalancer
selector:
app: posts
ports:
- name: posts
protocol: TCP
port: 4000
targetPort: 4000
app: posts × 3
The Deployment selector, the Pod labels and the Service selector. All three must match exactly. This is the only thing connecting posts-srv to the posts Pods (chapter 12).
posts-depl
The Deployment's name. It appears in kubectl get deploy and in Pod names: posts-depl-7d9f8c6b5d-4kq2p.
replicas: 1
One Pod per service, fine for learning. Because the event bus keeps events in memory, running 2 event-bus Pods would split that memory, so this app needs changes before it can scale.
:latest + imagePullPolicy: Never
Use the image already on the node; never contact a registry. Without Never, the :latest tag would make Kubernetes try to pull from Docker Hub and fail (chapter 8). See below.
containerPort, port, targetPort
All 4000 here. The app listens on 4000 (containerPort), the Service accepts on 4000 (port) and forwards to 4000 on the Pod (targetPort). Keeping them equal is simple; they only have to agree on targetPort = containerPort.
env
Environment variables for the container (chapter 16), read in Node as process.env.EVENT_BUS_URL. The value uses the event bus Service name as a hostname and its Service port.
posts-srv
The Service name, and therefore the DNS name other Pods use: http://posts-srv:4000. The event bus has exactly this in POSTS_URL.
type: LoadBalancer
Reachable from outside the cluster. On Docker Desktop that means http://localhost:4000 on your laptop. See why.
Port name
Optional, but required once a Service has more than one port. Naming it anyway is a good habit.
How services find each other
Look at the env vars side by side with the Service names. Every URL is http://<Service name>:<Service port>; no IP appears anywhere in the file.
| Env var (in Deployment) | Value | Must match this Service |
|---|---|---|
EVENT_BUS_URL (posts, comments, query, moderation) | http://event-bus-srv:4005 | event-bus-srv, port 4005 |
POSTS_URL (event-bus) | http://posts-srv:4000 | posts-srv, port 4000 |
COMMENTS_URL (event-bus) | http://comments-srv:4001 | comments-srv, port 4001 |
QUERY_URL (event-bus) | http://query-srv:4002 | query-srv, port 4002 |
MODERATION_URL (event-bus) | http://moderation-srv:4003 | moderation-srv, port 4003 |
In Node, the code reads the variable instead of hard-coding localhost:
// posts/index.js
const EVENT_BUS_URL = process.env.EVENT_BUS_URL || 'http://localhost:4005';
await axios.post(`${EVENT_BUS_URL}/events`, { type: 'PostCreated', data: post });
localhost:4005? Inside a Pod, localhost is that Pod only (chapter 6). The event bus is in a different Pod, so posts must go through its Service name. Rename a Service and forget its env var, and that service silently stops receiving events.Follow one comment
Step through what happens when you add a comment in the browser:
Browser ──POST localhost:4001/posts/42/comments──► comments-srv (LoadBalancer) └─► comments Pod :4001The React app, running in your browser, calls the comments Service through its LoadBalancer port on your laptop.
comments Pod ──POST http://event-bus-srv:4005/events──► event-bus Pod { type: "CommentCreated", status: "pending" }comments saves the comment as
pendingand emits an event, usingEVENT_BUS_URL.event-bus ──► http://posts-srv:4000/events ──► http://comments-srv:4001/events ──► http://query-srv:4002/events ──► http://moderation-srv:4003/eventsThe bus forwards the event to everyone, using its four env vars. All of this is Pod-to-Service traffic inside the cluster.
moderation Pod: "does it contain 'orange'?" → approved moderation ──POST http://event-bus-srv:4005/events──► event-bus { type: "CommentModerated", status: "approved" }moderation, reachable only inside the cluster, checks the comment and emits the result.
event-bus ──► comments: update status, emit "CommentUpdated" event-bus ──► query: store the final commentThe status change flows back through the bus.
Browser ──GET localhost:4002/posts──► query-srv ──► query Pod ◄── [{ id: 42, comments: [{ content: "…", status: "approved" }] }]The browser reloads from query, which has everything in one response. Five services cooperated, and none of them knows another's Pod IP.
Why some Services are LoadBalancer
| Service | Type | Who calls it |
|---|---|---|
client-srv | LoadBalancer | Your browser loads the React app |
posts-srv | LoadBalancer | Browser (create post) + event bus |
comments-srv | LoadBalancer | Browser (create comment) + event bus |
query-srv | LoadBalancer | Browser (load everything) + event bus |
event-bus-srv | ClusterIP (default) | Only other Pods |
moderation-srv | ClusterIP (default) | Only the event bus |
The rule: if the browser calls it, it must be reachable from outside the cluster. React code runs on your laptop, not in the client Pod, so it cannot use names like posts-srv; it calls http://localhost:4000. On Docker Desktop, a LoadBalancer Service is published on localhost:<port>, which is why this works locally.
$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
client-srv LoadBalancer 10.104.3.21 localhost 3000:31920/TCP
comments-srv LoadBalancer 10.98.120.7 localhost 4001:30544/TCP
event-bus-srv ClusterIP 10.110.44.190 <none> 4005/TCP
moderation-srv ClusterIP 10.97.8.66 <none> 4003/TCP
posts-srv LoadBalancer 10.96.201.14 localhost 4000:32107/TCP
query-srv LoadBalancer 10.101.77.150 localhost 4002:30381/TCP
imagePullPolicy: Never
Every container uses an image you built locally, such as micro-posts:latest, and sets imagePullPolicy: Never:
Only use the image already on the node. No registry needed, so it works with images you just built on Docker Desktop. If the image is missing, the Pod fails with ErrImageNeverPull.
Pull from a registry every time a container starts. This is the default for :latest or untagged images. micro-posts:latest is not on Docker Hub, so without Never you would get ErrImagePull.
Pull only if the node does not have it. The default for any other tag, such as micro-posts:1.0. Equally good for local images.
:latest. After docker build -t micro-posts:latest ., the running Pod keeps the old image, because nothing in the YAML changed, so kubectl apply does nothing. Restart the Deployment so new Pods start from the new image:$ docker build -t micro-posts:latest ./posts
$ kubectl rollout restart deployment posts-deplWith versioned tags (micro-posts:1.1) you change the YAML instead, and you get rollback history for free (chapter 10).The client's env vars
The React client runs its development server inside the container. Four settings make that work:
| Env var | Why |
|---|---|
HOST=0.0.0.0 | The important one. Listen on all network interfaces. If the dev server listens on localhost only, it accepts connections from inside its own container and nothing else, so client-srv cannot reach it. |
BROWSER=none | Do not try to open a browser window; there is no screen in a container. |
WATCHPACK_POLLING=true | Detect file changes by polling. File-change events often do not cross into containers. |
DISABLE_ESLINT_PLUGIN=true | Skip lint during the dev build: faster start, no lint errors blocking the page. |
Note the quotes: value: "true". Env values must be strings, and unquoted true is a YAML boolean, which Kubernetes rejects.
Apply, check, update
$ kubectl apply -f infra/k8s/all.yaml deployment.apps/event-bus-depl created service/event-bus-srv created deployment.apps/posts-depl created service/posts-srv created deployment.apps/comments-depl created service/comments-srv created deployment.apps/query-depl created service/query-srv created deployment.apps/moderation-depl created service/moderation-srv created deployment.apps/client-depl created service/client-srv createdOne command, 12 objects.
$ kubectl get deploy NAME READY UP-TO-DATE AVAILABLE client-depl 1/1 1 1 comments-depl 1/1 1 1 event-bus-depl 1/1 1 1 moderation-depl 1/1 1 1 posts-depl 1/1 1 1 query-depl 1/1 1 1All six running. Open
http://localhost:3000.$ kubectl logs deployment/event-bus-depl Listening on 4005 Received event PostCreatedkubectl logs deployment/<name>picks a Pod for you; no need to copy the random Pod name.$ kubectl exec -it deployment/posts-depl -- sh /app # wget -qO- http://event-bus-srv:4005/events /app # env | grep EVENT_BUS EVENT_BUS_URL=http://event-bus-srv:4005Check that a service can resolve and reach another by name, and that the env var arrived.
$ docker build -t micro-posts:latest ./posts $ kubectl rollout restart deployment posts-depl deployment.apps/posts-depl restartedAfter changing code: rebuild, then restart that one Deployment.
$ kubectl apply -f infra/k8s/all.yaml deployment.apps/event-bus-depl unchanged service/event-bus-srv unchanged deployment.apps/posts-depl configured ...After editing the file: apply again. Only what changed is updated.
$ kubectl delete -f infra/k8s/all.yamlTear everything down: every object in the file.
One file or many?
infra/k8s/
└── all.yaml
Good for: small apps and learning. Everything in one place; one command to apply or delete.
Gets painful when: the file passes a few hundred lines, several people edit it at once (merge conflicts), or you want to redeploy just one service.
infra/k8s/
├── event-bus-depl.yaml # Deployment + Service
├── posts-depl.yaml
├── comments-depl.yaml
├── query-depl.yaml
├── moderation-depl.yaml
└── client-depl.yaml
$ kubectl apply -f infra/k8s/ # the whole folder
$ kubectl apply -f infra/k8s/posts-depl.yaml # just one service
Good for: real projects. Each file holds one service's Deployment and Service, which belong together. Applying a folder still deploys everything in one command.
What to improve
This file is a great learning setup. Before production, you would change a few things, each covered in a later chapter:
- One entry point: replace the four LoadBalancers with one Ingress (chapter 27) that routes
/posts,/comments,/queryand/to the right Service. The browser then talks to one address. - Versioned image tags pushed to a registry, instead of
:latest+Never. - Resource requests and limits, so one service cannot starve the others (chapter 22).
- Readiness and liveness probes, so traffic only reaches Pods that really work (chapter 13).
- Config in ConfigMaps, so the URLs live in one place instead of being repeated in four Deployments (chapter 18).
- A production client build served by nginx, instead of the React dev server.
The full file
Show infra/k8s/all.yaml (correctly indented)
# All microservices in one file (separated by ---)
# Apply: kubectl apply -f infra/k8s/all.yaml
# --- event-bus ---
apiVersion: apps/v1
kind: Deployment
metadata:
name: event-bus-depl
spec:
replicas: 1
selector:
matchLabels:
app: event-bus
template:
metadata:
labels:
app: event-bus
spec:
containers:
- name: event-bus
image: micro-event-bus:latest
imagePullPolicy: Never
ports:
- containerPort: 4005
env:
- name: POSTS_URL
value: http://posts-srv:4000
- name: COMMENTS_URL
value: http://comments-srv:4001
- name: QUERY_URL
value: http://query-srv:4002
- name: MODERATION_URL
value: http://moderation-srv:4003
---
apiVersion: v1
kind: Service
metadata:
name: event-bus-srv
spec:
selector:
app: event-bus
ports:
- name: event-bus
protocol: TCP
port: 4005
targetPort: 4005
---
# --- posts ---
apiVersion: apps/v1
kind: Deployment
metadata:
name: posts-depl
spec:
replicas: 1
selector:
matchLabels:
app: posts
template:
metadata:
labels:
app: posts
spec:
containers:
- name: posts
image: micro-posts:latest
imagePullPolicy: Never
ports:
- containerPort: 4000
env:
- name: EVENT_BUS_URL
value: http://event-bus-srv:4005
---
apiVersion: v1
kind: Service
metadata:
name: posts-srv
spec:
type: LoadBalancer
selector:
app: posts
ports:
- name: posts
protocol: TCP
port: 4000
targetPort: 4000
---
# --- comments ---
apiVersion: apps/v1
kind: Deployment
metadata:
name: comments-depl
spec:
replicas: 1
selector:
matchLabels:
app: comments
template:
metadata:
labels:
app: comments
spec:
containers:
- name: comments
image: micro-comments:latest
imagePullPolicy: Never
ports:
- containerPort: 4001
env:
- name: EVENT_BUS_URL
value: http://event-bus-srv:4005
---
apiVersion: v1
kind: Service
metadata:
name: comments-srv
spec:
type: LoadBalancer
selector:
app: comments
ports:
- name: comments
protocol: TCP
port: 4001
targetPort: 4001
---
# --- query ---
apiVersion: apps/v1
kind: Deployment
metadata:
name: query-depl
spec:
replicas: 1
selector:
matchLabels:
app: query
template:
metadata:
labels:
app: query
spec:
containers:
- name: query
image: micro-query:latest
imagePullPolicy: Never
ports:
- containerPort: 4002
env:
- name: EVENT_BUS_URL
value: http://event-bus-srv:4005
---
apiVersion: v1
kind: Service
metadata:
name: query-srv
spec:
type: LoadBalancer
selector:
app: query
ports:
- name: query
protocol: TCP
port: 4002
targetPort: 4002
---
# --- moderation ---
apiVersion: apps/v1
kind: Deployment
metadata:
name: moderation-depl
spec:
replicas: 1
selector:
matchLabels:
app: moderation
template:
metadata:
labels:
app: moderation
spec:
containers:
- name: moderation
image: micro-moderation:latest
imagePullPolicy: Never
ports:
- containerPort: 4003
env:
- name: EVENT_BUS_URL
value: http://event-bus-srv:4005
---
apiVersion: v1
kind: Service
metadata:
name: moderation-srv
spec:
selector:
app: moderation
ports:
- name: moderation
protocol: TCP
port: 4003
targetPort: 4003
---
# --- client ---
apiVersion: apps/v1
kind: Deployment
metadata:
name: client-depl
spec:
replicas: 1
selector:
matchLabels:
app: client
template:
metadata:
labels:
app: client
spec:
containers:
- name: client
image: micro-client:latest
imagePullPolicy: Never
ports:
- containerPort: 3000
env:
- name: BROWSER
value: "none"
- name: DISABLE_ESLINT_PLUGIN
value: "true"
- name: WATCHPACK_POLLING
value: "true"
- name: HOST
value: "0.0.0.0"
---
apiVersion: v1
kind: Service
metadata:
name: client-srv
spec:
type: LoadBalancer
selector:
app: client
ports:
- name: client
protocol: TCP
port: 3000
targetPort: 3000
Quick check
event-bus-srv to bus-srv and apply. What breaks, and what else must you change?Show answer
posts, comments, query and moderation can no longer send events: event-bus-srv no longer resolves. Change EVENT_BUS_URL in all four Deployments to http://bus-srv:4005 and apply again. (A ConfigMap would make that one edit.)
http://moderation-srv:4003 but your browser cannot?Show answer
moderation-srv is ClusterIP: its name and IP exist only inside the cluster's network and DNS. The browser runs on your laptop, outside the cluster.
Multi-service Kubernetes YAML: common questions
How do microservices find each other in Kubernetes?
By Service name. Each app gets the other Service URL, such as http://event-bus-srv:4005, usually through an environment variable. Cluster DNS resolves the name.
Can I put many Kubernetes objects in one YAML file?
Yes. Separate them with --- and kubectl apply -f creates all of them. Larger projects often use one file per service in a folder instead.