Herramientas de usuario

Herramientas del sitio


clase:daw:daw-proyecto:tema04

Diferencias

Muestra las diferencias entre dos versiones de la página.

Enlace a la vista de comparación

Próxima revisión
Revisión previa
clase:daw:daw-proyecto:tema04 [2026/10/13 19:41] – creado Lorenzoclase:daw:daw-proyecto:tema04 [2026/10/14 14:08] (actual) – Tema nuevo «2. IA y agentes»: los temas 2 a 7 pasan a ser 3 a 8, con sus referencias cruzadas renumeradas claude
Línea 1: Línea 1:
-====== 4. Despliegue en el proyecto ======+====== 4. Docker en el proyecto ====== 
 + 
 +Objetivo del tema: cada worktree tiene **su propia aplicación completa funcionando** (base de datos, backend y frontend), sin chocar con las de los demás worktrees. Al acabar, ''/git-flow start'' deja el worktree con sus puertos asignados y ''./app.sh start'' levanta la aplicación del issue con datos. 
 + 
 +Lo que ya sabéis y aquí no se repite: imágenes y contenedores ([[clase:daw:daw:1eval:tema03|Despliegue 3]]), Dockerfile ([[clase:daw:daw:1eval:tema04|Despliegue 4]]), Docker Compose ([[clase:daw:daw:1eval:tema04.b|Despliegue 4.b]]) y redes y nginx-proxy ([[clase:daw:daw:1eval:tema04.c|Despliegue 4.c]]). Automatizar Maven y Node: [[clase:daw:proyecto:1eval:tema03|Proyecto 3]]. 
 + 
 +===== Tres contenedores ===== 
 + 
 +La aplicación son tres contenedores, uno por caja del diagrama C4 de contenedores: 
 + 
 +^ Servicio ^ Imagen ^ Qué hace ^ 
 +| ''db'' | ''mariadb'' | La base de datos. Solo accesible desde el backend | 
 +| ''backend'' | Construida: Maven → JRE | El JAR de Spring Boot en el puerto 8080 | 
 +| ''frontend'' | Construida: Angular → Nginx | Sirve el Angular compilado y hace de proxy de ''/api'' al backend | 
 + 
 +El navegador solo habla con ''frontend''. Todo lo que empieza por ''/api'' lo reenvía Nginx al backend, así el frontend no necesita saber en qué puerto está el backend y no hay problemas de CORS. 
 + 
 +Los dos Dockerfile son **multi-stage** y los escribe la IA. Lo único que hay que vigilar: 
 + 
 +<sxh bash;title:backend/Dockerfile> 
 +FROM maven:3.9-eclipse-temurin-25 AS build 
 +WORKDIR /app 
 +COPY pom.xml . 
 +RUN --mount=type=cache,target=/root/.m2 mvn -q dependency:go-offline 
 +COPY src ./src 
 +RUN --mount=type=cache,target=/root/.m2 mvn -q -DskipTests package 
 + 
 +FROM eclipse-temurin:25-jre 
 +RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* 
 +COPY --from=build /app/target/*.jar /app.jar 
 +ENTRYPOINT ["java", "-jar", "/app.jar"] 
 +</sxh> 
 + 
 +El ''curl'' es para el ''healthcheck'' de abajo. El ''--mount=type=cache'' sobre ''.m2'' es lo que hace que reconstruir la imagen tarde segundos y no minutos: sin él, cada ''build'' vuelve a descargar todas las dependencias. 
 + 
 +<sxh bash;title:frontend/Dockerfile> 
 +FROM node:22 AS build 
 +WORKDIR /app 
 +COPY package*.json . 
 +RUN npm ci 
 +COPY . . 
 +RUN npx ng build --configuration production 
 + 
 +FROM nginx:alpine 
 +COPY nginx.conf /etc/nginx/conf.d/default.conf 
 +COPY --from=build /app/dist/frontend/browser /usr/share/nginx/html 
 +</sxh> 
 + 
 +<sxh plain;title:frontend/nginx.conf> 
 +server { 
 +    listen 80; 
 +    root /usr/share/nginx/html; 
 + 
 +    location /api/ { 
 +        proxy_pass http://backend:8080/api/; 
 +    } 
 + 
 +    location / { 
 +        try_files $uri $uri/ /index.html; 
 +    } 
 +} 
 +</sxh> 
 + 
 +El ''try_files'' es obligatorio en una aplicación Angular: sin él, recargar el navegador en ''/clientes/12'' da un 404 de Nginx. 
 + 
 +===== Producción y desarrollo con los mismos ficheros ===== 
 + 
 +Docker Compose carga ''compose.yaml'' y, si existe, ''compose.override.yaml'' encima. Eso se usa así: 
 + 
 +  * ''compose.yaml'' **es producción**: imágenes construidas, la base de datos en una red interna sin puertos publicados, el frontend en la red de nginx-proxy con su ''VIRTUAL_HOST''. 
 +  * ''compose.override.yaml'' **cambia lo de desarrollo**. Está en el repositorio y Compose lo carga solo. En el servidor, el workflow de despliegue (tema 7) fija ''COMPOSE_FILE=compose.yaml'' y el override no se aplica. 
 + 
 +<sxh yaml;title:compose.yaml> 
 +services: 
 +  db: 
 +    image: mariadb:11 
 +    environment: 
 +      MARIADB_DATABASE: proyecto 
 +      MARIADB_USER: proyecto 
 +      MARIADB_PASSWORD: ${DB_PASSWORD} 
 +      MARIADB_ROOT_PASSWORD: ${DB_PASSWORD} 
 +    volumes: 
 +      - db-data:/var/lib/mysql 
 +    networks: 
 +      - interna 
 +    healthcheck: 
 +      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] 
 +      interval: 5s 
 +      retries: 20 
 + 
 +  backend: 
 +    build: ./backend 
 +    environment: 
 +      SPRING_DATASOURCE_URL: jdbc:mariadb://db:3306/proyecto 
 +      SPRING_DATASOURCE_USERNAME: proyecto 
 +      SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD} 
 +    depends_on: 
 +      db: 
 +        condition: service_healthy 
 +    networks: 
 +      - interna 
 +    healthcheck: 
 +      test: ["CMD", "curl", "-f", "http://localhost:8080/api/hola"] 
 +      interval: 5s 
 +      retries: 20 
 + 
 +  frontend: 
 +    build: ./frontend 
 +    environment: 
 +      VIRTUAL_HOST: <alumno>.proyectosdaw.cip.fpmislata.com 
 +    depends_on: 
 +      backend: 
 +        condition: service_healthy 
 +    networks: 
 +      - interna 
 +      - proxy 
 + 
 +volumes: 
 +  db-data: 
 + 
 +networks: 
 +  interna: 
 +  proxy: 
 +    external: true 
 +    name: nginx-proxy-network 
 +</sxh> 
 + 
 +Los ''healthcheck'' con ''depends_on: condition: service_healthy'' hacen que el backend no arranque hasta que MariaDB acepte conexiones, y el frontend hasta que el backend responda. Sin eso, el backend muere al arrancar porque la base de datos aún no está lista. 
 + 
 +<sxh yaml;title:compose.override.yaml> 
 +services: 
 +  db: 
 +    ports: 
 +      - "${DB_PORT:-3306}:3306" 
 + 
 +  backend: 
 +    ports: 
 +      - "${BACKEND_PORT:-8080}:8080" 
 +    environment: 
 +      SPRING_PROFILES_ACTIVE: ${PERFIL:-datos-prueba} 
 + 
 +  frontend: 
 +    build: 
 +      target: build 
 +    command: npx ng serve --host 0.0.0.0 --proxy-config proxy.conf.json 
 +    volumes: 
 +      - ./frontend:/app 
 +      - /app/node_modules 
 +    ports: 
 +      - "${FRONTEND_PORT:-4200}:4200" 
 +</sxh> 
 + 
 +Lo que cambia en desarrollo: 
 + 
 +  * **Se publican los puertos**, para entrar desde el navegador y para que Maven y Playwright lleguen a la base de datos y al frontend desde el host. 
 +  * **El frontend pasa a ''ng serve''** con la carpeta local ''frontend/'' montada dentro: al guardar un fichero, el navegador se recarga. ''proxy.conf.json'' manda ''/api'' a ''%%http://backend:8080%%''. El volumen anónimo sobre ''node_modules'' evita que los módulos instalados en la imagen se tapen con los del host. 
 +  * **El backend no tiene recarga en caliente**: cada cambio en Java es ''docker compose up --build backend''. Con la caché de Maven son unos segundos. Y no pasa nada. 
 +  * **Datos de prueba** activados (abajo). 
 + 
 +<note important>La red ''nginx-proxy-network'' es externa: en el servidor la crea nginx-proxy, pero en el portátil hay que crearla una vez, ''docker network create nginx-proxy-network'', o Compose se niega a arrancar. ''app.sh start'' lo hace si no existe.</note> 
 + 
 +===== Un compose por worktree ===== 
 + 
 +Dos worktrees con el mismo ''compose.yaml'' crearían contenedores con el mismo nombre y pedirían los mismos puertos. Lo resuelve un fichero ''.env'' **en cada worktree**, ignorado por git, que Compose lee solo: 
 + 
 +<sxh bash;title:.env> 
 +COMPOSE_PROJECT_NAME=mi-proyecto-12-alta-clientes 
 +DB_PORT=33112 
 +BACKEND_PORT=8112 
 +FRONTEND_PORT=4212 
 +DB_PASSWORD=dev 
 +</sxh> 
 + 
 +  * ''COMPOSE_PROJECT_NAME'' es el prefijo de contenedores, redes y volúmenes de ese worktree. Dos proyectos distintos no se tocan. 
 +  * Los puertos los busca ''/git-flow start'' al crear el worktree: prueba a partir de una base y se queda con los libres. 
 +  * Las variables llevan valor por defecto, ''${BACKEND_PORT:-8080}'', para cuando no hay ''.env'': en CI (tema 7). 
 + 
 +''/git-flow finish'' termina con ''docker compose down -v'': se van los contenedores **y el volumen** de la base de datos de ese worktree. No queda nada. 
 + 
 +===== La primera tabla ===== 
 + 
 +Hasta ahora el backend no tiene base de datos. Se añaden ahora, con un prompt directo al agente (el flujo de trabajo de verdad llega en el tema 5; esto es solo para tener algo que ver): 
 + 
 +  * Dependencias ''spring-boot-starter-data-jpa'', ''mariadb-java-client'' y ''flyway-mysql''. 
 +  * Una migración ''%%backend/src/main/resources/db/migration/V1__clientes.sql%%'' con una tabla. 
 +  * Un ''GET /api/clientes'' que la lista y una página de Angular que la muestra. 
 + 
 +El esquema **siempre** va en migraciones de Flyway, ''%%V<n>__descripcion.sql%%'', y nunca con ''ddl-auto''. Así cada worktree, cada runner de CI y el servidor construyen la misma base de datos desde cero. 
 + 
 +===== Datos de prueba ===== 
 + 
 +Cada worktree arranca con la base de datos vacía: no hay nada que ver en la aplicación ni se pueden escribir tests E2E que dependan de que exista un cliente. Los datos de prueba se separan del esquema: 
 + 
 +<code> 
 +backend/src/main/resources/db/ 
 +├── migration/            <- esquema: V1__clientes.sql, V2__pedidos.sql ... 
 +└── datos-prueba/         <- datos: R__clientes.sql, R__pedidos.sql ... 
 +</code> 
 + 
 +Los de ''datos-prueba/'' son migraciones **repetibles** de Flyway (''%%R__%%''): se ejecutan después de todas las versionadas, y como la base de datos de un worktree siempre nace vacía, se ejecutan una vez. Flyway solo mira esa carpeta cuando está activo el perfil ''datos-prueba'': 
 + 
 +<sxh plain;title:application-datos-prueba.properties> 
 +spring.flyway.locations=classpath:db/migration,classpath:db/datos-prueba 
 +</sxh> 
 + 
 +  * En desarrollo el perfil lo activa ''compose.override.yaml''. Para probar la aplicación vacía: ''PERFIL=default docker compose up -d --build backend''. 
 +  * En producción ''compose.yaml'' no activa el perfil: los datos de prueba **nunca llegan al servidor**. 
 +  * Para volver al estado inicial (antes de pasar los E2E, por ejemplo): ''docker compose down -v && docker compose up -d''. 
 + 
 +Cada entidad nueva trae su ''%%R__<entidad>.sql%%'' con unos cuantos registros; el steering de cc-sdd (tema 6) obliga a que cada CRUD lo traiga. 
 + 
 +===== El script app.sh ===== 
 + 
 +Operar la aplicación es siempre lo mismo y no necesita ninguna inteligencia, así que no es una skill: es un script de bash en la raíz del repositorio, con cuatro subcomandos y la gramática del tema 1. Bash está en los tres sistemas (en Windows lo trae Git, tema 3). Y es el mismo script que se usa en el servidor (tema 7): no hay una forma de operar en local y otra en producción. 
 + 
 +<sxh bash;title:app.sh> 
 +#!/bin/bash 
 +# La aplicación de este worktree: start | stop [-v] | status | logs 
 +set -e 
 +cd "$(dirname "$0")" 
 +case "$1" in 
 +  start) 
 +    docker network inspect nginx-proxy-network >/dev/null 2>&1 || docker network create nginx-proxy-network 
 +    docker compose up -d --build --wait 
 +    [ -f .env ] && . ./.env 
 +    echo "Aplicación en http://localhost:${FRONTEND_PORT:-4200}" ;; 
 +  stop)   shift; docker compose down "$@" ;; 
 +  status) docker compose ps ;; 
 +  logs)   docker compose logs --tail=200 ;; 
 +  *)      echo "uso: $0 start | stop [-v] | status | logs"; exit 1 ;; 
 +esac 
 +</sxh> 
 + 
 +  * ''./app.sh start'' vale para todo: la primera vez, después de cambiar el backend (reconstruye la imagen y la vuelve a levantar) y al día siguiente tras un ''stop''. ''--wait'' espera a los ''healthcheck'', así que cuando escribe la URL la aplicación ya responde. 
 +  * ''./app.sh stop'' para los contenedores y conserva la base de datos. ''./app.sh stop -v'' la borra también: es lo que hace ''/git-flow finish'' al cerrar el issue, y lo que se hace para volver a los datos de prueba iniciales. 
 +  * ''./app.sh status'' y ''./app.sh logs'' son lo primero que se mira cuando algo no responde. 
 +  * Trabaja sobre la carpeta en la que está y lee su ''.env'', así que cada worktree opera el suyo. En el servidor no hay ''.env'': valen los valores por defecto. 
 + 
 +<note>Los scripts se guardan con finales de línea de Linux o bash en Windows no los entiende. Una línea en ''.gitattributes'' lo garantiza para todos: ''*.sh text eol=lf''.</note> 
 + 
 +===== Dónde corren Maven y los tests ===== 
 + 
 +Las imágenes son de **ejecución**: el backend es un JRE sin Maven y el frontend es Nginx. Así que compilar y pasar tests se hace **en el host**, que para eso están instalados el JDK, Maven y Node: 
 + 
 +^ Qué ^ Dónde ^ Contra qué ^ 
 +| ''mvn test'' (unitarios y ArchUnit) | ''backend/'' del worktree | Nada: no necesitan la aplicación levantada | 
 +| ''npm run test:api'' (API) | ''frontend/'' del worktree | ''%%http://localhost:$FRONTEND_PORT/api%%'' | 
 +| ''npm run test:e2e'' (E2E) | ''frontend/'' del worktree | ''%%http://localhost:$FRONTEND_PORT%%'' | 
 + 
 +Son los tres comandos de tests del proyecto. Maven y npm funcionan igual en Linux, Windows y Mac, y son lo que ejecutan el alumno, el agente y CI (tema 7). ''mvn test'' forma parte del build y corre en cada compilación. Los de API y los E2E **no forman parte del build**: se lanzan aparte, cuando se quiere, con los otros dos comandos. Los dos son de Playwright y atacan la aplicación levantada del worktree (tema 6). En el tema 6 la skill ''/test'' los envuelve con la gramática del tema 1 (''/test api run'') y explica lo que sale en rojo; CI los ejecuta tal cual. 
 + 
 +==== npm run ==== 
 + 
 +''npm run <nombre>'' ejecuta un comando guardado en la sección ''scripts'' del ''package.json''. Es la forma de tener en el proyecto los comandos largos con un nombre corto, sin depender del sistema operativo: npm los ejecuta desde la carpeta del ''package.json'' y con los programas de ''node_modules/.bin'' en el PATH, así que ''playwright'' se escribe sin ''npx''. 
 + 
 +<sxh json;title:frontend/package.json> 
 +"scripts": { 
 +  "start": "ng serve", 
 +  "build": "ng build", 
 +  "test:api": "docker compose down -v && docker compose up -d --wait && playwright test --project=api", 
 +  "test:e2e": "docker compose down -v && docker compose up -d --wait && playwright test --project=e2e" 
 +} 
 +</sxh> 
 + 
 +Los dos scripts resetean la base de datos del worktree, esperan a que la aplicación esté sana y pasan los tests de un proyecto de Playwright (''api'' o ''e2e'', tema 6). El ''&&'' lo entienden igual ''cmd'' y bash. ''npm run'' sin nombre lista los scripts que hay. ''start'' y ''build'' ya los trae Angular: son los que ejecutan ''npm start'' y ''npm run build''. 
 + 
 +Esto va en ''AGENTS.md'' tal cual, porque es lo que el agente ejecuta para comprobar su trabajo. Si no lo sabe, intentará ''mvn test'' dentro del contenedor y fallará, o ni probará. 
 + 
 +===== Ejercicios ===== 
 + 
 +==== Ejercicio 1 ==== 
 +  * Crea un issue «Aplicación en Docker» e inícialo con ''/git-flow''. 
 +  * Pide al agente los dos ''Dockerfile'', ''compose.yaml'', ''compose.override.yaml'', ''frontend/nginx.conf'' y ''frontend/proxy.conf.json'' siguiendo este tema. Revisa que el backend tiene la caché de ''.m2'', que ''db'' no publica puertos en ''compose.yaml'' y que ''nginx.conf'' tiene el ''try_files''. 
 +  * Añade ''.env'' al ''.gitignore'' y crea uno a mano en el worktree con ''COMPOSE_PROJECT_NAME'', los tres puertos y ''DB_PASSWORD''. 
 +  * ''docker network create nginx-proxy-network'' y ''docker compose up -d --build''. Comprueba con ''docker compose ps'' que los tres servicios están //healthy//. 
 +  * Entra en ''%%http://localhost:<FRONTEND_PORT>%%'' y comprueba que se ve el ''/api/hola''. 
 + 
 +==== Ejercicio 2 ==== 
 +  * En el mismo worktree, pide al agente que añada JPA, MariaDB y Flyway, la migración ''%%V1__clientes.sql%%'', un ''GET /api/clientes'' y una página de Angular que liste los clientes. 
 +  * Crea ''application-datos-prueba.properties'' y ''db/datos-prueba/%%R__clientes.sql%%'' con cinco clientes. 
 +  * ''docker compose up -d --build backend'' y comprueba que la lista muestra los cinco clientes. 
 +  * Arranca con ''PERFIL=default'' tras un ''down -v'' y comprueba que la lista sale vacía. Vuelve a dejarlo con datos. 
 +  * Cambia un texto de la página de Angular, guarda y comprueba que el navegador se recarga solo. 
 + 
 +==== Ejercicio 3 ==== 
 +  * Añade a ''application.properties'' la URL con ''${DB_PORT:3306}'' y ejecuta ''mvn test'' desde ''backend/''. Tiene que pasar sin nada levantado. 
 +  * Escribe en ''AGENTS.md'': que la aplicación se opera con ''./app.sh start | stop | status | logs'', que los tests se lanzan en el host con ''mvn test'', ''npm run test:api'' y ''npm run test:e2e'', y que el esquema va siempre en migraciones. 
 +  * ''/git-flow commit'' y ''/git-flow finish''. 
 + 
 +==== Ejercicio 4 ==== 
 +  * Escribe ''app.sh'' y el ''.gitattributes''. Pruébalos: arrancar, cambiar un texto del backend, volver a arrancar y ver el cambio; parar, arrancar y ver que los datos siguen; parar con ''-v'', arrancar y ver los datos de prueba de nuevo. 
 +  * Amplía ''/git-flow'': ''start'' escribe el ''.env'' del worktree buscando puertos libres y dice cómo arrancar; ''finish'' ejecuta ''./app.sh stop -v'' antes de borrar el worktree. 
 +  * Finaliza el issue. 
 + 
 +==== Ejercicio 5 ==== 
 +  * Crea dos issues cualesquiera, inícialos y arranca los dos con ''./app.sh start''. Comprueba que cada worktree tiene su ''.env'' con puertos distintos y que las dos aplicaciones están levantadas a la vez (''docker ps''). 
 +  * Cambia los datos de prueba en uno de los dos y comprueba que el otro no se entera. 
 +  * Finaliza los dos y comprueba con ''docker ps -a'' y ''docker volume ls'' que no queda nada de ellos.
  
clase/daw/daw-proyecto/tema04.1791913274.txt.gz · Última modificación: por Lorenzo