Post

Proyecto Infraestructura HA para Bot de Whatsapp con N8N y K8S

Proyecto e implementacion de un entorno de alta disponibilidad en K8S para un Bot de Whatsapp desarrollado en N8N

Proyecto Infraestructura HA para Bot de Whatsapp con N8N y K8S

Proyecto Infraestructura HA para Bot de Whatsapp con N8N y K8S

Descripcion

El objetivo del proyecto es realizar un entorno de alta disponibilidad y escalabilidad para un bot de whatsapp creado con N8N, el cual tiene un alto flujo de peticiones utilizando Kubernetes (Orquestador con el cual N8N no es compatible). Como N8N (a dia de hoy) no esta pensado para utilizarse con Replicas en K8S. Se creo un servicio custom para monitorear el HPA de K8S y que al aumentar el nuemro de replicas de un entrypoint, este dispare la ejecucion de los deployments necesarios de N8N (MAX 3) para poder manejar la cantidad de trafico entrante.

Tecnologias utilizadas

Docker: Plataforma para ejecutar aplicaciones en contenedores ligeros y portables.

K8S: Sistema que gestiona y escala contenedores automáticamente en clústeres.

N8N: Herramienta visual de automatización para conectar apps y servicios sin código.

HaProxy: es un software de balanceo de carga y proxy inverso de alto rendimiento.

Custom Service: Servicio utilizado para monitorear el HPA de K8S y ejecutar los Deployments de Kubernetes para poder escalar/reducir el las instancias.

N8N Trigger: Backend donde llegan los mensajes y sirve como trigger para ejecutar los flujos de Chat de N8N

Diagrama

Diagrama de la infraestructura


Setup de servicios:

Entrypoint Service

Deployment.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
apiVersion: apps/v1
kind: Deployment
metadata:
  name: whatsapp-web-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: whatsapp-web-app
  template:
    metadata:
      labels:
        app: whatsapp-web-app
    spec:
      containers:
        - name: whatsapp-web-app
          image: ghcr.io/privado/whatsapp-web-app:v2
          ports:
            - containerPort: 3008
          envFrom:
            - secretRef:
                name: whatsapp-web-app-secret
          resources:
            requests:
              cpu: "100m"
              memory: "128Mi"
            limits:
              cpu: "500m"
              memory: "512Mi"
      imagePullSecrets:
        - name: ghcr-secret

Service.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: v1
kind: Service
metadata:
  name: whatsapp-web-app-service
spec:
  selector:
    app: whatsapp-web-app
  type: NodePort
  ports:
    - protocol: TCP
      port: 3008
      targetPort: 3008
      nodePort: 31008

Secret.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: v1
kind: Secret
metadata:
  name: whatsapp-web-app-secret
type: Opaque
stringData:
  POSTGRES_DB_HOST: x
  POSTGRES_DB_USER: x
  POSTGRES_DB_NAME: x
  POSTGRES_DB_PASSWORD: x
  POSTGRES_DB_PORT: "x"
  POSTGRES_DB_SCHEMA: x
  TOKEN_WSP: x
  NUMBER_ID_WSP: "x"
  TOKEN_VERIFY_API_WSP: x
  VERSION_API_WSP: v23.0
  PORT: "3008"

HPA.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: whatsapp-web-app-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: whatsapp-web-app
  minReplicas: 1
  maxReplicas: 3
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 60

HAProxy Service

Service.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: v1
kind: Service
metadata:
  name: haproxy-nodeport
spec:
  type: NodePort
  selector:
    app: haproxy
  ports:
    - protocol: TCP
      port: 5555
      targetPort: 5555
      nodePort: 30555

Deployment.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
apiVersion: apps/v1
kind: Deployment
metadata:
  name: haproxy
spec:
  replicas: 1
  selector:
    matchLabels:
      app: haproxy
  template:
    metadata:
      labels:
        app: haproxy
    spec:
      containers:
        - name: haproxy
          image: haproxy:2.9
          ports:
            - containerPort: 80
          volumeMounts:
            - name: config
              mountPath: /usr/local/etc/haproxy/haproxy.cfg
              subPath: haproxy.cfg
      volumes:
        - name: config
          configMap:
            name: haproxy-config

ConfigMap.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
apiVersion: v1
kind: ConfigMap
metadata:
  name: haproxy-config
data:
  haproxy.cfg: |
    global
        daemon
        maxconn 256

    defaults
        mode http
        timeout connect 5s
        timeout client  50s
        timeout server  50s

    frontend http-in
        bind *:5555
        default_backend k8s-backend

    backend k8s-backend
        balance roundrobin
        option httpchk GET /
        server n8n1 n8n.default.svc.cluster.local:5678 check
        server n8n3 n8n3.default.svc.cluster.local:5680 check
        server n8n4 n8n4.default.svc.cluster.local:5682 check

N8N Service

En esta parte se despliegan 3 servicios de N8N individuales no solo voy a ejemplificar con uno solo. Hay cambios minimos entre instancias como el puerto y el nombre

Service.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: v1
kind: Service
metadata:
  name: n8n4
spec:
  selector:
    app: n8n4
  type: NodePort
  ports:
    - name: http
      port: 5682
      targetPort: 5682
      nodePort: 30582

PV-PVC.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
apiVersion: v1
kind: PersistentVolume
metadata:
  name: n8n4-pv
