Asia/Dhaka
Posts

Scaling Microservices with Kubernetes and Docker

October 10, 2024
Microservices have revolutionized how we build and deploy modern applications. By breaking down monolithic applications into smaller, independent services, we gain flexibility, scalability, and resilience. But this architectural shift comes with its own challenges—challenges that Kubernetes is uniquely positioned to solve. Industry Landscape:
  • 85% of organizations have adopted or are planning to adopt microservices
  • Companies report 50-70% faster deployment cycles after migration
  • The microservices market is projected to reach $8.3 billion by 2026
  • 63% of enterprises use Kubernetes for container orchestration
However, success isn't guaranteed. Studies show that 40% of microservices initiatives fail or get scaled back due to:
  • Underestimated complexity
  • Inadequate tooling and monitoring
  • Poor service boundaries
  • Lack of organizational readiness
This guide will help you avoid these pitfalls. ✅ Choose microservices when:
  • Team size exceeds 20-30 developers
  • Application has distinct domains with clear boundaries
  • Different components require independent scaling
  • You need to use different technology stacks
  • Deployment velocity is a competitive advantage
  • System availability requirements exceed 99.9%
⛔ Avoid microservices when:
  • Team is smaller than 10 developers
  • Domain boundaries are unclear or constantly changing
  • Performance overhead of network calls is unacceptable
  • Distributed tracing and debugging capabilities are limited
  • Application is relatively simple with low traffic
Many successful organizations use a modular monolith as a stepping stone:
Python
# Modular monolith structure
my_app/
├── modules/
│   ├── authentication/
│   │   ├── __init__.py
│   │   ├── service.py
│   │   └── models.py
│   ├── payments/
│   │   ├── __init__.py
│   │   ├── service.py
│   │   └── models.py
│   └── inventory/
│       ├── __init__.py
│       ├── service.py
│       └── models.py
Each module is loosely coupled, making future extraction to microservices easier. Use bounded contexts to define service boundaries: Kubernetes provides the perfect orchestration platform for microservices:
  • Automatic Scaling: Scale services based on demand
  • Self-Healing: Automatically restart failed containers
  • Load Balancing: Distribute traffic efficiently
  • Service Discovery: Services can find each other dynamically
  • Rolling Updates: Zero-downtime deployments
  • Resource Management: Optimize infrastructure utilization
Bash
# Install Minikube
brew install minikube

# Start cluster
minikube start --cpus=4 --memory=8192

# Enable ingress
minikube addons enable ingress

# Verify
kubectl cluster-info
Yaml
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-service
  labels:
    app: api
    version: v1
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0  # Zero downtime
  selector:
    matchLabels:
      app: api
  template:
    metadata:
      labels:
        app: api
        version: v1
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "3000"
    spec:
      containers:
      - name: api
        image: your-registry/api:v1.2.3
        ports:
        - containerPort: 3000
          name: http
        env:
        - name: NODE_ENV
          value: "production"
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: db-credentials
              key: password
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"
        livenessProbe:
          httpGet:
            path: /health
            port: 3000
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /ready
            port: 3000
          initialDelaySeconds: 5
          periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
  name: api-service
spec:
  selector:
    app: api
  ports:
  - protocol: TCP
    port: 80
    targetPort: 3000
  type: ClusterIP
Gradually replace parts of the monolith without a risky big-bang rewrite:
Month 1-2: Preparation
Bash
# Set up monitoring and observability
- Deploy Prometheus and Grafana
- Implement distributed tracing with Jaeger
- Establish service mesh (Istio/Linkerd)

# Identify boundaries
- Map domain models
- Analyze dependencies
- Create service boundaries
Month 3-4: First Extraction
Yaml
# Extract a non-critical service first
Selected: Notification Service
Reason: 
  - Low dependency on other components
  - Well-defined boundary
  - Non-critical to core business
  - High independent scalability needs
Month 5-12: Iterative Extraction
  • Extract 1-2 services per month
  • Monitor and optimize each extraction
  • Build organizational capabilities
  • Refine processes and tooling
Option 1: Shared Database (Anti-pattern, but pragmatic start) Pros: Easy migration, no data sync issues Cons: Tight coupling, can't scale databases independently Option 2: Database per Service (Target state) Pros: Full independence, optimal scaling Cons: Data consistency challenges Option 3: Event-Driven Data Replication
Python
# Service A publishes events when data changes
@app.route('/orders', methods=['POST'])
def create_order():
    order = Order.create(request.json)
    
    # Emit event for other services
    event_bus.publish('order.created', {
        'order_id': order.id,
        'customer_id': order.customer_id,
        'total': order.total
    })
    
    return jsonify(order)

