Como criar um cluster Redis com Sentinel + HAProxy no Kubernetes (sem dor de cabeça)

8 min de leitura

Arquitetura Redis Sentinel e HAProxy no Kubernetes (fonte: ArysTech)

Resumo rápido: neste guia você monta um Redis altamente disponível no Kubernetes usando StatefulSet + Sentinel + HAProxy, com failover automático e um endpoint estável para as aplicações.

Atualizado em setembro de 2026: incluí os manifests completos do Sentinel e do HAProxy (com autenticação e resolução DNS dos pods), que faltavam na primeira versão.

Cenário real: o que quebra primeiro

Se você roda Redis em produção, cedo ou tarde vai acontecer:

  • um nó cai;
  • um pod reinicia no pior horário possível;
  • a aplicação começa a tomar timeout.

A boa notícia é que dá para reduzir esse caos com uma arquitetura simples e pragmática:

  1. Redis em StatefulSet: identidade e volume persistente por pod;
  2. Sentinel: detecta a falha e promove uma réplica;
  3. HAProxy: entrega um endpoint estável e aponta sempre para o master ativo.

Ou seja: a aplicação continua falando com o mesmo host e porta, e “quem é o master” vira problema da infraestrutura, como deveria ser.

Arquitetura

  • 3 pods Redis (redis-0, redis-1, redis-2) em StatefulSet: um master e duas réplicas;
  • 3 pods Sentinel, com quorum 2, que é o mínimo saudável;
  • 2 pods HAProxy, para não criar um novo ponto único de falha;
  • Services: um headless para o Redis e outro para o Sentinel (DNS estável por pod), e um Service normal para o HAProxy, que é o endpoint das aplicações.
Arquitetura Redis HA no Kubernetes com Sentinel e HAProxy
Visão geral: aplicações → HAProxy → master Redis, com Sentinel vigiando tudo. Imagem: ArysTech.

Por que StatefulSet e não Deployment?

Porque o StatefulSet foi feito exatamente para workloads com estado. Você ganha:

  • identidade de rede estável: redis-0.redis.default.svc.cluster.local é sempre o mesmo pod, mesmo que o IP mude;
  • storage persistente por pod, com um PVC por réplica;
  • ordem previsível de start, stop e rolling update.

Para Redis, isso é ouro. Em Deployment “com sorte funciona”; em StatefulSet “funciona por desenho”.

Sentinel: o cérebro do failover

O Sentinel faz quatro coisas:

  • monitora o master e as réplicas;
  • notifica eventos;
  • promove uma réplica a master quando o master some;
  • informa aos clientes qual é o master atual.

Boas práticas: rode no mínimo 3 sentinels, espalhe-os em nós diferentes (anti-affinity) e teste o failover periodicamente, inclusive no ambiente real.

Atenção: o Sentinel não transforma replicação assíncrona em mágica. Numa falha no meio de uma escrita, ainda existe uma janela em que o último dado escrito no master antigo se perde. Se isso importa, veja min-replicas-to-write e min-replicas-max-lag.

HAProxy: endpoint único para suas aplicações

Nem todo cliente Redis é sentinel-aware, e, mesmo quando é, muita equipe prefere simplificar. O HAProxy resolve isso com um health check TCP que conversa com o Redis:

  • envia AUTH e espera +OK;
  • envia PING e espera +PONG;
  • envia INFO replication e espera role:master.

Só o pod que responde role:master fica “UP” no backend, então o tráfego vai sempre para o master ativo, e as conexões com o master antigo são derrubadas quando ele deixa de ser master (on-marked-down shutdown-sessions).

Health check do HAProxy identificando o master Redis
Health check do HAProxy identificando o master. Imagem: Karl Stoney.

Os manifests

Tudo abaixo usa o namespace default. Se usar outro, troque o .default.svc nos nomes DNS.

1) Senha e Services

apiVersion: v1
kind: Secret
metadata:
  name: redis-auth
type: Opaque
stringData:
  password: "troque-esta-senha"
---
apiVersion: v1
kind: Service
metadata:
  name: redis            # headless: redis-0.redis, redis-1.redis...
spec:
  clusterIP: None
  selector:
    app: redis
  ports:
    - name: redis
      port: 6379
---
apiVersion: v1
kind: Service
metadata:
  name: redis-sentinel   # headless: redis-sentinel-0.redis-sentinel...
spec:
  clusterIP: None
  selector:
    app: redis-sentinel
  ports:
    - name: sentinel
      port: 26379

2) Scripts e configuração

