Architecture & Design Patterns
Moving to Kubernetes requires a fundamental shift in architectural thinking. It demands moving from "Pet" servers to "Cattle" compute, and from Monolithic binaries to distributed, decoupled Microservices.
This document covers the Macro (System Architecture) and the Micro (Pod Design Patterns).
1. MICROSERVICES ARCHITECTURE
1.1 The Shift: Monolith vs. Distributed
In Kubernetes, the unit of scale is not the server, but the Pod.
| Feature | Monolith | Microservices on K8s |
|---|---|---|
| Deployment | Single binary / WAR file | Multiple independent container images |
| Scaling | Scale the whole app (inefficient) | Scale only the bottleneck service (e.g., Billing) |
| Resilience | Memory leak crashes the whole OS | Failure is isolated to a specific Pod/ReplicaSet |
| Networking | In-memory function calls (fast) | Network gRPC/HTTP calls (latency & failure modes introduced) |
| Data | Single shared database | Database-per-service (Schema isolation) |
1.2 The "Strangler Fig" Pattern
Objective: Migrate a legacy Monolith to Kubernetes without a "Big Bang" rewrite.
Strategy:
- Identify a bounded context (e.g., the "Search" module).
- Extract it into a new microservice deployed on K8s.
- Proxy traffic using Ingress rules. Specific paths go to the new service; the "catch-all" goes to the legacy monolith.
- Repeat until the monolith is empty.
Implementation (Ingress):
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: strangler-router
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- host: api.mycompany.com
http:
paths:
# 1. New Microservice (Strangled functionality)
- path: /v2/search
pathType: Prefix
backend:
service:
name: search-service
port:
number: 80
# 2. Legacy Monolith (Everything else)
- path: /
pathType: Prefix
backend:
service:
name: legacy-monolith
port:
number: 8080
2. THE 12-FACTOR APP (Kubernetes Implementation)
The 12-Factor App methodology is the baseline requirement for cloud-native applications. Here is the strict Kubernetes implementation mapping:
| Factor | Concept | Kubernetes Primitive / Best Practice |
|---|---|---|
| 1. Codebase | One codebase, many deploys. | Docker Image tags. The same immutable image v1.2.3 travels from Dev → Stage → Prod. |
| 2. Dependencies | Explicitly declare. | Dockerfile. Must contain all libs (pip install, npm install). No reliance on host OS packages. |
| 3. Config | Config in environment. | ConfigMaps & Secrets. Inject via envFrom or Volume Mounts. Never bake config into the image. |
| 4. Backing Services | Treat as attached resources. | Services (ExternalName). A DB connection is just a URL. Changing from local MySQL to AWS RDS is a ConfigMap change only. |
| 5. Build, Release, Run | Strict separation. | CI/CD + Helm. Build (CI creates Image), Release (Helm combines Image + Config), Run (Pod execution). |
| 6. Processes | Stateless. | ReplicaSets. Pods are ephemeral. Any local file write is lost on restart. State belongs in StatefulSets or external DBs. |
| 7. Port Binding | Export services via ports. | Service Object. Apps listen on 0.0.0.0:8080. The K8s Service provides the stable VIP. |
| 8. Concurrency | Scale via process model. | HPA (Horizontal Pod Autoscaler). Scale by adding Pod replicas, not by increasing thread pools inside a massive JVM. |
| 9. Disposability | Fast startup, graceful shutdown. | SIGTERM Handling. Pods must trap SIGTERM, finish in-flight requests, and exit within terminationGracePeriodSeconds. |
| 10. Dev/Prod Parity | Keep environments similar. | Manifests/Charts. Use the same YAML for Dev and Prod. Only the values.yaml (replica counts, resource limits) changes. |
| 11. Logs | Logs as event streams. | Stdout/Stderr. Never log to files. The Container Runtime (CRI) captures stdout, and agents (Fluentd/Promtail) ship them. |
| 12. Admin Processes | One-off admin tasks. | Jobs. Database migrations are run as Kind: Job (Helm Hooks), not inside the app startup logic. |
3. MULTI-CONTAINER POD PATTERNS
While the "One Container Per Pod" rule is the general standard, tight coupling sometimes necessitates multiple containers sharing the same Network Namespace (localhost) and Storage Volumes.
Shared Context
In a multi-container Pod:
- Network: All containers share the same IP.
Container Acan talk toContainer Bvialocalhost:port. - Storage:
emptyDirvolumes can be mounted by both containers to share files. - Process: By default, PID namespaces are isolated. (Unless
shareProcessNamespace: trueis set).
Pattern 1: The Init Container
Purpose: Setup/Bootstrap. Runs sequentially and to completion before the main app starts.
Use Case:
- Waiting for a Database to be reachable.
- Downloading plugins or configuration files.
- Running schema migrations (though Jobs are preferred for heavy migrations).
Production YAML:
apiVersion: v1
kind: Pod
metadata:
name: secure-app-bootstrap
spec:
volumes:
- name: config-vol
emptyDir: {}
initContainers:
# 1. Network Dependency Check
- name: wait-for-db
image: busybox:1.36
command: ['sh', '-c', 'until nc -z -v -w 2 postgres-service 5432; do echo "Waiting for DB..."; sleep 2; done']
resources:
limits:
memory: "32Mi"
cpu: "50m"
# 2. File Setup
- name: download-config
image: my-utils:1.0
command: ["/bin/sh", "-c", "wget -O /config/app.conf http://config-server/profile"]
volumeMounts:
- name: config-vol
mountPath: /config
containers:
- name: main-app
image: my-app:2.0
volumeMounts:
- name: config-vol
mountPath: /app/config # Reads the file downloaded by Init Container
Pattern 2: The Sidecar
Purpose: Extend functionality. Runs parallel to the main container.
Use Case:
- Log Shipping: Main app writes to a shared volume file; Sidecar
tailsthat file and ships to Splunk/Elastic. - Config Watcher: Sidecar watches a mounted ConfigMap; if it changes, it signals the main app to reload (SIGHUP).
- Service Mesh: (Envoy/Istio) Proxies all ingress/egress traffic.
Visual Flow:
[ Main App ] --(writes)--> [ Shared Volume ] --(reads)--> [ Sidecar Agent ] --> External System
Production YAML (Log Shipper Example):
apiVersion: v1
kind: Pod
metadata:
name: app-with-sidecar
spec:
volumes:
- name: log-vol
emptyDir: {}
containers:
# 1. Main Application
- name: app
image: my-java-app:1.0
volumeMounts:
- name: log-vol
mountPath: /var/log/app
command: ["/bin/sh", "-c", "java -jar app.jar > /var/log/app/app.log"]
# 2. Sidecar (Log Agent)
- name: log-shipper
image: fluent/fluent-bit:latest
volumeMounts:
- name: log-vol
mountPath: /var/log/app
readOnly: true
# Configures fluent-bit to tail /var/log/app/app.log and ship it
Pattern 3: The Ambassador
Purpose: Proxy to the outside world. The main app connects to "localhost", and the Ambassador handles the complex routing to external clusters.
Use Case:
- Connecting to a database that has multiple read-replicas. The main app connects to
localhost:5432, the Ambassador decides which read-replica IP to forward to. - Handling OAuth/TLS termination for egress traffic.
Visual Flow:
[ Main App ] --(localhost:port)--> [ Ambassador ] --(Smart Routing)--> [ External Service ]
Pattern 4: The Adapter
Purpose: Standardize output.
Use Case:
- Monitoring: The main app exposes metrics in a proprietary JSON format. The Adapter hits that endpoint, converts it to Prometheus format, and exposes it on
/metrics.
Visual Flow:
[ Prometheus ] --(scrape)--> [ Adapter ] --(fetch/transform)--> [ Main App ]
4. CRITICAL DECISION: Database in K8s?
Architecture often begs the question: Should I put my PostgreSQL/MySQL inside Kubernetes?
| Scenario | Recommendation | Rationale |
|---|---|---|
| Stateless Apps | YES | Use Deployments. Perfect fit. |
| Development DB | YES | Cheap, easy to spin up/down with the app. |
| Production DB (General) | NO | Use Managed Services (AWS RDS, Cloud SQL, Azure SQL). Why? managing HA, backups, point-in-time recovery, and storage replication in K8s is complex and high-risk. |
| Production DB (Specialized) | MAYBE | If using Redis, Elasticsearch, or Cassandra, K8s is decent. Requirement: You MUST use a Kubernetes Operator (e.g., ECK, Postgres Operator) to manage the lifecycle. Do not manage StatefulSets manually for DBs. |
5. Summary Checklist for Architects
- Decoupling: Do not rely on Pod IPs. Use Services.
- Resource Limits: Every container in a multi-container pod creates contention. Define
requestsandlimitsfor all containers, including Sidecars. - Probes:
- Liveness: "Am I dead?" (Restart me).
- Readiness: "Am I busy?" (Don't send traffic).
- Sidecars: Often don't need probes, or they share the fate of the main container.
- Graceful Termination: If the main app dies, the Sidecar might stay running, keeping the Pod in
Terminatingstate. Ensure sidecars handle SIGTERM or use shared process namespace to kill them.