# Service B listens and maintains its own copy
@event_bus.subscribe('order.created')
def handle_order_created(event):
    # Update local read model
    local_db.orders.insert({
        'id': event['order_id'],
        'customer_id': event['customer_id'],
        # ... only fields needed by this service
    })
Optimize your Docker images with multi-stage builds:
Dockerfile
# Build stage
FROM node:18-alpine AS builder
WORKDIR /app

# Copy dependency files first (better caching)
COPY package*.json ./
RUN npm ci --only=production

# Copy source code
COPY . .

# Build if needed (e.g., TypeScript)
RUN npm run build

# Production stage
FROM node:18-alpine
WORKDIR /app

# Security: Run as non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nodejs -u 1001

# Copy only necessary files from builder
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/package.json ./

# Switch to non-root user
USER nodejs

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s \
  CMD node healthcheck.js || exit 1

EXPOSE 3000
CMD ["node", "dist/server.js"]
Results: Image size reduced from 1.2GB to 180MB
Yaml
# docker-compose.yml
version: '3.8'

services:
  api-gateway:
    build: ./gateway
    ports:
      - "8080:8080"
    environment:
      - AUTH_SERVICE_URL=http://auth:3001
      - ORDER_SERVICE_URL=http://orders:3002
    depends_on:
      - auth
      - orders

  auth:
    build: ./services/auth
    environment:
      - DB_HOST=postgres
      - REDIS_HOST=redis
    depends_on:
      - postgres
      - redis

  orders:
    build: ./services/orders
    environment:
      - DB_HOST=postgres
      - KAFKA_BROKERS=kafka:9092
    depends_on:
      - postgres
      - kafka

  postgres:
    image: postgres:15-alpine
    environment:
      - POSTGRES_PASSWORD=secret
    volumes:
      - postgres_data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data

  kafka:
    image: confluentinc/cp-kafka:latest
    environment:
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092

volumes:
  postgres_data:
  redis_data:
Enhance your microservices with a service mesh:
  • Traffic Management: A/B testing, canary deployments
  • Security: Automatic mTLS between services
  • Observability: Detailed metrics and tracing
  • Policy Enforcement: Rate limiting, access control
Yaml
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: api-service
spec:
  hosts:
  - api-service
  http:
  - match:
    - headers:
        user-type:
          exact: beta-tester
    route:
    - destination:
        host: api-service
        subset: v2
  - route:
    - destination:
        host: api-service
        subset: v1
      weight: 90
    - destination:
        host: api-service
        subset: v2
      weight: 10  # 10% traffic to new version
---
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: api-service
spec:
  host: api-service
  trafficPolicy:
    connectionPool:
      tcp:
        maxConnections: 100
      http:
        http1MaxPendingRequests: 50
        http2MaxRequests: 100
    outlierDetection:
      consecutive5xxErrors: 5
      interval: 30s
      baseEjectionTime: 30s
  subsets:
  - name: v1
    labels:
      version: v1
  - name: v2
    labels:
      version: v2
Yaml
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: order-service-circuit-breaker
spec:
  host: order-service
  trafficPolicy:
    connectionPool:
      tcp:
        maxConnections: 1
      http:
        http1MaxPendingRequests: 1
        maxRequestsPerConnection: 1
    outlierDetection:
      consecutiveErrors: 1
      interval: 1s
      baseEjectionTime: 3m
      maxEjectionPercent: 100
Yaml
# prometheus-config.yaml
global:
  scrape_interval: 15s
  evaluation_interval: 15s

# Alerting configuration
alerting:
  alertmanagers:
  - static_configs:
    - targets:
      - alertmanager:9093

rule_files:
  - /etc/prometheus/alerts.yml

scrape_configs:
  - job_name: 'kubernetes-pods'
    kubernetes_sd_configs:
    - role: pod
    relabel_configs:
    - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
      action: keep
      regex: true
    - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
      action: replace
      target_label: __metrics_path__
      regex: (.+)
    - source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port]
      action: replace
      regex: ([^:]+)(?::\d+)?;(\d+)
      replacement: $1:$2
      target_label: __address__
