Complete documentation for future sessions
- CLAUDE.md for AI agents to understand the codebase - GITEA-GUIDE.md centralizes all Gitea operations (API, Registry, Auth) - DEVELOPMENT-WORKFLOW.md explains complete dev process - ROADMAP.md, NEXT-SESSION.md for planning - QUICK-REFERENCE.md, TROUBLESHOOTING.md for daily use - 40+ detailed docs in /docs folder - Backend as submodule from Gitea Everything documented for autonomous operation. Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
456
docs/04-kubernetes/cluster-setup.md
Normal file
456
docs/04-kubernetes/cluster-setup.md
Normal file
@@ -0,0 +1,456 @@
|
||||
# Setup del Cluster Kubernetes
|
||||
|
||||
## Requisitos
|
||||
|
||||
- Kubernetes 1.28+
|
||||
- kubectl CLI
|
||||
- helm 3.x
|
||||
- 4 GB RAM mínimo
|
||||
- 20 GB storage
|
||||
|
||||
## Instalación Local (Kind/Minikube)
|
||||
|
||||
### Con Kind (recomendado para desarrollo)
|
||||
|
||||
```bash
|
||||
# Instalar kind
|
||||
brew install kind # macOS
|
||||
# o
|
||||
curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-amd64
|
||||
chmod +x ./kind
|
||||
sudo mv ./kind /usr/local/bin/kind
|
||||
|
||||
# Crear cluster con configuración personalizada
|
||||
cat <<EOF | kind create cluster --name aiworker --config=-
|
||||
kind: Cluster
|
||||
apiVersion: kind.x-k8s.io/v1alpha4
|
||||
nodes:
|
||||
- role: control-plane
|
||||
kubeadmConfigPatches:
|
||||
- |
|
||||
kind: InitConfiguration
|
||||
nodeRegistration:
|
||||
kubeletExtraArgs:
|
||||
node-labels: "ingress-ready=true"
|
||||
extraPortMappings:
|
||||
- containerPort: 80
|
||||
hostPort: 80
|
||||
protocol: TCP
|
||||
- containerPort: 443
|
||||
hostPort: 443
|
||||
protocol: TCP
|
||||
- role: worker
|
||||
- role: worker
|
||||
EOF
|
||||
|
||||
# Verificar
|
||||
kubectl cluster-info --context kind-aiworker
|
||||
kubectl get nodes
|
||||
```
|
||||
|
||||
### Con Minikube
|
||||
|
||||
```bash
|
||||
# Instalar minikube
|
||||
brew install minikube # macOS
|
||||
|
||||
# Iniciar cluster
|
||||
minikube start --cpus=4 --memory=8192 --disk-size=40g --driver=docker
|
||||
|
||||
# Habilitar addons
|
||||
minikube addons enable ingress
|
||||
minikube addons enable metrics-server
|
||||
minikube addons enable storage-provisioner
|
||||
|
||||
# Verificar
|
||||
kubectl get nodes
|
||||
```
|
||||
|
||||
## Instalación en Cloud
|
||||
|
||||
### Google Kubernetes Engine (GKE)
|
||||
|
||||
```bash
|
||||
# Instalar gcloud CLI
|
||||
brew install --cask google-cloud-sdk
|
||||
|
||||
# Autenticar
|
||||
gcloud auth login
|
||||
gcloud config set project YOUR_PROJECT_ID
|
||||
|
||||
# Crear cluster
|
||||
gcloud container clusters create aiworker \
|
||||
--zone us-central1-a \
|
||||
--num-nodes 3 \
|
||||
--machine-type n1-standard-2 \
|
||||
--disk-size 30 \
|
||||
--enable-autoscaling \
|
||||
--min-nodes 2 \
|
||||
--max-nodes 5 \
|
||||
--enable-autorepair \
|
||||
--enable-autoupgrade
|
||||
|
||||
# Obtener credenciales
|
||||
gcloud container clusters get-credentials aiworker --zone us-central1-a
|
||||
|
||||
# Verificar
|
||||
kubectl get nodes
|
||||
```
|
||||
|
||||
### Amazon EKS
|
||||
|
||||
```bash
|
||||
# Instalar eksctl
|
||||
brew install eksctl
|
||||
|
||||
# Crear cluster
|
||||
eksctl create cluster \
|
||||
--name aiworker \
|
||||
--region us-west-2 \
|
||||
--nodegroup-name workers \
|
||||
--node-type t3.medium \
|
||||
--nodes 3 \
|
||||
--nodes-min 2 \
|
||||
--nodes-max 5 \
|
||||
--managed
|
||||
|
||||
# Verificar
|
||||
kubectl get nodes
|
||||
```
|
||||
|
||||
### Azure AKS
|
||||
|
||||
```bash
|
||||
# Instalar Azure CLI
|
||||
brew install azure-cli
|
||||
|
||||
# Login
|
||||
az login
|
||||
|
||||
# Crear resource group
|
||||
az group create --name aiworker-rg --location eastus
|
||||
|
||||
# Crear cluster
|
||||
az aks create \
|
||||
--resource-group aiworker-rg \
|
||||
--name aiworker \
|
||||
--node-count 3 \
|
||||
--node-vm-size Standard_D2s_v3 \
|
||||
--enable-cluster-autoscaler \
|
||||
--min-count 2 \
|
||||
--max-count 5 \
|
||||
--generate-ssh-keys
|
||||
|
||||
# Obtener credenciales
|
||||
az aks get-credentials --resource-group aiworker-rg --name aiworker
|
||||
|
||||
# Verificar
|
||||
kubectl get nodes
|
||||
```
|
||||
|
||||
## Instalación de Componentes Base
|
||||
|
||||
### Nginx Ingress Controller
|
||||
|
||||
```bash
|
||||
# Instalar con Helm
|
||||
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
|
||||
helm repo update
|
||||
|
||||
helm install ingress-nginx ingress-nginx/ingress-nginx \
|
||||
--namespace ingress-nginx \
|
||||
--create-namespace \
|
||||
--set controller.replicaCount=2 \
|
||||
--set controller.nodeSelector."kubernetes\.io/os"=linux \
|
||||
--set controller.admissionWebhooks.patch.nodeSelector."kubernetes\.io/os"=linux
|
||||
|
||||
# Verificar
|
||||
kubectl get pods -n ingress-nginx
|
||||
kubectl get svc -n ingress-nginx
|
||||
```
|
||||
|
||||
### Cert-Manager (TLS)
|
||||
|
||||
```bash
|
||||
# Instalar cert-manager
|
||||
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.0/cert-manager.yaml
|
||||
|
||||
# Verificar
|
||||
kubectl get pods -n cert-manager
|
||||
|
||||
# Crear ClusterIssuer para Let's Encrypt
|
||||
cat <<EOF | kubectl apply -f -
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: ClusterIssuer
|
||||
metadata:
|
||||
name: letsencrypt-prod
|
||||
spec:
|
||||
acme:
|
||||
server: https://acme-v02.api.letsencrypt.org/directory
|
||||
email: your-email@example.com
|
||||
privateKeySecretRef:
|
||||
name: letsencrypt-prod
|
||||
solvers:
|
||||
- http01:
|
||||
ingress:
|
||||
class: nginx
|
||||
EOF
|
||||
```
|
||||
|
||||
### Metrics Server
|
||||
|
||||
```bash
|
||||
# Instalar metrics-server
|
||||
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
|
||||
|
||||
# Verificar
|
||||
kubectl get deployment metrics-server -n kube-system
|
||||
kubectl top nodes
|
||||
```
|
||||
|
||||
### Prometheus & Grafana (opcional)
|
||||
|
||||
```bash
|
||||
# Añadir repo
|
||||
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
|
||||
helm repo update
|
||||
|
||||
# Instalar kube-prometheus-stack
|
||||
helm install prometheus prometheus-community/kube-prometheus-stack \
|
||||
--namespace monitoring \
|
||||
--create-namespace \
|
||||
--set prometheus.prometheusSpec.retention=30d \
|
||||
--set grafana.adminPassword=admin
|
||||
|
||||
# Verificar
|
||||
kubectl get pods -n monitoring
|
||||
|
||||
# Port-forward para acceder a Grafana
|
||||
kubectl port-forward -n monitoring svc/prometheus-grafana 3001:80
|
||||
# http://localhost:3001 (admin/admin)
|
||||
```
|
||||
|
||||
## Creación de Namespaces
|
||||
|
||||
```bash
|
||||
# Script de creación de namespaces
|
||||
cat <<EOF | kubectl apply -f -
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: control-plane
|
||||
labels:
|
||||
name: control-plane
|
||||
environment: production
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: agents
|
||||
labels:
|
||||
name: agents
|
||||
environment: production
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: gitea
|
||||
labels:
|
||||
name: gitea
|
||||
environment: production
|
||||
EOF
|
||||
|
||||
# Verificar
|
||||
kubectl get namespaces
|
||||
```
|
||||
|
||||
## Configuración de RBAC
|
||||
|
||||
```bash
|
||||
# ServiceAccount para backend
|
||||
cat <<EOF | kubectl apply -f -
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: aiworker-backend
|
||||
namespace: control-plane
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: aiworker-backend
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["pods", "pods/log", "pods/exec"]
|
||||
verbs: ["get", "list", "create", "delete"]
|
||||
- apiGroups: [""]
|
||||
resources: ["namespaces"]
|
||||
verbs: ["get", "list", "create", "delete"]
|
||||
- apiGroups: ["apps"]
|
||||
resources: ["deployments", "replicasets"]
|
||||
verbs: ["get", "list", "create", "update", "patch", "delete"]
|
||||
- apiGroups: [""]
|
||||
resources: ["services"]
|
||||
verbs: ["get", "list", "create", "update", "delete"]
|
||||
- apiGroups: ["networking.k8s.io"]
|
||||
resources: ["ingresses"]
|
||||
verbs: ["get", "list", "create", "update", "delete"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: aiworker-backend
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: aiworker-backend
|
||||
namespace: control-plane
|
||||
roleRef:
|
||||
kind: ClusterRole
|
||||
name: aiworker-backend
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
EOF
|
||||
```
|
||||
|
||||
## Secrets y ConfigMaps
|
||||
|
||||
```bash
|
||||
# Crear secret para credentials
|
||||
kubectl create secret generic aiworker-secrets \
|
||||
--namespace=control-plane \
|
||||
--from-literal=db-password='your-db-password' \
|
||||
--from-literal=gitea-token='your-gitea-token' \
|
||||
--from-literal=anthropic-api-key='your-anthropic-key'
|
||||
|
||||
# ConfigMap para configuración
|
||||
cat <<EOF | kubectl apply -f -
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: aiworker-config
|
||||
namespace: control-plane
|
||||
data:
|
||||
GITEA_URL: "http://gitea.gitea.svc.cluster.local:3000"
|
||||
K8S_DEFAULT_NAMESPACE: "aiworker"
|
||||
NODE_ENV: "production"
|
||||
EOF
|
||||
```
|
||||
|
||||
## Storage Classes
|
||||
|
||||
```bash
|
||||
# Crear StorageClass para preview environments (fast SSD)
|
||||
cat <<EOF | kubectl apply -f -
|
||||
apiVersion: storage.k8s.io/v1
|
||||
kind: StorageClass
|
||||
metadata:
|
||||
name: fast-ssd
|
||||
provisioner: kubernetes.io/gce-pd # Cambiar según cloud provider
|
||||
parameters:
|
||||
type: pd-ssd
|
||||
replication-type: none
|
||||
reclaimPolicy: Delete
|
||||
volumeBindingMode: WaitForFirstConsumer
|
||||
EOF
|
||||
```
|
||||
|
||||
## Network Policies
|
||||
|
||||
```bash
|
||||
# Aislar namespaces de preview
|
||||
cat <<EOF | kubectl apply -f -
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: preview-isolation
|
||||
namespace: agents
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
env: preview
|
||||
policyTypes:
|
||||
- Ingress
|
||||
- Egress
|
||||
ingress:
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
name: control-plane
|
||||
egress:
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
name: gitea
|
||||
- to:
|
||||
- namespaceSelector: {}
|
||||
podSelector:
|
||||
matchLabels:
|
||||
k8s-app: kube-dns
|
||||
EOF
|
||||
```
|
||||
|
||||
## Verificación Final
|
||||
|
||||
```bash
|
||||
# Script de verificación
|
||||
cat > verify-cluster.sh <<'EOF'
|
||||
#!/bin/bash
|
||||
|
||||
echo "🔍 Verificando cluster..."
|
||||
|
||||
echo "✓ Nodes:"
|
||||
kubectl get nodes
|
||||
|
||||
echo "✓ Namespaces:"
|
||||
kubectl get namespaces
|
||||
|
||||
echo "✓ Ingress Controller:"
|
||||
kubectl get pods -n ingress-nginx
|
||||
|
||||
echo "✓ Cert-Manager:"
|
||||
kubectl get pods -n cert-manager
|
||||
|
||||
echo "✓ Metrics Server:"
|
||||
kubectl top nodes 2>/dev/null || echo "⚠️ Metrics not available yet"
|
||||
|
||||
echo "✓ Storage Classes:"
|
||||
kubectl get storageclass
|
||||
|
||||
echo "✅ Cluster setup complete!"
|
||||
EOF
|
||||
|
||||
chmod +x verify-cluster.sh
|
||||
./verify-cluster.sh
|
||||
```
|
||||
|
||||
## Mantenimiento
|
||||
|
||||
```bash
|
||||
# Actualizar componentes
|
||||
helm repo update
|
||||
helm upgrade ingress-nginx ingress-nginx/ingress-nginx -n ingress-nginx
|
||||
|
||||
# Limpiar recursos viejos
|
||||
kubectl delete pods --field-selector=status.phase=Failed -A
|
||||
kubectl delete pods --field-selector=status.phase=Succeeded -A
|
||||
|
||||
# Backup de configuración
|
||||
kubectl get all --all-namespaces -o yaml > cluster-backup.yaml
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
```bash
|
||||
# Ver logs de componentes
|
||||
kubectl logs -n ingress-nginx deployment/ingress-nginx-controller
|
||||
kubectl logs -n cert-manager deployment/cert-manager
|
||||
|
||||
# Describir recursos con problemas
|
||||
kubectl describe pod <pod-name> -n <namespace>
|
||||
|
||||
# Eventos del cluster
|
||||
kubectl get events --all-namespaces --sort-by='.lastTimestamp'
|
||||
|
||||
# Recursos consumidos
|
||||
kubectl top nodes
|
||||
kubectl top pods -A
|
||||
```
|
||||
706
docs/04-kubernetes/deployments.md
Normal file
706
docs/04-kubernetes/deployments.md
Normal file
@@ -0,0 +1,706 @@
|
||||
# Deployments en Kubernetes
|
||||
|
||||
## Backend API Deployment
|
||||
|
||||
```yaml
|
||||
# k8s/control-plane/backend-deployment.yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: aiworker-backend
|
||||
namespace: control-plane
|
||||
labels:
|
||||
app: aiworker-backend
|
||||
version: v1
|
||||
spec:
|
||||
replicas: 2
|
||||
selector:
|
||||
matchLabels:
|
||||
app: aiworker-backend
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: aiworker-backend
|
||||
version: v1
|
||||
spec:
|
||||
serviceAccountName: aiworker-backend
|
||||
containers:
|
||||
- name: backend
|
||||
image: aiworker/backend:latest
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 3000
|
||||
- name: mcp
|
||||
containerPort: 3100
|
||||
env:
|
||||
- name: NODE_ENV
|
||||
value: "production"
|
||||
- name: PORT
|
||||
value: "3000"
|
||||
- name: DB_HOST
|
||||
value: "mysql.control-plane.svc.cluster.local"
|
||||
- name: DB_PORT
|
||||
value: "3306"
|
||||
- name: DB_NAME
|
||||
value: "aiworker"
|
||||
- name: DB_USER
|
||||
value: "root"
|
||||
- name: DB_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: aiworker-secrets
|
||||
key: db-password
|
||||
- name: REDIS_HOST
|
||||
value: "redis.control-plane.svc.cluster.local"
|
||||
- name: REDIS_PORT
|
||||
value: "6379"
|
||||
- name: GITEA_URL
|
||||
value: "http://gitea.gitea.svc.cluster.local:3000"
|
||||
- name: GITEA_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: aiworker-secrets
|
||||
key: gitea-token
|
||||
- name: K8S_IN_CLUSTER
|
||||
value: "true"
|
||||
resources:
|
||||
requests:
|
||||
cpu: "500m"
|
||||
memory: "1Gi"
|
||||
limits:
|
||||
cpu: "2"
|
||||
memory: "4Gi"
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /api/health
|
||||
port: 3000
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /api/health
|
||||
port: 3000
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 5
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: aiworker-backend
|
||||
namespace: control-plane
|
||||
spec:
|
||||
selector:
|
||||
app: aiworker-backend
|
||||
ports:
|
||||
- name: http
|
||||
port: 3000
|
||||
targetPort: 3000
|
||||
- name: mcp
|
||||
port: 3100
|
||||
targetPort: 3100
|
||||
type: ClusterIP
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: aiworker-backend
|
||||
namespace: control-plane
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||
nginx.ingress.kubernetes.io/websocket-services: "aiworker-backend"
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
tls:
|
||||
- hosts:
|
||||
- api.aiworker.dev
|
||||
secretName: aiworker-backend-tls
|
||||
rules:
|
||||
- host: api.aiworker.dev
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: aiworker-backend
|
||||
port:
|
||||
number: 3000
|
||||
```
|
||||
|
||||
## MySQL Deployment
|
||||
|
||||
```yaml
|
||||
# k8s/control-plane/mysql-deployment.yaml
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: mysql-pvc
|
||||
namespace: control-plane
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 20Gi
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: mysql
|
||||
namespace: control-plane
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: mysql
|
||||
strategy:
|
||||
type: Recreate
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: mysql
|
||||
spec:
|
||||
containers:
|
||||
- name: mysql
|
||||
image: mysql:8.0
|
||||
ports:
|
||||
- containerPort: 3306
|
||||
name: mysql
|
||||
env:
|
||||
- name: MYSQL_ROOT_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: aiworker-secrets
|
||||
key: db-password
|
||||
- name: MYSQL_DATABASE
|
||||
value: "aiworker"
|
||||
volumeMounts:
|
||||
- name: mysql-storage
|
||||
mountPath: /var/lib/mysql
|
||||
resources:
|
||||
requests:
|
||||
cpu: "500m"
|
||||
memory: "1Gi"
|
||||
limits:
|
||||
cpu: "2"
|
||||
memory: "4Gi"
|
||||
livenessProbe:
|
||||
exec:
|
||||
command:
|
||||
- mysqladmin
|
||||
- ping
|
||||
- -h
|
||||
- localhost
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
volumes:
|
||||
- name: mysql-storage
|
||||
persistentVolumeClaim:
|
||||
claimName: mysql-pvc
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: mysql
|
||||
namespace: control-plane
|
||||
spec:
|
||||
selector:
|
||||
app: mysql
|
||||
ports:
|
||||
- port: 3306
|
||||
targetPort: 3306
|
||||
type: ClusterIP
|
||||
```
|
||||
|
||||
## Redis Deployment
|
||||
|
||||
```yaml
|
||||
# k8s/control-plane/redis-deployment.yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: redis
|
||||
namespace: control-plane
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: redis
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: redis
|
||||
spec:
|
||||
containers:
|
||||
- name: redis
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- containerPort: 6379
|
||||
name: redis
|
||||
args:
|
||||
- --maxmemory
|
||||
- 2gb
|
||||
- --maxmemory-policy
|
||||
- allkeys-lru
|
||||
resources:
|
||||
requests:
|
||||
cpu: "250m"
|
||||
memory: "512Mi"
|
||||
limits:
|
||||
cpu: "1"
|
||||
memory: "2Gi"
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
port: 6379
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 10
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: redis
|
||||
namespace: control-plane
|
||||
spec:
|
||||
selector:
|
||||
app: redis
|
||||
ports:
|
||||
- port: 6379
|
||||
targetPort: 6379
|
||||
type: ClusterIP
|
||||
```
|
||||
|
||||
## Claude Code Agent Pod Template
|
||||
|
||||
```yaml
|
||||
# k8s/agents/agent-pod-template.yaml
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: claude-agent-{agent-id}
|
||||
namespace: agents
|
||||
labels:
|
||||
app: claude-agent
|
||||
agent-id: "{agent-id}"
|
||||
managed-by: aiworker
|
||||
spec:
|
||||
containers:
|
||||
- name: agent
|
||||
image: aiworker/claude-agent:latest
|
||||
env:
|
||||
- name: AGENT_ID
|
||||
value: "{agent-id}"
|
||||
- name: MCP_SERVER_URL
|
||||
value: "http://aiworker-backend.control-plane.svc.cluster.local:3100"
|
||||
- name: ANTHROPIC_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: aiworker-secrets
|
||||
key: anthropic-api-key
|
||||
- name: GITEA_URL
|
||||
value: "http://gitea.gitea.svc.cluster.local:3000"
|
||||
- name: GIT_SSH_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: git-ssh-keys
|
||||
key: private-key
|
||||
resources:
|
||||
requests:
|
||||
cpu: "500m"
|
||||
memory: "1Gi"
|
||||
limits:
|
||||
cpu: "2"
|
||||
memory: "4Gi"
|
||||
volumeMounts:
|
||||
- name: workspace
|
||||
mountPath: /workspace
|
||||
- name: git-config
|
||||
mountPath: /root/.gitconfig
|
||||
subPath: .gitconfig
|
||||
volumes:
|
||||
- name: workspace
|
||||
emptyDir: {}
|
||||
- name: git-config
|
||||
configMap:
|
||||
name: git-config
|
||||
restartPolicy: Never
|
||||
```
|
||||
|
||||
## Preview Deployment Template
|
||||
|
||||
```typescript
|
||||
// services/kubernetes/templates/preview-deployment.ts
|
||||
export function generatePreviewDeployment(params: {
|
||||
taskId: string
|
||||
projectId: string
|
||||
projectName: string
|
||||
image: string
|
||||
branch: string
|
||||
envVars: Record<string, string>
|
||||
}) {
|
||||
const namespace = `preview-task-${params.taskId.slice(0, 8)}`
|
||||
const name = `${params.projectName}-preview`
|
||||
|
||||
return {
|
||||
apiVersion: 'apps/v1',
|
||||
kind: 'Deployment',
|
||||
metadata: {
|
||||
name,
|
||||
namespace,
|
||||
labels: {
|
||||
app: name,
|
||||
project: params.projectId,
|
||||
task: params.taskId,
|
||||
environment: 'preview',
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
replicas: 1,
|
||||
selector: {
|
||||
matchLabels: {
|
||||
app: name,
|
||||
},
|
||||
},
|
||||
template: {
|
||||
metadata: {
|
||||
labels: {
|
||||
app: name,
|
||||
project: params.projectId,
|
||||
task: params.taskId,
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
containers: [
|
||||
{
|
||||
name: 'app',
|
||||
image: `${params.image}:${params.branch}`,
|
||||
ports: [
|
||||
{
|
||||
name: 'http',
|
||||
containerPort: 3000,
|
||||
},
|
||||
],
|
||||
env: Object.entries(params.envVars).map(([key, value]) => ({
|
||||
name: key,
|
||||
value,
|
||||
})),
|
||||
resources: {
|
||||
requests: {
|
||||
cpu: '250m',
|
||||
memory: '512Mi',
|
||||
},
|
||||
limits: {
|
||||
cpu: '1',
|
||||
memory: '2Gi',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function generatePreviewService(params: {
|
||||
taskId: string
|
||||
projectName: string
|
||||
}) {
|
||||
const namespace = `preview-task-${params.taskId.slice(0, 8)}`
|
||||
const name = `${params.projectName}-preview`
|
||||
|
||||
return {
|
||||
apiVersion: 'v1',
|
||||
kind: 'Service',
|
||||
metadata: {
|
||||
name,
|
||||
namespace,
|
||||
},
|
||||
spec: {
|
||||
selector: {
|
||||
app: name,
|
||||
},
|
||||
ports: [
|
||||
{
|
||||
port: 80,
|
||||
targetPort: 3000,
|
||||
},
|
||||
],
|
||||
type: 'ClusterIP',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function generatePreviewIngress(params: {
|
||||
taskId: string
|
||||
projectName: string
|
||||
}) {
|
||||
const namespace = `preview-task-${params.taskId.slice(0, 8)}`
|
||||
const name = `${params.projectName}-preview`
|
||||
const host = `task-${params.taskId.slice(0, 8)}.preview.aiworker.dev`
|
||||
|
||||
return {
|
||||
apiVersion: 'networking.k8s.io/v1',
|
||||
kind: 'Ingress',
|
||||
metadata: {
|
||||
name,
|
||||
namespace,
|
||||
annotations: {
|
||||
'cert-manager.io/cluster-issuer': 'letsencrypt-prod',
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
ingressClassName: 'nginx',
|
||||
tls: [
|
||||
{
|
||||
hosts: [host],
|
||||
secretName: `${name}-tls`,
|
||||
},
|
||||
],
|
||||
rules: [
|
||||
{
|
||||
host,
|
||||
http: {
|
||||
paths: [
|
||||
{
|
||||
path: '/',
|
||||
pathType: 'Prefix',
|
||||
backend: {
|
||||
service: {
|
||||
name,
|
||||
port: {
|
||||
number: 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Kubernetes Client Implementation
|
||||
|
||||
```typescript
|
||||
// services/kubernetes/client.ts
|
||||
import { KubeConfig, AppsV1Api, CoreV1Api, NetworkingV1Api } from '@kubernetes/client-node'
|
||||
import { logger } from '../../utils/logger'
|
||||
|
||||
export class K8sClient {
|
||||
private kc: KubeConfig
|
||||
private appsApi: AppsV1Api
|
||||
private coreApi: CoreV1Api
|
||||
private networkingApi: NetworkingV1Api
|
||||
|
||||
constructor() {
|
||||
this.kc = new KubeConfig()
|
||||
|
||||
if (process.env.K8S_IN_CLUSTER === 'true') {
|
||||
this.kc.loadFromCluster()
|
||||
} else {
|
||||
this.kc.loadFromDefault()
|
||||
}
|
||||
|
||||
this.appsApi = this.kc.makeApiClient(AppsV1Api)
|
||||
this.coreApi = this.kc.makeApiClient(CoreV1Api)
|
||||
this.networkingApi = this.kc.makeApiClient(NetworkingV1Api)
|
||||
}
|
||||
|
||||
async createPreviewDeployment(params: {
|
||||
namespace: string
|
||||
taskId: string
|
||||
projectId: string
|
||||
image: string
|
||||
branch: string
|
||||
envVars: Record<string, string>
|
||||
}) {
|
||||
const { namespace, taskId, projectId } = params
|
||||
|
||||
// Create namespace
|
||||
await this.createNamespace(namespace, {
|
||||
project: projectId,
|
||||
environment: 'preview',
|
||||
taskId,
|
||||
})
|
||||
|
||||
// Create deployment
|
||||
const deployment = generatePreviewDeployment(params)
|
||||
await this.appsApi.createNamespacedDeployment(namespace, deployment)
|
||||
|
||||
// Create service
|
||||
const service = generatePreviewService(params)
|
||||
await this.coreApi.createNamespacedService(namespace, service)
|
||||
|
||||
// Create ingress
|
||||
const ingress = generatePreviewIngress(params)
|
||||
await this.networkingApi.createNamespacedIngress(namespace, ingress)
|
||||
|
||||
logger.info(`Created preview deployment for task ${taskId}`)
|
||||
|
||||
return {
|
||||
namespace,
|
||||
url: ingress.spec.rules[0].host,
|
||||
}
|
||||
}
|
||||
|
||||
async deletePreviewDeployment(namespace: string) {
|
||||
await this.deleteNamespace(namespace)
|
||||
logger.info(`Deleted preview deployment namespace: ${namespace}`)
|
||||
}
|
||||
|
||||
async createNamespace(name: string, labels: Record<string, string> = {}) {
|
||||
try {
|
||||
await this.coreApi.createNamespace({
|
||||
metadata: {
|
||||
name,
|
||||
labels: {
|
||||
'managed-by': 'aiworker',
|
||||
...labels,
|
||||
},
|
||||
},
|
||||
})
|
||||
logger.info(`Created namespace: ${name}`)
|
||||
} catch (error: any) {
|
||||
if (error.statusCode !== 409) { // Ignore if already exists
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async deleteNamespace(name: string) {
|
||||
await this.coreApi.deleteNamespace(name)
|
||||
}
|
||||
|
||||
async createAgentPod(agentId: string) {
|
||||
const podSpec = {
|
||||
metadata: {
|
||||
name: `claude-agent-${agentId.slice(0, 8)}`,
|
||||
namespace: 'agents',
|
||||
labels: {
|
||||
app: 'claude-agent',
|
||||
'agent-id': agentId,
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
containers: [
|
||||
{
|
||||
name: 'agent',
|
||||
image: 'aiworker/claude-agent:latest',
|
||||
env: [
|
||||
{ name: 'AGENT_ID', value: agentId },
|
||||
{
|
||||
name: 'MCP_SERVER_URL',
|
||||
value: 'http://aiworker-backend.control-plane.svc.cluster.local:3100',
|
||||
},
|
||||
{
|
||||
name: 'ANTHROPIC_API_KEY',
|
||||
valueFrom: {
|
||||
secretKeyRef: {
|
||||
name: 'aiworker-secrets',
|
||||
key: 'anthropic-api-key',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
resources: {
|
||||
requests: { cpu: '500m', memory: '1Gi' },
|
||||
limits: { cpu: '2', memory: '4Gi' },
|
||||
},
|
||||
},
|
||||
],
|
||||
restartPolicy: 'Never',
|
||||
},
|
||||
}
|
||||
|
||||
await this.coreApi.createNamespacedPod('agents', podSpec)
|
||||
logger.info(`Created agent pod: ${agentId}`)
|
||||
|
||||
return {
|
||||
podName: podSpec.metadata.name,
|
||||
namespace: 'agents',
|
||||
}
|
||||
}
|
||||
|
||||
async deletePod(namespace: string, podName: string) {
|
||||
await this.coreApi.deleteNamespacedPod(podName, namespace)
|
||||
}
|
||||
|
||||
async getPodLogs(namespace: string, podName: string, tailLines = 100) {
|
||||
const response = await this.coreApi.readNamespacedPodLog(
|
||||
podName,
|
||||
namespace,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
tailLines
|
||||
)
|
||||
return response.body
|
||||
}
|
||||
|
||||
async execInPod(params: {
|
||||
namespace: string
|
||||
podName: string
|
||||
command: string[]
|
||||
}) {
|
||||
// Implementation using WebSocketStream
|
||||
const exec = new Exec(this.kc)
|
||||
const stream = await exec.exec(
|
||||
params.namespace,
|
||||
params.podName,
|
||||
'agent',
|
||||
params.command,
|
||||
process.stdout,
|
||||
process.stderr,
|
||||
process.stdin,
|
||||
true // tty
|
||||
)
|
||||
|
||||
return stream
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Deployment Script
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# deploy-all.sh
|
||||
|
||||
set -e
|
||||
|
||||
echo "🚀 Deploying AiWorker to Kubernetes..."
|
||||
|
||||
# Apply secrets (should be done once manually with real values)
|
||||
echo "📦 Creating secrets..."
|
||||
kubectl apply -f k8s/secrets/
|
||||
|
||||
# Deploy control-plane
|
||||
echo "🎛️ Deploying control-plane..."
|
||||
kubectl apply -f k8s/control-plane/
|
||||
|
||||
# Deploy agents namespace
|
||||
echo "🤖 Setting up agents namespace..."
|
||||
kubectl apply -f k8s/agents/
|
||||
|
||||
# Deploy Gitea
|
||||
echo "📚 Deploying Gitea..."
|
||||
kubectl apply -f k8s/gitea/
|
||||
|
||||
# Wait for pods
|
||||
echo "⏳ Waiting for pods to be ready..."
|
||||
kubectl wait --for=condition=ready pod -l app=aiworker-backend -n control-plane --timeout=300s
|
||||
kubectl wait --for=condition=ready pod -l app=mysql -n control-plane --timeout=300s
|
||||
kubectl wait --for=condition=ready pod -l app=redis -n control-plane --timeout=300s
|
||||
|
||||
echo "✅ Deployment complete!"
|
||||
echo "📍 Backend API: https://api.aiworker.dev"
|
||||
echo "📍 Gitea: https://git.aiworker.dev"
|
||||
```
|
||||
456
docs/04-kubernetes/gitea-deployment.md
Normal file
456
docs/04-kubernetes/gitea-deployment.md
Normal file
@@ -0,0 +1,456 @@
|
||||
# Gitea Deployment en Kubernetes
|
||||
|
||||
## Gitea StatefulSet
|
||||
|
||||
```yaml
|
||||
# k8s/gitea/gitea-statefulset.yaml
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: gitea-data
|
||||
namespace: gitea
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 50Gi
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: gitea
|
||||
namespace: gitea
|
||||
spec:
|
||||
serviceName: gitea
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: gitea
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: gitea
|
||||
spec:
|
||||
containers:
|
||||
- name: gitea
|
||||
image: gitea/gitea:1.22
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 3000
|
||||
- name: ssh
|
||||
containerPort: 22
|
||||
env:
|
||||
- name: USER_UID
|
||||
value: "1000"
|
||||
- name: USER_GID
|
||||
value: "1000"
|
||||
- name: GITEA__database__DB_TYPE
|
||||
value: "mysql"
|
||||
- name: GITEA__database__HOST
|
||||
value: "mysql.control-plane.svc.cluster.local:3306"
|
||||
- name: GITEA__database__NAME
|
||||
value: "gitea"
|
||||
- name: GITEA__database__USER
|
||||
value: "root"
|
||||
- name: GITEA__database__PASSWD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: aiworker-secrets
|
||||
key: db-password
|
||||
- name: GITEA__server__DOMAIN
|
||||
value: "git.aiworker.dev"
|
||||
- name: GITEA__server__SSH_DOMAIN
|
||||
value: "git.aiworker.dev"
|
||||
- name: GITEA__server__ROOT_URL
|
||||
value: "https://git.aiworker.dev"
|
||||
- name: GITEA__server__HTTP_PORT
|
||||
value: "3000"
|
||||
- name: GITEA__server__SSH_PORT
|
||||
value: "2222"
|
||||
- name: GITEA__security__INSTALL_LOCK
|
||||
value: "true"
|
||||
- name: GITEA__webhook__ALLOWED_HOST_LIST
|
||||
value: "*.svc.cluster.local"
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
resources:
|
||||
requests:
|
||||
cpu: "500m"
|
||||
memory: "1Gi"
|
||||
limits:
|
||||
cpu: "2"
|
||||
memory: "4Gi"
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /api/healthz
|
||||
port: 3000
|
||||
initialDelaySeconds: 60
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /api/healthz
|
||||
port: 3000
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 5
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: gitea-data
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: gitea
|
||||
namespace: gitea
|
||||
spec:
|
||||
selector:
|
||||
app: gitea
|
||||
ports:
|
||||
- name: http
|
||||
port: 3000
|
||||
targetPort: 3000
|
||||
- name: ssh
|
||||
port: 2222
|
||||
targetPort: 22
|
||||
type: ClusterIP
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: gitea-ssh
|
||||
namespace: gitea
|
||||
annotations:
|
||||
service.beta.kubernetes.io/external-traffic: OnlyLocal
|
||||
spec:
|
||||
selector:
|
||||
app: gitea
|
||||
ports:
|
||||
- name: ssh
|
||||
port: 2222
|
||||
targetPort: 22
|
||||
protocol: TCP
|
||||
type: LoadBalancer
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: gitea
|
||||
namespace: gitea
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||
nginx.ingress.kubernetes.io/proxy-body-size: "512m"
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
tls:
|
||||
- hosts:
|
||||
- git.aiworker.dev
|
||||
secretName: gitea-tls
|
||||
rules:
|
||||
- host: git.aiworker.dev
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: gitea
|
||||
port:
|
||||
number: 3000
|
||||
```
|
||||
|
||||
## Gitea Configuration
|
||||
|
||||
```yaml
|
||||
# k8s/gitea/gitea-config.yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: gitea-config
|
||||
namespace: gitea
|
||||
data:
|
||||
app.ini: |
|
||||
[server]
|
||||
PROTOCOL = http
|
||||
DOMAIN = git.aiworker.dev
|
||||
ROOT_URL = https://git.aiworker.dev
|
||||
HTTP_PORT = 3000
|
||||
SSH_PORT = 2222
|
||||
DISABLE_SSH = false
|
||||
START_SSH_SERVER = true
|
||||
SSH_LISTEN_HOST = 0.0.0.0
|
||||
SSH_LISTEN_PORT = 22
|
||||
LFS_START_SERVER = true
|
||||
OFFLINE_MODE = false
|
||||
|
||||
[database]
|
||||
DB_TYPE = mysql
|
||||
HOST = mysql.control-plane.svc.cluster.local:3306
|
||||
NAME = gitea
|
||||
USER = root
|
||||
SSL_MODE = disable
|
||||
|
||||
[security]
|
||||
INSTALL_LOCK = true
|
||||
SECRET_KEY = your-secret-key-here
|
||||
INTERNAL_TOKEN = your-internal-token-here
|
||||
|
||||
[service]
|
||||
DISABLE_REGISTRATION = false
|
||||
REQUIRE_SIGNIN_VIEW = false
|
||||
ENABLE_NOTIFY_MAIL = false
|
||||
|
||||
[webhook]
|
||||
ALLOWED_HOST_LIST = *.svc.cluster.local,*.aiworker.dev
|
||||
|
||||
[api]
|
||||
ENABLE_SWAGGER = true
|
||||
|
||||
[actions]
|
||||
ENABLED = true
|
||||
|
||||
[repository]
|
||||
DEFAULT_BRANCH = main
|
||||
FORCE_PRIVATE = false
|
||||
|
||||
[ui]
|
||||
DEFAULT_THEME = arc-green
|
||||
```
|
||||
|
||||
## Inicialización de Gitea
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# scripts/init-gitea.sh
|
||||
|
||||
set -e
|
||||
|
||||
echo "🚀 Initializing Gitea..."
|
||||
|
||||
# Wait for Gitea to be ready
|
||||
echo "⏳ Waiting for Gitea pod..."
|
||||
kubectl wait --for=condition=ready pod -l app=gitea -n gitea --timeout=300s
|
||||
|
||||
# Port-forward temporalmente
|
||||
echo "🔌 Port-forwarding Gitea..."
|
||||
kubectl port-forward -n gitea svc/gitea 3001:3000 &
|
||||
PF_PID=$!
|
||||
sleep 5
|
||||
|
||||
# Create admin user
|
||||
echo "👤 Creating admin user..."
|
||||
kubectl exec -n gitea gitea-0 -- gitea admin user create \
|
||||
--username aiworker \
|
||||
--password admin123 \
|
||||
--email admin@aiworker.dev \
|
||||
--admin \
|
||||
--must-change-password=false
|
||||
|
||||
# Create organization
|
||||
echo "🏢 Creating organization..."
|
||||
kubectl exec -n gitea gitea-0 -- gitea admin user create \
|
||||
--username aiworker-bot \
|
||||
--password bot123 \
|
||||
--email bot@aiworker.dev
|
||||
|
||||
# Generate access token
|
||||
echo "🔑 Generating access token..."
|
||||
TOKEN=$(kubectl exec -n gitea gitea-0 -- gitea admin user generate-access-token \
|
||||
--username aiworker-bot \
|
||||
--scopes write:repository,write:issue,write:user \
|
||||
--raw)
|
||||
|
||||
echo "✅ Gitea initialized!"
|
||||
echo "📍 URL: https://git.aiworker.dev"
|
||||
echo "👤 User: aiworker / admin123"
|
||||
echo "🔑 Bot Token: $TOKEN"
|
||||
echo ""
|
||||
echo "⚠️ Save this token and update the secret:"
|
||||
echo "kubectl create secret generic aiworker-secrets -n control-plane \\"
|
||||
echo " --from-literal=gitea-token='$TOKEN' --dry-run=client -o yaml | kubectl apply -f -"
|
||||
|
||||
# Stop port-forward
|
||||
kill $PF_PID
|
||||
```
|
||||
|
||||
## Gitea Webhook Configuration
|
||||
|
||||
```typescript
|
||||
// services/gitea/setup.ts
|
||||
import { giteaClient } from './client'
|
||||
import { logger } from '../../utils/logger'
|
||||
|
||||
export async function setupGiteaWebhooks(owner: string, repo: string) {
|
||||
const backendUrl = process.env.BACKEND_URL || 'https://api.aiworker.dev'
|
||||
|
||||
try {
|
||||
// Create webhook for push events
|
||||
await giteaClient.createWebhook(owner, repo, {
|
||||
url: `${backendUrl}/api/webhooks/gitea`,
|
||||
contentType: 'json',
|
||||
secret: process.env.GITEA_WEBHOOK_SECRET || '',
|
||||
events: ['push', 'pull_request', 'pull_request_closed'],
|
||||
})
|
||||
|
||||
logger.info(`Webhooks configured for ${owner}/${repo}`)
|
||||
} catch (error) {
|
||||
logger.error('Failed to setup webhooks:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export async function initializeGiteaForProject(projectName: string) {
|
||||
const owner = process.env.GITEA_OWNER || 'aiworker'
|
||||
|
||||
// Create repository
|
||||
const repo = await giteaClient.createRepo(projectName, {
|
||||
description: `AiWorker project: ${projectName}`,
|
||||
private: true,
|
||||
autoInit: true,
|
||||
defaultBranch: 'main',
|
||||
})
|
||||
|
||||
// Setup webhooks
|
||||
await setupGiteaWebhooks(owner, projectName)
|
||||
|
||||
// Create initial branches
|
||||
await giteaClient.createBranch(owner, projectName, 'develop', 'main')
|
||||
await giteaClient.createBranch(owner, projectName, 'staging', 'main')
|
||||
|
||||
logger.info(`Gitea initialized for project: ${projectName}`)
|
||||
|
||||
return {
|
||||
repoUrl: repo.html_url,
|
||||
cloneUrl: repo.clone_url,
|
||||
sshUrl: repo.ssh_url,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Backup de Gitea
|
||||
|
||||
```yaml
|
||||
# k8s/gitea/gitea-backup-cronjob.yaml
|
||||
apiVersion: batch/v1
|
||||
kind: CronJob
|
||||
metadata:
|
||||
name: gitea-backup
|
||||
namespace: gitea
|
||||
spec:
|
||||
schedule: "0 2 * * *" # Daily at 2 AM
|
||||
jobTemplate:
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: backup
|
||||
image: gitea/gitea:1.22
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- |
|
||||
echo "Starting backup..."
|
||||
gitea dump -c /data/gitea/conf/app.ini -f /backups/gitea-backup-$(date +%Y%m%d).zip
|
||||
echo "Backup complete!"
|
||||
# Upload to S3 or other storage
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
- name: backups
|
||||
mountPath: /backups
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: gitea-data
|
||||
- name: backups
|
||||
persistentVolumeClaim:
|
||||
claimName: gitea-backups
|
||||
restartPolicy: OnFailure
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: gitea-backups
|
||||
namespace: gitea
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 100Gi
|
||||
```
|
||||
|
||||
## Monitoreo de Gitea
|
||||
|
||||
```yaml
|
||||
# k8s/gitea/gitea-servicemonitor.yaml
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
name: gitea
|
||||
namespace: gitea
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: gitea
|
||||
endpoints:
|
||||
- port: http
|
||||
path: /metrics
|
||||
interval: 30s
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
```bash
|
||||
# Ver logs
|
||||
kubectl logs -n gitea gitea-0 --tail=100 -f
|
||||
|
||||
# Entrar al pod
|
||||
kubectl exec -it -n gitea gitea-0 -- /bin/sh
|
||||
|
||||
# Verificar config
|
||||
kubectl exec -n gitea gitea-0 -- cat /data/gitea/conf/app.ini
|
||||
|
||||
# Regenerar admin user
|
||||
kubectl exec -n gitea gitea-0 -- gitea admin user change-password \
|
||||
--username aiworker --password newpassword
|
||||
|
||||
# Limpiar cache
|
||||
kubectl exec -n gitea gitea-0 -- rm -rf /data/gitea/queues/*
|
||||
```
|
||||
|
||||
## SSH Keys Setup
|
||||
|
||||
```bash
|
||||
# Generar SSH key para agentes
|
||||
ssh-keygen -t ed25519 -C "aiworker-agent" -f agent-key -N ""
|
||||
|
||||
# Crear secret
|
||||
kubectl create secret generic git-ssh-keys -n agents \
|
||||
--from-file=private-key=agent-key \
|
||||
--from-file=public-key=agent-key.pub
|
||||
|
||||
# Añadir public key a Gitea
|
||||
# (via API o manualmente en UI)
|
||||
```
|
||||
|
||||
## Git Config para Agentes
|
||||
|
||||
```yaml
|
||||
# k8s/agents/git-config.yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: git-config
|
||||
namespace: agents
|
||||
data:
|
||||
.gitconfig: |
|
||||
[user]
|
||||
name = AiWorker Agent
|
||||
email = agent@aiworker.dev
|
||||
[core]
|
||||
sshCommand = ssh -i /root/.ssh/id_ed25519 -o StrictHostKeyChecking=no
|
||||
[credential]
|
||||
helper = store
|
||||
```
|
||||
481
docs/04-kubernetes/namespaces.md
Normal file
481
docs/04-kubernetes/namespaces.md
Normal file
@@ -0,0 +1,481 @@
|
||||
# Estructura de Namespaces
|
||||
|
||||
## Arquitectura de Namespaces
|
||||
|
||||
```
|
||||
aiworker-cluster/
|
||||
├── control-plane/ # Backend, API, MCP Server
|
||||
├── agents/ # Claude Code agent pods
|
||||
├── gitea/ # Gitea server
|
||||
├── projects/
|
||||
│ └── <project-name>/
|
||||
│ ├── dev/ # Desarrollo continuo
|
||||
│ ├── preview-*/ # Preview deployments por tarea
|
||||
│ ├── staging/ # Staging environment
|
||||
│ └── production/ # Production environment
|
||||
└── monitoring/ # Prometheus, Grafana
|
||||
```
|
||||
|
||||
## Namespace: control-plane
|
||||
|
||||
**Propósito**: Backend API, MCP Server, servicios core
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: control-plane
|
||||
labels:
|
||||
name: control-plane
|
||||
environment: production
|
||||
managed-by: aiworker
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ResourceQuota
|
||||
metadata:
|
||||
name: control-plane-quota
|
||||
namespace: control-plane
|
||||
spec:
|
||||
hard:
|
||||
requests.cpu: "4"
|
||||
requests.memory: 8Gi
|
||||
limits.cpu: "8"
|
||||
limits.memory: 16Gi
|
||||
persistentvolumeclaims: "5"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: LimitRange
|
||||
metadata:
|
||||
name: control-plane-limits
|
||||
namespace: control-plane
|
||||
spec:
|
||||
limits:
|
||||
- max:
|
||||
cpu: "2"
|
||||
memory: 4Gi
|
||||
min:
|
||||
cpu: "100m"
|
||||
memory: 128Mi
|
||||
default:
|
||||
cpu: "500m"
|
||||
memory: 512Mi
|
||||
defaultRequest:
|
||||
cpu: "250m"
|
||||
memory: 256Mi
|
||||
type: Container
|
||||
```
|
||||
|
||||
### Servicios en control-plane
|
||||
|
||||
- **Backend API**: Express + Bun
|
||||
- **MCP Server**: Comunicación con agentes
|
||||
- **MySQL**: Base de datos
|
||||
- **Redis**: Cache y colas
|
||||
- **BullMQ Workers**: Procesamiento de jobs
|
||||
|
||||
## Namespace: agents
|
||||
|
||||
**Propósito**: Pods de Claude Code agents
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: agents
|
||||
labels:
|
||||
name: agents
|
||||
environment: production
|
||||
managed-by: aiworker
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ResourceQuota
|
||||
metadata:
|
||||
name: agents-quota
|
||||
namespace: agents
|
||||
spec:
|
||||
hard:
|
||||
requests.cpu: "20"
|
||||
requests.memory: 40Gi
|
||||
limits.cpu: "40"
|
||||
limits.memory: 80Gi
|
||||
pods: "50"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: LimitRange
|
||||
metadata:
|
||||
name: agents-limits
|
||||
namespace: agents
|
||||
spec:
|
||||
limits:
|
||||
- max:
|
||||
cpu: "2"
|
||||
memory: 4Gi
|
||||
min:
|
||||
cpu: "500m"
|
||||
memory: 1Gi
|
||||
default:
|
||||
cpu: "1"
|
||||
memory: 2Gi
|
||||
defaultRequest:
|
||||
cpu: "500m"
|
||||
memory: 1Gi
|
||||
type: Container
|
||||
```
|
||||
|
||||
### Network Policy para Agents
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: agents-network-policy
|
||||
namespace: agents
|
||||
spec:
|
||||
podSelector: {}
|
||||
policyTypes:
|
||||
- Ingress
|
||||
- Egress
|
||||
ingress:
|
||||
# Permitir tráfico desde control-plane
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
name: control-plane
|
||||
egress:
|
||||
# Permitir salida a control-plane (MCP Server)
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
name: control-plane
|
||||
# Permitir salida a gitea
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
name: gitea
|
||||
# Permitir DNS
|
||||
- to:
|
||||
- namespaceSelector: {}
|
||||
podSelector:
|
||||
matchLabels:
|
||||
k8s-app: kube-dns
|
||||
ports:
|
||||
- protocol: UDP
|
||||
port: 53
|
||||
# Permitir HTTPS externo (para Claude API)
|
||||
- to:
|
||||
- namespaceSelector: {}
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 443
|
||||
```
|
||||
|
||||
## Namespace: gitea
|
||||
|
||||
**Propósito**: Servidor Git auto-alojado
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: gitea
|
||||
labels:
|
||||
name: gitea
|
||||
environment: production
|
||||
managed-by: aiworker
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ResourceQuota
|
||||
metadata:
|
||||
name: gitea-quota
|
||||
namespace: gitea
|
||||
spec:
|
||||
hard:
|
||||
requests.cpu: "2"
|
||||
requests.memory: 4Gi
|
||||
limits.cpu: "4"
|
||||
limits.memory: 8Gi
|
||||
persistentvolumeclaims: "2"
|
||||
```
|
||||
|
||||
## Namespaces por Proyecto
|
||||
|
||||
### Estructura Dinámica
|
||||
|
||||
Para cada proyecto creado, se generan automáticamente 4 namespaces:
|
||||
|
||||
```typescript
|
||||
// services/kubernetes/namespaces.ts
|
||||
export async function createProjectNamespaces(projectName: string) {
|
||||
const baseName = projectName.toLowerCase().replace(/[^a-z0-9-]/g, '-')
|
||||
|
||||
const namespaces = [
|
||||
`${baseName}-dev`,
|
||||
`${baseName}-staging`,
|
||||
`${baseName}-production`,
|
||||
]
|
||||
|
||||
for (const ns of namespaces) {
|
||||
await k8sClient.createNamespace({
|
||||
name: ns,
|
||||
labels: {
|
||||
project: baseName,
|
||||
'managed-by': 'aiworker',
|
||||
},
|
||||
})
|
||||
|
||||
// Aplicar resource quotas
|
||||
await k8sClient.applyResourceQuota(ns, {
|
||||
requests: { cpu: '2', memory: '4Gi' },
|
||||
limits: { cpu: '4', memory: '8Gi' },
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Namespace: project-dev
|
||||
|
||||
**Propósito**: Desarrollo continuo, deploy automático de main/develop
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: my-project-dev
|
||||
labels:
|
||||
project: my-project
|
||||
environment: dev
|
||||
managed-by: aiworker
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ResourceQuota
|
||||
metadata:
|
||||
name: dev-quota
|
||||
namespace: my-project-dev
|
||||
spec:
|
||||
hard:
|
||||
requests.cpu: "1"
|
||||
requests.memory: 2Gi
|
||||
limits.cpu: "2"
|
||||
limits.memory: 4Gi
|
||||
pods: "5"
|
||||
```
|
||||
|
||||
### Namespace: preview-task-{id}
|
||||
|
||||
**Propósito**: Preview deployment temporal para una tarea específica
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: preview-task-abc123
|
||||
labels:
|
||||
project: my-project
|
||||
environment: preview
|
||||
task-id: abc123
|
||||
managed-by: aiworker
|
||||
ttl: "168h" # 7 days
|
||||
annotations:
|
||||
created-at: "2026-01-19T12:00:00Z"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ResourceQuota
|
||||
metadata:
|
||||
name: preview-quota
|
||||
namespace: preview-task-abc123
|
||||
spec:
|
||||
hard:
|
||||
requests.cpu: "500m"
|
||||
requests.memory: 1Gi
|
||||
limits.cpu: "1"
|
||||
limits.memory: 2Gi
|
||||
pods: "3"
|
||||
```
|
||||
|
||||
**Limpieza automática**:
|
||||
```typescript
|
||||
// Cleanup job que corre diariamente
|
||||
export async function cleanupOldPreviewNamespaces() {
|
||||
const allNamespaces = await k8sClient.listNamespaces()
|
||||
|
||||
for (const ns of allNamespaces) {
|
||||
if (ns.metadata?.labels?.environment === 'preview') {
|
||||
const createdAt = new Date(ns.metadata.annotations?.['created-at'])
|
||||
const ageHours = (Date.now() - createdAt.getTime()) / (1000 * 60 * 60)
|
||||
|
||||
if (ageHours > 168) { // 7 days
|
||||
await k8sClient.deleteNamespace(ns.metadata.name)
|
||||
logger.info(`Deleted old preview namespace: ${ns.metadata.name}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Namespace: project-staging
|
||||
|
||||
**Propósito**: Staging environment, testing antes de producción
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: my-project-staging
|
||||
labels:
|
||||
project: my-project
|
||||
environment: staging
|
||||
managed-by: aiworker
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ResourceQuota
|
||||
metadata:
|
||||
name: staging-quota
|
||||
namespace: my-project-staging
|
||||
spec:
|
||||
hard:
|
||||
requests.cpu: "2"
|
||||
requests.memory: 4Gi
|
||||
limits.cpu: "4"
|
||||
limits.memory: 8Gi
|
||||
pods: "10"
|
||||
```
|
||||
|
||||
### Namespace: project-production
|
||||
|
||||
**Propósito**: Production environment
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: my-project-production
|
||||
labels:
|
||||
project: my-project
|
||||
environment: production
|
||||
managed-by: aiworker
|
||||
protected: "true"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ResourceQuota
|
||||
metadata:
|
||||
name: production-quota
|
||||
namespace: my-project-production
|
||||
spec:
|
||||
hard:
|
||||
requests.cpu: "4"
|
||||
requests.memory: 8Gi
|
||||
limits.cpu: "8"
|
||||
limits.memory: 16Gi
|
||||
pods: "20"
|
||||
---
|
||||
# Pod Disruption Budget para alta disponibilidad
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: app-pdb
|
||||
namespace: my-project-production
|
||||
spec:
|
||||
minAvailable: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: my-project
|
||||
```
|
||||
|
||||
## Namespace: monitoring
|
||||
|
||||
**Propósito**: Prometheus, Grafana, logs
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: monitoring
|
||||
labels:
|
||||
name: monitoring
|
||||
environment: production
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ResourceQuota
|
||||
metadata:
|
||||
name: monitoring-quota
|
||||
namespace: monitoring
|
||||
spec:
|
||||
hard:
|
||||
requests.cpu: "4"
|
||||
requests.memory: 8Gi
|
||||
limits.cpu: "8"
|
||||
limits.memory: 16Gi
|
||||
persistentvolumeclaims: "10"
|
||||
```
|
||||
|
||||
## Gestión de Namespaces desde el Backend
|
||||
|
||||
```typescript
|
||||
// services/kubernetes/namespaces.ts
|
||||
import { KubeConfig, CoreV1Api } from '@kubernetes/client-node'
|
||||
|
||||
export class NamespaceManager {
|
||||
private k8sApi: CoreV1Api
|
||||
|
||||
constructor() {
|
||||
const kc = new KubeConfig()
|
||||
kc.loadFromDefault()
|
||||
this.k8sApi = kc.makeApiClient(CoreV1Api)
|
||||
}
|
||||
|
||||
async createNamespace(name: string, labels: Record<string, string> = {}) {
|
||||
await this.k8sApi.createNamespace({
|
||||
metadata: {
|
||||
name,
|
||||
labels: {
|
||||
'managed-by': 'aiworker',
|
||||
...labels,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async deleteNamespace(name: string) {
|
||||
await this.k8sApi.deleteNamespace(name)
|
||||
}
|
||||
|
||||
async listNamespaces(labelSelector?: string) {
|
||||
const response = await this.k8sApi.listNamespace(undefined, undefined, undefined, undefined, labelSelector)
|
||||
return response.body.items
|
||||
}
|
||||
|
||||
async namespaceExists(name: string): Promise<boolean> {
|
||||
try {
|
||||
await this.k8sApi.readNamespace(name)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Dashboard de Namespaces
|
||||
|
||||
En el frontend, mostrar todos los namespaces con sus recursos:
|
||||
|
||||
```typescript
|
||||
// hooks/useNamespaces.ts
|
||||
export function useNamespaces(projectId?: string) {
|
||||
return useQuery({
|
||||
queryKey: ['namespaces', projectId],
|
||||
queryFn: async () => {
|
||||
const { data } = await api.get('/namespaces', {
|
||||
params: { projectId },
|
||||
})
|
||||
return data.namespaces
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Vista en el dashboard:
|
||||
- **Mapa de namespaces** por proyecto
|
||||
- **Uso de recursos** (CPU, memoria) por namespace
|
||||
- **Número de pods** activos
|
||||
- **Botón de cleanup** para preview namespaces antiguos
|
||||
474
docs/04-kubernetes/networking.md
Normal file
474
docs/04-kubernetes/networking.md
Normal file
@@ -0,0 +1,474 @@
|
||||
# Networking e Ingress
|
||||
|
||||
## Arquitectura de Red
|
||||
|
||||
```
|
||||
Internet
|
||||
│
|
||||
▼
|
||||
[LoadBalancer] (Cloud Provider)
|
||||
│
|
||||
▼
|
||||
[Nginx Ingress Controller]
|
||||
│
|
||||
├──► api.aiworker.dev ──► Backend (control-plane)
|
||||
├──► git.aiworker.dev ──► Gitea (gitea)
|
||||
├──► app.aiworker.dev ──► Frontend (control-plane)
|
||||
├──► *.preview.aiworker.dev ──► Preview Deployments
|
||||
├──► staging-*.aiworker.dev ──► Staging Envs
|
||||
└──► *.aiworker.dev ──► Production Apps
|
||||
```
|
||||
|
||||
## Ingress Configuration
|
||||
|
||||
### Wildcard Certificate
|
||||
|
||||
```yaml
|
||||
# k8s/ingress/wildcard-certificate.yaml
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: Certificate
|
||||
metadata:
|
||||
name: wildcard-aiworker
|
||||
namespace: ingress-nginx
|
||||
spec:
|
||||
secretName: wildcard-aiworker-tls
|
||||
issuerRef:
|
||||
name: letsencrypt-prod
|
||||
kind: ClusterIssuer
|
||||
commonName: "*.aiworker.dev"
|
||||
dnsNames:
|
||||
- "aiworker.dev"
|
||||
- "*.aiworker.dev"
|
||||
- "*.preview.aiworker.dev"
|
||||
```
|
||||
|
||||
### Backend Ingress
|
||||
|
||||
```yaml
|
||||
# k8s/ingress/backend-ingress.yaml
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: backend-ingress
|
||||
namespace: control-plane
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||
nginx.ingress.kubernetes.io/websocket-services: "aiworker-backend"
|
||||
nginx.ingress.kubernetes.io/proxy-body-size: "50m"
|
||||
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
|
||||
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
|
||||
nginx.ingress.kubernetes.io/cors-allow-origin: "https://app.aiworker.dev"
|
||||
nginx.ingress.kubernetes.io/cors-allow-methods: "GET, POST, PUT, PATCH, DELETE, OPTIONS"
|
||||
nginx.ingress.kubernetes.io/cors-allow-credentials: "true"
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
tls:
|
||||
- hosts:
|
||||
- api.aiworker.dev
|
||||
secretName: backend-tls
|
||||
rules:
|
||||
- host: api.aiworker.dev
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: aiworker-backend
|
||||
port:
|
||||
number: 3000
|
||||
```
|
||||
|
||||
### Frontend Ingress
|
||||
|
||||
```yaml
|
||||
# k8s/ingress/frontend-ingress.yaml
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: frontend-ingress
|
||||
namespace: control-plane
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
|
||||
nginx.ingress.kubernetes.io/configuration-snippet: |
|
||||
more_set_headers "X-Frame-Options: DENY";
|
||||
more_set_headers "X-Content-Type-Options: nosniff";
|
||||
more_set_headers "X-XSS-Protection: 1; mode=block";
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
tls:
|
||||
- hosts:
|
||||
- app.aiworker.dev
|
||||
secretName: frontend-tls
|
||||
rules:
|
||||
- host: app.aiworker.dev
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: aiworker-frontend
|
||||
port:
|
||||
number: 80
|
||||
```
|
||||
|
||||
### Preview Deployments Ingress Template
|
||||
|
||||
```typescript
|
||||
// services/kubernetes/ingress.ts
|
||||
export function generatePreviewIngress(params: {
|
||||
taskId: string
|
||||
projectName: string
|
||||
namespace: string
|
||||
}) {
|
||||
const shortId = params.taskId.slice(0, 8)
|
||||
const host = `task-${shortId}.preview.aiworker.dev`
|
||||
|
||||
return {
|
||||
apiVersion: 'networking.k8s.io/v1',
|
||||
kind: 'Ingress',
|
||||
metadata: {
|
||||
name: `${params.projectName}-preview`,
|
||||
namespace: params.namespace,
|
||||
annotations: {
|
||||
'cert-manager.io/cluster-issuer': 'letsencrypt-prod',
|
||||
'nginx.ingress.kubernetes.io/ssl-redirect': 'true',
|
||||
'nginx.ingress.kubernetes.io/auth-type': 'basic',
|
||||
'nginx.ingress.kubernetes.io/auth-secret': 'preview-basic-auth',
|
||||
'nginx.ingress.kubernetes.io/auth-realm': 'Preview Environment',
|
||||
},
|
||||
labels: {
|
||||
environment: 'preview',
|
||||
task: params.taskId,
|
||||
project: params.projectName,
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
ingressClassName: 'nginx',
|
||||
tls: [
|
||||
{
|
||||
hosts: [host],
|
||||
secretName: `${params.projectName}-preview-tls`,
|
||||
},
|
||||
],
|
||||
rules: [
|
||||
{
|
||||
host,
|
||||
http: {
|
||||
paths: [
|
||||
{
|
||||
path: '/',
|
||||
pathType: 'Prefix',
|
||||
backend: {
|
||||
service: {
|
||||
name: `${params.projectName}-preview`,
|
||||
port: {
|
||||
number: 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Service Mesh (Opcional)
|
||||
|
||||
Si necesitas más control sobre el tráfico, considera usar Istio o Linkerd:
|
||||
|
||||
### Istio Gateway
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.istio.io/v1beta1
|
||||
kind: Gateway
|
||||
metadata:
|
||||
name: aiworker-gateway
|
||||
namespace: istio-system
|
||||
spec:
|
||||
selector:
|
||||
istio: ingressgateway
|
||||
servers:
|
||||
- port:
|
||||
number: 443
|
||||
name: https
|
||||
protocol: HTTPS
|
||||
tls:
|
||||
mode: SIMPLE
|
||||
credentialName: wildcard-aiworker-tls
|
||||
hosts:
|
||||
- "*.aiworker.dev"
|
||||
---
|
||||
apiVersion: networking.istio.io/v1beta1
|
||||
kind: VirtualService
|
||||
metadata:
|
||||
name: backend-vs
|
||||
namespace: control-plane
|
||||
spec:
|
||||
hosts:
|
||||
- "api.aiworker.dev"
|
||||
gateways:
|
||||
- istio-system/aiworker-gateway
|
||||
http:
|
||||
- match:
|
||||
- uri:
|
||||
prefix: /api
|
||||
route:
|
||||
- destination:
|
||||
host: aiworker-backend
|
||||
port:
|
||||
number: 3000
|
||||
```
|
||||
|
||||
## DNS Configuration
|
||||
|
||||
### Cloudflare DNS Records
|
||||
|
||||
```bash
|
||||
# A records
|
||||
api.aiworker.dev A <loadbalancer-ip>
|
||||
git.aiworker.dev A <loadbalancer-ip>
|
||||
app.aiworker.dev A <loadbalancer-ip>
|
||||
|
||||
# Wildcard for preview and dynamic environments
|
||||
*.preview.aiworker.dev A <loadbalancer-ip>
|
||||
*.aiworker.dev A <loadbalancer-ip>
|
||||
```
|
||||
|
||||
### External DNS (Automated)
|
||||
|
||||
```yaml
|
||||
# k8s/external-dns/external-dns-deployment.yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: external-dns
|
||||
namespace: kube-system
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: external-dns
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: external-dns
|
||||
spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: registry.k8s.io/external-dns/external-dns:v0.14.0
|
||||
args:
|
||||
- --source=ingress
|
||||
- --domain-filter=aiworker.dev
|
||||
- --provider=cloudflare
|
||||
env:
|
||||
- name: CF_API_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: cloudflare-api-token
|
||||
key: token
|
||||
```
|
||||
|
||||
## Network Policies
|
||||
|
||||
### Isolate Preview Environments
|
||||
|
||||
```yaml
|
||||
# k8s/network-policies/preview-isolation.yaml
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: preview-isolation
|
||||
namespace: agents
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
environment: preview
|
||||
policyTypes:
|
||||
- Ingress
|
||||
- Egress
|
||||
ingress:
|
||||
# Allow from ingress controller
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
name: ingress-nginx
|
||||
# Allow from control-plane
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
name: control-plane
|
||||
egress:
|
||||
# Allow to gitea
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
name: gitea
|
||||
# Allow to external HTTPS (npm, apt, etc)
|
||||
- to:
|
||||
- namespaceSelector: {}
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 443
|
||||
# Allow DNS
|
||||
- to:
|
||||
- namespaceSelector: {}
|
||||
podSelector:
|
||||
matchLabels:
|
||||
k8s-app: kube-dns
|
||||
ports:
|
||||
- protocol: UDP
|
||||
port: 53
|
||||
```
|
||||
|
||||
### Allow Backend to All
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: backend-egress
|
||||
namespace: control-plane
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app: aiworker-backend
|
||||
policyTypes:
|
||||
- Egress
|
||||
egress:
|
||||
- {} # Allow all egress
|
||||
```
|
||||
|
||||
## Load Balancing
|
||||
|
||||
### Session Affinity for WebSocket
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: aiworker-backend
|
||||
namespace: control-plane
|
||||
annotations:
|
||||
service.beta.kubernetes.io/external-traffic: OnlyLocal
|
||||
spec:
|
||||
selector:
|
||||
app: aiworker-backend
|
||||
ports:
|
||||
- name: http
|
||||
port: 3000
|
||||
targetPort: 3000
|
||||
sessionAffinity: ClientIP
|
||||
sessionAffinityConfig:
|
||||
clientIP:
|
||||
timeoutSeconds: 3600
|
||||
type: ClusterIP
|
||||
```
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: backend-ingress
|
||||
namespace: control-plane
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/rate-limit: "100"
|
||||
nginx.ingress.kubernetes.io/rate-limit-burst: "200"
|
||||
nginx.ingress.kubernetes.io/rate-limit-key: "$binary_remote_addr"
|
||||
spec:
|
||||
# ... spec
|
||||
```
|
||||
|
||||
## Health Checks
|
||||
|
||||
### Liveness and Readiness Probes
|
||||
|
||||
```yaml
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /api/health
|
||||
port: 3000
|
||||
httpHeaders:
|
||||
- name: X-Health-Check
|
||||
value: liveness
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /api/health/ready
|
||||
port: 3000
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
```
|
||||
|
||||
### Health Endpoint Implementation
|
||||
|
||||
```typescript
|
||||
// api/routes/health.ts
|
||||
import { Router } from 'express'
|
||||
import { getDatabase } from '../../config/database'
|
||||
import { getRedis } from '../../config/redis'
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.get('/health', async (req, res) => {
|
||||
res.json({
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
})
|
||||
})
|
||||
|
||||
router.get('/health/ready', async (req, res) => {
|
||||
try {
|
||||
// Check DB
|
||||
const db = getDatabase()
|
||||
await db.execute('SELECT 1')
|
||||
|
||||
// Check Redis
|
||||
const redis = getRedis()
|
||||
await redis.ping()
|
||||
|
||||
res.json({
|
||||
status: 'ready',
|
||||
services: {
|
||||
database: 'connected',
|
||||
redis: 'connected',
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
res.status(503).json({
|
||||
status: 'not ready',
|
||||
error: error.message,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
```
|
||||
|
||||
## Monitoring Traffic
|
||||
|
||||
```bash
|
||||
# Ver logs de Nginx Ingress
|
||||
kubectl logs -n ingress-nginx deployment/ingress-nginx-controller --tail=100 -f
|
||||
|
||||
# Ver métricas
|
||||
kubectl top pods -n ingress-nginx
|
||||
|
||||
# Ver configuración generada
|
||||
kubectl exec -n ingress-nginx <pod> -- cat /etc/nginx/nginx.conf
|
||||
```
|
||||
Reference in New Issue
Block a user