GitLab: migración a VM nueva y upgrade de 15.11 a la última versión
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
| Fase | Qué | Downtime | Estado |
|---|---|---|---|
| 0 | Preparación en la VM actual (disco, backups, aviso) | No | ✅ Hecho |
| 1 | Armar la VM nueva, la DB nueva y la VM de runners | No | ✅ Hecho |
| 2 | Ensayo reducido con datos reales (restore + saltos hasta 16.11 + PG 14→17) | No | ✅ Hecho |
| 3 | Ventana: congelar, copiar, restaurar | Sí | ✅ Hecho (17:01 → 17:24 ART) |
| 4 | Ventana: upgrades encadenados hasta la última versión | Sí | ✅ Hecho (17:25 → 21:05 ART), 19.5.1 |
| 5 | Ventana: cutover de IP, verificación y reapertura | Sí | ✅ Reabierto 21:32 ART (faltan pruebas de Jenkins/Sonar y el aviso) |
| 6 | Post-migración y baja de la VM vieja | No | ⬜ Pendiente |
Relevamiento (2026-10-22)
| Item | Valor |
|---|---|
| VM | gitlab · us-central1-a · sils-keeper-infra · custom-2-11008 (2 vCPU, ~10,7 GB de RAM) · disco pd-standard de 100 GB |
| IP | gitlab-ip = 34.172.216.218 (estática) |
| SO | Ubuntu 18.04 (bionic). Lleva ~408 días sin reiniciar |
| GitLab | EE 15.11.5, instalado con Omnibus, sin licencia (funciones de la versión Free) |
| Base de datos | Externa: Cloud SQL pg-keeper-instance (PostgreSQL 14), base gitlabhq_production. En la misma instancia están sonarqube y sils_busquedas |
| Postgres local | Corre pero no se usa |
| Volumen | 40 usuarios activos · 240 proyectos · repos de ~870 MB · uploads de ~240 MB |
| Runners | 10 runners con executor Docker, gitlab-runner 17.0.0, en la misma VM |
| Disco | Al 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 |
| TLS | Certbot 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-backupno puede hacer backup de la DB: Omnibus 15.11 traepg_dump12 y 13, y el servidor es PostgreSQL 14. La DB se copia con Cloud SQL ygitlab-backupse corre conSKIP=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-instancecomparte la instancia con Sonar ysils_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
| Componente | Antes | Después |
|---|---|---|
| GitLab | VM gitlab, Ubuntu 18.04, 15.11.5 | VM gitlab-v2, Ubuntu 22.04, e2-standard-4, disco pd-balanced, última versión |
| DB | pg-keeper-instance (compartida, PG 14, IP pública) | pg-gitlab (dedicada, PG 17, IP privada) |
| Runners | En la VM de GitLab | VM gitlab-runners aparte |
| IP / DNS | gitlab-ip en gitlab | gitlab-ip en gitlab-v2 (el DNS no cambia) |
| Backups | Ninguno | gitlab-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) ydocker 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 versionadogs://sils-gitlab-backups/config/2026-10-22/ -
gitlab-backup create SKIP=db STRATEGY=copyde 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-v2se crea con permisos de escritura sobregs://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):
| Destino | Hooks | Proyectos | Impacto en la ventana |
|---|---|---|---|
jenkins.sils.tech | 429 | 138 | Builds no disparados → relanzar a mano |
us-central1-devops-407912.cloudfunctions.net | 61 | 61 | Eventos perdidos |
api.clickup.com | 40 | 33 | Sin sincronización con ClickUp |
search.clickup-prod.com | 13 | 13 | Ídem |
iydlogicapp.ypf.com | 4 | 3 | Externo (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):
| Usuario | Token | Último uso | Comentario |
|---|---|---|---|
| francusa | sonar | 2026-10-21 | Integración Sonar |
| bot del grupo silsv2 | jenkins grupo silsv2 | 2026-10-21 | Integración Jenkins |
| clickup-bot | clickup-sync | 2026-10-21 | Sincronización con ClickUp |
| Diego.F | Clickup | 2026-10-22 | |
| claude-bot | claude mcp | 2026-10-19 | |
| bot del grupo 164 | claude-reviewer-silsx | 2026-08-10 | |
| francusa | glab_cli, claude-discord, sync-authors-docusaurus | 2026-06 a 2026-10 | |
| jose.cardozo, julian.sanchez1 | Gitlab-Clickup-token, clickup | 2025-07 | Posiblemente sin uso |
| deploysypf, pabloTulian, francusa | jenkins_deploysypf, pablonote, gitlab_cli | 2023–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, redsils-keeper-infra/subnet-keeper, tagshttp-server/https-server, scope de escritura en Storage. IP interna10.0.0.4, externa efímera136.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.0con/etc/gitlab/skip-auto-reconfigurecreado antes, para que no arranque solo. Paquete enhold - Copiar
gitlab.rby ajustarlo (la copia del original está en/root/oldcfg/degitlab-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 apg-gitlab - Backup a GCS (
backup_upload_connectioncongoogle_application_default→gs://sils-gitlab-backups, retención de 7 días) -
gitlab_email_enabled = falsesolo para el ensayo (sacarlo en la ventana)
-
- Copiar
gitlab-secrets.jsony/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 interna10.0.0.5) con Docker 29 ygitlab-runner19.5.0. Servicio deshabilitado hasta la fase 5 - Confirmar las paradas de upgrade con
config/upgrade_path.ymly actualizar la tabla de la fase 4 - Bajar de antemano los 15
.debde los saltos a/var/cache/gitlab-debs/degitlab-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 privada10.1.0.17) y borrar del clonsonarqubeysils_busquedas -
gitlab-backup create SKIP=dben la VM vieja (el de la fase 0) y copiar el tar agitlab-v2por IP interna: 14 s - Bloquear la salida a internet de
gitlab-v2(reglasgitlab-ensayo-deny-egressygitlab-ensayo-allow-egress-internalcon el taggitlab-ensayo; solo queda abierto 10.0.0.0/8) - Restaurar en
gitlab-v2apuntando al clon (los pasos de la fase 3, sin congelar). Resultado:gitlab:checksin errores,gitlab:doctor:secretscon 0 fallas, 240/240 repos presentes, web 200
Hallazgos del restore (ya incorporados en la fase 3):
| Paso | Tiempo |
|---|---|
gitlab-ctl reconfigure inicial contra Cloud SQL | 3 min 08 s |
| Restore de repositorios | 3 min 17 s |
| Restore manual de uploads, artifacts, LFS, etc. | 16 s |
- El
chowncon comodín no funciona:/var/opt/gitlab/backupsesgit:root 700, así quesudo chown git:git /var/opt/gitlab/backups/*.tarlo expande el usuario sin permisos y no matchea nada. Hay que usar el nombre completo del archivo. - El restore de 15.11 falla con el
tarde 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 conSKIP=db,uploads,builds,artifacts,pages,lfs,terraform_state,packagesy extraer esos.tar.gza 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 patchcorta la espera a los 10 min, pero la operación sigue; se espera congcloud beta sql operations wait <id>
Hallazgos de los saltos:
| Salto | Instalación | Background migrations | Total |
|---|---|---|---|
| 15.11.13 | 3 min 04 s | 1 min (esperando) | 4 min 06 s |
| 16.3.9 | 5 min 37 s | 19 min (esperando) | 24 min 45 s |
| 16.7.10 | 5 min 38 s | 23 min 27 s (esperando) | 29 min 05 s |
| 16.11.10 | 5 min 04 s | 1 min 54 s (forzadas con finalize) | 8 min (con gitlab:check) |
- 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#finalizepara cada migración pendiente, el mismo mecanismo quegitlab:background_migrations:finalize). En 16.11 eso tardó 24 s de trabajo real. El script de saltos ya lo hace automáticamente. - 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. - Omnibus no reinicia Redis al actualizar: después de 16.11,
gitlab:checkavisa que Redis sigue en 6.2.11. Congitlab-ctl restart redispasa 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, corregitlab:checky 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: no se hizo en el ensayo. Se verificó web 200,/etc/hostsgitlab: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-v2no tuvo salida a internet (reglas de egress con el taggitlab-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 stopysystemctl 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 faltareconfigureen la VM vieja - Frenar la aplicación:
gitlab-ctl stopdepuma,sidekiq,gitlab-exporterygitlab-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-remotepor 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 basessonarqubeysils_busquedasy quitar IP pública y authorized networks. Queda solo con IP privada10.1.0.20, protección contra borrado y backups automáticos (heredados) - Copiar a
gitlab-v2el tar del backup por IP interna (12 s).gitlab-secrets.jsonygitlab.rbya 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-v2elgitlab-secrets.jsonoriginal (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 depg-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(dejarskip-auto-backup) ygitlab-ctl reconfigure(1 min 12 s) -
gitlab-ctl start(los servicios estaban frenados desde el mediodía yreconfigureno los levanta; sin gitaly el restore falla congitaly.socket: no such file) y despuésgitlab-ctl stop pumaygitlab-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=repositoriesy después cada.tar.gzen su directorio (vaciado antes), conchown -R git:git:Archivo Directorio uploads.tar.gz/var/opt/gitlab/gitlab-rails/uploadsbuilds.tar.gz/var/opt/gitlab/gitlab-ci/buildsartifacts.tar.gz/var/opt/gitlab/gitlab-rails/shared/artifactspages.tar.gz/var/opt/gitlab/gitlab-rails/shared/pageslfs.tar.gz/var/opt/gitlab/gitlab-rails/shared/lfs-objectsterraform_state.tar.gz/var/opt/gitlab/gitlab-rails/shared/terraform_statepackages.tar.gz/var/opt/gitlab/gitlab-rails/shared/packages -
gitlab-ctl restart,gitlab-rake gitlab:check SANITIZE=true(OK) ygitlab-rake gitlab:doctor:secrets(0 fallas). 240/240 repos presentes -
git ls-remotepor SSH contra la IP degitlab-v2con una clave de usuario real:sils-docsmaster end74442f, 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-gitlabantes 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):
dpkg -i /var/cache/gitlab-debs/gitlab-ee_<versión>-ee.0_amd64.deb(ya descargados)- Forzar las background migrations pendientes (
gitlab-finalize-bgmig.rb) y verificar que no quede ninguna gitlab-rake gitlab:check SANITIZE=true- 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:
| Salto | Versión | Notas | Ensayo | Ventana |
|---|---|---|---|---|
| 1 | 15.11.13 | Último parche de 15.11 | ✅ | ✅ 5,5 min |
| 2 | 16.3.9 | Parada obligatoria | ✅ | ✅ 8,9 min |
| 3 | 16.7.10 | Parada obligatoria | ✅ | ✅ 10,3 min |
| 4 | 16.11.10 | Parada obligatoria. Después: gitlab-ctl restart redis | ✅ | ✅ 8,9 min |
| 5 | 17.1.8 | Parada condicional; la hacemos igual | — | ✅ 9,4 min |
| 6 | 17.3.7 | Parada obligatoria | — | ✅ 9,3 min |
| 7 | 17.5.5 | Parada obligatoria | — | ✅ 9,4 min |
| 8 | 17.8.7 | Parada obligatoria | — | ✅ 10,8 min |
| 9 | 17.11.7 | Parada obligatoria. Después: gitlab-ctl restart redis (→ 7.2.7) | — | ✅ 15,2 min |
| — | Cloud SQL pg-gitlab 14 → 17 | Con GitLab frenado (gitlab-ctl stop). Backup antes del upgrade | ✅ 17,9 min | ✅ 16,5 min |
| 10 | 18.2.8 | Parada obligatoria. Dos problemas, ver abajo | — | ✅ ~35 min |
| 11 | 18.5.7 | Parada obligatoria | — | ⬜ |
| 12 | 18.8.11 | Parada obligatoria | — | ⬜ |
| 13 | 18.11.13 | Parada obligatoria | — | ⬜ |
| 14 | 19.2.9 | Parada obligatoria (exige PG 17) | — | ⬜ |
| 15 | 19.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):
- Dueños mezclados en la DB. La migración
20250428073611_rename_web_hook_logs_sequencefalló conPG::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 rolcloudsqlsuperuser, y el resto degitlab(las secuencias cambian de dueño junto con su tabla). Se pasaron agitlabobjeto por objeto (ALTER TABLE/VIEW/ROUTINE ... OWNER TO gitlab, en una transacción), solo enpublic,gitlab_partitions_dynamicygitlab_partitions_staticy excluyendo los objetos de extensiones (pg_trgm,btree_gist). No se usóREASSIGN OWNEDporque también toca objetos compartidos de la instancia. Script:/root/fixowners.rbengitlab-v2. Como elreconfigurequedó a medias, el reintento condpkg -ifalló en el preinstall (Malformed configuration JSON file); se resolvió congitlab-ctl reconfigure. BackfillSentNotificationsAfterPartitionno puede terminar. La migración18.0agregasent_notifications.created_atcon default2025-04-02para todas las filas existentes (37.085). La migración18.2crea la tabla particionada por mes desde 2025-04, pero la política esretain_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 usamosincoming_email. Decisión: marcar la migración como terminada (update_columns(status: 3)), sin copiar las filas.- 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-gitlabantes del upgrade de PG (ventana: pre-PG17 en GitLab 17.11.7) -
pg-gitlabactualizado 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
| Salto | Versión | Total |
|---|---|---|
| 11 | 18.5.7 | 10,9 min (30 migraciones forzadas) |
| 12 | 18.8.11 | 12,6 min |
| 13 | 18.11.13 | 11 min |
| 14 | 19.2.9 | 11 min |
| 15 | 19.5.1 | 11,9 min |
Fase 5: ventana, cutover y reapertura (con downtime)
- Instalar
certbotengitlab-v2y 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 ED25519SHA256:3f0Z+vaVY+znv5lj0rQda+TU9qXosW+Vu7oudPcMnko, el mismo de siempre. Desde ahí,gcloud compute ssh gitlab-v2necesita--strict-host-key-checking=nouna vez -
gitlab-ctl stopen la VM vieja - Sacar la IP de la VM
gitlaby la IP efímera degitlab-v2, y asignargitlab-ip(34.172.216.218) agitlab-v2(00:17 → 00:18 UTC / 21:18 ART) - Apagar la VM
gitlab(TERMINATED, no se borró) - Probar sin
/etc/hosts:https://gitlab.sils.tech200, 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.tomloriginal,gitlab-runner19.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-v2con34.172.216.218(la misma de siempre, así que las allowlists siguen valiendo); llega ajenkins.sils.tech(200) ysonar.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 degitlab-runners(35.184.210.61). Si algún job se conecta a algo con allowlist por IP (por ejemplo, las authorized networks depg-keeper-instance, que incluyen34.172.216.218, o Cloud Armor), va a fallar. Opciones: reservar una IP estática paragitlab-runnersy 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=10en/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
sendmailygitlab-v2no 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 desde34.172.216.218, conFrom: gitlab@gitlab.sils.techyReply-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 aASPMX.L.GOOGLE.COMcon250 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 (
dpkgfrenado en la pregunta del conffile/etc/letsencrypt/cli.ini). Se terminó con--force-confoldy 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ó ans2/ns3.hostmar.com, 8.8.8.8 y 1.1.1.1 en ~3 min). Confirmado a las 11:36 ART:spf=passydmarc=passen el header de una notificación real. Los mails de las 10:05 y 10:27 todavía dabanspf=nonepor la caché negativa de Gmail (el TTL negativo de la zona es 4 h: mínimo entre el TTL del SOA, 14400, y su campominimum, 86400). Antes dabaspf=noney DMARCfail(p=none). Se cargó en el DNS desils.tech(hostmar) unTXTcon nombregitlaby valorv=spf1 ip4:34.172.216.218 -all.gitlab.sils.teches un registro A, así que admite el TXT; no toca el SPF desils.tech(include:comp.hostmar.com). Con eso SPF y DMARC pasan apass(alineación relajada consils.tech). Verificar condig TXT gitlab.sils.techy un mail de prueba -
Sacar del
config.tomldegitlab-runnersel 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-v2ygitlab-runnerscon carga real del equipo (sigitlab-runnersse satura con 3 jobs a la vez, pasar ae2-standard-4) -
Backup diario (configurado el 2026-10-23):
Qué Cuándo (UTC / ART) Dónde Retención gitlab-backup createcompleto (repos, DB, uploads, artifacts, LFS...)06:00 / 03:00 gs://sils-gitlab-backups/(raíz)7 días en la VM ( backup_keep_time), 30 días en GCSgitlab-ctl backup-etc(gitlab.rb+gitlab-secrets.json)después del anterior gs://sils-gitlab-backups/config/daily/30 días en GCS Backups automáticos de Cloud SQL pg-gitlab+ PITR04:00 UTC Cloud SQL 7 backups, 7 días de PITR - Script:
/usr/local/sbin/gitlab-daily-backup.sh(cron de root0 6 * * *,flockpara no superponerse,PATHexplícito porquegcloudestá en/snap/bin). Log en/var/log/gitlab-daily-backup.log(líneasINICIO,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 traepg_dump17.11, igual que Cloud SQL - Ciclo de vida del bucket: borra
*_gitlab_backup.taryconfig/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, condb/database.sql.gzde 305 MB, sin componentes salteados)
- Script:
-
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) + clonpg-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:secrets0 fallas,gitlab:checkOK, sin migraciones pendientes. En 19.5 ya no aparece el problema deltarque tuvimos con el restore de 15.11Procedimiento probado de restore (desastre total):
- VM Ubuntu 22.04 y
apt-get install gitlab-ee=<misma versión del backup>-ee.0con/etc/gitlab/skip-auto-reconfigurecreado antes - Bajar de GCS el último
*_gitlab_backup.tar(a/var/opt/gitlab/backups/) y el últimoconfig/daily/gitlab_config_*.tar; extraer de estegitlab.rbygitlab-secrets.jsona/etc/gitlab/ - DB: una instancia de Cloud SQL con el usuario
gitlaby la basegitlabhq_production(lo más simple: restaurar un backup depg-gitlabo clonarla) y su IP engitlab_rails['db_host'] - Certificado: copiar
/etc/letsencrypto emitir uno nuevo (no viaja en el backup) rm /etc/gitlab/skip-auto-reconfigure,gitlab-ctl reconfigure,gitlab-ctl start,gitlab-ctl stop puma,gitlab-ctl stop sidekiqchown git:git <backup>ygitlab-backup restore BACKUP=<timestamp> force=yesgitlab-ctl restart,gitlab-rake gitlab:check SANITIZE=true,gitlab-rake gitlab:doctor:secrets- Copiar las host keys de SSH (están en
config/2026-10-22/del bucket), Postfix, cron de certbot y backup, y movergitlab-ip
- VM Ubuntu 22.04 y
-
Snapshot schedule
gitlab-v2-diario(región us-central1): snapshot diario del discogitlab-v2a 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
401por hora de Jenkins (34.123.105.34, user agentJersey/3.1.11) contraGET /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/transportcongitlab.sils.tech discard:y notificaciones globales endisabledpararootyghost; la cola quedó vacía). Detalle: Postfix no entrega a@gitlab.sils.tech(mydestination = localhost), así que la salida del cron de certbot (aroot) y los avisos de rebote (agitlab@) quedan en cola reintentando contra el puerto 25 de la propia IP. Los rebotes son de notificaciones arootyghost, 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 usuarioroot -
Nota: los pipelines #231 y #232 de
sils-docs(12:57 UTC del 23/10) quedaron enrunner_interruptedpor el reinicio degitlab-runneral sacar el runner muerto; los siguientes, OK -
Revisar pipelines que puedan romperse por cambios de 17/18 (por ejemplo, la allowlist de
CI_JOB_TOKENentre proyectos) -
Limpieza de webhooks de ClickUp (2026-10-26): se borraron los 53 (40 a
api.clickup.com/:id/gitlab/webhookEvent, que responde siempre410 Gone, y 13 asearch.clickup-prod.com/webhooks/:id/, que responde siempre500). 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) engs://sils-gitlab-backups/config/2026-10-26-clickup-webhooks-borrados.json. La sincronización por API con el usuarioclickup-bot(PATclickup-sync) no se tocó y sigue activa -
IP fija para los runners (2026-10-26): la IP efímera
35.184.210.61degitlab-runnersse 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 depg-keeper-instance(entradagitlab, 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.61a ninguna allowlist. Si un servicio externo (fuera de GCP) filtraba por la IP vieja, hay que agregarle35.184.210.61 -
Sacar
34.172.216.218de las authorized networks depg-keeper-instancesi ya no la usa nadie (GitLab ahora usapg-gitlabpor IP privada) -
Borrar
gitlabhq_productiondepg-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.debde/var/cache/gitlab-debs(20 GB) -
Borrar la VM
gitlaby su disco (después de 2 semanas sin problemas). Conservarsnapshot-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:
- Apagar
gitlab-v2. - Si la IP ya se movió, devolver
gitlab-ipa la VMgitlab. - Encender la VM
gitlab. Arranca contrapg-keeper-instance, que no fue modificada (todas las migraciones corrieron sobrepg-gitlab). gitlab-ctl starty sacar la página de mantenimiento.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.
| Tramo | Estimado |
|---|---|
| Estimación con los tiempos medidos en el ensayo: |
| Tramo | Medido en el ensayo | Estimado |
|---|---|---|
| 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 forzadas | 16.11: 8 min | ~2 h 15 min |
| Upgrade de Cloud SQL de 14 a 17 | 17 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
| Fecha | Qué se hizo | Quién |
|---|---|---|
| 2026-10-22 | Relevamiento del servidor y plan | Franco |
| 2026-10-22 | Fase 0: disco liberado (98% → 25%), configs en GCS, backup de prueba (11,6 min / 1,36 GB), backup de Cloud SQL, inventario | Franco |
| 2026-10-22 | Revocados 3 PATs sin uso desde 2023–2024 (ids 10, 11, 15) | Franco |
| 2026-10-22 09:20 | Ventana fijada para hoy 17:00–23:00 ART; banner de mantenimiento publicado en GitLab | Franco |
| 2026-10-22 09:45 | Fase 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 → 17 | Franco |
| 2026-10-22 09:55 | Ensayo: 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 curso | Franco |
| 2026-10-22 11:40 | Ensayo 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 h | Franco |
| 2026-10-22 12:45 | Reinstalació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 frenados | Franco |
| 2026-10-22 17:01 | Inicio de la ventana. VM vieja congelada | Franco |
| 2026-10-22 17:24 | Fase 3 completa: restore en gitlab-v2 sobre pg-gitlab | Franco |
| 2026-10-22 19:03 | Saltos hasta 17.11.7 | Franco |
| 2026-10-22 19:26 | pg-gitlab en PostgreSQL 17 | Franco |
| 2026-10-22 20:01 | 18.2.8 después de arreglar dueños de objetos en la DB y marcar terminada la migración de sent_notifications | Franco |
| 2026-10-22 21:05 | GitLab 19.5.1 | Franco |
| 2026-10-22 21:18 | IP movida a gitlab-v2, VM vieja apagada | Franco |
| 2026-10-22 21:32 | GitLab reabierto. Runners 9/10 vivos, pipeline real OK, banner sacado | Franco |
| 2026-10-23 09:30 | Chequeo: 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 nuevo | Franco |