Introducción
Keycloak es un sistema de identidad y acceso de código abierto (IAM) que permite gestionar la autenticación y autorización en aplicaciones modernas mediante protocolos estándar como OpenID Connect, OAuth 2.0 y SAML 2.0. En entornos de producción es fundamental desplegarlo en alta disponibilidad para evitar que un único punto de fallo deje sin acceso a todas las aplicaciones que dependen de él.
En este manual configuraremos una arquitectura completa de Keycloak en alta disponibilidad con:
- Clúster etcd de 3 nodos para coordinación distribuida
- PostgreSQL en HA con Patroni (3 nodos) como backend de datos
- 2 nodos Keycloak con replicación de sesiones via Infinispan
- 2 servidores Nginx como balanceadores de carga con Keepalived (VIP)
La tabla de direccionamiento utilizada a lo largo del manual es la siguiente:
| Rol | Hostname | IP |
|---|---|---|
| etcd + Patroni + PostgreSQL node1 | db01 | 192.168.1.214 |
| etcd + Patroni + PostgreSQL node2 | db02 | 192.168.1.215 |
| etcd + Patroni + PostgreSQL node3 | db03 | 192.168.1.216 |
| Keycloak node1 | kc01 | 192.168.1.217 |
| Keycloak node2 | kc02 | 192.168.1.218 |
| Nginx + Keepalived node1 | lb01 | 192.168.1.219 |
| Nginx + Keepalived node2 | lb02 | 192.168.1.220 |
| VIP (Virtual IP) | — | 192.168.1.221 |
Requisitos previos
Para seguir este manual necesitamos:
- 3 servidores Debian para base de datos (db01, db02, db03)
- 2 servidores Debian para Keycloak (kc01, kc02)
- 2 servidores Debian para Nginx/Keepalived (lb01, lb02)
- Java 17+ en los nodos Keycloak
- Acceso root/sudo en todos los nodos
- Conectividad de red entre todos los nodos
Los puertos que deben estar abiertos entre los distintos niveles de la arquitectura:
Puerto(s) Servicio Dirección
----------- ------------------ ----------------------------------
2379/tcp etcd client API entre nodos db
2380/tcp etcd peer entre nodos db
5432/tcp PostgreSQL db nodes ↔ Keycloak nodes
8008/tcp Patroni REST API entre nodos db
8080/tcp Keycloak HTTP lb nodes → kc nodes
8443/tcp Keycloak HTTPS lb nodes → kc nodes
7800/tcp JGroups / Infinispan entre nodos kc
80/tcp Nginx HTTP clientes → lb nodes
443/tcp Nginx HTTPS clientes → lb nodes
VRRP (112) Keepalived entre lb nodesInstalación de la base de datos en HA
3.1 Instalación de paquetes (en los 3 nodos db)
Ejecutamos en db01, db02 y db03:
sudo apt update && sudo apt install -y postgresql postgresql-contrib patroni etcd-server etcd-client curl3.2 Configuración de etcd
Configuramos etcd en cada nodo editando /etc/default/etcd. Cada nodo tiene su propia dirección IP de escucha y de anuncio.
NODE1 — db01 (192.168.1.214):
# /etc/default/etcd — db01
ETCD_LISTEN_PEER_URLS="http://0.0.0.0:2380"
ETCD_LISTEN_CLIENT_URLS="http://0.0.0.0:2379"
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://192.168.1.214:2380"
ETCD_ADVERTISE_CLIENT_URLS="http://192.168.1.214:2379"
ETCD_INITIAL_CLUSTER="node1=http://192.168.1.214:2380,node2=http://192.168.1.215:2380,node3=http://192.168.1.216:2380"
ETCD_INITIAL_CLUSTER_STATE="new"
ETCD_INITIAL_CLUSTER_TOKEN="etcd-cluster"
ETCD_NAME="node1"
ETCD_DATA_DIR="/var/lib/etcd"
ETCD_LOG_LEVEL="info"
ETCD_ENABLE_V2="true"NODE2 — db02 (192.168.1.215):
# /etc/default/etcd — db02
ETCD_LISTEN_PEER_URLS="http://0.0.0.0:2380"
ETCD_LISTEN_CLIENT_URLS="http://0.0.0.0:2379"
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://192.168.1.215:2380"
ETCD_ADVERTISE_CLIENT_URLS="http://192.168.1.215:2379"
ETCD_INITIAL_CLUSTER="node1=http://192.168.1.214:2380,node2=http://192.168.1.215:2380,node3=http://192.168.1.216:2380"
ETCD_INITIAL_CLUSTER_STATE="new"
ETCD_INITIAL_CLUSTER_TOKEN="etcd-cluster"
ETCD_NAME="node2"
ETCD_DATA_DIR="/var/lib/etcd"
ETCD_LOG_LEVEL="info"
ETCD_ENABLE_V2="true"NODE3 — db03 (192.168.1.216):
# /etc/default/etcd — db03
ETCD_LISTEN_PEER_URLS="http://0.0.0.0:2380"
ETCD_LISTEN_CLIENT_URLS="http://0.0.0.0:2379"
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://192.168.1.216:2380"
ETCD_ADVERTISE_CLIENT_URLS="http://192.168.1.216:2379"
ETCD_INITIAL_CLUSTER="node1=http://192.168.1.214:2380,node2=http://192.168.1.215:2380,node3=http://192.168.1.216:2380"
ETCD_INITIAL_CLUSTER_STATE="new"
ETCD_INITIAL_CLUSTER_TOKEN="etcd-cluster"
ETCD_NAME="node3"
ETCD_DATA_DIR="/var/lib/etcd"
ETCD_LOG_LEVEL="info"
ETCD_ENABLE_V2="true"Habilitamos e iniciamos etcd en los 3 nodos:
sudo systemctl enable etcd
sudo systemctl restart etcd3.3 Verificación de etcd
Comprobamos que el clúster etcd está sano desde cualquier nodo:
etcdctl --endpoints=http://192.168.1.214:2379,http://192.168.1.215:2379,http://192.168.1.216:2379 endpoint healthSalida esperada:
http://192.168.1.214:2379 is healthy: successfully committed proposal: took = 3.2ms
http://192.168.1.215:2379 is healthy: successfully committed proposal: took = 3.5ms
http://192.168.1.216:2379 is healthy: successfully committed proposal: took = 3.8ms3.4 Configuración de Patroni
Creamos el directorio y el fichero de configuración de Patroni en cada nodo. Patroni se apoyará en etcd como DCS (Distributed Configuration Store).
NODE1 — db01 (192.168.1.214) — /etc/patroni/config.yml:
scope: keycloak-cluster
namespace: /db/
name: node1
restapi:
listen: 192.168.1.214:8008
connect_address: 192.168.1.214:8008
etcd:
hosts:
- 192.168.1.214:2379
- 192.168.1.215:2379
- 192.168.1.216:2379
bootstrap:
dcs:
ttl: 30
loop_wait: 10
retry_timeout: 10
maximum_lag_on_failover: 1048576
postgresql:
use_pg_rewind: true
use_slots: true
parameters:
wal_level: replica
hot_standby: "on"
max_wal_senders: 10
max_replication_slots: 10
wal_log_hints: "on"
initdb:
- encoding: UTF8
- data-checksums
pg_hba:
- host replication replicator 192.168.1.214/32 md5
- host replication replicator 192.168.1.215/32 md5
- host replication replicator 192.168.1.216/32 md5
- host all all 192.168.1.0/24 md5
users:
admin:
password: admin_password
options:
- createrole
- createdb
postgresql:
listen: 192.168.1.214:5432
connect_address: 192.168.1.214:5432
data_dir: /var/lib/postgresql/14/main
bin_dir: /usr/lib/postgresql/14/bin
pgpass: /tmp/pgpass0
authentication:
replication:
username: replicator
password: replicator_password
superuser:
username: postgres
password: postgres_password
rewind:
username: rewind_user
password: rewind_password
tags:
nofailover: false
noloadbalance: false
clonedfrom: false
nosync: falseNODE2 — db02 (192.168.1.215) — /etc/patroni/config.yml:
scope: keycloak-cluster
namespace: /db/
name: node2
restapi:
listen: 192.168.1.215:8008
connect_address: 192.168.1.215:8008
etcd:
hosts:
- 192.168.1.214:2379
- 192.168.1.215:2379
- 192.168.1.216:2379
bootstrap:
dcs:
ttl: 30
loop_wait: 10
retry_timeout: 10
maximum_lag_on_failover: 1048576
postgresql:
use_pg_rewind: true
use_slots: true
parameters:
wal_level: replica
hot_standby: "on"
max_wal_senders: 10
max_replication_slots: 10
wal_log_hints: "on"
initdb:
- encoding: UTF8
- data-checksums
pg_hba:
- host replication replicator 192.168.1.214/32 md5
- host replication replicator 192.168.1.215/32 md5
- host replication replicator 192.168.1.216/32 md5
- host all all 192.168.1.0/24 md5
users:
admin:
password: admin_password
options:
- createrole
- createdb
postgresql:
listen: 192.168.1.215:5432
connect_address: 192.168.1.215:5432
data_dir: /var/lib/postgresql/14/main
bin_dir: /usr/lib/postgresql/14/bin
pgpass: /tmp/pgpass0
authentication:
replication:
username: replicator
password: replicator_password
superuser:
username: postgres
password: postgres_password
rewind:
username: rewind_user
password: rewind_password
tags:
nofailover: false
noloadbalance: false
clonedfrom: false
nosync: falseNODE3 — db03 (192.168.1.216) — /etc/patroni/config.yml:
scope: keycloak-cluster
namespace: /db/
name: node3
restapi:
listen: 192.168.1.216:8008
connect_address: 192.168.1.216:8008
etcd:
hosts:
- 192.168.1.214:2379
- 192.168.1.215:2379
- 192.168.1.216:2379
bootstrap:
dcs:
ttl: 30
loop_wait: 10
retry_timeout: 10
maximum_lag_on_failover: 1048576
postgresql:
use_pg_rewind: true
use_slots: true
parameters:
wal_level: replica
hot_standby: "on"
max_wal_senders: 10
max_replication_slots: 10
wal_log_hints: "on"
initdb:
- encoding: UTF8
- data-checksums
pg_hba:
- host replication replicator 192.168.1.214/32 md5
- host replication replicator 192.168.1.215/32 md5
- host replication replicator 192.168.1.216/32 md5
- host all all 192.168.1.0/24 md5
users:
admin:
password: admin_password
options:
- createrole
- createdb
postgresql:
listen: 192.168.1.216:5432
connect_address: 192.168.1.216:5432
data_dir: /var/lib/postgresql/14/main
bin_dir: /usr/lib/postgresql/14/bin
pgpass: /tmp/pgpass0
authentication:
replication:
username: replicator
password: replicator_password
superuser:
username: postgres
password: postgres_password
rewind:
username: rewind_user
password: rewind_password
tags:
nofailover: false
noloadbalance: false
clonedfrom: false
nosync: falseDetenemos PostgreSQL (Patroni lo gestionará por completo) y arrancamos Patroni en los 3 nodos:
sudo systemctl stop postgresql
sudo systemctl enable patroni
sudo systemctl start patroni3.5 Verificación del clúster Patroni
patronictl -c /etc/patroni/config.yml listSalida esperada (node1 como líder, node2 y node3 como réplicas):
+ Cluster: keycloak-cluster (7234567890123456789) ------+----+-----------+
| Member | Host | Role | State | TL | Lag in MB |
+--------+------------------+---------+---------+----+-----------+
| node1 | 192.168.1.214:5432 | Leader | running | 1 | |
| node2 | 192.168.1.215:5432 | Replica | running | 1 | 0 |
| node3 | 192.168.1.216:5432 | Replica | running | 1 | 0 |
+--------+------------------+---------+---------+----+-----------+3.6 Creación de la base de datos Keycloak
Nos conectamos al nodo líder (en este ejemplo node1/db01) y creamos la base de datos y el usuario para Keycloak:
sudo -u postgres psql
CREATE USER keycloak WITH PASSWORD 'keycloak_password';
CREATE DATABASE keycloak OWNER keycloak;
GRANT ALL PRIVILEGES ON DATABASE keycloak TO keycloak;
\qA continuación añadimos las entradas necesarias en pg_hba.conf para que los nodos Keycloak puedan conectarse (si no las añadiste ya en la sección pg_hba del bootstrap de Patroni):
# Añadir al final de pg_hba.conf (gestionado por Patroni via patronictl edit-config o directamente)
host keycloak keycloak 192.168.1.217/32 md5
host keycloak keycloak 192.168.1.218/32 md5
# Recargar la configuración
patronictl -c /etc/patroni/config.yml reload keycloak-clusterInstalación de Keycloak (en kc01 y kc02)
4.1 Instalación de Java 17
Keycloak requiere Java 17 o superior. Lo instalamos en ambos nodos Keycloak:
sudo apt update && sudo apt install -y openjdk-17-jdk
java -versionSalida esperada:
openjdk version "17.0.11" 2024-04-16
OpenJDK Runtime Environment (build 17.0.11+9-Debian-1)
OpenJDK 64-Bit Server VM (build 17.0.11+9-Debian-1, mixed mode, sharing)4.2 Descarga e instalación de Keycloak
Descargamos la distribución oficial de Keycloak, la extraemos en /opt y creamos el usuario del sistema:
export KC_VERSION=24.0.4
cd /opt
sudo wget https://github.com/keycloak/keycloak/releases/download/${KC_VERSION}/keycloak-${KC_VERSION}.tar.gz
sudo tar xzf keycloak-${KC_VERSION}.tar.gz
sudo ln -s /opt/keycloak-${KC_VERSION} /opt/keycloak
sudo groupadd keycloak
sudo useradd -r -g keycloak -d /opt/keycloak -s /sbin/nologin keycloak
sudo chown -R keycloak:keycloak /opt/keycloak-${KC_VERSION}4.3 Configuración de Keycloak para producción
Editamos /opt/keycloak/conf/keycloak.conf en ambos nodos. La URL de la base de datos apunta a la VIP (192.168.1.221) que siempre resolverá al nodo PostgreSQL activo vía HAProxy o al balanceador de Patroni; alternativamente podemos usar directamente la IP del líder actual. Lo más robusto es desplegar HAProxy o pgBouncer apuntando a la REST API de Patroni, pero para simplificar el manual usamos la VIP:
# Base
hostname=keycloak.example.com
http-enabled=true
http-port=8080
proxy-headers=xforwarded
# Database
db=postgres
db-url=jdbc:postgresql://192.168.1.221:5432/keycloak
db-username=keycloak
db-password=keycloak_password
# Cluster / Infinispan
cache=ispn
cache-stack=tcp
# Health & Metrics
health-enabled=true
metrics-enabled=true4.4 Configuración del clúster Infinispan (JGroups TCP)
Keycloak utiliza Infinispan para replicar sesiones de usuario entre nodos. Configuramos el stack TCP con TCPPING para descubrimiento estático. Editamos /opt/keycloak/conf/cache-ispn.xml y localizamos la sección del stack TCP para sustituir el protocolo de descubrimiento por TCPPING:
<!-- Dentro de <stack name="tcp"> -->
<TCP bind_addr="match-interface:eth0"
bind_port="7800"
recv_buf_size="20000000"
send_buf_size="640000"
max_bundle_size="64000" />
<TCPPING initial_hosts="192.168.1.217[7800],192.168.1.218[7800]"
ergonomics="false"
port_range="0" />
<MERGE3 min_interval="10000"
max_interval="30000" />
<FD_SOCK />
<FD_ALL timeout="60000" interval="15000" />
<VERIFY_SUSPECT timeout="5000" />
<BARRIER />
<pbcast.NAKACK2 use_mcast_xmit="false"
discard_delivered_msgs="true" />
<UNICAST3 />
<pbcast.STABLE desired_avg_gossip="50000"
max_bytes="4000000" />
<pbcast.GMS print_local_addr="true"
join_timeout="2000" />
<UFC max_credits="2000000"
min_threshold="0.4" />
<MFC max_credits="2000000"
min_threshold="0.4" />
<FRAG2 frag_size="60000" />4.5 Build y primer arranque
Antes de iniciar Keycloak en modo producción debemos ejecutar el comando build para compilar la configuración de forma optimizada:
sudo -u keycloak /opt/keycloak/bin/kc.sh buildPrimer arranque para crear el usuario administrador (solo es necesario la primera vez en uno de los nodos):
sudo -u keycloak KEYCLOAK_ADMIN=admin KEYCLOAK_ADMIN_PASSWORD=admin \
/opt/keycloak/bin/kc.sh start --optimizedUna vez arrancado correctamente, paramos con Ctrl+C y procedemos a configurar el servicio systemd.
4.6 Servicio systemd
Creamos el fichero de unidad /etc/systemd/system/keycloak.service:
[Unit]
Description=Keycloak IAM
After=network.target
[Service]
Type=exec
User=keycloak
Group=keycloak
ExecStart=/opt/keycloak/bin/kc.sh start --optimized
Restart=on-failure
RestartSec=10
LimitNOFILE=65536
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable keycloak
sudo systemctl start keycloak4.7 Verificación del clúster Keycloak
Comprobamos que Keycloak responde correctamente y que los dos nodos han formado el clúster Infinispan:
# Health endpoint (desde cualquier nodo o el balanceador)
curl -s http://192.168.1.217:8080/health | python3 -m json.tool
curl -s http://192.168.1.218:8080/health | python3 -m json.tool
# Verificar que el clúster Infinispan tiene 2 miembros (en los logs de Keycloak)
sudo journalctl -u keycloak -f | grep -i "cluster\|infinispan\|jgroups\|members"En los logs deberías ver algo similar a:
INFO [org.infinispan.CLUSTER] (keycloak-cache-init) ISPN000094: Received new cluster view for channel ISPN:
[kc01|1] (2) [kc01, kc02]Balanceador de carga Nginx con Keepalived
5.1 Instalación (en lb01 y lb02)
sudo apt update && sudo apt install -y nginx keepalived5.2 Configuración de Nginx
Creamos el fichero /etc/nginx/conf.d/keycloak.conf en ambos balanceadores. Usamos least_conn para distribuir la carga de forma equitativa y configuramos cabeceras de proxy para que Keycloak conozca la IP real del cliente:
upstream keycloak_backend {
least_conn;
server 192.168.1.217:8080 max_fails=3 fail_timeout=30s;
server 192.168.1.218:8080 max_fails=3 fail_timeout=30s;
keepalive 32;
}
server {
listen 80;
server_name keycloak.example.com;
location / {
proxy_pass http://keycloak_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
proxy_read_timeout 300s;
proxy_connect_timeout 10s;
}
location /health {
proxy_pass http://keycloak_backend/health;
proxy_set_header Host $host;
access_log off;
}
}
Verificamos la configuración y recargamos Nginx:
sudo nginx -t && sudo systemctl reload nginx5.3 Configuración de Keepalived
Keepalived implementa VRRP para mover la IP virtual (VIP 192.168.1.221) automáticamente al nodo activo. Primero creamos el script de comprobación de salud de Nginx:
cat > /etc/keepalived/check_nginx.sh <<'EOF'
#!/bin/bash
if ! systemctl is-active --quiet nginx; then
exit 1
fi
if ! curl -sf http://127.0.0.1/health > /dev/null 2>&1; then
exit 1
fi
exit 0
EOF
chmod +x /etc/keepalived/check_nginx.shMASTER — lb01 (192.168.1.219) — /etc/keepalived/keepalived.conf:
global_defs {
router_id LB01
script_user root
enable_script_security
}
vrrp_script chk_nginx {
script "/etc/keepalived/check_nginx.sh"
interval 2
weight -20
fall 2
rise 2
}
vrrp_instance VI_1 {
state MASTER
interface eth0
virtual_router_id 51
priority 100
advert_int 1
authentication {
auth_type PASS
auth_pass keycloak_vrrp_secret
}
virtual_ipaddress {
192.168.1.221/24
}
track_script {
chk_nginx
}
}BACKUP — lb02 (192.168.1.220) — /etc/keepalived/keepalived.conf:
global_defs {
router_id LB02
script_user root
enable_script_security
}
vrrp_script chk_nginx {
script "/etc/keepalived/check_nginx.sh"
interval 2
weight -20
fall 2
rise 2
}
vrrp_instance VI_1 {
state BACKUP
interface eth0
virtual_router_id 51
priority 90
advert_int 1
authentication {
auth_type PASS
auth_pass keycloak_vrrp_secret
}
virtual_ipaddress {
192.168.1.221/24
}
track_script {
chk_nginx
}
}Habilitamos e iniciamos Keepalived en ambos balanceadores:
sudo systemctl enable keepalived
sudo systemctl start keepalived5.4 Verificación del balanceador
Comprobamos que la VIP está asignada a lb01 y que el acceso a Keycloak funciona a través de ella:
# Verificar que la VIP está en lb01
ip addr show eth0 | grep 192.168.1.221
# Verificar acceso a Keycloak a través de la VIP
curl -s http://192.168.1.221/health | python3 -m json.toolSalida esperada del health check:
{
"status": "UP",
"checks": [
{
"name": "Keycloak readiness check",
"status": "UP"
}
]
}Pruebas de alta disponibilidad
Una vez completada la instalación, es imprescindible verificar que el sistema tolera correctamente los fallos en cada capa.
Test 1: fallo de un nodo Keycloak
# En kc01: detener Keycloak
sudo systemctl stop keycloak
# Desde un cliente externo: verificar que el acceso sigue funcionando vía VIP
curl -s http://192.168.1.221/health
# Verificar que Nginx está enviando tráfico solo a kc02
sudo tail -f /var/log/nginx/access.log
# Restaurar
sudo systemctl start keycloakLas sesiones activas de los usuarios deben mantenerse gracias a la replicación de Infinispan entre kc01 y kc02.
Test 2: failover del líder PostgreSQL/Patroni
# Forzar un failover de Patroni (desde cualquier nodo db)
patronictl -c /etc/patroni/config.yml failover keycloak-cluster --master node1 --force
# Verificar que un nuevo líder ha sido elegido
patronictl -c /etc/patroni/config.yml list
# Verificar que Keycloak sigue respondiendo
curl -s http://192.168.1.221/healthPatroni elegerá automáticamente una nueva réplica como líder en menos de 30 segundos (valor de ttl).
Test 3: fallo del balanceador MASTER (lb01)
# En lb01: detener Keepalived para simular el fallo
sudo systemctl stop keepalived
# En lb02: verificar que la VIP ha migrado
ip addr show eth0 | grep 192.168.1.221
# Verificar que el acceso a Keycloak sigue funcionando
curl -s http://192.168.1.221/health
# Restaurar lb01
sudo systemctl start keepalivedKeepalived detectará la pérdida de los anuncios VRRP de lb01 y lb02 asumirá el rol de MASTER, asignándose la VIP en menos de 3 segundos.
Consideraciones de seguridad
Para un despliegue robusto en producción, ten en cuenta las siguientes recomendaciones:
- TLS en todas las capas: habilita HTTPS en Nginx (Let's Encrypt o certificado corporativo), cifrado TLS en etcd (
ETCD_CERT_FILE,ETCD_KEY_FILE), en PostgreSQL (ssl = on) y en las comunicaciones Keycloak-proxy. - Contraseñas seguras: cambia inmediatamente todas las contraseñas de ejemplo usadas en este manual (
keycloak_password,replicator_password,postgres_password,keycloak_vrrp_secret). Usa un gestor de secretos como HashiCorp Vault o AWS Secrets Manager. - Reglas de firewall: implementa reglas estrictas entre capas. Los nodos Keycloak solo deben aceptar conexiones desde los balanceadores en el puerto 8080. Los nodos de base de datos solo deben aceptar conexiones desde los nodos Keycloak en el puerto 5432. Los puertos de etcd (2379/2380) deben ser accesibles únicamente entre los nodos db.
- Consola de administración: restringe el acceso a
/auth/admin/por IP mediante una directivaallow/denyen Nginx, o usa un VPN. - Backups de PostgreSQL: configura copias de seguridad periódicas con
pg_basebackupopgBackRest. Patroni no es un sustituto de los backups: protege contra fallos, no contra errores humanos. - Monitorización: expone las métricas de Keycloak (
metrics-enabled=true) a Prometheus y visualízalas en Grafana. Monitoriza el estado del clúster Patroni conpatroni_exportery el estado de etcd con su propio exporter. - Rotación de logs: configura
logrotatepara los logs de Nginx y Keycloak para evitar el agotamiento del espacio en disco.
:wq!