Monaf Horany
Tüm runbook’lar

Runbook’lar

Deploying a NestJS API to Kubernetes with Ingress, TLS and MetalLB

A multi-stage image, a Deployment and Service, then the three pieces that turn a NodePort into something you would actually put a domain in front of: NGINX Ingress, cert-manager and MetalLB.

NestJSIngresscert-managerMetalLBHelm6 adım · 40–60 min

Bu runbook’lar İngilizce tutuluyor. Komutlar ve yapılandırma her dilde aynı; üçe bölmek yalnızca bir kopyanın güncelliğini yitirmesi riskini ekler.

Bu rehberdeki adresler 10.0.0.0/24 aralığında yer tutucudur ve alan adı example.dev’dir. Geri kalan her şey çalışma notlarından birebir alınmıştır.

  1. 01

    Build a small image

    Three stages: production dependencies, a build, then a runtime that copies only what it needs. The build toolchain never reaches the final image.

    Dockerfile
    dockerfile
    FROM node:20-alpine AS deps
    WORKDIR /app
    COPY package*.json ./
    RUN npm ci --omit=dev
    
    FROM node:20-alpine AS builder
    WORKDIR /app
    COPY . .
    RUN npm ci
    RUN npm run build
    
    FROM node:20-alpine
    WORKDIR /app
    ENV NODE_ENV=production
    COPY --from=deps /app/node_modules ./node_modules
    COPY --from=builder /app/dist ./dist
    EXPOSE 3000
    CMD ["node","dist/main.js"]
    bash
    docker build -t YOUR_DOCKERHUB_USER/nest-api:v1 .
    docker login
    docker push YOUR_DOCKERHUB_USER/nest-api:v1
  2. 02

    Deployment and Service

    nest-api.yaml
    yaml
    apiVersion: v1
    kind: Namespace
    metadata:
      name: demo
    ---
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: nest-api
      namespace: demo
    spec:
      replicas: 1
      selector:
        matchLabels:
          app: nest-api
      template:
        metadata:
          labels:
            app: nest-api
        spec:
          containers:
            - name: api
              image: YOUR_DOCKERHUB_USER/nest-api:v1
              ports:
                - containerPort: 3000
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: nest-api
      namespace: demo
    spec:
      type: NodePort
      selector:
        app: nest-api
      ports:
        - port: 3000
          targetPort: 3000
          nodePort: 30080
    bash
    kubectl apply -f nest-api.yaml
    
    # a NodePort answers on every node, not just the one running the pod
    curl http://10.0.0.80:30080/
    curl http://10.0.0.81:30080/
  3. 03

    NGINX Ingress and cert-manager

    bash
    helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
    helm repo update
    helm upgrade --install ingress-nginx ingress-nginx/ingress-nginx \
      --namespace ingress-nginx --create-namespace \
      --set controller.service.type=NodePort \
      --set controller.service.nodePorts.http=30080 \
      --set controller.service.nodePorts.https=30443
    
    helm repo add jetstack https://charts.jetstack.io
    helm repo update
    helm upgrade --install cert-manager jetstack/cert-manager \
      --namespace cert-manager --create-namespace \
      --set crds.enabled=true
    A self-signed issuer is enough on a lab cluster
    yaml
    apiVersion: cert-manager.io/v1
    kind: ClusterIssuer
    metadata:
      name: selfsigned
    spec:
      selfSigned: {}
  4. 04

    Route a hostname to the service

    sslip.io resolves any embedded address back to itself, which gives you a real hostname on a network with no DNS server — useful precisely because Ingress rules match on host.

    ingress.yaml
    yaml
    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      name: nest-api
      namespace: demo
      annotations:
        nginx.ingress.kubernetes.io/backend-protocol: "HTTP"
    spec:
      ingressClassName: nginx
      rules:
        - host: api.10.0.0.80.sslip.io
          http:
            paths:
              - path: /
                pathType: Prefix
                backend:
                  service:
                    name: nest-api
                    port:
                      number: 3000
  5. 05

    MetalLB, for an actual LoadBalancer

    On bare metal, type: LoadBalancer stays <pending> forever — there is no cloud controller to assign an address. MetalLB is what fills that gap: hand it a spare range on your LAN and it answers ARP for those addresses.

    bash
    kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.14.5/config/manifests/metallb-native.yaml
    kubectl -n metallb-system wait --for=condition=Available deploy/controller --timeout=120s
    The range must be outside your DHCP scope
    yaml
    apiVersion: metallb.io/v1beta1
    kind: IPAddressPool
    metadata:
      name: pool
      namespace: metallb-system
    spec:
      addresses:
        - 10.0.0.200-10.0.0.210
    ---
    apiVersion: metallb.io/v1beta1
    kind: L2Advertisement
    metadata:
      name: l2
      namespace: metallb-system
    spec: {}
  6. 06

    Prove it holds

    A deployment that has never been put under load is a deployment you are guessing about.

    bash
    wrk -t8 -c200 -d60s https://api.10.0.0.200.sslip.io/health
    hey -z 60s -c 100 https://api.10.0.0.200.sslip.io/health
    
    # confirm requests actually spread across pods
    for i in $(seq 1 10); do curl -sk https://api.10.0.0.200.sslip.io/whoami; echo; done