مناف حوراني
كلّ الأدلّة

أدلّة التشغيل

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 خطوة · 40–60 min

هذه الأدلّة محفوظة بالإنجليزية. فالأوامر وملفّات الإعداد واحدة في كلّ اللغات، وتوزيعها على ثلاث نسخ لا يضيف إلّا احتمال تخلّف إحداها عن التحديث.

العناوين في هذا الدليل عناوين بديلة ضمن النطاق 10.0.0.0/24 والنطاق المستخدم هو example.dev. وما عدا ذلك منقول حرفياً عن الملاحظات الأصلية.

  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