lab de una mañana · 2026-07-04

OpsGuard-AI sobre k3s
lo que se desplegó, qué funciona y qué no

No un resumen para creerte: un log para que lo veas, lo repliques desde cero y lo trastees. Cada comando de aquí es copiable y se ejecutó de verdad en tu nodo nobara-pc.

clúster k3s v1.34.6 1 nodo · nobara-pc timebox ~4h · usado ~35 min namespace opsguard sin API key (solo Gate 1)

// Los 5 objetivos, estado real

Verde = funciona y lo verificaste. Ámbar = matiz importante. Rojo = no entra hoy, y por qué no es deuda.

01 · cluster
k3s sano + kubectl
Ya estaba corriendo (91 días). No se montó hoy — honesto.
02 · image
Imagen en containerd
Build con Docker → importada al containerd de k3s (389 MiB).
03 · manifests
Deploy·Svc·Ing·Secret
Secret y Deployment sólidos. Service/Ingress dan 502: sin backend HTTP.
04 · scan
Escaneo real en clúster
Job → Gate 1 caza 2 claves AWS → BLOCK. Logs vía kubectl.
05 · métricas
Prometheus/Grafana
No entra. OpsGuard no tiene /metrics. Es el lab 2, no deuda.

// Qué hay vivo ahora mismo

Salida literal de kubectl -n opsguard get all,ingress,secret,configmap. Esto sigue corriendo en tu máquina mientras lees — puedes abrir otra terminal y contrastarlo.

kubectl -n opsguard get all,ingress,secret,configmap
kubectl -n opsguard get all,ingress,secret,configmap
salida real
NAME                                   READY   STATUS    RESTARTS   AGE
pod/opsguard-runner-7ddc69655c-2kgts   1/1     Running   0          77m

NAME               TYPE        CLUSTER-IP     PORT(S)   AGE
service/opsguard   ClusterIP   10.43.94.180   80/TCP    77m

NAME                              READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/opsguard-runner   1/1     1            1           77m

NAME                                 CLASS     HOSTS                ADDRESS        PORTS
ingress.../opsguard                  traefik   opsguard.localhost   192.168.1.51   80

NAME                      TYPE     DATA   AGE
secret/opsguard-secrets   Opaque   1      77m
configmap/opsguard-scan-demo   1    74m

El Job/opsguard-scan ya no aparece: se autoborró con ttlSecondsAfterFinished: 3600 (1 h tras terminar). Es correcto — un Job es de usar y tirar. Más abajo tienes el comando para relanzarlo cuando quieras.

// El hallazgo honesto (y por qué es lo mejor del lab)

Esto es lo que el clúster te dijo y tu resumen mental no: OpsGuard no es un servicio.

El Ingress devuelve HTTP 502 — a propósito

OpsGuard es una CLI batch: escanea un diff, imprime el veredicto y termina. No escucha en ningún puerto. Así que el Service y el Ingress enrutan hacia un backend que no existe. Verificado:

curl al Ingress — comprobar el 502
curl -s -o /dev/null -w "HTTP %{http_code}\n" \
  -H "Host: opsguard.localhost" http://192.168.1.51/

# respuesta:
HTTP 502   # Traefik y el Service resuelven; no hay nada escuchando detrás

Traducción para entrevista: el primitivo K8s correcto para OpsGuard es un Job / CronJob disparado por CI, no un Deployment con Service e Ingress. El Secret (API key aislada, consumida por envFrom) y el Job son defendibles al 100%. El Service e Ingress se dejaron escritos como práctica de manifiestos, con el 502 a la vista y anotado en los propios ficheros. Nada maquillado.

// Cómo fue, paso a paso

La bitácora del diario. Incluye los dos tropiezos, porque replicar sin ellos te haría chocar igual.


$ Replicar desde cero

Si borras todo (kubectl delete ns opsguard) y quieres reconstruirlo, esto es el camino completo. Ejecuta desde projects/ops_guard/.

0 · kubectl operativo sin sudo (una vez)

kubeconfig de usuario
mkdir -p ~/.kube
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
sudo chown $USER:$USER ~/.kube/config && chmod 600 ~/.kube/config
export KUBECONFIG=$HOME/.kube/config   # k3s exporta uno global que hay que pisar
kubectl get nodes

1 · construir la imagen e importarla a k3s

build + save + import
docker build -t opsguard:lab .
docker save opsguard:lab -o /tmp/opsguard-lab.tar
sudo k3s ctr images import /tmp/opsguard-lab.tar
sudo k3s ctr images ls | grep opsguard   # verificar que está en containerd

2 · namespace y Secret (la key nunca va en git)

