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:
Hector Ros
2026-01-20 00:36:53 +01:00
commit db71705842
49 changed files with 19162 additions and 0 deletions

View 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
```

View 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"
```

View 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
```

View 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

View 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
```