Saltar al contenido principal

GitLab: migración a VM nueva y upgrade de 15.11 a la última versión

· 32 min de lectura
Franco R.
El chico de software @ sils.tech

Nuestro GitLab self-hosted (gitlab.sils.tech) está en 15.11.5, una versión de mayo de 2023, corriendo sobre Ubuntu 18.04. Para esa versión de Ubuntu no hay paquetes de GitLab posteriores a la 16.11, así que no alcanza con actualizar el paquete: hay que mover GitLab a una VM nueva.

La estrategia es dejar el GitLab actual fuera de servicio y congelado durante la migración. Así nadie puede escribir mientras copiamos los datos y no se pierde nada. La VM vieja queda apagada e intacta como plan de vuelta atrás.

Este post es el plan de trabajo. Los checkboxes se van tildando a medida que avanzamos.

Estado general​

FaseQuéDowntimeEstado
0Preparación en la VM actual (disco, backups, aviso)No✅ Hecho
1Armar la VM nueva, la DB nueva y la VM de runnersNo✅ Hecho
2Ensayo reducido con datos reales (restore + saltos hasta 16.11 + PG 14→17)No✅ Hecho
3Ventana: congelar, copiar, restaurarSí✅ Hecho (17:01 → 17:24 ART)
4Ventana: upgrades encadenados hasta la última versiónSí✅ Hecho (17:25 → 21:05 ART), 19.5.1
5Ventana: cutover de IP, verificación y reaperturaSí✅ Reabierto 21:32 ART (faltan pruebas de Jenkins/Sonar y el aviso)
6Post-migración y baja de la VM viejaNo⬜ Pendiente

Relevamiento (2026-10-22)​

ItemValor
VMgitlab · us-central1-a · sils-keeper-infra · custom-2-11008 (2 vCPU, ~10,7 GB de RAM) · disco pd-standard de 100 GB
IPgitlab-ip = 34.172.216.218 (estática)
SOUbuntu 18.04 (bionic). Lleva ~408 días sin reiniciar
GitLabEE 15.11.5, instalado con Omnibus, sin licencia (funciones de la versión Free)
Base de datosExterna: Cloud SQL pg-keeper-instance (PostgreSQL 14), base gitlabhq_production. En la misma instancia están sonarqube y sils_busquedas
Postgres localCorre pero no se usa
Volumen40 usuarios activos · 240 proyectos · repos de ~870 MB · uploads de ~240 MB
Runners10 runners con executor Docker, gitlab-runner 17.0.0, en la misma VM
DiscoAl 99%, con 1,6 GB libres. ~73 GB son caches de runners e imágenes Docker sin uso
BackupsÚltimo gitlab-backup de 2023-05. No hay cron de backup. Snapshot manual snapshot-gitlab del 2026-10-21
TLSCertbot en modo standalone, renovado por un cron de root (frena el nginx de GitLab)

Condiciones que definen el plan​

  • No hay paquetes para Ubuntu 18.04 después de la 16.11 → hace falta VM nueva.
  • gitlab-backup no puede hacer backup de la DB: Omnibus 15.11 trae pg_dump 12 y 13, y el servidor es PostgreSQL 14. La DB se copia con Cloud SQL y gitlab-backup se corre con SKIP=db.
  • GitLab 18 exige PostgreSQL 16.5 o más, y GitLab 19 exige 17 o más. GitLab 17 solo soporta hasta la 16, así que hacemos un solo upgrade de Cloud SQL, de 14 a 17, entre 17.11 y 18.2, con GitLab frenado. Como pg-keeper-instance comparte la instancia con Sonar y sils_busquedas, no la actualizamos: GitLab pasa a una instancia de Cloud SQL propia (pg-gitlab), que sale de un clon. Eso además deja la DB original intacta para volver atrás.
  • Sin licencia no tenemos el Maintenance Mode de GitLab (es de Premium). El "fuera de servicio" se arma con un aviso previo (broadcast message), una página 503 en nginx y los servicios de la aplicación frenados.

Arquitectura destino​

ComponenteAntesDespués
GitLabVM gitlab, Ubuntu 18.04, 15.11.5VM gitlab-v2, Ubuntu 22.04, e2-standard-4, disco pd-balanced, última versión
DBpg-keeper-instance (compartida, PG 14, IP pública)pg-gitlab (dedicada, PG 17, IP privada)
RunnersEn la VM de GitLabVM gitlab-runners aparte
IP / DNSgitlab-ip en gitlabgitlab-ip en gitlab-v2 (el DNS no cambia)
BackupsNingunogitlab-backup diario a GCS + backups automáticos de Cloud SQL + snapshot schedule

Usamos Ubuntu 22.04 porque tiene paquetes de 15.11.5 (hay que restaurar sobre la misma versión) y de todas las versiones siguientes. Ubuntu 24.04 recién tiene paquetes desde la 17.x; el salto a 24.04 queda como tarea futura.

