Plataforma
Los servicios de plataforma que FluxCD despliega antes que cualquier aplicación, nivel a nivel.
Nivel 1: red
Calico
La CNI del clúster. Flux no puede instalarla (sin CNI no arranca ningún pod, ni siquiera los de Flux), así que la instala el stack de Kubernetes al construir el clúster, y Flux adopta ese Helm release.
| Chart | tigera-operator, release calico en tigera-operator |
| Red de pods | 172.16.0.0/16, IP-in-IP |
| Valores | infrastructure/base/10-networking/controllers/calico/values.yaml |
El HelmRelease tiene el mismo nombre y namespace que la instalación inicial, de forma que Flux lo actualiza en lugar de instalar uno nuevo. Además, eliminarlo de Git no desinstala Calico y Flux nunca es dueño de su namespace.
Calico no tiene rollback automático: volver atrás la red del clúster es una decisión manual. Si una actualización rompe la red, Flux cae con ella. La API funciona en la red del host, así que desde un equipo con acceso:
helm rollback calico -n tigera-operator
flux suspend helmrelease calico -n tigera-operator
Una versión menor nueva requiere aplicar antes sus CRDs a mano y comprobar que soporta la versión de Kubernetes del clúster.
MetalLB
Sin balanceador de la nube, MetalLB da a los servicios LoadBalancer una dirección de la LAN y la anuncia por ARP. Cada entorno tiene dos pools:
| Entorno | ingress (a mano) | default (automático) |
|---|---|---|
| testing | 192.168.1.170 – .171 | 192.168.1.172 – .179 |
| production | 192.168.1.180 – .181 | 192.168.1.182 – .189 |
Las dos direcciones de ingress están reservadas para Traefik (intranet y extranet). Cualquier otro servicio recibe la siguiente libre de default.
Estos rangos deben quedar fuera del DHCP, y dos clústeres nunca deben anunciar el mismo rango a la vez.
Reloader
Un pod lee sus Secrets y ConfigMaps al arrancar. Reloader reinicia los workloads cuando cambian, de forma que una contraseña rotada en Git llega a la aplicación sin pasos manuales. Solo actúa sobre los workloads que lo piden:
metadata:
annotations:
reloader.stakater.com/auto: "true"
Metrics Server
Recoge el uso actual de CPU y memoria de nodos y pods; es lo que lee kubectl top. Usa --kubelet-insecure-tls porque los kubelets usan los certificados autofirmados de kubeadm. El histórico vive en la monitorización externa.
Nivel 2: ingress
cert-manager
Pide los certificados a Let's Encrypt, los guarda como Secrets y los renueva 30 días antes de caducar. Un Ingress lo pide con una anotación.
| ClusterIssuer | Reto | Uso |
|---|---|---|
letsencrypt-dns | DNS-01 con la API de Cloudflare | Cualquier nombre, accesible desde Internet o no |
letsencrypt-http | HTTP-01 a través del Traefik extranet | Nombres publicados en Internet |
letsencrypt-staging-dns | DNS-01 contra el entorno de pruebas | Probar la configuración sin gastar el límite de Let's Encrypt |
En testing todo usa letsencrypt-dns. El token de Cloudflare (permiso Zone › DNS › Edit) se guarda cifrado:
sops infrastructure/testing/20-ingress/configs/cloudflare-api-token-secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: cloudflare-api-token-secret
namespace: cert-manager-system
stringData:
api-token: <token>
Traefik
El ingress controller. Hay dos instancias separadas, de forma que un servicio pensado para la LAN no se pueda publicar en Internet por error:
| Instancia | IngressClass | Accesible desde | testing |
|---|---|---|---|
traefik-intranet | intranet | La LAN | 192.168.1.170 |
traefik-extranet | extranet | Internet, a través de Cloudflare (en producción) | 192.168.1.171 |
Las dos tienen dos réplicas en nodos diferentes, redirigen HTTP a HTTPS, conservan la IP real del cliente y no tienen clase por defecto: un Ingress sin clase no lo sirve ninguna. En testing ninguna está publicada; las dos clases existen para replicar producción.
Publicar un servicio:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app
annotations:
cert-manager.io/cluster-issuer: letsencrypt-dns
spec:
ingressClassName: intranet # o extranet
tls:
- hosts: [app.test.intranet.moon.cat]
secretName: app-tls
rules:
- host: app.test.intranet.moon.cat
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: app
port:
number: 80
El registro DNS del nombre debe apuntar a la dirección de la instancia elegida. En Sora, se crea como alias en la red de Horizon.
Observabilidad
Dentro del clúster no se guarda nada. Un único colector, Grafana Alloy, recoge métricas, logs y trazas y los envía a la máquina de monitorización. Todo lleva la etiqueta cluster (tanya en testing).
| Señal | Origen | Cómo se selecciona |
|---|---|---|
| Métricas | kubelet, cAdvisor, kube-state-metrics | Siempre |
| Métricas | Workloads que las exponen | Anotación k8s.grafana.com/scrape: "true" |
| Logs | Pods | Etiqueta monitoring/logs: "true" |
| Logs | kubelet.service y crio.service de cada nodo | Siempre |
| Logs, trazas y métricas | Aplicaciones con OpenTelemetry | Lo que envíen |
Las métricas de CPU, memoria y disco de cada nodo no se recogen aquí: las publica el Node Exporter de cada VM, que sigue informando aunque Kubernetes esté caído.
Las aplicaciones envían OTLP por HTTP al receptor del clúster, sin credenciales:
OTEL_EXPORTER_OTLP_ENDPOINT=http://k8s-monitoring-alloy-receiver.monitoring-system.svc:4318
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
Una aplicación con SDK de OpenTelemetry envía sus logs por OpenTelemetry y no lleva la etiqueta monitoring/logs; con ambas, cada línea se guarda dos veces. La etiqueta es para lo que no tiene OpenTelemetry (Traefik, cert-manager, Keycloak, ...).
Las credenciales de envío son las salidas del trabajo traefik del stack de monitorización, guardadas cifradas:
sops infrastructure/testing/30-observability/configs/monitoring-ingest.yaml
apiVersion: v1
kind: Secret
metadata:
name: monitoring-ingest
namespace: monitoring-system
stringData:
username: <usuario de ingesta>
password: <contraseña de ingesta>
Para comprobar qué llega, en Grafana › Explore: up{cluster="tanya"} == 0 en Prometheus (targets caídos) o {cluster="tanya", namespace="traefik-system"} en Loki.
Keycloak
El proveedor de identidad: las aplicaciones delegan el login en él mediante OpenID Connect. Se instala con el Keycloak Operator oficial, siguiendo los tags de su repositorio.
| testing | |
|---|---|
| Hostname | auth.test.lb.moon.cat |
| Ingress | extranet, certificado de letsencrypt-dns |
| Base de datos | keycloak en mio-akiyama.horizon.moon.cat:6432 (PgBouncer) |
FluxCD gestiona el operador, el servidor, su base de datos, su nombre y su ingress. Lo que hay dentro (realms, clientes, usuarios, ...) lo configura Horizon. La base de datos empieza vacía y Keycloak crea sus tablas al arrancar; se crea antes con la acción Create User del stack de PostgreSQL, y sus credenciales se guardan cifradas:
sops infrastructure/testing/40-identity/configs/keycloak-pg-auth.yaml
apiVersion: v1
kind: Secret
metadata:
name: keycloak-pg-auth
namespace: keycloak-system
stringData:
username: keycloak
password: <contraseña>
Al primer arranque, el operador crea un administrador temporal en el secret keycloak-initial-admin, que es el que se usa para conectar Horizon.
Tema moon
La página de login usa el tema moon, guardado en el repositorio y montado sobre la imagen oficial: sin imagen propia ni volúmenes. Muestra una imagen de fondo junto a la tarjeta de login, se adapta a PC, tablet y móvil, sigue el modo claro u oscuro del navegador y, en testing, añade una cinta TESTING ENVIRONMENT. Solo extiende el tema de Keycloak, sin sobrescribir plantillas, para que sus actualizaciones sigan funcionando. Cada realm lo elige como tema de login desde Horizon.
Validar:
kubectl -n keycloak-system get pods # keycloak-operator-…, keycloak-0 Running
curl -s https://auth.test.lb.moon.cat/realms/master/.well-known/openid-configuration | jq -r .issuer
# https://auth.test.lb.moon.cat/realms/master
El issuer debe ser exactamente el nombre público por https. Cualquier otra cosa significa que el hostname o las cabeceras del proxy son incorrectos, y los logins fallarán.