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:
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"
|
||||
```
|
||||
Reference in New Issue
Block a user