Saltar a contenido

Mantener sincronizado el usuario de base de datos

Usuarios de base de datos crea el usuario tenax una sola vez. Eso basta en la mayoría de los casos. Esta página es para entornos GitOps en los que el usuario debe seguir coincidiendo con su Secret sin que nadie ejecute nada:

  • la contraseña del Secret se rota;
  • la base de datos se reconstruye o se restaura desde un volcado antiguo, y el usuario desaparece;
  • alguien cambia el usuario a mano (quita un permiso, cambia la contraseña);
  • la base de datos se crea después de los manifiestos que describen su usuario (un clúster nuevo).

Un Job de un solo uso no resuelve ninguno de estos casos: se ejecuta una vez, y Kubernetes no vuelve a ejecutar un Job terminado cuando cambia el Secret. Un CronJob que vuelve a aplicar el mismo SQL idempotente sí: en un periodo como máximo, el usuario vuelve a coincidir con el Secret.

Cómo funciona

Un CronJob por base de datos, en el namespace de la base de datos. En cada ejecución:

  1. espera a que la base de datos responda (con el límite del deadline del Job);
  2. inicia sesión como administrador, con el Secret que usa la propia base de datos;
  3. crea el usuario tenax si falta y le asigna la contraseña y los permisos a partir del Secret de Tenax.

Cada sentencia es idempotente, así que una ejecución que lo encuentra todo en su sitio no cambia nada. Las contraseñas llegan al contenedor como variables de entorno desde Secrets; nunca aparecen en una línea de comandos ni en un log.

PostgreSQL

Sustituye los cuatro valores marcados con # ←: el namespace, el Service de la base de datos, la base de datos, y el Secret y la clave que contienen la contraseña de administrador.

apiVersion: batch/v1
kind: CronJob
metadata:
  name: tenax-user-postgres
  namespace: team-a                                   # ←
spec:
  schedule: "*/30 * * * *"
  concurrencyPolicy: Forbid
  successfulJobsHistoryLimit: 1
  failedJobsHistoryLimit: 3
  jobTemplate:
    spec:
      backoffLimit: 2
      activeDeadlineSeconds: 300
      template:
        spec:
          restartPolicy: OnFailure
          automountServiceAccountToken: false
          securityContext: { runAsNonRoot: true, runAsUser: 999, runAsGroup: 999 }
          containers:
            - name: psql
              image: docker.io/library/postgres:16
              resources: { requests: { cpu: 10m, memory: 32Mi }, limits: { memory: 128Mi } }
              securityContext: { allowPrivilegeEscalation: false, capabilities: { drop: [ALL] } }
              env:
                - { name: PGHOST, value: postgres }                     # ← the database's Service
                - { name: PGDATABASE, value: app }                      # ← any database on the server
                - { name: PGUSER, value: postgres }
                - { name: PGCONNECT_TIMEOUT, value: "10" }
                - name: PGPASSWORD
                  valueFrom: { secretKeyRef: { name: postgres-admin, key: password } }   # ←
                - name: TENAX_PASSWORD
                  valueFrom: { secretKeyRef: { name: tenax-postgres, key: password } }
              command: [sh, -ec]
              args:
                - |
                  until pg_isready -q; do echo "waiting for $PGHOST"; sleep 5; done
                  psql -v ON_ERROR_STOP=1 -X <<'SQL'
                  \getenv tenax_password TENAX_PASSWORD
                  SELECT 'CREATE ROLE tenax LOGIN REPLICATION' WHERE NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'tenax') \gexec
                  ALTER ROLE tenax WITH LOGIN REPLICATION NOSUPERUSER NOCREATEDB NOCREATEROLE PASSWORD :'tenax_password';
                  GRANT pg_read_all_data, pg_monitor TO tenax;
                  ALTER ROLE tenax SET idle_in_transaction_session_timeout = '10min';
                  SQL
                  echo "tenax user in sync on $PGHOST"

\getenv requiere psql 15 o posterior en la imagen; el servidor puede ser cualquier versión soportada. Define max_slot_wal_keep_size en la configuración del servidor (por ejemplo -c max_slot_wal_keep_size=2GB), no aquí: es un ajuste del servidor, no forma parte del usuario.

MySQL

apiVersion: batch/v1
kind: CronJob
metadata:
  name: tenax-user-mysql
  namespace: team-a                                   # ←
spec:
  schedule: "*/30 * * * *"
  concurrencyPolicy: Forbid
  successfulJobsHistoryLimit: 1
  failedJobsHistoryLimit: 3
  jobTemplate:
    spec:
      backoffLimit: 2
      activeDeadlineSeconds: 300
      template:
        spec:
          restartPolicy: OnFailure
          automountServiceAccountToken: false
          securityContext: { runAsNonRoot: true, runAsUser: 999, runAsGroup: 999 }
          containers:
            - name: mysql
              image: docker.io/library/mysql:8.4
              resources: { requests: { cpu: 10m, memory: 64Mi }, limits: { memory: 256Mi } }
              securityContext: { allowPrivilegeEscalation: false, capabilities: { drop: [ALL] } }
              env:
                - { name: MYSQL_HOST, value: mysql }                    # ← the database's Service
                - name: ROOT_PASSWORD
                  valueFrom: { secretKeyRef: { name: mysql-admin, key: root-password } }  # ←
                - name: TENAX_PASSWORD
                  valueFrom: { secretKeyRef: { name: tenax-mysql, key: password } }
              command: [bash, -ec]
              args:
                - |
                  umask 077
                  printf '[client]\nuser=root\npassword=%s\nhost=%s\nconnect-timeout=10\n' "$ROOT_PASSWORD" "$MYSQL_HOST" > /tmp/root.cnf
                  until mysqladmin --defaults-extra-file=/tmp/root.cnf ping --silent; do echo "waiting for $MYSQL_HOST"; sleep 5; done
                  mysql --defaults-extra-file=/tmp/root.cnf <<SQL
                  CREATE USER IF NOT EXISTS 'tenax'@'%' IDENTIFIED BY '${TENAX_PASSWORD}';
                  ALTER USER 'tenax'@'%' IDENTIFIED BY '${TENAX_PASSWORD}';
                  GRANT SELECT, SHOW VIEW, TRIGGER, EVENT, LOCK TABLES, PROCESS, RELOAD, SHOW_ROUTINE,
                        REPLICATION CLIENT, REPLICATION SLAVE ON *.* TO 'tenax'@'%';
                  SQL
                  rm -f /tmp/root.cnf
                  echo "tenax user in sync on $MYSQL_HOST"

La contraseña entra en el SQL sin más escapado que el del literal de cadena, así que genérala en hexadecimal (openssl rand -hex 24).

Ejecutarlo ahora

kubectl -n team-a create job --from=cronjob/tenax-user-postgres tenax-user-now
kubectl -n team-a logs -f job/tenax-user-now

Cada 30 minutos es más que suficiente: el propio Tenax reintenta un inicio de sesión rechazado y lo informa en el objetivo, así que una rotación se recoge como mucho un periodo después. La regla de pg_hba.conf y los ajustes del binlog siguen siendo configuración del servidor (Usuarios de base de datos, paso 3); este CronJob no los toca.