Stage 7 · Master
Containerization and Deployment
Deployments, Services, and Ingress in a Microservice Platform
What each Kubernetes primitive does, why a production backend needs all three, and the manifest design Meridian should introduce after its current deploy/k8s placeholder.
Three Primitives, Three Jobs
A Deployment answers the question 'how should this process run over time?' It manages replicas, rollouts, and pod replacement. A Service answers 'how do other workloads find the current healthy pods?' It provides a stable virtual IP and DNS name even though pod IPs change constantly. An Ingress answers 'how does external HTTP traffic enter the cluster?' It terminates at the edge and routes requests to an internal Service. Production systems need all three because process lifecycle, internal discovery, and external exposure are different problems.
| Primitive | Primary responsibility | What breaks without it |
|---|---|---|
| Deployment | Keep a desired number of pods running and roll them forward safely | Pods become ad hoc processes with no rollout strategy or self-healing behavior. |
| Service | Give clients a stable in-cluster name and send traffic only to healthy pods | Every caller would need direct pod IP knowledge, which disappears on restart or reschedule. |
| Ingress | Expose selected HTTP routes from outside the cluster | Every service would need its own public LoadBalancer or direct node exposure, which increases cost and attack surface. |
Current Repository State
The meridian repository currently contains deploy/k8s/.gitkeep and no manifests. That emptiness is the correct grounding point for this lesson. The chapter therefore specifies the Kubernetes design that matches the existing application structure: apps/gateway, apps/identity-service, apps/community-service, and apps/billing-service as separate release units, with gateway as the only public edge.
The Target Network Shape
The repository and the cluster should tell the same story. Meridian already has four service modules and one shared platform library. Kubernetes should preserve those boundaries by creating one Deployment and one ClusterIP Service per running service. The gateway then uses the downstream base URLs it already models in apps/gateway/internal/config/config.go, but those URLs change from local http://localhost ports to in-cluster DNS names such as http://identity-service:8081.
A gateway should call a stable Service name, not a pod IP. During a rollout the pod set changes; the Service abstraction is what keeps discovery constant while the endpoints underneath move.
Planned Manifest Layout
deploy/k8s/base/kustomization.yamlcreateResponsibility: Defines the base manifest set for the platform.
Why now: A kustomization file gives one reviewable entrypoint for applying the whole stack consistently.
deploy/k8s/base/namespace.yamlcreateResponsibility: Creates a dedicated namespace such as meridian for the platform workloads.
Why now: Namespace boundaries simplify DNS, RBAC, rollout commands, and operational ownership.
deploy/k8s/base/gateway/deployment.yamlcreateResponsibility: Runs gateway replicas with health probes and rolling-update settings.
Why now: gateway is the only public entrypoint and needs explicit availability behavior.
deploy/k8s/base/gateway/service.yamlcreateResponsibility: Creates the stable ClusterIP that the Ingress will target.
Why now: Ingress should route to a Service, never directly to pods.
deploy/k8s/base/identity-service/deployment.yamlcreateResponsibility: Runs identity-service pods on their own rollout lifecycle.
Why now: Identity concerns should scale and deploy independently from community and billing behavior.
deploy/k8s/base/identity-service/service.yamlcreateResponsibility: Publishes identity-service inside the cluster at a stable DNS name.
Why now: gateway already models IDENTITY_SERVICE_URL; Kubernetes needs an internal target for it.
deploy/k8s/base/community-service/deployment.yamlcreateResponsibility: Runs community-service independently.
Why now: Community traffic and failure modes are distinct from billing and identity concerns.
deploy/k8s/base/community-service/service.yamlcreateResponsibility: Publishes community-service inside the cluster.
Why now: Internal callers need stable discovery for a service whose pods will roll and reschedule.
deploy/k8s/base/billing-service/deployment.yamlcreateResponsibility: Runs billing-service independently from the request path of the other services.
Why now: Financial workloads often evolve different scaling and rollout requirements.
deploy/k8s/base/billing-service/service.yamlcreateResponsibility: Publishes billing-service inside the cluster.
Why now: gateway needs a stable internal endpoint for BILLING_SERVICE_URL.
deploy/k8s/base/ingress.yamlcreateResponsibility: Maps the public hostname to the gateway Service only.
Why now: The browser must never call downstream services directly; README.md and docs/architecture/services.md already establish that rule.
Gateway-Only Public Exposure
Meridian's README and service architecture document already define one public entrypoint: apps/gateway. Kubernetes should enforce that architecture instead of weakening it. identity-service, community-service, and billing-service should be ClusterIP-only services, reachable from inside the cluster but not directly from the internet. That preserves a single place for authentication, request IDs, tenant resolution, rate limiting, and edge policy.
| Exposure model | Why it is tempting | Why Meridian should reject or accept it |
|---|---|---|
| Public Ingress per service | Easy to test each service directly | Reject. It multiplies the external attack surface and bypasses the gateway's responsibility as the only public edge. |
| Single Ingress to gateway | Slightly more routing work inside the gateway | Accept. It keeps security and tenancy decisions concentrated in one place. |
| No Ingress, only NodePort or host networking | Looks simple in a lab cluster | Reject for production. It gives weak traffic management and poor portability. |
Baseline YAML Specification
apiVersion: apps/v1
kind: Deployment
metadata:
name: gateway
namespace: meridian
spec:
replicas: 2
selector:
matchLabels:
app: gateway
template:
metadata:
labels:
app: gateway
spec:
containers:
- name: gateway
image: ghcr.io/thesyscoder/meridian/gateway:sha-PLACEHOLDER
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /healthz
port: 8080
livenessProbe:
httpGet:
path: /healthz
port: 8080
---
apiVersion: v1
kind: Service
metadata:
name: gateway
namespace: meridian
spec:
selector:
app: gateway
ports:
- name: http
port: 80
targetPort: 8080
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: meridian
namespace: meridian
spec:
ingressClassName: nginx
rules:
- host: api.meridian.example
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: gateway
port:
number: 80The probe path matches the real repository's current /healthz endpoint. Later lessons refine that health contract for graceful rollouts, but this is the correct minimal starting point.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.