Javascript
// Node.js service with Prometheus metrics
const express = require('express');
const promClient = require('prom-client');

const app = express();

// Create metrics
const httpRequestDuration = new promClient.Histogram({
  name: 'http_request_duration_seconds',
  help: 'Duration of HTTP requests in seconds',
  labelNames: ['method', 'route', 'status_code']
});

const activeConnections = new promClient.Gauge({
  name: 'active_connections',
  help: 'Number of active connections'
});

// Middleware to track requests
app.use((req, res, next) => {
  const start = Date.now();
  activeConnections.inc();
  
  res.on('finish', () => {
    const duration = (Date.now() - start) / 1000;
    httpRequestDuration
      .labels(req.method, req.route?.path || req.path, res.statusCode)
      .observe(duration);
    activeConnections.dec();
  });
  
  next();
});

// Metrics endpoint
app.get('/metrics', async (req, res) => {
  res.set('Content-Type', promClient.register.contentType);
  res.end(await promClient.register.metrics());
});
Implement a robust CI/CD pipeline using GitHub Actions:
Yaml
name: Deploy Microservice
on:
  push:
    branches: [main]
    paths:
      - 'services/api/**'

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Run Unit Tests
        run: |
          cd services/api
          npm ci
          npm test
      
      - name: Run Integration Tests
        run: |
          docker-compose -f docker-compose.test.yml up -d
          npm run test:integration
          docker-compose -f docker-compose.test.yml down

  build:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Build and Push Docker Image
        run: |
          echo ${{ secrets.REGISTRY_PASSWORD }} | docker login -u ${{ secrets.REGISTRY_USER }} --password-stdin
          docker build -t ${{ secrets.REGISTRY }}/api:${{ github.sha }} ./services/api
          docker build -t ${{ secrets.REGISTRY }}/api:latest ./services/api
          docker push ${{ secrets.REGISTRY }}/api:${{ github.sha }}
          docker push ${{ secrets.REGISTRY }}/api:latest
      
      - name: Scan Image for Vulnerabilities
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: ${{ secrets.REGISTRY }}/api:${{ github.sha }}
          format: 'sarif'
          output: 'trivy-results.sarif'

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Deploy to Kubernetes
        run: |
          echo ${{ secrets.KUBECONFIG }} | base64 -d > kubeconfig
          export KUBECONFIG=./kubeconfig
          
          kubectl set image deployment/api-service \
            api=${{ secrets.REGISTRY }}/api:${{ github.sha }} \
            --record
          
          kubectl rollout status deployment/api-service
          
          # Rollback if health checks fail
          if ! kubectl rollout status deployment/api-service; then
            kubectl rollout undo deployment/api-service
            exit 1
          fi
Each microservice owns its data:
  • Independence: Services can choose their optimal database
  • Isolation: Changes don't affect other services
  • Scalability: Scale databases independently
Implementation:
Python
# Each service with its own database connection
class OrderService:
    def __init__(self):
        self.db = PostgreSQLConnection(
            host=os.getenv('ORDER_DB_HOST'),
            database='orders'
        )

class InventoryService:
    def __init__(self):
        # Different database type based on needs
        self.db = MongoDBConnection(
            host=os.getenv('INVENTORY_DB_HOST'),
            database='inventory'
        )
Javascript
// Saga orchestrator example
class OrderSaga {
  async execute(orderData) {
    const saga = new Saga();
    
    // Step 1: Reserve inventory
    saga.addStep({
      action: async () => {
        const result = await inventoryService.reserve(orderData.items);
        return { reservationId: result.id };
      },
      compensate: async (context) => {
        await inventoryService.release(context.reservationId);
      }
    });
    
    // Step 2: Process payment
    saga.addStep({
      action: async () => {
        const result = await paymentService.charge({
          amount: orderData.total,
          customerId: orderData.customerId
        });
        return { paymentId: result.id };
      },
      compensate: async (context) => {
        await paymentService.refund(context.paymentId);
      }
    });
    
    // Step 3: Create order
    saga.addStep({
      action: async (context) => {
        const order = await orderService.create({
          ...orderData,
          reservationId: context.reservationId,
          paymentId: context.paymentId
        });
        return { orderId: order.id };
      },
      compensate: async (context) => {
        await orderService.cancel(context.orderId);
      }
    });
    
    try {
      const result = await saga.execute();
      await this.notifyOrderCreated(result.orderId);
      return result;
    } catch (error) {
      // Saga automatically runs compensating transactions
      logger.error('Order saga failed', error);
      throw error;
    }
  }
}
Ensure service compatibility without integration tests:
Javascript
// Consumer test (Order Service)
const { Pact } = require('@pact-foundation/pact');

