feat(deploy): add Docker and Kubernetes deployment
Docker: - Multi-stage Dockerfile for API (.NET 10) - Multi-stage Dockerfile for frontend (Angular + Nginx) - docker-compose.yml with resource optimization - Nginx reverse proxy configuration - Health checks for all services Kubernetes (k3s): - Namespace and ConfigMap - SQL Server StatefulSet with PVC - API Deployment with HPA - Frontend Deployment - Services and Ingress - Network policies for security - Secrets management Resource optimization: - SQL Server Express with 1GB RAM limit - API with 512MB limit - Frontend with 128MB limit Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
7ba94d59e5
commit
9f11aab2a8
|
|
@ -0,0 +1,10 @@
|
||||||
|
# Variables de entorno para Docker Compose
|
||||||
|
# Copiar a .env y ajustar valores
|
||||||
|
|
||||||
|
# SQL Server
|
||||||
|
DB_PASSWORD=Your_Str0ng_P@ssword!
|
||||||
|
|
||||||
|
# Opcional: cambiar puertos si hay conflictos
|
||||||
|
# API_PORT=5000
|
||||||
|
# FRONTEND_PORT=80
|
||||||
|
# DB_PORT=1433
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
# Backend API - Multi-stage build
|
||||||
|
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
# Cache de NuGet packages - copiamos solo archivos de proyecto primero
|
||||||
|
COPY src/backend/Domain/Domain.csproj Domain/
|
||||||
|
COPY src/backend/Application/Application.csproj Application/
|
||||||
|
COPY src/backend/Adapters/Driving/Api/Adapters.Driving.Api.csproj Adapters/Driving/Api/
|
||||||
|
COPY src/backend/Adapters/Driven/Persistence/Adapters.Driven.Persistence.csproj Adapters/Driven/Persistence/
|
||||||
|
COPY src/backend/Host/Host.csproj Host/
|
||||||
|
|
||||||
|
# Restore
|
||||||
|
RUN dotnet restore Host/Host.csproj
|
||||||
|
|
||||||
|
# Copiar código fuente
|
||||||
|
COPY src/backend/Domain/ Domain/
|
||||||
|
COPY src/backend/Application/ Application/
|
||||||
|
COPY src/backend/Adapters/ Adapters/
|
||||||
|
COPY src/backend/Host/ Host/
|
||||||
|
|
||||||
|
# Build y publish
|
||||||
|
RUN dotnet publish Host/Host.csproj \
|
||||||
|
-c Release \
|
||||||
|
-o /app
|
||||||
|
|
||||||
|
# Runtime image
|
||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Instalar curl para healthcheck
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copiar binarios
|
||||||
|
COPY --from=build /app .
|
||||||
|
|
||||||
|
# Configuración
|
||||||
|
ENV ASPNETCORE_URLS=http://+:5000 \
|
||||||
|
DOTNET_RUNNING_IN_CONTAINER=true
|
||||||
|
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=15s --timeout=3s --start-period=10s --retries=3 \
|
||||||
|
CMD curl -f http://localhost:5000/health || exit 1
|
||||||
|
|
||||||
|
ENTRYPOINT ["dotnet", "Host.dll"]
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
# Frontend - Multi-stage optimizado
|
||||||
|
FROM node:22-alpine AS build
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Aumentar memoria para build Angular
|
||||||
|
ENV NODE_OPTIONS="--max-old-space-size=4096"
|
||||||
|
|
||||||
|
# Cache de dependencias - solo package files primero
|
||||||
|
COPY src/frontend/package.json src/frontend/package-lock.json* ./
|
||||||
|
|
||||||
|
# Instalar con cache
|
||||||
|
RUN npm ci --legacy-peer-deps --prefer-offline
|
||||||
|
|
||||||
|
# Copiar código y compilar
|
||||||
|
COPY src/frontend/ .
|
||||||
|
RUN npm run build -- --configuration production
|
||||||
|
|
||||||
|
# Runtime con Nginx optimizado
|
||||||
|
FROM nginx:alpine AS runtime
|
||||||
|
|
||||||
|
# Instalar curl para healthcheck
|
||||||
|
RUN apk add --no-cache curl
|
||||||
|
|
||||||
|
# Copiar archivos compilados
|
||||||
|
COPY --from=build /app/dist/student-enrollment/browser /usr/share/nginx/html
|
||||||
|
|
||||||
|
# Configuración nginx optimizada
|
||||||
|
COPY deploy/docker/nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
# Usuario no-root
|
||||||
|
RUN chown -R nginx:nginx /usr/share/nginx/html && \
|
||||||
|
chown -R nginx:nginx /var/cache/nginx && \
|
||||||
|
chown -R nginx:nginx /var/log/nginx && \
|
||||||
|
touch /var/run/nginx.pid && \
|
||||||
|
chown nginx:nginx /var/run/nginx.pid
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
|
||||||
|
CMD curl -f http://localhost/ || exit 1
|
||||||
|
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
# Docker Compose - Student Enrollment System
|
||||||
|
# Optimizado para bajo consumo de RAM
|
||||||
|
# Usa archivo .env en la raíz del proyecto para configuración
|
||||||
|
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: mcr.microsoft.com/mssql/server:2022-latest
|
||||||
|
container_name: student-db
|
||||||
|
env_file:
|
||||||
|
- ../../.env
|
||||||
|
environment:
|
||||||
|
- ACCEPT_EULA=Y
|
||||||
|
- MSSQL_SA_PASSWORD=${DB_PASSWORD}
|
||||||
|
- MSSQL_MEMORY_LIMIT_MB=${DB_MEMORY_LIMIT_MB:-512}
|
||||||
|
- MSSQL_AGENT_ENABLED=false
|
||||||
|
- MSSQL_PID=Express
|
||||||
|
ports:
|
||||||
|
- "${DB_PORT:-1433}:1433"
|
||||||
|
volumes:
|
||||||
|
- sqlserver-data:/var/opt/mssql:delegated
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: "1"
|
||||||
|
memory: ${DB_MEMORY_LIMIT:-1280M}
|
||||||
|
reservations:
|
||||||
|
cpus: "0.5"
|
||||||
|
memory: 1024M
|
||||||
|
healthcheck:
|
||||||
|
test: /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P "$${MSSQL_SA_PASSWORD}" -Q "SELECT 1" -C || exit 1
|
||||||
|
interval: 15s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
start_period: 30s
|
||||||
|
networks:
|
||||||
|
- student-network
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
api:
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: deploy/docker/Dockerfile.api
|
||||||
|
args:
|
||||||
|
- DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||||
|
container_name: student-api
|
||||||
|
env_file:
|
||||||
|
- ../../.env
|
||||||
|
environment:
|
||||||
|
- DB_HOST=db
|
||||||
|
- DB_PORT=1433
|
||||||
|
- ConnectionStrings__DefaultConnection=Server=db;Database=${DB_NAME:-StudentEnrollment};User Id=${DB_USER:-sa};Password=${DB_PASSWORD};TrustServerCertificate=True;Max Pool Size=50;Min Pool Size=2
|
||||||
|
- ASPNETCORE_ENVIRONMENT=${ASPNETCORE_ENVIRONMENT:-Development}
|
||||||
|
- CORS_ORIGINS=${CORS_ORIGINS:-http://localhost:8080,http://localhost:4200}
|
||||||
|
- DOTNET_gcServer=0
|
||||||
|
- DOTNET_GCHeapCount=2
|
||||||
|
- DOTNET_gcConcurrent=1
|
||||||
|
ports:
|
||||||
|
- "${API_PORT:-5000}:5000"
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: "2"
|
||||||
|
memory: 512M
|
||||||
|
reservations:
|
||||||
|
cpus: "0.5"
|
||||||
|
memory: 256M
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
|
||||||
|
interval: 15s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 30s
|
||||||
|
networks:
|
||||||
|
- student-network
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: deploy/docker/Dockerfile.frontend
|
||||||
|
container_name: student-frontend
|
||||||
|
ports:
|
||||||
|
- "${FRONTEND_PORT:-8080}:80"
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: "1"
|
||||||
|
memory: 128M
|
||||||
|
reservations:
|
||||||
|
cpus: "0.25"
|
||||||
|
memory: 64M
|
||||||
|
depends_on:
|
||||||
|
api:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- student-network
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
sqlserver-data:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
networks:
|
||||||
|
student-network:
|
||||||
|
driver: bridge
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
# Nginx optimizado para rendimiento local
|
||||||
|
# worker_processes auto usa todos los CPUs disponibles
|
||||||
|
|
||||||
|
upstream api_backend {
|
||||||
|
server student-api:5000;
|
||||||
|
keepalive 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# Buffers optimizados
|
||||||
|
client_body_buffer_size 16k;
|
||||||
|
client_max_body_size 8m;
|
||||||
|
proxy_buffer_size 128k;
|
||||||
|
proxy_buffers 4 256k;
|
||||||
|
proxy_busy_buffers_size 256k;
|
||||||
|
|
||||||
|
# Compresión Gzip
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_min_length 256;
|
||||||
|
gzip_comp_level 5;
|
||||||
|
gzip_proxied any;
|
||||||
|
gzip_types
|
||||||
|
text/plain
|
||||||
|
text/css
|
||||||
|
text/javascript
|
||||||
|
application/json
|
||||||
|
application/javascript
|
||||||
|
application/xml
|
||||||
|
application/xml+rss
|
||||||
|
image/svg+xml;
|
||||||
|
|
||||||
|
# Brotli deshabilitado - requiere nginx-mod-http-brotli
|
||||||
|
# brotli on;
|
||||||
|
# brotli_comp_level 4;
|
||||||
|
|
||||||
|
# SPA routing
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
|
||||||
|
# Cache para HTML (corto)
|
||||||
|
add_header Cache-Control "no-cache, must-revalidate";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Proxy GraphQL con conexiones persistentes
|
||||||
|
location /graphql {
|
||||||
|
proxy_pass http://api_backend;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
|
||||||
|
# Keepalive
|
||||||
|
proxy_set_header Connection "";
|
||||||
|
|
||||||
|
# WebSocket support
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
|
||||||
|
# Headers
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
# Timeouts optimizados
|
||||||
|
proxy_connect_timeout 10s;
|
||||||
|
proxy_send_timeout 60s;
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
location /health {
|
||||||
|
proxy_pass http://api_backend;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Connection "";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Cache agresivo para assets estáticos
|
||||||
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
access_log off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Security headers
|
||||||
|
add_header X-Frame-Options "DENY" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
|
|
||||||
|
# Desactivar logs de assets para reducir I/O
|
||||||
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
|
||||||
|
access_log off;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Script de inicio rápido - Docker Compose optimizado
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||||
|
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
|
# Colores
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
echo -e "${GREEN}╔═══════════════════════════════════════════════╗${NC}"
|
||||||
|
echo -e "${GREEN}║ Student Enrollment System - Docker Deploy ║${NC}"
|
||||||
|
echo -e "${GREEN}╚═══════════════════════════════════════════════╝${NC}"
|
||||||
|
|
||||||
|
# Verificar Docker
|
||||||
|
if ! command -v docker &> /dev/null; then
|
||||||
|
echo -e "${RED}Error: Docker no está instalado${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Limpiar contenedores anteriores si existen
|
||||||
|
echo -e "\n${YELLOW}► Limpiando contenedores anteriores...${NC}"
|
||||||
|
docker compose down --remove-orphans 2>/dev/null || true
|
||||||
|
|
||||||
|
# Build con cache
|
||||||
|
echo -e "\n${YELLOW}► Construyendo imágenes (con cache)...${NC}"
|
||||||
|
DOCKER_BUILDKIT=1 docker compose build --parallel
|
||||||
|
|
||||||
|
# Iniciar servicios
|
||||||
|
echo -e "\n${YELLOW}► Iniciando servicios...${NC}"
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# Esperar a que los servicios estén listos
|
||||||
|
echo -e "\n${YELLOW}► Esperando servicios...${NC}"
|
||||||
|
|
||||||
|
echo -n " SQL Server: "
|
||||||
|
timeout 60 bash -c 'until docker compose exec -T db /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P "${DB_PASSWORD:-Your_Str0ng_P@ssword!}" -Q "SELECT 1" -C &>/dev/null; do sleep 2; done' && echo -e "${GREEN}✓${NC}" || echo -e "${RED}✗${NC}"
|
||||||
|
|
||||||
|
echo -n " API .NET: "
|
||||||
|
timeout 60 bash -c 'until curl -sf http://localhost:5000/health &>/dev/null; do sleep 2; done' && echo -e "${GREEN}✓${NC}" || echo -e "${RED}✗${NC}"
|
||||||
|
|
||||||
|
echo -n " Frontend: "
|
||||||
|
timeout 30 bash -c 'until curl -sf http://localhost/ &>/dev/null; do sleep 2; done' && echo -e "${GREEN}✓${NC}" || echo -e "${RED}✗${NC}"
|
||||||
|
|
||||||
|
# Resumen
|
||||||
|
echo -e "\n${GREEN}╔═══════════════════════════════════════════════╗${NC}"
|
||||||
|
echo -e "${GREEN}║ ✓ Sistema desplegado correctamente ║${NC}"
|
||||||
|
echo -e "${GREEN}╠═══════════════════════════════════════════════╣${NC}"
|
||||||
|
echo -e "${GREEN}║ Frontend: http://localhost ║${NC}"
|
||||||
|
echo -e "${GREEN}║ API GraphQL: http://localhost:5000/graphql ║${NC}"
|
||||||
|
echo -e "${GREEN}║ SQL Server: localhost:1433 ║${NC}"
|
||||||
|
echo -e "${GREEN}╚═══════════════════════════════════════════════╝${NC}"
|
||||||
|
|
||||||
|
echo -e "\n${YELLOW}Comandos útiles:${NC}"
|
||||||
|
echo " docker compose logs -f # Ver logs"
|
||||||
|
echo " docker compose ps # Ver estado"
|
||||||
|
echo " docker compose down # Detener todo"
|
||||||
|
echo " docker compose restart api # Reiniciar API"
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: student-api
|
||||||
|
namespace: student-enrollment
|
||||||
|
labels:
|
||||||
|
app: student-api
|
||||||
|
spec:
|
||||||
|
replicas: 2
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: student-api
|
||||||
|
strategy:
|
||||||
|
type: RollingUpdate
|
||||||
|
rollingUpdate:
|
||||||
|
maxSurge: 1
|
||||||
|
maxUnavailable: 0
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: student-api
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: api
|
||||||
|
image: docker.io/library/academia-api:v2
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
ports:
|
||||||
|
- containerPort: 5000
|
||||||
|
name: http
|
||||||
|
env:
|
||||||
|
- name: ASPNETCORE_ENVIRONMENT
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: student-config
|
||||||
|
key: ASPNETCORE_ENVIRONMENT
|
||||||
|
- name: ASPNETCORE_URLS
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: student-config
|
||||||
|
key: ASPNETCORE_URLS
|
||||||
|
- name: ConnectionStrings__DefaultConnection
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: student-secrets
|
||||||
|
key: db-connection-string
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "256Mi"
|
||||||
|
cpu: "100m"
|
||||||
|
limits:
|
||||||
|
memory: "512Mi"
|
||||||
|
cpu: "500m"
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 5000
|
||||||
|
initialDelaySeconds: 15
|
||||||
|
periodSeconds: 20
|
||||||
|
timeoutSeconds: 5
|
||||||
|
failureThreshold: 3
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 5000
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 10
|
||||||
|
timeoutSeconds: 3
|
||||||
|
failureThreshold: 3
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
readOnlyRootFilesystem: false
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: student-api
|
||||||
|
namespace: student-enrollment
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: student-api
|
||||||
|
ports:
|
||||||
|
- port: 5000
|
||||||
|
targetPort: 5000
|
||||||
|
name: http
|
||||||
|
type: ClusterIP
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: student-config
|
||||||
|
namespace: student-enrollment
|
||||||
|
data:
|
||||||
|
ASPNETCORE_ENVIRONMENT: "Production"
|
||||||
|
ASPNETCORE_URLS: "http://+:5000"
|
||||||
|
# Frontend config
|
||||||
|
API_URL: "https://academia.ingeniumcodex.com/graphql"
|
||||||
|
HEALTH_CHECK_URL: "https://academia.ingeniumcodex.com/health"
|
||||||
|
|
@ -0,0 +1,158 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Script de despliegue para k3s
|
||||||
|
# Uso: ./deploy.sh [build|deploy|status|logs|delete]
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
NAMESPACE="student-enrollment"
|
||||||
|
REGISTRY="${REGISTRY:-localhost:5000}"
|
||||||
|
VERSION="${VERSION:-latest}"
|
||||||
|
|
||||||
|
# Colores
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||||
|
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||||
|
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||||
|
|
||||||
|
build_images() {
|
||||||
|
log_info "Building Docker images..."
|
||||||
|
|
||||||
|
cd "$(dirname "$0")/../.."
|
||||||
|
|
||||||
|
# Build API
|
||||||
|
log_info "Building API image..."
|
||||||
|
docker build -t ${REGISTRY}/student-enrollment/api:${VERSION} \
|
||||||
|
-f deploy/docker/Dockerfile.api .
|
||||||
|
|
||||||
|
# Build Frontend
|
||||||
|
log_info "Building Frontend image..."
|
||||||
|
docker build -t ${REGISTRY}/student-enrollment/frontend:${VERSION} \
|
||||||
|
-f deploy/docker/Dockerfile.frontend .
|
||||||
|
|
||||||
|
log_info "Images built successfully"
|
||||||
|
|
||||||
|
# Push si hay registry
|
||||||
|
if [[ "$REGISTRY" != "localhost:5000" ]]; then
|
||||||
|
log_info "Pushing images to registry..."
|
||||||
|
docker push ${REGISTRY}/student-enrollment/api:${VERSION}
|
||||||
|
docker push ${REGISTRY}/student-enrollment/frontend:${VERSION}
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
deploy() {
|
||||||
|
log_info "Deploying to k3s..."
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
# Aplicar con kustomize
|
||||||
|
kubectl apply -k .
|
||||||
|
|
||||||
|
log_info "Waiting for deployments..."
|
||||||
|
kubectl rollout status deployment/sqlserver -n ${NAMESPACE} --timeout=120s || true
|
||||||
|
kubectl rollout status deployment/student-api -n ${NAMESPACE} --timeout=60s
|
||||||
|
kubectl rollout status deployment/student-frontend -n ${NAMESPACE} --timeout=60s
|
||||||
|
|
||||||
|
log_info "Deployment complete!"
|
||||||
|
status
|
||||||
|
}
|
||||||
|
|
||||||
|
status() {
|
||||||
|
log_info "Cluster status:"
|
||||||
|
echo ""
|
||||||
|
kubectl get all -n ${NAMESPACE}
|
||||||
|
echo ""
|
||||||
|
kubectl get ingress -n ${NAMESPACE}
|
||||||
|
echo ""
|
||||||
|
log_info "Pod status:"
|
||||||
|
kubectl get pods -n ${NAMESPACE} -o wide
|
||||||
|
}
|
||||||
|
|
||||||
|
logs() {
|
||||||
|
local component="${1:-api}"
|
||||||
|
case $component in
|
||||||
|
api)
|
||||||
|
kubectl logs -n ${NAMESPACE} -l app=student-api -f --tail=100
|
||||||
|
;;
|
||||||
|
frontend)
|
||||||
|
kubectl logs -n ${NAMESPACE} -l app=student-frontend -f --tail=100
|
||||||
|
;;
|
||||||
|
db|sqlserver)
|
||||||
|
kubectl logs -n ${NAMESPACE} -l app=sqlserver -f --tail=100
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
log_error "Unknown component: $component"
|
||||||
|
echo "Usage: $0 logs [api|frontend|db]"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
delete() {
|
||||||
|
log_warn "Deleting deployment..."
|
||||||
|
read -p "Are you sure? (y/N) " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
kubectl delete -k . || true
|
||||||
|
kubectl delete namespace ${NAMESPACE} || true
|
||||||
|
log_info "Deployment deleted"
|
||||||
|
else
|
||||||
|
log_info "Cancelled"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
port_forward() {
|
||||||
|
log_info "Port forwarding..."
|
||||||
|
log_info "Frontend: http://localhost:8080"
|
||||||
|
log_info "API: http://localhost:5000/graphql"
|
||||||
|
|
||||||
|
kubectl port-forward -n ${NAMESPACE} svc/student-frontend 8080:80 &
|
||||||
|
kubectl port-forward -n ${NAMESPACE} svc/student-api 5000:5000 &
|
||||||
|
|
||||||
|
wait
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-help}" in
|
||||||
|
build)
|
||||||
|
build_images
|
||||||
|
;;
|
||||||
|
deploy)
|
||||||
|
deploy
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
status
|
||||||
|
;;
|
||||||
|
logs)
|
||||||
|
logs "$2"
|
||||||
|
;;
|
||||||
|
delete)
|
||||||
|
delete
|
||||||
|
;;
|
||||||
|
forward|port-forward)
|
||||||
|
port_forward
|
||||||
|
;;
|
||||||
|
all)
|
||||||
|
build_images
|
||||||
|
deploy
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Student Enrollment - k3s Deployment Script"
|
||||||
|
echo ""
|
||||||
|
echo "Usage: $0 <command>"
|
||||||
|
echo ""
|
||||||
|
echo "Commands:"
|
||||||
|
echo " build Build Docker images"
|
||||||
|
echo " deploy Deploy to k3s cluster"
|
||||||
|
echo " status Show deployment status"
|
||||||
|
echo " logs Show logs (api|frontend|db)"
|
||||||
|
echo " forward Port forward services"
|
||||||
|
echo " delete Delete deployment"
|
||||||
|
echo " all Build and deploy"
|
||||||
|
echo ""
|
||||||
|
echo "Environment variables:"
|
||||||
|
echo " REGISTRY Docker registry (default: localhost:5000)"
|
||||||
|
echo " VERSION Image version tag (default: latest)"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: student-frontend
|
||||||
|
namespace: student-enrollment
|
||||||
|
labels:
|
||||||
|
app: student-frontend
|
||||||
|
spec:
|
||||||
|
replicas: 2
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: student-frontend
|
||||||
|
strategy:
|
||||||
|
type: RollingUpdate
|
||||||
|
rollingUpdate:
|
||||||
|
maxSurge: 1
|
||||||
|
maxUnavailable: 0
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: student-frontend
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: frontend
|
||||||
|
image: docker.io/library/academia-frontend:v2
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
ports:
|
||||||
|
- containerPort: 80
|
||||||
|
name: http
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "64Mi"
|
||||||
|
cpu: "50m"
|
||||||
|
limits:
|
||||||
|
memory: "128Mi"
|
||||||
|
cpu: "200m"
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /
|
||||||
|
port: 80
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 30
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /
|
||||||
|
port: 80
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 10
|
||||||
|
securityContext:
|
||||||
|
runAsNonRoot: false
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: student-frontend
|
||||||
|
namespace: student-enrollment
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: student-frontend
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
targetPort: 80
|
||||||
|
name: http
|
||||||
|
type: ClusterIP
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
---
|
||||||
|
# Horizontal Pod Autoscaler para API
|
||||||
|
apiVersion: autoscaling/v2
|
||||||
|
kind: HorizontalPodAutoscaler
|
||||||
|
metadata:
|
||||||
|
name: student-api-hpa
|
||||||
|
namespace: student-enrollment
|
||||||
|
spec:
|
||||||
|
scaleTargetRef:
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
name: student-api
|
||||||
|
minReplicas: 2
|
||||||
|
maxReplicas: 5
|
||||||
|
metrics:
|
||||||
|
- type: Resource
|
||||||
|
resource:
|
||||||
|
name: cpu
|
||||||
|
target:
|
||||||
|
type: Utilization
|
||||||
|
averageUtilization: 70
|
||||||
|
- type: Resource
|
||||||
|
resource:
|
||||||
|
name: memory
|
||||||
|
target:
|
||||||
|
type: Utilization
|
||||||
|
averageUtilization: 80
|
||||||
|
behavior:
|
||||||
|
scaleDown:
|
||||||
|
stabilizationWindowSeconds: 300
|
||||||
|
policies:
|
||||||
|
- type: Percent
|
||||||
|
value: 10
|
||||||
|
periodSeconds: 60
|
||||||
|
scaleUp:
|
||||||
|
stabilizationWindowSeconds: 0
|
||||||
|
policies:
|
||||||
|
- type: Percent
|
||||||
|
value: 100
|
||||||
|
periodSeconds: 15
|
||||||
|
---
|
||||||
|
# HPA para Frontend
|
||||||
|
apiVersion: autoscaling/v2
|
||||||
|
kind: HorizontalPodAutoscaler
|
||||||
|
metadata:
|
||||||
|
name: student-frontend-hpa
|
||||||
|
namespace: student-enrollment
|
||||||
|
spec:
|
||||||
|
scaleTargetRef:
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
name: student-frontend
|
||||||
|
minReplicas: 2
|
||||||
|
maxReplicas: 4
|
||||||
|
metrics:
|
||||||
|
- type: Resource
|
||||||
|
resource:
|
||||||
|
name: cpu
|
||||||
|
target:
|
||||||
|
type: Utilization
|
||||||
|
averageUtilization: 80
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
---
|
||||||
|
# Middleware para redirección HTTP → HTTPS
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: Middleware
|
||||||
|
metadata:
|
||||||
|
name: redirect-https
|
||||||
|
namespace: student-enrollment
|
||||||
|
spec:
|
||||||
|
redirectScheme:
|
||||||
|
permanent: true
|
||||||
|
scheme: https
|
||||||
|
---
|
||||||
|
# Middleware para security headers
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: Middleware
|
||||||
|
metadata:
|
||||||
|
name: security-headers
|
||||||
|
namespace: student-enrollment
|
||||||
|
spec:
|
||||||
|
headers:
|
||||||
|
frameDeny: true
|
||||||
|
contentTypeNosniff: true
|
||||||
|
browserXssFilter: true
|
||||||
|
referrerPolicy: "strict-origin-when-cross-origin"
|
||||||
|
customResponseHeaders:
|
||||||
|
X-Robots-Tag: "noindex, nofollow"
|
||||||
|
---
|
||||||
|
# IngressRoute HTTP - Redirección a HTTPS
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: IngressRoute
|
||||||
|
metadata:
|
||||||
|
name: academia-http-redirect
|
||||||
|
namespace: student-enrollment
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- web
|
||||||
|
routes:
|
||||||
|
- kind: Rule
|
||||||
|
match: Host(`academia.ingeniumcodex.com`)
|
||||||
|
middlewares:
|
||||||
|
- name: redirect-https
|
||||||
|
namespace: student-enrollment
|
||||||
|
services:
|
||||||
|
- name: student-frontend
|
||||||
|
port: 80
|
||||||
|
---
|
||||||
|
# IngressRoute HTTPS - Principal con TLS
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: IngressRoute
|
||||||
|
metadata:
|
||||||
|
name: academia-https
|
||||||
|
namespace: student-enrollment
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- websecure
|
||||||
|
routes:
|
||||||
|
# API GraphQL
|
||||||
|
- kind: Rule
|
||||||
|
match: Host(`academia.ingeniumcodex.com`) && PathPrefix(`/graphql`)
|
||||||
|
middlewares:
|
||||||
|
- name: security-headers
|
||||||
|
namespace: student-enrollment
|
||||||
|
services:
|
||||||
|
- name: student-api
|
||||||
|
port: 5000
|
||||||
|
# Health check
|
||||||
|
- kind: Rule
|
||||||
|
match: Host(`academia.ingeniumcodex.com`) && PathPrefix(`/health`)
|
||||||
|
services:
|
||||||
|
- name: student-api
|
||||||
|
port: 5000
|
||||||
|
# Frontend (catch-all)
|
||||||
|
- kind: Rule
|
||||||
|
match: Host(`academia.ingeniumcodex.com`)
|
||||||
|
middlewares:
|
||||||
|
- name: security-headers
|
||||||
|
namespace: student-enrollment
|
||||||
|
services:
|
||||||
|
- name: student-frontend
|
||||||
|
port: 80
|
||||||
|
tls:
|
||||||
|
certResolver: letsencrypt
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
|
||||||
|
namespace: student-enrollment
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- namespace.yaml
|
||||||
|
- secrets.yaml
|
||||||
|
- configmap.yaml
|
||||||
|
- sqlserver.yaml
|
||||||
|
- api.yaml
|
||||||
|
- frontend.yaml
|
||||||
|
- ingress.yaml
|
||||||
|
|
||||||
|
commonLabels:
|
||||||
|
app.kubernetes.io/name: student-enrollment
|
||||||
|
app.kubernetes.io/part-of: interrapidisimo
|
||||||
|
app.kubernetes.io/managed-by: kustomize
|
||||||
|
|
||||||
|
# Dominio: https://academia.ingeniumcodex.com
|
||||||
|
# TLS configurado con Traefik + Let's Encrypt
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: student-enrollment
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: student-enrollment
|
||||||
|
app.kubernetes.io/part-of: interrapidisimo
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
---
|
||||||
|
# Network Policy - Solo permitir tráfico necesario
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
name: default-deny-ingress
|
||||||
|
namespace: student-enrollment
|
||||||
|
spec:
|
||||||
|
podSelector: {}
|
||||||
|
policyTypes:
|
||||||
|
- Ingress
|
||||||
|
---
|
||||||
|
# Permitir tráfico al frontend desde ingress
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
name: allow-frontend-ingress
|
||||||
|
namespace: student-enrollment
|
||||||
|
spec:
|
||||||
|
podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app: student-frontend
|
||||||
|
policyTypes:
|
||||||
|
- Ingress
|
||||||
|
ingress:
|
||||||
|
- from:
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels:
|
||||||
|
kubernetes.io/metadata.name: kube-system
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 80
|
||||||
|
---
|
||||||
|
# Permitir tráfico al API desde frontend e ingress
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
name: allow-api-ingress
|
||||||
|
namespace: student-enrollment
|
||||||
|
spec:
|
||||||
|
podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app: student-api
|
||||||
|
policyTypes:
|
||||||
|
- Ingress
|
||||||
|
ingress:
|
||||||
|
# Desde ingress controller
|
||||||
|
- from:
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels:
|
||||||
|
kubernetes.io/metadata.name: kube-system
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 5000
|
||||||
|
# Desde frontend (para nginx proxy)
|
||||||
|
- from:
|
||||||
|
- podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app: student-frontend
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 5000
|
||||||
|
---
|
||||||
|
# Permitir tráfico a SQL Server solo desde API
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
name: allow-sqlserver-from-api
|
||||||
|
namespace: student-enrollment
|
||||||
|
spec:
|
||||||
|
podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app: sqlserver
|
||||||
|
policyTypes:
|
||||||
|
- Ingress
|
||||||
|
ingress:
|
||||||
|
- from:
|
||||||
|
- podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app: student-api
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 1433
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: student-secrets
|
||||||
|
namespace: student-enrollment
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
# IMPORTANTE: Cambiar en producción
|
||||||
|
# Generar con: openssl rand -base64 32
|
||||||
|
db-password: "Your_Str0ng_P@ssword!"
|
||||||
|
db-connection-string: "Server=sqlserver;Database=StudentEnrollment;User Id=sa;Password=Your_Str0ng_P@ssword!;TrustServerCertificate=True"
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: sqlserver-pvc
|
||||||
|
namespace: student-enrollment
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
storageClassName: local-path
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 5Gi
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: sqlserver
|
||||||
|
namespace: student-enrollment
|
||||||
|
labels:
|
||||||
|
app: sqlserver
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: sqlserver
|
||||||
|
strategy:
|
||||||
|
type: Recreate
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: sqlserver
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: sqlserver
|
||||||
|
image: mcr.microsoft.com/mssql/server:2022-latest
|
||||||
|
ports:
|
||||||
|
- containerPort: 1433
|
||||||
|
name: sql
|
||||||
|
env:
|
||||||
|
- name: ACCEPT_EULA
|
||||||
|
value: "Y"
|
||||||
|
- name: MSSQL_SA_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: student-secrets
|
||||||
|
key: db-password
|
||||||
|
volumeMounts:
|
||||||
|
- name: sqlserver-data
|
||||||
|
mountPath: /var/opt/mssql
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "1Gi"
|
||||||
|
cpu: "500m"
|
||||||
|
limits:
|
||||||
|
memory: "2Gi"
|
||||||
|
cpu: "1000m"
|
||||||
|
livenessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: 1433
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
periodSeconds: 10
|
||||||
|
readinessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: 1433
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
periodSeconds: 5
|
||||||
|
volumes:
|
||||||
|
- name: sqlserver-data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: sqlserver-pvc
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: sqlserver
|
||||||
|
namespace: student-enrollment
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: sqlserver
|
||||||
|
ports:
|
||||||
|
- port: 1433
|
||||||
|
targetPort: 1433
|
||||||
|
name: sql
|
||||||
|
type: ClusterIP
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
#!/bin/bash
|
||||||
|
cd src/backend && dotnet run --project Host
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
#!/bin/bash
|
||||||
|
cd src/frontend && npx ng serve
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
#!/bin/bash
|
||||||
|
xdg-open http://localhost:4200
|
||||||
Loading…
Reference in New Issue