O truque está no script de start do Redis. Antes de subir, cada pod pergunta ao Sentinel quem é o master atual. Se ninguém responder, é o primeiro boot, e o redis-0 assume. Isso evita que um pod reiniciado volte achando que é master e cause um split-brain.

apiVersion: v1
kind: ConfigMap
metadata:
  name: redis-config
data:
  start-redis.sh: |
    #!/bin/sh
    set -e
    ME="$(hostname).redis.default.svc.cluster.local"
    MASTER="redis-0.redis.default.svc.cluster.local"
    # Se o Sentinel já conhece um master, obedece a ele
    CUR=$(redis-cli -h redis-sentinel -p 26379 sentinel get-master-addr-by-name mymaster 2>/dev/null | head -1 || true)
    [ -n "$CUR" ] && MASTER="$CUR"
    ARGS="--requirepass $REDIS_PASSWORD --masterauth $REDIS_PASSWORD --replica-announce-ip $ME --appendonly yes"
    if [ "$ME" != "$MASTER" ]; then
      ARGS="$ARGS --replicaof $MASTER 6379"
    fi
    exec redis-server $ARGS

  sentinel.conf: |
    port 26379
    sentinel resolve-hostnames yes
    sentinel announce-hostnames yes
    sentinel monitor mymaster redis-0.redis.default.svc.cluster.local 6379 2
    sentinel auth-pass mymaster __PASSWORD__
    sentinel down-after-milliseconds mymaster 5000
    sentinel failover-timeout mymaster 60000
    sentinel parallel-syncs mymaster 1

  start-sentinel.sh: |
    #!/bin/sh
    set -e
    # O Sentinel reescreve o próprio arquivo, então ele precisa ser gravável
    cp /config/sentinel.conf /data/sentinel.conf
    sed -i "s/__PASSWORD__/$REDIS_PASSWORD/" /data/sentinel.conf
    echo "sentinel announce-ip $(hostname).redis-sentinel.default.svc.cluster.local" >> /data/sentinel.conf
    exec redis-sentinel /data/sentinel.conf

3) StatefulSet do Redis

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: redis
spec:
  serviceName: redis
  replicas: 3
  selector:
    matchLabels:
      app: redis
  template:
    metadata:
      labels:
        app: redis
    spec:
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              podAffinityTerm:
                topologyKey: kubernetes.io/hostname
                labelSelector:
                  matchLabels:
                    app: redis
      containers:
        - name: redis
          image: redis:7
          command: ["sh", "/config/start-redis.sh"]
          env:
            - name: REDIS_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: redis-auth
                  key: password
          ports:
            - containerPort: 6379
          resources:
            requests: { cpu: 100m, memory: 256Mi }
            limits: { memory: 512Mi }
          volumeMounts:
            - name: data
              mountPath: /data
            - name: config
              mountPath: /config
      volumes:
        - name: config
          configMap:
            name: redis-config
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 10Gi

4) StatefulSet do Sentinel

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: redis-sentinel
spec:
  serviceName: redis-sentinel
  replicas: 3
  selector:
    matchLabels:
      app: redis-sentinel
  template:
    metadata:
      labels:
        app: redis-sentinel
    spec:
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              podAffinityTerm:
                topologyKey: kubernetes.io/hostname
                labelSelector:
                  matchLabels:
                    app: redis-sentinel
      containers:
        - name: sentinel
          image: redis:7
          command: ["sh", "/config/start-sentinel.sh"]
          env:
            - name: REDIS_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: redis-auth
                  key: password
          ports:
            - containerPort: 26379
          resources:
            requests: { cpu: 50m, memory: 64Mi }
            limits: { memory: 128Mi }
          volumeMounts:
            - name: config
              mountPath: /config
            - name: data
              mountPath: /data
      volumes:
        - name: config
          configMap:
            name: redis-config
        - name: data
          emptyDir: {}

5) HAProxy

Dois detalhes que fazem toda a diferença: o bloco resolvers faz o HAProxy re-resolver o DNS dos pods, já que o IP muda a cada restart, e a senha vem de variável de ambiente, porque o HAProxy expande ${VAR} dentro de aspas duplas.

apiVersion: v1
kind: ConfigMap
metadata:
  name: haproxy-config