describe('Order Service - Inventory Contract', () => {
  const provider = new Pact({
    consumer: 'OrderService',
    provider: 'InventoryService'
  });

  it('can check inventory availability', async () => {
    await provider.addInteraction({
      state: 'product 123 has stock',
      uponReceiving: 'a request to check availability',
      withRequest: {
        method: 'GET',
        path: '/inventory/123',
        headers: { 'Accept': 'application/json' }
      },
      willRespondWith: {
        status: 200,
        body: {
          productId: 123,
          available: 50
        }
      }
    });

    const response = await orderService.checkInventory(123);
    expect(response.available).toBe(50);
  });
});

// Provider verification (Inventory Service)
const { Verifier } = require('@pact-foundation/pact');

describe('Inventory Service Pact Verification', () => {
  it('validates consumer expectations', async () => {
    await new Verifier({
      provider: 'InventoryService',
      providerBaseUrl: 'http://localhost:3000',
      pactUrls: ['./pacts/orderservice-inventoryservice.json']
    }).verifyProvider();
  });
});
Test resilience by injecting failures:
Yaml
# Network delay experiment
apiVersion: chaos-mesh.org/v1alpha1
kind: NetworkChaos
metadata:
  name: network-delay
spec:
  action: delay
  mode: one
  selector:
    namespaces:
      - production
    labelSelectors:
      app: api-service
  delay:
    latency: "10s"
    correlation: "100"
    jitter: "0ms"
  duration: "30s"
---
# Pod failure experiment
apiVersion: chaos-mesh.org/v1alpha1
kind: PodChaos
metadata:
  name: pod-failure
spec:
  action: pod-failure
  mode: fixed
  value: "1"
  selector:
    namespaces:
      - production
    labelSelectors:
      app: api-service
  duration: "60s"
Yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: api-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api-service
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80
  - type: Pods
    pods:
      metric:
        name: http_requests_per_second
      target:
        type: AverageValue
        averageValue: "1000"
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300  # Wait 5 min before scaling down
      policies:
      - type: Percent
        value: 50
        periodSeconds: 60  # Max 50% reduction per minute
    scaleUp:
      stabilizationWindowSeconds: 0
      policies:
      - type: Percent
        value: 100
        periodSeconds: 15  # Aggressive scale up
Calculate optimal resource requests:
Python
# Based on 95th percentile usage over 30 days
import numpy as np

cpu_usage_samples = get_metrics('cpu_usage', days=30)
memory_usage_samples = get_metrics('memory_usage', days=30)

cpu_request = np.percentile(cpu_usage_samples, 50)  # P50
cpu_limit = np.percentile(cpu_usage_samples, 95)     # P95

memory_request = np.percentile(memory_usage_samples, 50)
memory_limit = np.percentile(memory_usage_samples, 95)

print(f"""
Recommended resources:
  requests:
    cpu: {cpu_request}m
    memory: {memory_request}Mi
  limits:
    cpu: {cpu_limit}m
    memory: {memory_limit}Mi
""")
Bash
# Analyze actual usage
kubectl top pods --containers=true

# Adjust resources to match usage
# Before: 1000m CPU requested, 200m used → 80% waste
# After: 300m CPU requested, 200m used → 33% headroom
Savings: 40-60% reduction in infrastructure costs
Yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: cluster-autoscaler
data:
  config.yaml: |
    minNodes: 3
    maxNodes: 20
    scaleDownDelayAfterAdd: 10m
    scaleDownUnneededTime: 10m
    scaleDownUtilizationThreshold: 0.5
Yaml
apiVersion: v1
kind: Node
metadata:
  labels:
    node.kubernetes.io/instance-type: spot
spec:
  taints:
  - effect: NoSchedule
    key: spot
    value: "true"
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: batch-processor
spec:
  template:
    spec:
      tolerations:
      - key: spot
        operator: Equal
        value: "true"
        effect: NoSchedule
      nodeSelector:
        node.kubernetes.io/instance-type: spot