spec:
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: /root/kuber/n8n4/.n8n
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: n8n4-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi

Deployment:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
apiVersion: apps/v1
kind: Deployment
metadata:
  name: n8n4
spec:
  replicas: 1
  selector:
    matchLabels:
      app: n8n4
  template:
    metadata:
      labels:
        app: n8n4
    spec:
      containers:
        - name: n8n4
          image: n8nio/n8n:latest
          env:
            - name: GENERIC_TIMEZONE
              value: "America/Argentina/Buenos_Aires"
            - name: N8N_BASIC_AUTH_ACTIVE
              value: "true"
            - name: N8N_BASIC_AUTH_USER
              valueFrom:
                secretKeyRef:
                  name: n8n4-secret
                  key: N8N_BASIC_AUTH_USER
            - name: N8N_BASIC_AUTH_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: n8n4-secret
                  key: N8N_BASIC_AUTH_PASSWORD
#            - name: DB_TYPE
#              value: postgresdb
#            - name: DB_POSTGRESDB_HOST
#              value: postgresql
#            - name: DB_POSTGRESDB_PORT
#              value: "5432"
#            - name: DB_POSTGRESDB_DATABASE
#              value: n8n
#            - name: DB_POSTGRESDB_USER
#              valueFrom:
#                secretKeyRef:
#                  name: n8n-secret
#                  key: DB_POSTGRESDB_USER
#            - name: DB_POSTGRESDB_PASSWORD
#              valueFrom:
#                secretKeyRef:
#                  name: n8n-secret
#                  key: DB_POSTGRESDB_PASSWORD
            - name: WEBHOOK_URL
              value: "https://privado.ar/"
            - name: N8N_PROTOCOL
              value: "https"
            - name: N8N_COMMUNITY_PACKAGES_ALLOW_TOOL_USAGE
              value: "true"
            - name: N8N_SECURE_COOKIE
              value: "false"
            - name: N8N_PORT
              value: "5682"
          ports:
            - containerPort: 5682
          volumeMounts:
            - name: n8n4-storage
              mountPath: /home/node/.n8n
      volumes:
        - name: n8n4-storage
          persistentVolumeClaim:
            claimName: n8n4-pvc

Secrets.yaml:

1
2
3
4
5
6
7
8
9
10
apiVersion: v1
kind: Secret
metadata:
  name: n8n4-secret
type: Opaque
stringData:
  N8N_BASIC_AUTH_USER: x
  N8N_BASIC_AUTH_PASSWORD: x
#  DB_POSTGRESDB_USER: n8nuser
#  DB_POSTGRESDB_PASSWORD: n8npass

HPA-Watcher

hpa-watcher.service:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[Unit]
Description=Watcher de HPA para whatsapp-web-app
After=network.target

[Service]
ExecStart=/usr/local/bin/hpa-watcher.sh
Restart=always
User=root
Environment=KUBECONFIG=/root/.kube/config
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

hpa-watcher.sh:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#!/bin/bash

# Configuración
HPA_NAME="whatsapp-web-app-hpa"
NAMESPACE="default"
CHECK_INTERVAL=10

# Nombre de los deployments adicionales
WORKER_DEPLOYMENTS=("n8n3" "n8n4")
YAML_DIR="/usr/local/bin/deployments"

# Validación inicial
if ! kubectl version --short &>/dev/null; then
  echo "[ERROR] kubectl no está configurado correctamente o no tiene permisos."
  exit 1
fi

echo "[INFO] Monitoreando HPA: $HPA_NAME"

# Obtener el valor inicial de réplicas
PREV_REPLICAS=$(kubectl get hpa "$HPA_NAME" -n "$NAMESPACE" -o jsonpath='{.status.currentReplicas}')

while true; do
  CURRENT_REPLICAS=$(kubectl get hpa "$HPA_NAME" -n "$NAMESPACE" -o jsonpath='{.status.currentReplicas}')
  
  if [ "$CURRENT_REPLICAS" != "$PREV_REPLICAS" ]; then
    echo "[EVENT] Cambio de réplicas: $PREV_REPLICAS$CURRENT_REPLICAS"

    # Controlar despliegue de n8n3 y n8n4 según número de réplicas
    if [ "$CURRENT_REPLICAS" -ge 2 ]; then
      echo "[DEPLOY] Desplegando n8n3"
      kubectl apply -f "$YAML_DIR/n8n3.yaml" -n "$NAMESPACE"
    else
      echo "[DELETE] Eliminando n8n3"
      kubectl delete deployment n8n3 -n "$NAMESPACE" --ignore-not-found
    fi

    if [ "$CURRENT_REPLICAS" -ge 3 ]; then
      echo "[DEPLOY] Desplegando n8n4"
      kubectl apply -f "$YAML_DIR/n8n4.yaml" -n "$NAMESPACE"
    else
      echo "[DELETE] Eliminando n8n4"
      kubectl delete deployment n8n4 -n "$NAMESPACE" --ignore-not-found
    fi

    PREV_REPLICAS=$CURRENT_REPLICAS
  fi

  sleep "$CHECK_INTERVAL"
done

A TENER EN CUENTA

Este post es con fines demostrativos, todos los datos sensibles han sido removidos y lo presentado difiere con lo que se encuentra productivo para proteger la privacidad de la organizacion

This post is licensed under CC BY 4.0 by the author.