data:
  haproxy.cfg: |
    resolvers k8s
      parse-resolv-conf
      hold valid 5s

    defaults
      mode tcp
      timeout connect 3s
      timeout client 1h
      timeout server 1h

    frontend redis-write
      bind *:6379
      default_backend redis-primary

    backend redis-primary
      option tcp-check
      tcp-check connect
      tcp-check send "AUTH ${REDIS_PASSWORD}\r\n"
      tcp-check expect string +OK
      tcp-check send PING\r\n
      tcp-check expect string +PONG
      tcp-check send info\ replication\r\n
      tcp-check expect string role:master
      tcp-check send QUIT\r\n
      tcp-check expect string +OK
      default-server inter 1s fall 3 rise 2 on-marked-down shutdown-sessions resolvers k8s init-addr none
      server redis-0 redis-0.redis.default.svc.cluster.local:6379 check
      server redis-1 redis-1.redis.default.svc.cluster.local:6379 check
      server redis-2 redis-2.redis.default.svc.cluster.local:6379 check
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis-haproxy
spec:
  replicas: 2
  selector:
    matchLabels:
      app: redis-haproxy
  template:
    metadata:
      labels:
        app: redis-haproxy
    spec:
      containers:
        - name: haproxy
          image: haproxy:lts
          env:
            - name: REDIS_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: redis-auth
                  key: password
          ports:
            - containerPort: 6379
          volumeMounts:
            - name: config
              mountPath: /usr/local/etc/haproxy
      volumes:
        - name: config
          configMap:
            name: haproxy-config
---
apiVersion: v1
kind: Service
metadata:
  name: redis-haproxy    # é este endereço que as aplicações usam
spec:
  selector:
    app: redis-haproxy
  ports:
    - port: 6379
      targetPort: 6379

Salve tudo num arquivo e aplique:

kubectl apply -f redis-ha.yaml
kubectl get pods -l 'app in (redis,redis-sentinel,redis-haproxy)' -w

As aplicações passam a usar redis-haproxy.default.svc.cluster.local:6379, com a senha do Secret.

Dica: estes manifests são um esqueleto didático, bom para entender cada peça. Em produção vale considerar um operator ou um Helm chart mantido pela comunidade, que já tratam casos de borda como um Sentinel reiniciando durante um failover.

Testando o failover

Se isso falhar no teste, falharia às 3h da manhã. Melhor descobrir agora.

  1. Descubra quem é o master atual:
    kubectl exec redis-sentinel-0 -- redis-cli -p 26379 sentinel get-master-addr-by-name mymaster
  2. Derrube o pod master (troque redis-0 pelo master do passo anterior):
    kubectl delete pod redis-0
  3. Acompanhe a eleição nos logs do Sentinel. Procure por +switch-master:
    kubectl logs -f redis-sentinel-0 | grep -E "sdown|odown|switch-master"
  4. Confirme que o HAProxy já aponta para o novo master:
    kubectl run -it --rm redis-cli --image=redis:7 --restart=Never -- \
      redis-cli -h redis-haproxy -a 'troque-esta-senha' info replication | grep role
  5. Veja o impacto real na aplicação: quantos timeouts, retries e erros ela registrou.
  6. Espere o pod derrubado voltar e confira que ele subiu como réplica, não como um segundo master.

Checklist de produção

  • ☐ PVC por pod Redis e appendonly yes
  • ☐ Anti-affinity para Redis e Sentinel, de preferência required se houver nós suficientes
  • ☐ Requests e limits de CPU e memória definidos
  • ☐ PodDisruptionBudget (maxUnavailable: 1) para Redis, Sentinel e HAProxy
  • ☐ NetworkPolicy limitando o acesso às portas 6379 e 26379
  • ☐ Autenticação no Redis (requirepass e masterauth) com a senha num Secret
  • ☐ Monitoramento: latência, memória, número de failovers e role de cada pod (redis_exporter + Prometheus)
  • ☐ Teste de failover com procedimento documentado

Erros comuns que quase todo mundo comete

  1. Rodar tudo no mesmo nó e chamar isso de HA.
  2. Esquecer o headless service e depender de IP de pod.
  3. Deixar o Redis sem autenticação “porque é rede interna”.
  4. Deixar o pod reiniciado voltar como master e criar dois masters (split-brain).
  5. HAProxy sem resolvers, preso no IP antigo do pod.
  6. Um único HAProxy virando o novo ponto único de falha.
  7. Não testar o failover e confiar na fé.

Conclusão

Se você quer Redis estável no Kubernetes, este combo funciona muito bem: StatefulSet para dar identidade e guardar os dados, Sentinel para o failover automático e HAProxy para um endpoint simples e estável.

Não é uma arquitetura “zero complexidade”, mas é o tipo de complexidade que compra tranquilidade operacional.

Referências

Créditos das imagens: ArysTech (post sobre Redis Sentinel + HAProxy em MicroK8s) e Karl Stoney (Redis Sentinel behind HAProxy).


Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *