Chapter 17 of 47 · Putting it together

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:

The blog app in the clusterThe browser on your laptop reaches client-srv, posts-srv, comments-srv and query-srv, which are LoadBalancer Services. posts, comments, query and moderation all exchange events through event-bus-srv. event-bus-srv and moderation-srv are ClusterIP Services, reachable only inside the cluster. Browseryour laptop cluster client-srvLoadBalancer · 3000 posts-srvLoadBalancer · 4000 comments-srvLoadBalancer · 4001 query-srvLoadBalancer · 4002 events event-bus-srvClusterIP · 4005 moderation-srvClusterIP · 4003 Every box is a Service in front ofa Deployment with 1 Pod

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.

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>-depl for the Deployment, <name>-srv for the Service, label app: <name>.
  • kubectl apply -f infra/k8s/all.yaml creates 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).

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)ValueMust match this Service
EVENT_BUS_URL (posts, comments, query, moderation)http://event-bus-srv:4005event-bus-srv, port 4005
POSTS_URL (event-bus)http://posts-srv:4000posts-srv, port 4000
COMMENTS_URL (event-bus)http://comments-srv:4001comments-srv, port 4001
QUERY_URL (event-bus)http://query-srv:4002query-srv, port 4002
MODERATION_URL (event-bus)http://moderation-srv:4003moderation-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 });
Why not 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:

  1. Browser ──POST localhost:4001/posts/42/comments──► comments-srv (LoadBalancer)
                                                       └─► comments Pod :4001

    The React app, running in your browser, calls the comments Service through its LoadBalancer port on your laptop.

  2. comments Pod ──POST http://event-bus-srv:4005/events──► event-bus Pod
                 { type: "CommentCreated", status: "pending" }

    comments saves the comment as pending and emits an event, using EVENT_BUS_URL.

  3. event-bus ──► http://posts-srv:4000/events
              ──► http://comments-srv:4001/events
              ──► http://query-srv:4002/events
              ──► http://moderation-srv:4003/events

    The bus forwards the event to everyone, using its four env vars. All of this is Pod-to-Service traffic inside the cluster.

  4. 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.

  5. event-bus ──► comments: update status, emit "CommentUpdated"
    event-bus ──► query:    store the final comment

    The status change flows back through the bus.

  6. 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

ServiceTypeWho calls it
client-srvLoadBalancerYour browser loads the React app
posts-srvLoadBalancerBrowser (create post) + event bus
comments-srvLoadBalancerBrowser (create comment) + event bus
query-srvLoadBalancerBrowser (load everything) + event bus
event-bus-srvClusterIP (default)Only other Pods
moderation-srvClusterIP (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

On minikube, LoadBalancer Services stay <pending> until you run minikube tunnel. On kind you need an extra load-balancer add-on, or use kubectl port-forward.

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.

The catch with :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-depl
With 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 varWhy
HOST=0.0.0.0The 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=noneDo not try to open a browser window; there is no screen in a container.
WATCHPACK_POLLING=trueDetect file changes by polling. File-change events often do not cross into containers.
DISABLE_ESLINT_PLUGIN=trueSkip 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

  1. $ 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 created

    One command, 12 objects.

  2. $ 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            1

    All six running. Open http://localhost:3000.

  3. $ kubectl logs deployment/event-bus-depl
    Listening on 4005
    Received event PostCreated

    kubectl logs deployment/<name> picks a Pod for you; no need to copy the random Pod name.

  4. $ 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:4005

    Check that a service can resolve and reach another by name, and that the env var arrived.

  5. $ docker build -t micro-posts:latest ./posts
    $ kubectl rollout restart deployment posts-depl
    deployment.apps/posts-depl restarted

    After changing code: rebuild, then restart that one Deployment.

  6. $ 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.

  7. $ kubectl delete -f infra/k8s/all.yaml

    Tear 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.

What to improve

This file is a great learning setup. Before production, you would change a few things, each covered in a later chapter:

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

You rename 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.)

Why can the event bus call 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.