246 lines
12 KiB
Markdown
246 lines
12 KiB
Markdown
# Jenkins SCO — Deploy Server
|
||
|
||
Jenkins centralizado para el despliegue de software en terminales SCO (Self-Checkout) en producción.
|
||
|
||
## Estructura del repositorio
|
||
|
||
```
|
||
.
|
||
├── docker-compose.yml
|
||
└── data/
|
||
└── jenkins/
|
||
├── Dockerfile # Imagen Jenkins con plugins
|
||
├── jenkins.yaml # Configuración JCasC (nodos, seguridad, jobs)
|
||
├── deploy/
|
||
│ └── install.sh # Script desplegado en cada SCO
|
||
└── pipelines/
|
||
├── Jenkinsfile.ralph # Pipeline del cliente Ralph
|
||
└── Jenkinsfile.lab # Pipeline del entorno de laboratorio
|
||
```
|
||
|
||
## Arquitectura
|
||
|
||
- **Jenkins master**: corre en Docker en un servidor (`172.19.13.204`). No expone puertos al host; solo es alcanzable dentro de la red interna Docker `proxy`.
|
||
- **Nginx Proxy Manager (NPM)**: reverse proxy interno que resuelve el hostname `jenkins.laoficina1782.com` y hace forward hacia `jenkins-server:8080`. Corre en el mismo servidor, red `proxy`.
|
||
- **Cloudflare Tunnel (`cloudflared`)**: es el único punto de entrada desde internet. El contenedor `cloudflared` abre una conexión saliente hacia el edge de Cloudflare (autenticada con `TUNNEL_TOKEN`), por lo que **no hace falta abrir puertos entrantes en el firewall/router** del servidor. El tunnel está configurado del lado de Cloudflare para enrutar el hostname público hacia `http://npm:80` dentro de la red `proxy`.
|
||
- **Agentes (SCOs)**: cada terminal SCO conecta al master vía WebSocket (no JNLP), outbound hacia `wss://jenkins.laoficina1782.com/`. El proceso del agente corre como `root`.
|
||
- **Storage**: Minio almacena el software de cada cliente en buckets separados (ej: bucket `ralph`).
|
||
|
||
### Flujo de tráfico
|
||
|
||
```
|
||
Internet
|
||
│
|
||
▼
|
||
Cloudflare Edge (DNS + proxy)
|
||
│ túnel saliente autenticado (TUNNEL_TOKEN)
|
||
▼
|
||
cloudflared (contenedor, red "proxy", sin puertos publicados)
|
||
│ http://npm:80
|
||
▼
|
||
NPM (nginx-proxy-manager) — vhost jenkins.laoficina1782.com
|
||
│ http://jenkins-server:8080
|
||
▼
|
||
jenkins-server (sin puertos publicados al host)
|
||
```
|
||
|
||
Jenkins **no** expone puertos al host: NPM y `cloudflared` se comunican con él por nombre de contenedor dentro de la red Docker `proxy`. Como no hay puertos entrantes abiertos en el servidor, toda la superficie pública depende de Cloudflare (WAF, DNS, TLS terminado en el edge).
|
||
|
||
> ⚠️ NPM publica además `80`, `443` y `81` directamente al host (ver `docker-compose.yml`). Si el firewall del servidor permite esas conexiones, Jenkins/NPM también serían alcanzables sin pasar por Cloudflare. Para que el tunnel sea realmente el único punto de entrada, esos puertos deberían quedar bloqueados a nivel de firewall/red salvo acceso local.
|
||
|
||
### Variables de entorno
|
||
|
||
El tunnel necesita el token generado en el dashboard de Cloudflare (Zero Trust → Networks → Tunnels):
|
||
|
||
```sh
|
||
cp .env.example .env
|
||
# editar .env y setear CLOUDFLARE_TUNNEL_TOKEN=<token>
|
||
```
|
||
|
||
### Labels de agentes
|
||
|
||
Formato: `sco <cliente> <tienda>`
|
||
|
||
| Cliente | Tienda | Agentes |
|
||
|---|---|---|
|
||
| ralph | yabucoa | sco02–sco07 |
|
||
| ralph | san-lorenzo | sco03–sco06 |
|
||
| ralph | rio-grande | sco01–sco06 |
|
||
|
||
## Levantar el servidor
|
||
|
||
```sh
|
||
docker compose up -d --build
|
||
```
|
||
|
||
Jenkins queda disponible en `https://jenkins.laoficina1782.com` una vez configurados el proxy y el tunnel.
|
||
|
||
### Nginx Proxy Manager
|
||
|
||
Panel de administración: `http://172.19.13.204:81` (o localmente en el servidor).
|
||
|
||
Configuración del proxy host para Jenkins:
|
||
|
||
| Campo | Valor |
|
||
|-------|-------|
|
||
| Domain Names | `jenkins.laoficina1782.com` |
|
||
| Scheme | `http` |
|
||
| Forward Hostname | `jenkins-server` |
|
||
| Forward Port | `8080` |
|
||
| Websockets Support | ✅ activado |
|
||
|
||
### Cloudflare Tunnel
|
||
|
||
1. En el dashboard de Cloudflare (Zero Trust → Networks → Tunnels), crear un tunnel y copiar su token.
|
||
2. Configurar la ruta pública (Public Hostname) del tunnel: `jenkins.laoficina1782.com` → `http://npm:80`.
|
||
3. Setear el token en `.env` como `CLOUDFLARE_TUNNEL_TOKEN` (ver arriba).
|
||
4. Levantar el stack: el contenedor `cloudflared` se conecta solo, sin necesidad de abrir puertos en el servidor.
|
||
|
||
## Conectar un agente SCO
|
||
|
||
1. Descargar el agente desde Jenkins:
|
||
```sh
|
||
curl -sO http://<server-ip>:8080/jnlpJars/agent.jar
|
||
mv agent.jar /opt/jenkins-agent/agent.jar
|
||
```
|
||
|
||
2. Obtener el secret del nodo: en Jenkins → *Manage Jenkins → Nodes → (nombre del nodo)*.
|
||
|
||
3. Crear el servicio systemd en el SCO:
|
||
```ini
|
||
# /etc/systemd/system/jenkins-agent.service
|
||
[Unit]
|
||
Description=Jenkins Agent
|
||
After=network.target
|
||
|
||
[Service]
|
||
User=root
|
||
ExecStart=/usr/bin/java -jar /opt/jenkins-agent/agent.jar \
|
||
-url https://jenkins.laoficina1782.com/ \
|
||
-webSocket \
|
||
-secret <SECRET> \
|
||
-name "<nombre-del-nodo>" \
|
||
-workDir "/opt/jenkins-agent"
|
||
Restart=always
|
||
RestartSec=10
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
```
|
||
|
||
4. Habilitar y arrancar:
|
||
```sh
|
||
systemctl daemon-reload
|
||
systemctl enable --now jenkins-agent
|
||
```
|
||
|
||
## Ejecutar un deploy
|
||
|
||
1. En Jenkins, abrir el job `deploy-ralph` (o `deploy-lab` para el entorno de laboratorio).
|
||
2. Click en **Build with Parameters**.
|
||
3. Seleccionar la **tienda** destino en el dropdown (yabucoa / san-lorenzo / rio-grande para `deploy-ralph`; oficina para `deploy-lab`).
|
||
4. Opcionalmente, completar **NODO** con el nombre exacto de un SCO (ej. `ralph-yabucoa-sco05`) para deployar solo en ese nodo. Si se deja vacío, despliega en paralelo en todos los SCOs online de la tienda seleccionada.
|
||
|
||
También se puede disparar desde consola con el Jenkins CLI o `curl`, ver [Ejecutar un job desde consola](#ejecutar-un-job-desde-consola).
|
||
|
||
### Ejecutar un job desde consola
|
||
|
||
```sh
|
||
curl -O https://jenkins.laoficina1782.com/jnlpJars/jenkins-cli.jar
|
||
|
||
java -jar jenkins-cli.jar -s https://jenkins.laoficina1782.com/ \
|
||
-auth TU_USUARIO:TU_API_TOKEN \
|
||
build deploy-lab -p TIENDA=oficina -s -v
|
||
|
||
# Deploy a un solo nodo de la tienda
|
||
java -jar jenkins-cli.jar -s https://jenkins.laoficina1782.com/ \
|
||
-auth TU_USUARIO:TU_API_TOKEN \
|
||
build deploy-lab -p TIENDA=oficina -p NODO=lab-oficina-sco-77 -s -v
|
||
```
|
||
|
||
- `-s` espera a que termine el build; `-v` muestra el log en consola.
|
||
- El API Token se genera en el usuario de Jenkins → Configure → API Token.
|
||
- Alternativa sin el CLI: `curl -X POST https://jenkins.laoficina1782.com/job/deploy-lab/buildWithParameters --user TU_USUARIO:TU_API_TOKEN --data-urlencode "TIENDA=oficina"`.
|
||
|
||
## Qué hace `install.sh`
|
||
|
||
Recibe el nombre del cliente como argumento (`$1`):
|
||
|
||
1. Genera config de rclone apuntando a Minio a través del hostname público `https://minio-api.laoficina1782.com` (antes apuntaba directo a la IP del servidor por el puerto `9000`; ahora pasa por el mismo túnel/proxy que Jenkins, con `force_path_style = true` porque el endpoint ya no es un bucket-subdominio).
|
||
2. Sincroniza el bucket `$CUSTOMER` desde Minio hacia `/opt/sco/` en el SCO (excluyendo datos persistentes del backend), con `--s3-sign-accept-encoding=false --multi-thread-streams=0` para evitar problemas de firma/concurrencia a través del proxy.
|
||
|
||
Los pasos de instalación local (permisos, servicios systemd, autostart, crontabs, paquetes Python, imágenes Docker, inicialización de bases de datos del cpi-server, `engine.properties` de SymmetricDS) ya no están en este script — quedaron fuera del alcance actual de `install.sh`, que hoy solo se ocupa de traer el software del cliente a `/opt/sco/`.
|
||
|
||
## Scripts de alta (create-node.sh / create-client.sh)
|
||
|
||
`data/jenkins/scripts/` tiene dos scripts para no tener que editar `jenkins.yaml`, los Jenkinsfiles y `docker-compose.yml` a mano.
|
||
|
||
**Prerequisito**: [`yq`](https://github.com/mikefarah/yq) (la versión de Go, **no** la de Python — son incompatibles, ver más abajo) y que los scripts tengan permiso de ejecución:
|
||
```sh
|
||
chmod +x data/jenkins/scripts/*.sh
|
||
```
|
||
|
||
Ambos **solo editan archivos** — no hacen `docker compose up -d --build` ni commits. Después de correrlos, revisá con `git diff` y aplicá el rebuild vos mismo.
|
||
|
||
### create-node.sh — agregar un nodo a un cliente ya existente
|
||
|
||
```sh
|
||
data/jenkins/scripts/create-node.sh --cliente <cliente> --tienda <tienda> --numero <numero>
|
||
```
|
||
|
||
- Arma el nombre (`<cliente>-<tienda>-sco<numero>`, `numero` normalizado a 3 dígitos) y el label (`sco <cliente> <tienda>`) siguiendo la convención existente, y agrega el nodo al principio de `.jenkins.nodes` en `jenkins.yaml`.
|
||
- `cliente` y `tienda` deben ser minúsculas, números y guiones (`^[a-z0-9-]+$`) — nada de mayúsculas ni espacios.
|
||
- Valida que el cliente ya esté dado de alta (que exista `Jenkinsfile.<cliente>`) y que la tienda esté en su dropdown de `TIENDA`. Si falla cualquiera de las dos, corta sin tocar `jenkins.yaml`.
|
||
- Detecta nodos duplicados (mismo nombre final) y corta sin duplicar.
|
||
- `--skip-checks` salta las validaciones de cliente/tienda existentes — lo usa `create-client.sh` internamente al dar de alta un cliente nuevo (en ese momento el Jenkinsfile todavía no existe).
|
||
|
||
Ejemplo:
|
||
```sh
|
||
data/jenkins/scripts/create-node.sh --cliente ralph --tienda yabucoa --numero 8
|
||
# -> nodo "ralph-yabucoa-sco008", label "sco ralph yabucoa"
|
||
```
|
||
|
||
### create-client.sh — dar de alta un cliente nuevo completo
|
||
|
||
```sh
|
||
data/jenkins/scripts/create-client.sh --cliente <cliente> \
|
||
--tienda <nombre> [--tienda <nombre> ...] \
|
||
--nodo <tienda>:<numero> [--nodo <tienda>:<numero> ...]
|
||
```
|
||
|
||
- `--tienda` es repetible: declara cada tienda que va a aparecer en el dropdown del Jenkinsfile (se deduplica automáticamente si se repite).
|
||
- `--nodo` es repetible: uno por cada SCO a crear, en formato `<tienda>:<numero>`. Cada `tienda` referenciada en un `--nodo` tiene que haber sido declarada con algún `--tienda`.
|
||
- Corta antes de tocar nada si el cliente ya existe (ya hay `Jenkinsfile.<cliente>` o ya hay un job `deploy-<cliente>` en `jenkins.yaml`).
|
||
- Si falla la creación de algún nodo a mitad del loop (ej. `--nodo` repetido), revierte automáticamente los nodos ya creados en esa misma corrida antes de cortar — no deja nodos huérfanos.
|
||
- Al terminar, genera `data/jenkins/pipelines/Jenkinsfile.<cliente>` (mismo esqueleto que `Jenkinsfile.ralph`, con el parámetro `NODO` opcional), agrega el job `deploy-<cliente>` a `jenkins.yaml` y el volumen correspondiente a `docker-compose.yml`.
|
||
|
||
Ejemplo:
|
||
```sh
|
||
data/jenkins/scripts/create-client.sh --cliente econo-mbs \
|
||
--tienda yabucoa --tienda san-lorenzo \
|
||
--nodo yabucoa:1 --nodo yabucoa:2 --nodo yabucoa:3 \
|
||
--nodo san-lorenzo:3 --nodo san-lorenzo:4
|
||
```
|
||
|
||
### Troubleshooting
|
||
|
||
- **`usage: yq [-h] [--yaml-output] ...` / `yq: error: argument files: can't open '.jenkins...'`**: tenés instalado el `yq` de Python (kislyuk/yq), que usa sintaxis de `jq` y es incompatible. Desinstalalo (`pip uninstall yq`) e instalá el de Go:
|
||
```sh
|
||
wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/local/bin/yq
|
||
chmod +x /usr/local/bin/yq
|
||
hash -r # si ya habías corrido "yq" antes en esa misma terminal, bash puede tener cacheada la ruta vieja
|
||
yq --version # debe decir "yq (https://github.com/mikefarah/yq/) version v4..."
|
||
```
|
||
- **`Permission denied` al correr un script**: falta el bit de ejecución — `chmod +x data/jenkins/scripts/*.sh`.
|
||
- **`printf: NNN: invalid octal number`**: no debería pasar (ya está resuelto), pero si aparece es porque un `--numero` con cero a la izquierda se está interpretando como octal en vez de decimal.
|
||
|
||
## Agregar un nuevo cliente
|
||
|
||
Con los scripts (recomendado, ver arriba), o a mano siguiendo estos pasos:
|
||
|
||
1. Agregar los nodos del cliente en `data/jenkins/jenkins.yaml` con el label `sco <cliente> <tienda>`.
|
||
2. Crear `data/jenkins/pipelines/Jenkinsfile.<cliente>` con el dropdown de tiendas.
|
||
3. Agregar el job en la sección `jobs` de `jenkins.yaml`.
|
||
4. Montar el nuevo Jenkinsfile en `docker-compose.yml`.
|
||
5. Recrear el contenedor: `docker compose up -d --build`.
|