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