namespace + secret imperativo
kubectl apply -f deploy/k8s/00-namespace.yaml

# Secret real, fuera de git. Gate 1 no usa la key (valor dummy vale).
# Para usar Gate 2 (LLM), pon aquí tu OPENROUTER_API_KEY real.
kubectl -n opsguard create secret generic opsguard-secrets \
  --from-literal=OPENROUTER_API_KEY='dummy-not-used-by-gate1'

3 · desplegar y verificar

apply runner + service + ingress
kubectl apply -f deploy/k8s/20-deployment.yaml \
              -f deploy/k8s/30-service.yaml \
              -f deploy/k8s/40-ingress.yaml
kubectl -n opsguard rollout status deploy/opsguard-runner
kubectl -n opsguard get pods,svc,ingress

4 · lanzar el escaneo real (el Job)

configmap + job + logs
kubectl apply -f deploy/k8s/45-configmap-scan.yaml \
              -f deploy/k8s/50-job-scan.yaml

# el Job termina en Failed a propósito (BLOCK = exit≠0 = fail-closed)
kubectl -n opsguard wait --for=condition=failed job/opsguard-scan --timeout=60s
kubectl -n opsguard logs job/opsguard-scan
lo que verás en los logs
🛡️  OpsGuard-AI — Security Gate Active

🚨 DETECTED 2 STATIC VIOLATIONS:
  [AWS Access Key]  AKIAIOSFODNN7EXAMPLE          Diff
  [AWS Secret Key]  wJalrXUtnFEMI/K7MDENG/bPxRf...  Diff

⛔ PIPELINE BLOCKED: SECURITY VIOLATION DETECTED

$ Trastear

Cosas para meterle mano y entender cómo responde. Todo sobre lo que ya está desplegado.

Entrar en el pod runner y escanear a mano

exec interactivo
kubectl -n opsguard exec -it deploy/opsguard-runner -- sh
# dentro del pod:
echo $OPENROUTER_API_KEY        # viene del Secret por envFrom
cd /tmp && git init demo && cd demo
git config user.email t@t.co && git config user.name t
cp /app/opsguard.yml . && echo ok > a.py && git add . && git commit -qm base
echo 'AWS_KEY="AKIAIOSFODNN7EXAMPLE"' > leak.py && git add leak.py
opsguard scan                   # mira cómo bloquea

Cambiar el caso de prueba y relanzar el Job

editar el ConfigMap → recrear Job
# edita el script del escaneo (prueba un fichero SIN secretos y mira que pase)
kubectl -n opsguard edit configmap opsguard-scan-demo

kubectl -n opsguard delete job opsguard-scan --ignore-not-found
kubectl apply -f deploy/k8s/50-job-scan.yaml
kubectl -n opsguard logs -f job/opsguard-scan

Confirmar tú mismo el 502 del Ingress

curl
curl -s -o /dev/null -w "HTTP %{http_code}\n" \
  -H "Host: opsguard.localhost" http://192.168.1.51/
# HTTP 502 → no hay backend HTTP. Ese es el punto.

Ver el escalado, describe, eventos

observar el runner
kubectl -n opsguard describe deploy opsguard-runner
kubectl -n opsguard get events --sort-by=.lastTimestamp | tail -20
kubectl -n opsguard scale deploy/opsguard-runner --replicas=2   # y vuelve a 1

Borrar todo cuando acabes

teardown
kubectl delete ns opsguard          # se lleva pod, svc, ingress, secret, configmap
sudo k3s ctr images rm docker.io/library/opsguard:lab   # opcional: la imagen

El namespace opsguard está aislado de tu homelab (Grafana, smartctl-exporter siguen intactos en Docker). Borrarlo no toca nada más.

// Lo que decides tú (no lo hace la IA)

El lab es materia prima. Estas dos cosas son tuyas — por eso no las toqué.

Decisión 1 · el post

Escribir el bruto del primer post de la serie con tu voz. La historia fuerte ya está: “desplegué mi herramienta en k8s y el clúster me dijo la verdad — no es un servicio, es un Job, y el Ingress me devolvió un 502 honesto”. El diario en deploy/k8s/DIARIO_LAB.md es tu materia prima; el borrador sale luego de /linkedin-editor con tu texto.

Decisión 2 · el CV

Si OpsGuard vuelve al CV, mi propuesta es que sea como “desplegado en Kubernetes (k3s) como Job de CI con secreto gestionado”, nunca como microservicio (el 502 lo desmonta en entrevista). Es una propuesta: cualquier cambio pasa por _meta/cv_maestro/AGENT.md y por tu instrucción explícita en otra sesión.