diff --git a/deploy/docker/.env.example b/deploy/docker/.env.example new file mode 100644 index 0000000..49a4e11 --- /dev/null +++ b/deploy/docker/.env.example @@ -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 diff --git a/deploy/docker/Dockerfile.api b/deploy/docker/Dockerfile.api new file mode 100644 index 0000000..9005a9b --- /dev/null +++ b/deploy/docker/Dockerfile.api @@ -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"] diff --git a/deploy/docker/Dockerfile.frontend b/deploy/docker/Dockerfile.frontend new file mode 100644 index 0000000..210f1ed --- /dev/null +++ b/deploy/docker/Dockerfile.frontend @@ -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;"] diff --git a/deploy/docker/docker-compose.yml b/deploy/docker/docker-compose.yml new file mode 100644 index 0000000..c7bae70 --- /dev/null +++ b/deploy/docker/docker-compose.yml @@ -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 diff --git a/deploy/docker/nginx.conf b/deploy/docker/nginx.conf new file mode 100644 index 0000000..501f3fb --- /dev/null +++ b/deploy/docker/nginx.conf @@ -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; + } +} diff --git a/deploy/docker/start.sh b/deploy/docker/start.sh new file mode 100755 index 0000000..73519fd --- /dev/null +++ b/deploy/docker/start.sh @@ -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" diff --git a/deploy/k3s/api.yaml b/deploy/k3s/api.yaml new file mode 100644 index 0000000..0ceeb39 --- /dev/null +++ b/deploy/k3s/api.yaml @@ -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 diff --git a/deploy/k3s/configmap.yaml b/deploy/k3s/configmap.yaml new file mode 100644 index 0000000..737e052 --- /dev/null +++ b/deploy/k3s/configmap.yaml @@ -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" diff --git a/deploy/k3s/deploy.sh b/deploy/k3s/deploy.sh new file mode 100755 index 0000000..27b8282 --- /dev/null +++ b/deploy/k3s/deploy.sh @@ -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 " + 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 diff --git a/deploy/k3s/frontend.yaml b/deploy/k3s/frontend.yaml new file mode 100644 index 0000000..f1cd8ba --- /dev/null +++ b/deploy/k3s/frontend.yaml @@ -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 diff --git a/deploy/k3s/hpa.yaml b/deploy/k3s/hpa.yaml new file mode 100644 index 0000000..5b7f8ca --- /dev/null +++ b/deploy/k3s/hpa.yaml @@ -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 diff --git a/deploy/k3s/ingress.yaml b/deploy/k3s/ingress.yaml new file mode 100644 index 0000000..2ebf6c2 --- /dev/null +++ b/deploy/k3s/ingress.yaml @@ -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 diff --git a/deploy/k3s/kustomization.yaml b/deploy/k3s/kustomization.yaml new file mode 100644 index 0000000..196c827 --- /dev/null +++ b/deploy/k3s/kustomization.yaml @@ -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 diff --git a/deploy/k3s/namespace.yaml b/deploy/k3s/namespace.yaml new file mode 100644 index 0000000..98858d7 --- /dev/null +++ b/deploy/k3s/namespace.yaml @@ -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 diff --git a/deploy/k3s/networkpolicy.yaml b/deploy/k3s/networkpolicy.yaml new file mode 100644 index 0000000..20ebb6f --- /dev/null +++ b/deploy/k3s/networkpolicy.yaml @@ -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 diff --git a/deploy/k3s/secrets.yaml b/deploy/k3s/secrets.yaml new file mode 100644 index 0000000..f1dfb8a --- /dev/null +++ b/deploy/k3s/secrets.yaml @@ -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" diff --git a/deploy/k3s/sqlserver.yaml b/deploy/k3s/sqlserver.yaml new file mode 100644 index 0000000..1641bbd --- /dev/null +++ b/deploy/k3s/sqlserver.yaml @@ -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 diff --git a/start.backend.sh b/start.backend.sh new file mode 100644 index 0000000..c8bd4b4 --- /dev/null +++ b/start.backend.sh @@ -0,0 +1,2 @@ +#!/bin/bash +cd src/backend && dotnet run --project Host \ No newline at end of file diff --git a/start.frontend.sh b/start.frontend.sh new file mode 100644 index 0000000..de615e1 --- /dev/null +++ b/start.frontend.sh @@ -0,0 +1,2 @@ +#!/bin/bash +cd src/frontend && npx ng serve \ No newline at end of file diff --git a/start.webclient.sh b/start.webclient.sh new file mode 100644 index 0000000..1824ea5 --- /dev/null +++ b/start.webclient.sh @@ -0,0 +1,2 @@ +#!/bin/bash +xdg-open http://localhost:4200 \ No newline at end of file