Tools: Strangler Fig on IBM Kubernetes: Modernizing a Monolith Without Breaking Production

Tools: Strangler Fig on IBM Kubernetes: Modernizing a Monolith Without Breaking Production

Source: Dev.to

Why the Strangler Fig Pattern Still Works ## What You Will Build ## Prerequisites ## Step 1: Log in to IBM Cloud and Connect to Kubernetes ## Step 2: Create a Dedicated Namespace ## Step 3: Containerize the Existing Monolith ## Step 4: Push the Image to IBM Cloud Container Registry ## Step 5: Deploy the Monolith to Kubernetes ## Step 6: Put the Monolith Behind Ingress ## Step 7: Pick the First “Edge” Capability to Extract ## Step 8: Build the New Edge Service (auth-service) ## Step 9: Deploy the New Service to Kubernetes ## Step 10: Strangle Traffic Using Ingress Routing ## Step 11: Rollback Strategy ## Step 12: Repeat the Pattern Safely ## What You Achieved ## Final Thoughts Most enterprise monoliths don’t fail because of bad code. They fail because changing them safely becomes too risky. A full rewrite to microservices sounds attractive, but in practice it often leads to: The Strangler Fig pattern offers a safer alternative: modernize incrementally while keeping the system running. In this article, I walk through a step-by-step, production-safe approach to applying the Strangler Fig pattern using IBM Cloud Kubernetes Service (IKS), including real commands and manifests you can run. By the end of this guide, you will: Keep modernization isolated and easy to clean up later. The goal here is no behavior change, just package the monolith. Example Dockerfile (Node.js monolith) Add minimal health endpoints (if you don’t already have them) Log in to the registry and create a namespace (one-time): Tag and push your image: 5.1 Deployment manifest (deployment.yaml) Apply and confirm rollout: 5.2 Service manifest (service.yaml) Ingress becomes your routing control plane for strangling. At this point: 100% traffic still goes to the monolith. Start with something: low risk clear boundaries minimal writes /api/auth/* /api/reporting/* read-only catalog endpoints For this walkthrough, we’ll extract: Minimal example endpoint: Dockerfile for the new service: 9.1 Deployment (auth-deploy.yaml) 9.2 Service (auth-svc.yaml) Update ingress.yaml so /api/auth/* routes to the new service, and everything else stays on the monolith: Keep rollback boring and fast. Option A: Route back to monolith Edit Ingress and remove the /api/auth path (or point it to monolith-svc), then re-apply: Option B: Undo the deployment rollout Once the first extracted capability is stable: Choose the next bounded domain Build it as a separate service Route it with Ingress Keep rollback available at every step the monolith shrinks risk decreases modernization becomes routine rather than a “big migration” No downtime No big rewrite Production-safe modernization Kubernetes as an enabler, not a forcing function The Strangler Fig pattern works because it respects reality. You don’t modernize by deleting the past. You modernize by outgrowing it safely. If you’re sitting on a monolith today, this approach lets you move forward without breaking what already works. Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse COMMAND_BLOCK: ibmcloud login -a https://cloud.ibm.com ibmcloud target -r <REGION> -g <RESOURCE_GROUP> Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: ibmcloud login -a https://cloud.ibm.com ibmcloud target -r <REGION> -g <RESOURCE_GROUP> COMMAND_BLOCK: ibmcloud login -a https://cloud.ibm.com ibmcloud target -r <REGION> -g <RESOURCE_GROUP> COMMAND_BLOCK: kubectl create namespace monolith-demo kubectl config set-context --current --namespace=monolith-demo kubectl get ns Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: kubectl create namespace monolith-demo kubectl config set-context --current --namespace=monolith-demo kubectl get ns COMMAND_BLOCK: kubectl create namespace monolith-demo kubectl config set-context --current --namespace=monolith-demo kubectl get ns CODE_BLOCK: FROM node:20-alpine AS build WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . FROM node:20-alpine WORKDIR /app COPY --from=build /app /app EXPOSE 8080 CMD ["npm","start"] Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: FROM node:20-alpine AS build WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . FROM node:20-alpine WORKDIR /app COPY --from=build /app /app EXPOSE 8080 CMD ["npm","start"] CODE_BLOCK: FROM node:20-alpine AS build WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . FROM node:20-alpine WORKDIR /app COPY --from=build /app /app EXPOSE 8080 CMD ["npm","start"] COMMAND_BLOCK: // Example endpoints app.get("/health", (req, res) => res.status(200).send("ok")); app.get("/ready", (req, res) => res.status(200).send("ready")); Build the image: docker build -t monolith:1.0.0 . Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: // Example endpoints app.get("/health", (req, res) => res.status(200).send("ok")); app.get("/ready", (req, res) => res.status(200).send("ready")); Build the image: docker build -t monolith:1.0.0 . COMMAND_BLOCK: // Example endpoints app.get("/health", (req, res) => res.status(200).send("ok")); app.get("/ready", (req, res) => res.status(200).send("ready")); Build the image: docker build -t monolith:1.0.0 . CODE_BLOCK: ibmcloud cr login ibmcloud cr namespace-add <REGISTRY_NAMESPACE> Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: ibmcloud cr login ibmcloud cr namespace-add <REGISTRY_NAMESPACE> CODE_BLOCK: ibmcloud cr login ibmcloud cr namespace-add <REGISTRY_NAMESPACE> COMMAND_BLOCK: docker tag monolith:1.0.0 <REGISTRY>/<REGISTRY_NAMESPACE>/monolith:1.0.0 docker push <REGISTRY>/<REGISTRY_NAMESPACE>/monolith:1.0.0 Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: docker tag monolith:1.0.0 <REGISTRY>/<REGISTRY_NAMESPACE>/monolith:1.0.0 docker push <REGISTRY>/<REGISTRY_NAMESPACE>/monolith:1.0.0 COMMAND_BLOCK: docker tag monolith:1.0.0 <REGISTRY>/<REGISTRY_NAMESPACE>/monolith:1.0.0 docker push <REGISTRY>/<REGISTRY_NAMESPACE>/monolith:1.0.0 CODE_BLOCK: ibmcloud cr images | grep monolith Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: ibmcloud cr images | grep monolith CODE_BLOCK: ibmcloud cr images | grep monolith CODE_BLOCK: apiVersion: apps/v1 kind: Deployment metadata: name: monolith spec: replicas: 2 selector: matchLabels: app: monolith template: metadata: labels: app: monolith spec: containers: - name: monolith image: <REGISTRY>/<REGISTRY_NAMESPACE>/monolith:1.0.0 ports: - containerPort: 8080 readinessProbe: httpGet: path: /ready port: 8080 initialDelaySeconds: 5 periodSeconds: 5 livenessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 15 periodSeconds: 10 Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: apiVersion: apps/v1 kind: Deployment metadata: name: monolith spec: replicas: 2 selector: matchLabels: app: monolith template: metadata: labels: app: monolith spec: containers: - name: monolith image: <REGISTRY>/<REGISTRY_NAMESPACE>/monolith:1.0.0 ports: - containerPort: 8080 readinessProbe: httpGet: path: /ready port: 8080 initialDelaySeconds: 5 periodSeconds: 5 livenessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 15 periodSeconds: 10 CODE_BLOCK: apiVersion: apps/v1 kind: Deployment metadata: name: monolith spec: replicas: 2 selector: matchLabels: app: monolith template: metadata: labels: app: monolith spec: containers: - name: monolith image: <REGISTRY>/<REGISTRY_NAMESPACE>/monolith:1.0.0 ports: - containerPort: 8080 readinessProbe: httpGet: path: /ready port: 8080 initialDelaySeconds: 5 periodSeconds: 5 livenessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 15 periodSeconds: 10 COMMAND_BLOCK: kubectl apply -f deployment.yaml kubectl rollout status deploy/monolith kubectl get pods -l app=monolith Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: kubectl apply -f deployment.yaml kubectl rollout status deploy/monolith kubectl get pods -l app=monolith COMMAND_BLOCK: kubectl apply -f deployment.yaml kubectl rollout status deploy/monolith kubectl get pods -l app=monolith CODE_BLOCK: apiVersion: v1 kind: Service metadata: name: monolith-svc spec: selector: app: monolith ports: - name: http port: 80 targetPort: 8080 type: ClusterIP Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: apiVersion: v1 kind: Service metadata: name: monolith-svc spec: selector: app: monolith ports: - name: http port: 80 targetPort: 8080 type: ClusterIP CODE_BLOCK: apiVersion: v1 kind: Service metadata: name: monolith-svc spec: selector: app: monolith ports: - name: http port: 80 targetPort: 8080 type: ClusterIP COMMAND_BLOCK: kubectl apply -f service.yaml kubectl get svc monolith-svc Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: kubectl apply -f service.yaml kubectl get svc monolith-svc COMMAND_BLOCK: kubectl apply -f service.yaml kubectl get svc monolith-svc COMMAND_BLOCK: kubectl port-forward svc/monolith-svc 8080:80 curl -i http://localhost:8080/health Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: kubectl port-forward svc/monolith-svc 8080:80 curl -i http://localhost:8080/health COMMAND_BLOCK: kubectl port-forward svc/monolith-svc 8080:80 curl -i http://localhost:8080/health CODE_BLOCK: apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: app-ingress spec: rules: - host: <APP_DOMAIN> http: paths: - path: / pathType: Prefix backend: service: name: monolith-svc port: number: 80 Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: app-ingress spec: rules: - host: <APP_DOMAIN> http: paths: - path: / pathType: Prefix backend: service: name: monolith-svc port: number: 80 CODE_BLOCK: apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: app-ingress spec: rules: - host: <APP_DOMAIN> http: paths: - path: / pathType: Prefix backend: service: name: monolith-svc port: number: 80 COMMAND_BLOCK: kubectl apply -f ingress.yaml kubectl get ingress app-ingress -o wide Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: kubectl apply -f ingress.yaml kubectl get ingress app-ingress -o wide COMMAND_BLOCK: kubectl apply -f ingress.yaml kubectl get ingress app-ingress -o wide COMMAND_BLOCK: app.get("/health", (req, res) => res.status(200).send("ok")); app.get("/ready", (req, res) => res.status(200).send("ready")); app.get("/api/auth/ping", (req, res) => { res.json({ service: "auth-service", status: "pong" }); }); Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: app.get("/health", (req, res) => res.status(200).send("ok")); app.get("/ready", (req, res) => res.status(200).send("ready")); app.get("/api/auth/ping", (req, res) => { res.json({ service: "auth-service", status: "pong" }); }); COMMAND_BLOCK: app.get("/health", (req, res) => res.status(200).send("ok")); app.get("/ready", (req, res) => res.status(200).send("ready")); app.get("/api/auth/ping", (req, res) => { res.json({ service: "auth-service", status: "pong" }); }); CODE_BLOCK: FROM node:20-alpine WORKDIR /app COPY . . EXPOSE 8081 CMD ["node","server.js"] Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: FROM node:20-alpine WORKDIR /app COPY . . EXPOSE 8081 CMD ["node","server.js"] CODE_BLOCK: FROM node:20-alpine WORKDIR /app COPY . . EXPOSE 8081 CMD ["node","server.js"] COMMAND_BLOCK: docker build -t auth-service:1.0.0 . docker tag auth-service:1.0.0 <REGISTRY>/<REGISTRY_NAMESPACE>/auth-service:1.0.0 docker push <REGISTRY>/<REGISTRY_NAMESPACE>/auth-service:1.0.0 Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: docker build -t auth-service:1.0.0 . docker tag auth-service:1.0.0 <REGISTRY>/<REGISTRY_NAMESPACE>/auth-service:1.0.0 docker push <REGISTRY>/<REGISTRY_NAMESPACE>/auth-service:1.0.0 COMMAND_BLOCK: docker build -t auth-service:1.0.0 . docker tag auth-service:1.0.0 <REGISTRY>/<REGISTRY_NAMESPACE>/auth-service:1.0.0 docker push <REGISTRY>/<REGISTRY_NAMESPACE>/auth-service:1.0.0 CODE_BLOCK: apiVersion: apps/v1 kind: Deployment metadata: name: auth-service spec: replicas: 2 selector: matchLabels: app: auth-service template: metadata: labels: app: auth-service spec: containers: - name: auth-service image: <REGISTRY>/<REGISTRY_NAMESPACE>/auth-service:1.0.0 ports: - containerPort: 8081 readinessProbe: httpGet: path: /ready port: 8081 initialDelaySeconds: 5 periodSeconds: 5 livenessProbe: httpGet: path: /health port: 8081 initialDelaySeconds: 15 periodSeconds: 10 Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: apiVersion: apps/v1 kind: Deployment metadata: name: auth-service spec: replicas: 2 selector: matchLabels: app: auth-service template: metadata: labels: app: auth-service spec: containers: - name: auth-service image: <REGISTRY>/<REGISTRY_NAMESPACE>/auth-service:1.0.0 ports: - containerPort: 8081 readinessProbe: httpGet: path: /ready port: 8081 initialDelaySeconds: 5 periodSeconds: 5 livenessProbe: httpGet: path: /health port: 8081 initialDelaySeconds: 15 periodSeconds: 10 CODE_BLOCK: apiVersion: apps/v1 kind: Deployment metadata: name: auth-service spec: replicas: 2 selector: matchLabels: app: auth-service template: metadata: labels: app: auth-service spec: containers: - name: auth-service image: <REGISTRY>/<REGISTRY_NAMESPACE>/auth-service:1.0.0 ports: - containerPort: 8081 readinessProbe: httpGet: path: /ready port: 8081 initialDelaySeconds: 5 periodSeconds: 5 livenessProbe: httpGet: path: /health port: 8081 initialDelaySeconds: 15 periodSeconds: 10 COMMAND_BLOCK: kubectl apply -f auth-deploy.yaml kubectl rollout status deploy/auth-service kubectl get pods -l app=auth-service Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: kubectl apply -f auth-deploy.yaml kubectl rollout status deploy/auth-service kubectl get pods -l app=auth-service COMMAND_BLOCK: kubectl apply -f auth-deploy.yaml kubectl rollout status deploy/auth-service kubectl get pods -l app=auth-service CODE_BLOCK: apiVersion: v1 kind: Service metadata: name: auth-svc spec: selector: app: auth-service ports: - name: http port: 80 targetPort: 8081 type: ClusterIP Apply: kubectl apply -f auth-svc.yaml kubectl get svc auth-svc Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: apiVersion: v1 kind: Service metadata: name: auth-svc spec: selector: app: auth-service ports: - name: http port: 80 targetPort: 8081 type: ClusterIP Apply: kubectl apply -f auth-svc.yaml kubectl get svc auth-svc CODE_BLOCK: apiVersion: v1 kind: Service metadata: name: auth-svc spec: selector: app: auth-service ports: - name: http port: 80 targetPort: 8081 type: ClusterIP Apply: kubectl apply -f auth-svc.yaml kubectl get svc auth-svc CODE_BLOCK: apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: app-ingress spec: rules: - host: <APP_DOMAIN> http: paths: - path: /api/auth pathType: Prefix backend: service: name: auth-svc port: number: 80 - path: / pathType: Prefix backend: service: name: monolith-svc port: number: 80 Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: app-ingress spec: rules: - host: <APP_DOMAIN> http: paths: - path: /api/auth pathType: Prefix backend: service: name: auth-svc port: number: 80 - path: / pathType: Prefix backend: service: name: monolith-svc port: number: 80 CODE_BLOCK: apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: app-ingress spec: rules: - host: <APP_DOMAIN> http: paths: - path: /api/auth pathType: Prefix backend: service: name: auth-svc port: number: 80 - path: / pathType: Prefix backend: service: name: monolith-svc port: number: 80 COMMAND_BLOCK: kubectl apply -f ingress.yaml kubectl get ingress app-ingress -o wide Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: kubectl apply -f ingress.yaml kubectl get ingress app-ingress -o wide COMMAND_BLOCK: kubectl apply -f ingress.yaml kubectl get ingress app-ingress -o wide COMMAND_BLOCK: curl http://<APP_DOMAIN>/api/auth/ping Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: curl http://<APP_DOMAIN>/api/auth/ping COMMAND_BLOCK: curl http://<APP_DOMAIN>/api/auth/ping CODE_BLOCK: {"service":"auth-service","status":"pong"} Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: {"service":"auth-service","status":"pong"} CODE_BLOCK: {"service":"auth-service","status":"pong"} COMMAND_BLOCK: kubectl apply -f ingress.yaml Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: kubectl apply -f ingress.yaml COMMAND_BLOCK: kubectl apply -f ingress.yaml COMMAND_BLOCK: kubectl rollout undo deploy/auth-service Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: kubectl rollout undo deploy/auth-service COMMAND_BLOCK: kubectl rollout undo deploy/auth-service - Long delivery cycles - High data risk - Business disruption - Containerize an existing monolithic application - Deploy it to IBM Cloud Kubernetes Service - Place it behind Ingress - Deploy a new “edge” service - Route traffic gradually using path-based routing - Keep rollback simple and safe - IBM Cloud account - An existing IKS cluster - Tools installed locally: ibmcloud kubectl docker