Fase 0: preparación en la VM actual (sin downtime)​

  • Liberar disco borrando las caches de los runners: docker volume prune --all -f (Docker 24 no borra volúmenes con nombre sin --all) y docker image prune -a -f. Se liberaron 43 GB de volúmenes y 29 GB de imágenes
  • Verificar que quedan más de 20 GB libres (df -h /): pasó del 98% al 25% (74 GB libres)
  • Copiar /etc/gitlab/gitlab.rb, /etc/gitlab/gitlab-secrets.json, /etc/letsencrypt/, /etc/gitlab-runner/config.toml, las host keys de SSH y el crontab de root al bucket privado y versionado gs://sils-gitlab-backups/config/2026-10-22/
  • gitlab-backup create SKIP=db STRATEGY=copy de prueba: 11,6 min y 1,36 GB (1792669088_2026_10_22_15.11.5-ee, queda en /var/opt/gitlab/backups/ de la VM vieja)
  • Backup on-demand de pg-keeper-instance (descripción: pre-migracion-gitlab fase0 2026-10-22)
  • Inventario de lo que depende de GitLab: ver Inventario de dependencias
  • Listar los PATs que usan las integraciones (Jenkins, bots, Sonar): ver el inventario. (Corrección 2026-10-23: se esperaba que la migración de 16.0 les pusiera vencimiento a los tokens sin fecha; en la práctica siguieron sin vencimiento y activos después de llegar a 19.5.1)
  • Definir la fecha de la ventana: 2026-10-22 de 17:00 a 23:00 ART (20:00 a 02:00 UTC)
  • Publicar un broadcast message en GitLab (banner no descartable, id=1, visible hasta las 23:00 ART)
  • Avisar en el canal del equipo (y a YPF por los webhooks de iydlogicapp.ypf.com, si corresponde)

Nota (2026-10-22): la VM vieja tiene el scope de Storage en solo lectura, así que no puede subir nada al bucket (cambiarlo obliga a apagarla). Bajar el backup por IAP a ~140 KiB/s tarda horas. En la ventana, el tar va de VM a VM por IP interna (scp dentro de la VPC). gitlab-v2 se crea con permisos de escritura sobre gs://sils-gitlab-backups.

Inventario de dependencias​

Relevado el 2026-10-22 con gitlab-rails runner. Todo esto deja de funcionar durante la ventana.

Webhooks (no hay system hooks):

DestinoHooksProyectosImpacto en la ventana
jenkins.sils.tech429138Builds no disparados → relanzar a mano
us-central1-devops-407912.cloudfunctions.net6161Eventos perdidos
api.clickup.com4033Sin sincronización con ClickUp
search.clickup-prod.com1313Ídem
iydlogicapp.ypf.com43Externo (YPF): avisar si corresponde

Integraciones activas: Discord (163 proyectos) y Prometheus (1).

Aplicaciones OAuth (login con GitLab): SonarQube (2), Jenkins, Grafana (2), Cypress, Sentry (2), Decap CMS (docs). Siguen funcionando después de la migración porque viven en la DB, pero no durante la ventana.

PATs activos sin vencimiento (verificado el 2026-10-23: siguen sin vencimiento después del upgrade; igual conviene ponerles fecha y rotarlos):

UsuarioTokenÚltimo usoComentario
francusasonar2026-10-21Integración Sonar
bot del grupo silsv2jenkins grupo silsv22026-10-21Integración Jenkins
clickup-botclickup-sync2026-10-21Sincronización con ClickUp
Diego.FClickup2026-10-22
claude-botclaude mcp2026-10-19
bot del grupo 164claude-reviewer-silsx2026-08-10
francusaglab_cli, claude-discord, sync-authors-docusaurus2026-06 a 2026-10
jose.cardozo, julian.sanchez1Gitlab-Clickup-token, clickup2025-07Posiblemente sin uso
deploysypf, pabloTulian, francusajenkins_deploysypf, pablonote, gitlab_cli2023–2024✅ Revocados el 2026-10-22

Con vencimiento: jenkins/gitlabci (2030-04-01), eugenia.vogt/may-26 (2027-12-31), EmaHernandez/claude_token (2026-12-31). Hay 1 deploy token activo.

