Monolith to Kubernetes Migration: A Noxet Engineering Case Study
In this case study, Noxet infrastructure engineers share the technical methodology behind transitioning a high-traffic monolithic backend from bare-metal servers into Kubernetes orchestration. The project addressed classic enterprise challenges: lengthy manual deployment cycles, performance bottlenecks under traffic surges, and severe outage risks during service updates.
Baseline State and Architectural Bottlenecks
Prior to our modernization effort, the application ran as a monolithic service across two dedicated bare-metal servers managed via systemd and Nginx:
- In-Memory Stateful Dependencies: User sessions were stored in process memory, forcing load balancers to rely on sticky sessions (IP/cookie hashing), resulting in uneven traffic distribution.
- Local Disk File I/O: File uploads were saved directly to host storage, requiring fragile network file shares (NFS) to keep the nodes synchronized.
- Manual Deployments: Releases took upwards of 3 hours, required scheduled late-night maintenance windows, and frequently incurred downtime due to service restart lag.
- Absence of Elasticity: Traffic surges during marketing campaigns pushed CPU and memory usage to 100%, causing cascading 502/504 Bad Gateway errors.
Phase 1. 12-Factor Application Refactoring
Containerizing a monolith “as is” without adapting its internal architecture merely relocates technical debt to a new abstraction layer. We began with targeted architectural refactoring:
- Externalizing Session State (Stateless): Session data was migrated into an in-memory Redis Sentinel cluster. This allowed horizontally scaling application instances without interrupting active user authentication.
- Object Storage Adoption: Local disk file operations were replaced with an S3 API interface connected to an on-premise MinIO cluster.
- Graceful Shutdown: The service was refactored to intercept
SIGTERMsignals. Upon receipt, the pod ceases accepting new ingress connections, completes active transactions within a 15-second grace period, and exits cleanly. - Structured JSON Telemetry: Log output was redirected to
stdout/stderrin structured JSON format, enabling unified log streaming via Vector into VictoriaLogs / OpenSearch.
Phase 2. Container Build Optimization
Security compliance and container image size directly govern pod scheduling latency during scale-out events. We designed an optimized multi-stage Dockerfile:
# --- Build Stage (Builder) ---
FROM golang:1.22-alpine AS builder
WORKDIR /app
RUN apk add --no-cache git ca-certificates
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o /bin/server ./cmd/server
# --- Minimal Runtime Image ---
FROM gcr.io/distroless/static-debian12:nonroot
WORKDIR /app
COPY --from=builder /bin/server /app/server
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/app/server"]
Build Optimization Outcomes:
- Final image size plummeted from 1.4 GB to 28 MB.
- Complete removal of package managers, debug tools, and shell interpreters (
sh,bash,curl) eliminated common lateral movement attack vectors. - Process runs under an unprivileged user context (
nonroot:nonroot).
Phase 3. Kubernetes Deployment Architecture
To guarantee zero-downtime rolling updates and automated scaling, deployment manifests were standardized via Helm:
apiVersion: apps/v1
kind: Deployment
metadata:
name: core-api
labels:
app: core-api
spec:
replicas: 4
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 25%
maxUnavailable: 0
selector:
matchLabels:
app: core-api
template:
metadata:
labels:
app: core-api
spec:
terminationGracePeriodSeconds: 30
containers:
- name: app
image: registry.noxet.ru/project/core-api:v2.4.1
resources:
requests:
cpu: "300m"
memory: "512Mi"
limits:
cpu: "1500m"
memory: "1536Mi"
readinessProbe:
httpGet:
path: /healthz/ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 2
livenessProbe:
httpGet:
path: /healthz/live
port: 8080
initialDelaySeconds: 15
periodSeconds: 10
High-Availability Safeguards:
maxUnavailable: 0: Pre-existing pods remain operational until newly created pods pass theirreadinessProbeand successfully join the endpoint slice.- Horizontal Pod Autoscaling (HPA): Automatically scales the deployment between 4 and 24 replicas based on 70% CPU target utilization or custom RPS metrics.
- PodDisruptionBudget (PDB): Enforces a minimum availability threshold of 75% operational pods during routine node maintenance and cluster upgrades.
Phase 4. Automated GitLab CI/CD Pipeline
Build, testing, and deployment workflows are automated via GitLab CI leveraging Kaniko (privileged socket-free container compilation):
stages:
- test
- build
- deploy
run_tests:
stage: test
image: golang:1.22-alpine
script:
- go test -v -race ./...
build_image:
stage: build
image:
name: gcr.io/kaniko-project/executor:v1.20.0-debug
entrypoint: [""]
script:
- /kaniko/executor
--context "${CI_PROJECT_DIR}"
--dockerfile "${CI_PROJECT_DIR}/Dockerfile"
--destination "${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA}"
--destination "${CI_REGISTRY_IMAGE}:latest"
--cache=true
only:
- main
deploy_production:
stage: deploy
image: alpine/helm:3.14.0
script:
- helm upgrade --install core-api ./helm \
--namespace production \
--set image.tag=${CI_COMMIT_SHORT_SHA} \
--wait --timeout 5m
environment:
name: production
only:
- main
Measurable Infrastructure Outcomes
- Cycle Time Acceleration: Release pipeline duration dropped from 2–3 hours of manual intervention down to 6 minutes for the complete automated workflow.
- Zero Downtime Deployments: Leveraging
RollingUpdate,readinessProbe, andpreStophooks eliminated dropped connections during application releases. - Automated Elasticity: During promotional traffic surges, the cluster automatically expands replica pools within 90 seconds, maintaining p99 API latency under 45 ms.
- Enhanced Security Posture: Enforced network boundaries via Kubernetes NetworkPolicies and read-only container root filesystems.
Planning a migration to Kubernetes or seeking zero-downtime CI/CD architecture? Consult with Noxet infrastructure engineers.