Saltar a contenido

Migrar un servicio legacy a k3s

Procedimiento para migrar un servicio que hoy corre en Docker Compose / Portainer (hosts lebron/steph) al cluster k3s. Referencia canónica: Heimdall (2026-09-07, PRs gorkarevilla/homelab#67 y #68).

Flujo general (dos PRs)

  1. PR 1 — manifiestos + rutas + documentación. Despliega el servicio en el cluster (accesible solo interno). No toca Cloudflare.
  2. Migración de datos — copiar el estado del legacy al PVC (manual).
  3. PR 2 — Cloudflare IaC (DNS + túnel), solo si el servicio se expone externamente. Corte atómico tras migrar los datos.
  4. Retirar legacy — quitar el servicio del docker-compose.yml en el repo gorkarevilla/portainer-compose.

Antes de nada, seguir el flujo de diseño/plan del repo: spec en docs/specs/YYYY-MM-DD-<servicio>-design.md y plan en docs/plans/YYYY-MM-DD-<servicio>.md.

Precondiciones

  • kubectl apuntando al cluster: export KUBECONFIG="$PWD/k3s/ansible/.kubeconfig".
  • gh autenticado (para abrir PRs).
  • terraform ≥ 1.15.8 (solo fmt/validate local; el plan/apply los hace el CI).
  • Acceso SSH a los hosts legacy (ver abajo).

Hosts legacy (SSH)

Host IP Usuario Notas
lebron 192.168.1.2 ramuh Portainer + casi todos los stacks. Usuario con sudo y grupo docker.
~~steph~~ 192.168.1.4 ramuh ~~webs gorkarevilla.com y mirenygorka~~ → Vercel, VM parada (sep 2026).
ssh ramuh@192.168.1.2    # lebron (key-based auth, sin password)

Los datos de los stacks viven en /opt/<stack>/<servicio>/config (propiedad ramuh), p. ej. /opt/utils/heimdall/config. Para localizar el servicio y su config exactos, ver su página en docs/legacy/<servicio>.md y el docker-compose.yml correspondiente en el repo gorkarevilla/portainer-compose (carpeta = stack).

PR 1 — manifiestos + rutas + docs

Estructura (referencia: Heimdall):

clusters/luka/apps/<svc>/application.yaml                 # Argo CD Application (CreateNamespace, autosync)
clusters/luka/config/<svc>/deployment.yaml
clusters/luka/config/<svc>/pvc.yaml                       # + annotation Prune=false,Delete=false
clusters/luka/config/<svc>/service.yaml
clusters/luka/config/traefik-routes/<svc>-httproute.yaml  # interna (websecure) + externa (web)
clusters/luka/config/traefik-routes/<svc>-referencegrant.yaml

Reglas:

  • Application: path: clusters/luka/config/<svc>, namespace <svc>, CreateNamespace=true, autosync prune+selfHeal. La descubre app-of-apps.
  • PVC: SIEMPRE la anotación argocd.argoproj.io/sync-options: Prune=false,Delete=false (el PVC es la única copia del estado; lo mismo que vaultwarden-cluster.yaml).
  • Deployment: imagen pinneada (no latest); si el servicio guarda estado en un SQLite/archivo sobre el PVC → strategy: Recreate + replicas: 1. Mantener PUID/PGID/TZ del legacy y securityContext.fsGroup coherente.
  • HTTPRoute: dos rutas si se expone externo — interna <svc>.local.gorkarevilla.com (websecure, cert wildcard) y externa <svc>.gorkarevilla.com (web, HTTP plano para el túnel). La externa queda inerte hasta el PR 2.
  • ReferenceGrant en el ns <svc> (Traefik → Service <svc>).

Documentación obligatoria (AGENTS.md), en el mismo PR 1:

  1. docs/services/<svc>.md (plantilla docs/services/_template.md).
  2. Actualizar docs/services/index.md (tabla Aplicaciones).
  3. Añadir al nav de mkdocs.yml (sección Aplicaciones).
  4. Marcar docs/legacy/<svc>.md, docs/legacy/index.md (mapa de migración) y docs/legacy/roadmap.md como migrado.
  5. Actualizar el roadmap del README.md.

Secrets: si el servicio legacy usa variables de entorno (se ven en la UI de Portainer, no se commitean), darlas de alta por Vaultwarden → VKS (ver docs/runbooks/vaultwarden.md y AGENTS.md), no Sealed Secret salvo bootstrap.

Commit con convención feat(<svc>): ... y PR contra main (squash).

Migración de datos

Con el servicio desplegado (PR 1 mergeado) y antes del PR 2. El legacy queda parado (no se rearranca) para evitar divergencia; la URL externa caerá brevemente hasta el corte.

# 1. Parar legacy (restart=no) + checksum + empaquetar (fuente quiesced)
ssh ramuh@192.168.1.2 'docker update --restart=no <svc> && docker stop <svc> && \
  sha256sum /opt/<stack>/<svc>/config/<ruta-estado> | tee /tmp/<svc>-sha.txt && \
  tar -C /opt/<stack>/<svc> -czf /tmp/<svc>-config.tgz config'

# 2. Traer a local
scp ramuh@192.168.1.2:/tmp/<svc>-config.tgz /tmp/<svc>-config.tgz
scp ramuh@192.168.1.2:/tmp/<svc>-sha.txt /tmp/<svc>-sha.txt

# 3. Escalar a 0 (sin escritor) y restaurar con pod temporal que monta el PVC
export KUBECONFIG="$PWD/k3s/ansible/.kubeconfig"
kubectl -n <svc> scale deploy <svc> --replicas=0
kubectl -n <svc> apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: <svc>-restore
  namespace: <svc>
spec:
  restartPolicy: Never
  containers:
    - name: restore
      image: busybox:1.36
      command: ["sleep", "600"]
      volumeMounts:
        - name: config
          mountPath: /config
  volumes:
    - name: config
      persistentVolumeClaim:
        claimName: <svc>-config
EOF
kubectl -n <svc> wait --for=condition=Ready pod/<svc>-restore --timeout=120s
kubectl -n <svc> cp /tmp/<svc>-config.tgz <svc>-restore:/tmp/<svc>-config.tgz
kubectl -n <svc> exec <svc>-restore -- tar -xzf /tmp/<svc>-config.tgz -C /

# 4. Verificar checksum EN el pod temporal (busybox trae sha256sum) y borrarlo
kubectl -n <svc> exec <svc>-restore -- sha256sum /config/<ruta-estado>
cat /tmp/<svc>-sha.txt   # deben coincidir
kubectl -n <svc> delete pod <svc>-restore

# 5. Escalar a 1
kubectl -n <svc> scale deploy <svc> --replicas=1
kubectl -n <svc> rollout status deploy/<svc> --timeout=120s

El tarball contiene config/…; al extraer en / se escribe en /config. El tar preserva el propietario del legacy (coincide con PUID/PGID). El checksum se calcula en el pod temporal de busybox, no en la imagen de la app (que puede no traer sha256sum).

PR 2 — Cloudflare IaC (solo exposición externa)

En terraform/cloudflare/:

  1. dns.tf: quitar el hostname de local.proxied_hosts (deja de apuntar al túnel Lebron) y añadir un cloudflare_record dedicado <svc> → homelabtunnel.gorkarevilla.com (patrón del recurso passwords).
  2. tunnel.tf: añadir una ingress_rule <svc>.gorkarevilla.com → http://traefik.traefik.svc.cluster.local:80 en la config del túnel homelab.
  3. docs/network/cloudflare.md: quitar el hostname de la lista hometunnel, añadirlo a "Otros CNAME" y a la tabla de ingress del túnel homelab.

Commit feat(cloudflare): .... El terraform plan lo publica el CI (cloudflare-plan.yml); el apply es automático al mergear a main (cloudflare-apply.yml). Mergear solo después de migrar los datos.

Retirar legacy

En el repo gorkarevilla/portainer-compose:

  1. Quitar el servicio del docker-compose.yml de su stack.
  2. PR contra main (squash).

El contenedor legacy ya queda parado (docker update --restart=no <svc>); al quitar el servicio del compose, Portainer deja de recrearlo en redeploy.

Checklist final

  • [ ] Servicio Synced/Healthy en Argo CD; pod Running; PVC Bound.
  • [ ] UI responde 200 interno (https://<svc>.local.gorkarevilla.com).
  • [ ] Datos migrados y checksum verificado.
  • [ ] Externo 200 (https://<svc>.gorkarevilla.com) tras el PR 2 (si aplica).
  • [ ] Contenedor legacy parado (restart=no).
  • [ ] Servicio fuera del docker-compose.yml legacy (PR en portainer-compose).
  • [ ] Documentación actualizada (services, index, mkdocs, legacy, README).