Fase 1: infraestructura nueva (sin downtime)​

  • Crear la VM gitlab-v2: Ubuntu 22.04, e2-standard-4, pd-balanced de 100 GB, red sils-keeper-infra/subnet-keeper, tags http-server/https-server, scope de escritura en Storage. IP interna 10.0.0.4, externa efímera 136.115.217.129
  • Verificar que el paquete existe: apt-cache madison gitlab-ee | grep 15.11.5 (está en el repo de jammy)
  • Instalar gitlab-ee=15.11.5-ee.0 con /etc/gitlab/skip-auto-reconfigure creado antes, para que no arranque solo. Paquete en hold
  • Copiar gitlab.rb y ajustarlo (la copia del original está en /root/oldcfg/ de gitlab-v2):
    • postgresql['enable'] = false (no queremos el Postgres local)
    • gitlab_rails['db_host'] → por ahora la IP privada del clon de ensayo (10.1.0.17); en la fase 3 pasa a pg-gitlab
    • Backup a GCS (backup_upload_connection con google_application_default → gs://sils-gitlab-backups, retención de 7 días)
    • gitlab_email_enabled = false solo para el ensayo (sacarlo en la ventana)
  • Copiar gitlab-secrets.json y /etc/letsencrypt/
  • Copiar el cron de certbot (en la fase 5, junto con la IP)
  • Crear la VM gitlab-runners (e2-standard-2, pd-balanced de 150 GB, IP interna 10.0.0.5) con Docker 29 y gitlab-runner 19.5.0. Servicio deshabilitado hasta la fase 5
  • Confirmar las paradas de upgrade con config/upgrade_path.yml y actualizar la tabla de la fase 4
  • Bajar de antemano los 15 .deb de los saltos a /var/cache/gitlab-debs/ de gitlab-v2 (20 GB)

Fase 2: ensayo con datos reales (sin downtime, recomendado)​

El ensayo hace toda la migración con una copia, sin tocar producción. Sirve para medir tiempos reales y encontrar problemas antes de la ventana. Al terminar se descarta todo.

Como la ventana quedó para el mismo día, el ensayo es reducido: restore, saltos hasta 16.11 y upgrade de Cloud SQL de 14 a 17 sobre el clon.

  • Clonar pg-keeper-instance → pg-gitlab-ensayo (IP privada 10.1.0.17) y borrar del clon sonarqube y sils_busquedas
  • gitlab-backup create SKIP=db en la VM vieja (el de la fase 0) y copiar el tar a gitlab-v2 por IP interna: 14 s
  • Bloquear la salida a internet de gitlab-v2 (reglas gitlab-ensayo-deny-egress y gitlab-ensayo-allow-egress-internal con el tag gitlab-ensayo; solo queda abierto 10.0.0.0/8)
  • Restaurar en gitlab-v2 apuntando al clon (los pasos de la fase 3, sin congelar). Resultado: gitlab:check sin errores, gitlab:doctor:secrets con 0 fallas, 240/240 repos presentes, web 200

Hallazgos del restore (ya incorporados en la fase 3):

PasoTiempo
gitlab-ctl reconfigure inicial contra Cloud SQL3 min 08 s
Restore de repositorios3 min 17 s
Restore manual de uploads, artifacts, LFS, etc.16 s
  1. El chown con comodín no funciona: /var/opt/gitlab/backups es git:root 700, así que sudo chown git:git /var/opt/gitlab/backups/*.tar lo expande el usuario sin permisos y no matchea nada. Hay que usar el nombre completo del archivo.
  2. El restore de 15.11 falla con el tar de Ubuntu 22.04 al restaurar uploads (tar: .: Cannot unlink: Invalid argument, por --unlink-first --recursive-unlink). Los repos se restauran bien. La solución es restaurar con SKIP=db,uploads,builds,artifacts,pages,lfs,terraform_state,packages y extraer esos .tar.gz a mano en su directorio (ver fase 3).
  • Hacer los saltos 1 a 4 de la fase 4 (hasta 16.11), anotando la duración de cada uno
  • Probar el upgrade de Cloud SQL de 14 a 17 sobre el clon, midiendo cuánto tarda: 17 min 51 s, sin errores. gcloud sql instances patch corta la espera a los 10 min, pero la operación sigue; se espera con gcloud beta sql operations wait <id>

Hallazgos de los saltos:

SaltoInstalaciónBackground migrationsTotal
15.11.133 min 04 s1 min (esperando)4 min 06 s
16.3.95 min 37 s19 min (esperando)24 min 45 s
16.7.105 min 38 s23 min 27 s (esperando)29 min 05 s
16.11.105 min 04 s1 min 54 s (forzadas con finalize)8 min (con gitlab:check)
  1. Esperar las background migrations no entra en la ventana: a ~25 min por salto, 15 saltos son 6–7 h. GitLab las procesa de a lotes con pausas, pensado para instancias grandes con tráfico. Con la base chica y GitLab sin usuarios se pueden forzar en el momento (BatchedMigrationRunner#finalize para cada migración pendiente, el mismo mecanismo que gitlab:background_migrations:finalize). En 16.11 eso tardó 24 s de trabajo real. El script de saltos ya lo hace automáticamente.
  2. El preinstall de cada paquete hace un backup de la DB y lo sube a GCS. En el ensayo falló porque no hay salida a internet, y en la ventana sumaría ~4 min por salto. Se desactiva con touch /etc/gitlab/skip-auto-backup; la DB queda cubierta por los backups de Cloud SQL.
  3. Omnibus no reinicia Redis al actualizar: después de 16.11, gitlab:check avisa que Redis sigue en 6.2.11. Con gitlab-ctl restart redis pasa a 7.0.15. Hay que reiniciarlo en los puntos de control (16.11, 17.11, 18.11 y al final).

Scripts que quedan en gitlab-v2 para la ventana:

  • /usr/local/sbin/gitlab-hops.sh <versiones...>: instala cada .deb, fuerza las background migrations, espera que no quede ninguna, corre gitlab:check y loguea en /var/log/gitlab-hops.log
  • /usr/local/sbin/gitlab-finalize-bgmig.rb: el runner que fuerza las migraciones pendientes
  • Login, clone y push de prueba con /etc/hosts: no se hizo en el ensayo. Se verificó web 200, gitlab:check, secretos y repos desde la VM. El login, el clone y el push se prueban en la ventana, antes del cutover (fase 3)
  • Ajustar este plan con lo que haya aparecido
  • Borrar el clon (tenía la protección contra borrado heredada; hubo que sacársela solo al clon), sacar las reglas de firewall del ensayo y reinstalar 15.11.5 limpio en gitlab-v2, conservando /etc/gitlab, los scripts y los .deb

⚠️ Durante el ensayo, gitlab-v2 no tuvo salida a internet (reglas de egress con el tag gitlab-ensayo) y tuvo los mails desactivados, para que Sidekiq no disparara webhooks ni mandara mails desde el clon.

Fase 3: ventana, congelar y copiar (con downtime)​

Inicio de la ventana: GitLab fuera de servicio.

Congelar la VM vieja​

  • Avisar en el canal del equipo que empieza la ventana
  • Frenar los runners: gitlab-runner stop y systemctl disable gitlab-runner (no había jobs corriendo)
  • Página de mantenimiento: se reemplazó /opt/gitlab/embedded/service/gitlab-rails/public/502.html (el original quedó en /root/502.html.orig). Así no hace falta reconfigure en la VM vieja
  • Frenar la aplicación: gitlab-ctl stop de puma, sidekiq, gitlab-exporter y gitlab-kas (20:01 UTC / 17:01 ART)
  • Verificar el congelamiento: la web y la API devuelven 502 con la página de mantenimiento, git ls-remote por HTTPS da 502 y por SSH da "No se pudo leer del repositorio remoto"
  • Verificar que no queda nada escribiendo en la DB: 0 conexiones establecidas desde la VM vieja a Cloud SQL (ss -tn state established dst 34.70.140.46)

Copiar los datos​

  • gitlab-backup create SKIP=db STRATEGY=copy: 6 min 32 s, 1792699359_2026_10_22_15.11.5-ee (1,4 GB)
  • Clonar pg-keeper-instance → pg-gitlab (PostgreSQL 14), lanzado a las 20:01:37 UTC con la VM ya congelada. Listo en ~7 min
  • En pg-gitlab: borrar las bases sonarqube y sils_busquedas y quitar IP pública y authorized networks. Queda solo con IP privada 10.1.0.20, protección contra borrado y backups automáticos (heredados)
  • Copiar a gitlab-v2 el tar del backup por IP interna (12 s). gitlab-secrets.json y gitlab.rb ya estaban desde la fase 1
  • Apagar la VM vieja ahora: se deja prendida y congelada para que siga mostrando la página de mantenimiento (sin puma/sidekiq y con 0 conexiones a la DB). Se apaga en la fase 5, al sacarle la IP

Restaurar en la VM nueva​

  • Poner en gitlab-v2 el gitlab-secrets.json original (sin esto los datos cifrados quedan inutilizables: CI variables, 2FA, tokens de runners)

  • En /etc/gitlab/gitlab.rb (copia previa en /root/gitlab.rb.pre-ventana):

    • gitlab_rails['db_host'] → 10.1.0.20 (IP privada de pg-gitlab)
    • Sacar gitlab_email_enabled = false (era solo para el ensayo)
    • Sacar gitlab_rails['auto_migrate'] = false (al final del archivo). Si queda, los saltos no corren las migraciones
  • rm /etc/gitlab/skip-auto-reconfigure (dejar skip-auto-backup) y gitlab-ctl reconfigure (1 min 12 s)

  • gitlab-ctl start (los servicios estaban frenados desde el mediodía y reconfigure no los levanta; sin gitaly el restore falla con gitaly.socket: no such file) y después gitlab-ctl stop puma y gitlab-ctl stop sidekiq

  • sudo chown git:git /var/opt/gitlab/backups/<nombre-completo>_gitlab_backup.tar (sin comodín)

  • gitlab-backup restore BACKUP=<timestamp> SKIP=db,uploads,builds,artifacts,pages,lfs,terraform_state,packages force=yes (solo repos): 4 min 11 s, 0 errores

  • Extraer a mano el resto (10 s): tar -xf <backup>.tar -C stage --exclude=repositories y después cada .tar.gz en su directorio (vaciado antes), con chown -R git:git:

    ArchivoDirectorio
    uploads.tar.gz/var/opt/gitlab/gitlab-rails/uploads
    builds.tar.gz/var/opt/gitlab/gitlab-ci/builds
    artifacts.tar.gz/var/opt/gitlab/gitlab-rails/shared/artifacts
    pages.tar.gz/var/opt/gitlab/gitlab-rails/shared/pages
    lfs.tar.gz/var/opt/gitlab/gitlab-rails/shared/lfs-objects
    terraform_state.tar.gz/var/opt/gitlab/gitlab-rails/shared/terraform_state
    packages.tar.gz/var/opt/gitlab/gitlab-rails/shared/packages
  • gitlab-ctl restart, gitlab-rake gitlab:check SANITIZE=true (OK) y gitlab-rake gitlab:doctor:secrets (0 fallas). 240/240 repos presentes

  • git ls-remote por SSH contra la IP de gitlab-v2 con una clave de usuario real: sils-docs master en d74442f, el último push antes de congelar

  • Login en la web con /etc/hosts (se prueba después de los saltos, antes del cutover)

  • Backup on-demand de pg-gitlab antes de los saltos (ventana: pre-saltos 15.11.5)

Fase 4: ventana, upgrades encadenados (con downtime)​

En cada salto:

Todo esto lo hace /usr/local/sbin/gitlab-hops.sh (requiere /etc/gitlab/skip-auto-backup):

  1. dpkg -i /var/cache/gitlab-debs/gitlab-ee_<versión>-ee.0_amd64.deb (ya descargados)
  2. Forzar las background migrations pendientes (gitlab-finalize-bgmig.rb) y verificar que no quede ninguna
  3. gitlab-rake gitlab:check SANITIZE=true
  4. Anotar la duración en /var/log/gitlab-hops.log

Se lanza por tramos, para frenar en los puntos de control: setsid nohup /usr/local/sbin/gitlab-hops.sh 15.11.13 16.3.9 16.7.10 16.11.10 &

Paradas confirmadas el 2026-10-22 con config/upgrade_path.yml. Se usa el último parche de cada minor disponible en el repo de jammy:

SaltoVersiónNotasEnsayoVentana
115.11.13Último parche de 15.11✅✅ 5,5 min
216.3.9Parada obligatoria✅✅ 8,9 min
316.7.10Parada obligatoria✅✅ 10,3 min
416.11.10Parada obligatoria. Después: gitlab-ctl restart redis✅✅ 8,9 min
517.1.8Parada condicional; la hacemos igual—✅ 9,4 min
617.3.7Parada obligatoria—✅ 9,3 min
717.5.5Parada obligatoria—✅ 9,4 min
817.8.7Parada obligatoria—✅ 10,8 min
917.11.7Parada obligatoria. Después: gitlab-ctl restart redis (→ 7.2.7)—✅ 15,2 min
—Cloud SQL pg-gitlab 14 → 17Con GitLab frenado (gitlab-ctl stop). Backup antes del upgrade✅ 17,9 min✅ 16,5 min
1018.2.8Parada obligatoria. Dos problemas, ver abajo—✅ ~35 min
1118.5.7Parada obligatoria—⬜
1218.8.11Parada obligatoria—⬜
1318.11.13Parada obligatoria—⬜
1419.2.9Parada obligatoria (exige PG 17)—⬜
1519.5.1Última disponible—⬜

Si alguna background migration falla, no se sigue: se reintenta desde Admin o se revisan los logs de Sidekiq.

Problemas en el salto a 18.2.8 (en la ventana):

  1. Dueños mezclados en la DB. La migración 20250428073611_rename_web_hook_logs_sequence falló con PG::ObjectNotInPrerequisiteState: sequence must have same owner as table it is linked to. La DB venía de una importación vieja: 735 tablas, 13 tablas particionadas, 6 vistas y 18 funciones propias de GitLab eran del rol cloudsqlsuperuser, y el resto de gitlab (las secuencias cambian de dueño junto con su tabla). Se pasaron a gitlab objeto por objeto (ALTER TABLE/VIEW/ROUTINE ... OWNER TO gitlab, en una transacción), solo en public, gitlab_partitions_dynamic y gitlab_partitions_static y excluyendo los objetos de extensiones (pg_trgm, btree_gist). No se usó REASSIGN OWNED porque también toca objetos compartidos de la instancia. Script: /root/fixowners.rb en gitlab-v2. Como el reconfigure quedó a medias, el reintento con dpkg -i falló en el preinstall (Malformed configuration JSON file); se resolvió con gitlab-ctl reconfigure.
  2. BackfillSentNotificationsAfterPartition no puede terminar. La migración 18.0 agrega sent_notifications.created_at con default 2025-04-02 para todas las filas existentes (37.085). La migración 18.2 crea la tabla particionada por mes desde 2025-04, pero la política es retain_for: 1.year, así que en 2026-10 GitLab borra las particiones viejas al sincronizar y las filas no entran en ninguna (PG::CheckViolation: no partition ... found for row). Es un bug para quien actualiza más de un año tarde. Esas filas son claves de "responder por mail" y "desuscribirse" de notificaciones viejas que GitLab descartaría igual por retención, y no usamos incoming_email. Decisión: marcar la migración como terminada (update_columns(status: 3)), sin copiar las filas.
  3. Con GitLab 17.11 no se arrancan Puma ni Sidekiq contra PG 17 (no está soportado): se levantan los demás servicios, se instala 18.2.8 y recién ahí gitlab-ctl start.

Checkboxes de control:

  • Saltos 1 a 4 (hasta 16.11) completos (20:25 → 21:03 UTC)
  • Saltos 5 a 9 (hasta 17.11) completos (21:03 → 22:03 UTC). Punto de control: Redis 7.2.7, 0 background migrations pendientes o fallidas, sin migraciones de esquema pendientes, secretos OK, web 200
  • Backup on-demand de pg-gitlab antes del upgrade de PG (ventana: pre-PG17 en GitLab 17.11.7)
  • pg-gitlab actualizado a PostgreSQL 17 (22:09 → 22:26 UTC)
  • Saltos 10 a 15 (hasta 19.5.1) completos (22:27 → 00:05 UTC, incluyendo los problemas de 18.2)
  • Punto de control final: GitLab 19.5.1-ee sobre PostgreSQL 17.11, Redis 7.2.16, 0 background migrations pendientes o fallidas, sin migraciones de esquema pendientes, secretos OK, 240/240 repos, todos los servicios arriba, web 200
SaltoVersiónTotal
1118.5.710,9 min (30 migraciones forzadas)
1218.8.1112,6 min
1318.11.1311 min
1419.2.911 min
1519.5.111,9 min

Fase 5: ventana, cutover y reapertura (con downtime)​

  • Instalar certbot en gitlab-v2 y el cron de renovación de root (igual al de la VM vieja). Certificado vigente hasta 2026-12-15
  • Copiar las host keys de SSH de la VM vieja a gitlab-v2 (las propias quedaron en /root/ssh-hostkeys-v2/). Fingerprint ED25519 SHA256:3f0Z+vaVY+znv5lj0rQda+TU9qXosW+Vu7oudPcMnko, el mismo de siempre. Desde ahí, gcloud compute ssh gitlab-v2 necesita --strict-host-key-checking=no una vez
  • gitlab-ctl stop en la VM vieja
  • Sacar la IP de la VM gitlab y la IP efímera de gitlab-v2, y asignar gitlab-ip (34.172.216.218) a gitlab-v2 (00:17 → 00:18 UTC / 21:18 ART)
  • Apagar la VM gitlab (TERMINATED, no se borró)
  • Probar sin /etc/hosts: https://gitlab.sils.tech 200, certificado válido (ssl_verify_result=0), API responde
  • Probar git por SSH: ssh -T git@gitlab.sils.tech → Welcome to GitLab, @francusa!, sin warning de host key
  • Escritura real: push de 5 commits de este post a sils-docs (d74442f..253621e)
  • gitlab-runners: scopes de la cuenta de servicio iguales a los de la VM vieja (por si algún job usa las credenciales de la VM), config.toml original, gitlab-runner 19.5.0 habilitado y arrancado
  • gitlab-runner verify: 9 de 10 vivos. "Runner de sils-design" da 403 (is removed); ya figuraba sin contacto desde 2025-03-31, no es por la migración
  • Pipeline real de punta a punta: sils-docs #228 (build-lib) success en 6,4 min en el runner nuevo
  • Salida a internet de gitlab-v2 con 34.172.216.218 (la misma de siempre, así que las allowlists siguen valiendo); llega a jenkins.sils.tech (200) y sonar.sils.tech (200)
  • Jenkins: login con GitLab y un webhook real (un push en un proyecto con webhook a Jenkins)
  • Sonar: decoración de un MR
  • Quitar el broadcast message de mantenimiento (00:32 UTC / 21:32 ART)
  • Avisar en el canal que GitLab está de vuelta, recordando que los builds disparados por webhooks durante la ventana se perdieron y hay que relanzarlos

⚠️ Los runners salen a internet con otra IP: antes compartían la de GitLab (34.172.216.218) y ahora salen con la efímera de gitlab-runners (35.184.210.61). Si algún job se conecta a algo con allowlist por IP (por ejemplo, las authorized networks de pg-keeper-instance, que incluyen 34.172.216.218, o Cloud Armor), va a fallar. Opciones: reservar una IP estática para gitlab-runners y agregarla a las allowlists, o sacar los runners por un Cloud NAT con IP fija.

Fin de la ventana: GitLab reabierto a las 21:32 ART (17:01 → 21:32, ~4,5 h de servicio caído).

Fase 6: post-migración (sin downtime)​

  • Swap de 4 GB en gitlab-v2 (/swapfile, en /etc/fstab, vm.swappiness=10 en /etc/sysctl.d/99-gitlab-swap.conf) como red de seguridad ante picos de memoria. Uso medido después de reabrir: carga 0,6–0,8 sobre 4 vCPU y 8 GB de RAM libres de 16

  • Mails de GitLab (faltaba en el plan). GitLab manda con sendmail y gitlab-v2 no tenía MTA: después de la reapertura se perdieron 3 mails (No such file or directory - /usr/sbin/sendmail). Según el header de un mail de la VM vieja, salían por un Postfix local directo a los MX desde 34.172.216.218, con From: gitlab@gitlab.sils.tech y Reply-To: noreply@gitlab.sils.tech. Se replicó: Postfix solo para envío (inet_interfaces = loopback-only, mydestination = localhost, myhostname = gitlab.sils.tech). El puerto 25 de salida está abierto en el proyecto. Mail de prueba entregado a ASPMX.L.GOOGLE.COM con 250 OK (2026-10-23 09:53 ART). Los 3 mails perdidos entre la reapertura y el arreglo no se reenvían (decisión: llegarían tarde)

  • Certbot había quedado a medio configurar (dpkg frenado en la pregunta del conffile /etc/letsencrypt/cli.ini). Se terminó con --force-confold y se deshabilitó certbot.timer (renueva sin los hooks que frenan nginx). La renovación queda solo con el cron de root, como antes

  • SPF para gitlab.sils.tech (cargado el 2026-10-23; propagó a ns2/ns3.hostmar.com, 8.8.8.8 y 1.1.1.1 en ~3 min). Confirmado a las 11:36 ART: spf=pass y dmarc=pass en el header de una notificación real. Los mails de las 10:05 y 10:27 todavía daban spf=none por la caché negativa de Gmail (el TTL negativo de la zona es 4 h: mínimo entre el TTL del SOA, 14400, y su campo minimum, 86400). Antes daba spf=none y DMARC fail (p=none). Se cargó en el DNS de sils.tech (hostmar) un TXT con nombre gitlab y valor v=spf1 ip4:34.172.216.218 -all. gitlab.sils.tech es un registro A, así que admite el TXT; no toca el SPF de sils.tech (include:comp.hostmar.com). Con eso SPF y DMARC pasan a pass (alineación relajada con sils.tech). Verificar con dig TXT gitlab.sils.tech y un mail de prueba

  • Sacar del config.toml de gitlab-runners el bloque "Runner de sils-design" (token revocado desde 2025, generaba errores 403 cada pocos minutos). Backup en /etc/gitlab-runner/config.toml.bak-20261023125703. Quedan 9 runners, todos vivos

  • Revisar el uso de gitlab-v2 y gitlab-runners con carga real del equipo (si gitlab-runners se satura con 3 jobs a la vez, pasar a e2-standard-4)

  • Backup diario (configurado el 2026-10-23):

    QuéCuándo (UTC / ART)DóndeRetención
    gitlab-backup create completo (repos, DB, uploads, artifacts, LFS...)06:00 / 03:00gs://sils-gitlab-backups/ (raíz)7 días en la VM (backup_keep_time), 30 días en GCS
    gitlab-ctl backup-etc (gitlab.rb + gitlab-secrets.json)después del anteriorgs://sils-gitlab-backups/config/daily/30 días en GCS
    Backups automáticos de Cloud SQL pg-gitlab + PITR04:00 UTCCloud SQL7 backups, 7 días de PITR
    • Script: /usr/local/sbin/gitlab-daily-backup.sh (cron de root 0 6 * * *, flock para no superponerse, PATH explícito porque gcloud está en /snap/bin). Log en /var/log/gitlab-daily-backup.log (líneas INICIO, DATOS_OK/FALLO, CONFIG_OK/FALLO, FIN rc=), rotado semanalmente por /etc/logrotate.d/gitlab-daily-backup
    • Ahora la DB sí entra en gitlab-backup: Omnibus 19 trae pg_dump 17.11, igual que Cloud SQL
    • Ciclo de vida del bucket: borra *_gitlab_backup.tar y config/daily/ a los 30 días, y las versiones no vigentes a los 7. config/2026-10-22/ (la copia de la migración) no se borra
    • Primera corrida: 3 min 33 s, 1,61 GiB (1792771395_2026_10_23_19.5.1-ee, con db/database.sql.gz de 305 MB, sin componentes salteados)
  • Prueba de restore completo (2026-10-23, ~35 min en total, todo descartado al terminar): VM gitlab-restoretest (Ubuntu 22.04, sin salida a internet) + clon pg-gitlab-restoretest, restaurando desde GCS el último backup diario y su config. Restore con DB incluida en 8 min (DB 3,3 min, repos 3,6 min), 0 errores. Conteos idénticos a producción al momento del backup (241 proyectos y repos, 54 usuarios, 17.916 MRs, 86.940 notas, 37.553 pipelines, 86 CI variables descifrables), doctor:secrets 0 fallas, gitlab:check OK, sin migraciones pendientes. En 19.5 ya no aparece el problema del tar que tuvimos con el restore de 15.11

    Procedimiento probado de restore (desastre total):

    1. VM Ubuntu 22.04 y apt-get install gitlab-ee=<misma versión del backup>-ee.0 con /etc/gitlab/skip-auto-reconfigure creado antes
    2. Bajar de GCS el último *_gitlab_backup.tar (a /var/opt/gitlab/backups/) y el último config/daily/gitlab_config_*.tar; extraer de este gitlab.rb y gitlab-secrets.json a /etc/gitlab/
    3. DB: una instancia de Cloud SQL con el usuario gitlab y la base gitlabhq_production (lo más simple: restaurar un backup de pg-gitlab o clonarla) y su IP en gitlab_rails['db_host']
    4. Certificado: copiar /etc/letsencrypt o emitir uno nuevo (no viaja en el backup)
    5. rm /etc/gitlab/skip-auto-reconfigure, gitlab-ctl reconfigure, gitlab-ctl start, gitlab-ctl stop puma, gitlab-ctl stop sidekiq
    6. chown git:git <backup> y gitlab-backup restore BACKUP=<timestamp> force=yes
    7. gitlab-ctl restart, gitlab-rake gitlab:check SANITIZE=true, gitlab-rake gitlab:doctor:secrets
    8. Copiar las host keys de SSH (están en config/2026-10-22/ del bucket), Postfix, cron de certbot y backup, y mover gitlab-ip
  • Snapshot schedule gitlab-v2-diario (región us-central1): snapshot diario del disco gitlab-v2 a las 07:00 UTC (04:00 ART), retención 14 días, guardados en us-central1 y se conservan aunque se borre el disco

  • Ponerles vencimiento a los PATs de las integraciones (hoy siguen sin fecha) y limpiar los vencidos

  • Investigar los ~9.000 401 por hora de Jenkins (34.123.105.34, user agent Jersey/3.1.11) contra GET /api/v4/user, sin token reconocible, desde la reapertura (00:xx UTC del 23/10). Los logins con GitLab, los webhooks y los commit status funcionan, así que hoy no hay impacto visible. No se sabe si ya pasaba con el GitLab viejo (sus logs quedaron en el disco de la VM apagada). Actualización 2026-10-26: se fue apagando solo (23/10: 205 mil, 24/10: 191 mil, 25/10: 22 mil, 26/10 hasta las 09:00: 1.600), consistente con sesiones viejas de Jenkins que fueron expirando

  • Chequeo 2026-10-26: 3 backups nocturnos OK (~3 min, 1,6 GiB c/u) en GCS con su config, 3 snapshots diarios READY, 0 errores 5xx, 0 migraciones pendientes, runners 9/9 sin errores, swap usada ~870 MB de 4 GB (con 8 GB de RAM disponibles)

  • Ruido de mail local (resuelto el 2026-10-26: MAILTO="" en el crontab de root, /etc/postfix/transport con gitlab.sils.tech discard: y notificaciones globales en disabled para root y ghost; la cola quedó vacía). Detalle: Postfix no entrega a @gitlab.sils.tech (mydestination = localhost), así que la salida del cron de certbot (a root) y los avisos de rebote (a gitlab@) quedan en cola reintentando contra el puerto 25 de la propia IP. Los rebotes son de notificaciones a root y ghost, que tienen mail @example.com. Arreglo propuesto: MAILTO="" en el crontab de root, descartar el dominio local en Postfix (transport_maps: gitlab.sils.tech discard:) y ponerle un mail real (o deshabilitar notificaciones) al usuario root

  • Nota: los pipelines #231 y #232 de sils-docs (12:57 UTC del 23/10) quedaron en runner_interrupted por el reinicio de gitlab-runner al sacar el runner muerto; los siguientes, OK

  • Revisar pipelines que puedan romperse por cambios de 17/18 (por ejemplo, la allowlist de CI_JOB_TOKEN entre proyectos)

  • Limpieza de webhooks de ClickUp (2026-10-26): se borraron los 53 (40 a api.clickup.com/:id/gitlab/webhookEvent, que responde siempre 410 Gone, y 13 a search.clickup-prod.com/webhooks/:id/, que responde siempre 500). Ninguno tuvo una entrega exitosa en los 90 días de logs; ya fallaban antes de la migración. Respaldo de la configuración (proyecto, URL y eventos) en gs://sils-gitlab-backups/config/2026-10-26-clickup-webhooks-borrados.json. La sincronización por API con el usuario clickup-bot (PAT clickup-sync) no se tocó y sigue activa

  • IP fija para los runners (2026-10-26): la IP efímera 35.184.210.61 de gitlab-runners se promovió a estática (gitlab-runners-ip, us-central1), sin cambio de IP ni corte. Se buscaron allowlists con la IP vieja (34.172.216.218) en Cloud SQL, Cloud Armor y firewall de los 15 proyectos accesibles: solo aparece en las authorized networks de pg-keeper-instance (entrada gitlab, que era la conexión del GitLab viejo a su DB, no de los runners). No hubo jobs fallidos por conexión desde la migración, así que no se agregó 35.184.210.61 a ninguna allowlist. Si un servicio externo (fuera de GCP) filtraba por la IP vieja, hay que agregarle 35.184.210.61

  • Sacar 34.172.216.218 de las authorized networks de pg-keeper-instance si ya no la usa nadie (GitLab ahora usa pg-gitlab por IP privada)

  • Borrar gitlabhq_production de pg-keeper-instance (después del período de gracia)

  • Borrar los backups locales del ensayo y de la ventana en gitlab-v2 (/var/opt/gitlab/backups) y los .deb de /var/cache/gitlab-debs (20 GB)

  • Borrar la VM gitlab y su disco (después de 2 semanas sin problemas). Conservar snapshot-gitlab

  • Documentar el procedimiento de upgrade de rutina para no volver a quedar tan atrás

Plan de vuelta atrás​

Mientras la VM vieja y pg-keeper-instance no se tocan, volver atrás es simple. Hasta el final de la fase 5 se puede abortar en cualquier momento:

  1. Apagar gitlab-v2.
  2. Si la IP ya se movió, devolver gitlab-ip a la VM gitlab.
  3. Encender la VM gitlab. Arranca contra pg-keeper-instance, que no fue modificada (todas las migraciones corrieron sobre pg-gitlab).
  4. gitlab-ctl start y sacar la página de mantenimiento.
  5. gitlab-runner start.

Una vez reabierto el GitLab nuevo, volver atrás implica perder lo escrito después de la reapertura, así que el punto de no retorno real es el fin de la fase 5.

Impacto y downtime​

Mientras dura la ventana (fases 3 a 5), GitLab no está disponible: no hay web, git por HTTPS/SSH, CI ni API. Tampoco funcionan el login de Jenkins con GitLab, los webhooks a Jenkins (los eventos de la ventana se pierden) ni la decoración de MRs en Sonar.

TramoEstimado
Estimación con los tiempos medidos en el ensayo:
TramoMedido en el ensayoEstimado
Congelar + backup (12 min) en paralelo con el clon de Cloud SQL (~13 min)backup 11,6 min~25 min
Copiar el tar (14 s) + reconfigure (3 min) + restore de repos (3,3 min) + archivos (16 s) + verificación~7 min~20 min
15 saltos de versión, a ~8–9 min cada uno con las migraciones forzadas16.11: 8 min~2 h 15 min
Upgrade de Cloud SQL de 14 a 1717 min 51 s~20 min
Cutover + verificación—~30 min
Total~4 h (17:00 → ~21:00 ART)

Queda margen de ~2 h dentro de la ventana para imprevistos. Las versiones 17.x, 18.x y 19.x no se ensayaron.

Registro de avance​

FechaQué se hizoQuién
2026-10-22Relevamiento del servidor y planFranco
2026-10-22Fase 0: disco liberado (98% → 25%), configs en GCS, backup de prueba (11,6 min / 1,36 GB), backup de Cloud SQL, inventarioFranco
2026-10-22Revocados 3 PATs sin uso desde 2023–2024 (ids 10, 11, 15)Franco
2026-10-22 09:20Ventana fijada para hoy 17:00–23:00 ART; banner de mantenimiento publicado en GitLabFranco
2026-10-22 09:45Fase 1: gitlab-v2 y gitlab-runners creadas, 15.11.5 instalado, configuración copiada, 15 .deb descargados. Paradas confirmadas; el plan de DB pasa a un solo salto 14 → 17Franco
2026-10-22 09:55Ensayo: restore OK sobre pg-gitlab-ensayo (secretos OK, 240/240 repos). Dos problemas encontrados y resueltos (chown con comodín, tar de jammy). Saltos a 16.11 en cursoFranco
2026-10-22 11:40Ensayo terminado: saltos hasta 16.11 OK, migraciones forzadas con finalize (de ~20 min a ~2 min por salto), Cloud SQL 14 → 17 en 18 min. Clon borrado y gitlab-v2 reinstalada limpia en 15.11.5. Estimación de la ventana: ~4 hFranco
2026-10-22 12:45Reinstalación de 15.11.5: con gitlab.rb presente, el paquete corre reconfigure aunque exista skip-auto-reconfigure. Quedó colgado (runsvdir frenado) intentando migrar contra la IP del clon borrado; se cortó, se puso db_host en placeholder y se terminó de configurar con auto_migrate = false. gitlab-v2 queda en 15.11.5 configurado, con los servicios frenadosFranco
2026-10-22 17:01Inicio de la ventana. VM vieja congeladaFranco
2026-10-22 17:24Fase 3 completa: restore en gitlab-v2 sobre pg-gitlabFranco
2026-10-22 19:03Saltos hasta 17.11.7Franco
2026-10-22 19:26pg-gitlab en PostgreSQL 17Franco
2026-10-22 20:0118.2.8 después de arreglar dueños de objetos en la DB y marcar terminada la migración de sent_notificationsFranco
2026-10-22 21:05GitLab 19.5.1Franco
2026-10-22 21:18IP movida a gitlab-v2, VM vieja apagadaFranco
2026-10-22 21:32GitLab reabierto. Runners 9/10 vivos, pipeline real OK, banner sacadoFranco
2026-10-23 09:30Chequeo: 0 errores 5xx, webhooks a Jenkins OK, 15 usuarios activos. Los webhooks de ClickUp (410/500) ya fallaban antes. Faltaba el MTA: se instaló Postfix y los mails salen de nuevoFranco