Savings: Up to 70% cost reduction for suitable workloads Problem: ImagePullBackOff
Bash
# Diagnose
kubectl describe pod <pod-name>

# Common causes:
# 1. Wrong image name/tag
# 2. Missing registry credentials
# 3. Network issues

# Fix: Create image pull secret
kubectl create secret docker-registry regcred \
  --docker-server=myregistry.io \
  --docker-username=user \
  --docker-password=pass
Problem: CrashLoopBackOff
Bash
# Check logs
kubectl logs <pod-name> --previous

# Common causes:
# 1. Application error
# 2. Missing environment variables
# 3. Failed health checks

# Debug with shell
kubectl run debug --rm -it --image=busybox -- /bin/sh
Bash
# Test service connectivity
kubectl run curl --image=curlimages/curl -it --rm -- \
  curl http://api-service.default.svc.cluster.local

# Check DNS resolution
kubectl run dnsutils --image=tutum/dnsutils -it --rm -- \
  nslookup api-service

# Network policy debugging
kubectl get networkpolicies
kubectl describe networkpolicy <policy-name>
Bash
# CPU/Memory usage
kubectl top nodes
kubectl top pods

# Detailed resource analysis
kubectl describe node <node-name>

# Application profiling
kubectl port-forward pod/<pod-name> 9229:9229
# Then connect profiler to localhost:9229
  1. Network Policies: Restrict communication between pods
Yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: api-network-policy
spec:
  podSelector:
    matchLabels:
      app: api
  policyTypes:
  - Ingress
  - Egress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: gateway
    ports:
    - protocol: TCP
      port: 3000
  egress:
  - to:
    - podSelector:
        matchLabels:
          app: database
    ports:
    - protocol: TCP
      port: 5432
  1. RBAC: Implement role-based access control
Yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: service-deployer
rules:
- apiGroups: ["apps"]
  resources: ["deployments"]
  verbs: ["get", "list", "update", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: deploy-role-binding
subjects:
- kind: ServiceAccount
  name: ci-cd-bot
roleRef:
  kind: Role
  name: service-deployer
  apiGroup: rbac.authorization.k8s.io
  1. Secrets Management: Use external vaults
Yaml
# Using External Secrets Operator
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: app-secrets
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: vault-backend
    kind: SecretStore
  target:
    name: app-secrets
    creationPolicy: Owner
  data:
  - secretKey: database-password
    remoteRef:
      key: secret/data/database
      property: password
  1. Image Scanning: Scan containers for vulnerabilities
Bash
# Trivy scan in CI/CD
trivy image --severity HIGH,CRITICAL myapp:latest

# Fail pipeline if vulnerabilities found
trivy image --exit-code 1 --severity CRITICAL myapp:latest
  1. Pod Security Standards: Enforce security standards
Yaml
apiVersion: v1
kind: Namespace
metadata:
  name: production
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/warn: restricted
A monolithic e-commerce platform with:
  • 500K daily active users
  • 15-minute average deployment time
  • Frequent production incidents
  • 200+ developer team
Migrated to microservices over 18 months: Architecture:
  • 45 microservices
  • 3 Kubernetes clusters (dev, staging, prod)
  • Istio service mesh
  • Event-driven with Kafka
Results:
  • ✅ Deployment time: 15 min → 5 min
  • ✅ Deployment frequency: 2x/week → 50x/day
  • ✅ Incident recovery: 4 hours → 15 minutes
  • ✅ System uptime: 99.5% → 99.95%
  • ✅ Team velocity: +40%
  • ✅ Infrastructure costs: -25% (through optimization)
Key Learnings:
  1. Start with non-critical services
  2. Invest heavily in observability early
  3. Automate everything from day one
  4. Cultural change is harder than technical
Microservices with Kubernetes provide a powerful foundation for building scalable, resilient applications. Success requires:
  1. Thoughtful design: Not every system needs microservices
  2. Strong foundations: Robust CI/CD, monitoring, and service mesh
  3. Gradual migration: Use strangler pattern, not big-bang rewrites
  4. Continuous optimization: Monitor costs and performance
  5. Team alignment: Ensure organization is ready for distributed systems
By following these best practices and leveraging the ecosystem of tools, you can create a robust microservices architecture that grows with your needs. Start small, iterate quickly, and always monitor your services in production. Remember: the goal isn't microservices—it's building systems that deliver value to users efficiently and reliably.
On this page
Hey! Got questions?Ask me anything 👋