Files
aiworker/docs/06-deployment/gitops.md
Hector Ros db71705842 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>
2026-01-20 00:37:19 +01:00

532 lines
11 KiB
Markdown

# GitOps con ArgoCD
## ¿Qué es GitOps?
GitOps usa Git como fuente única de verdad para infraestructura y aplicaciones. Los cambios se hacen via commits, y herramientas como ArgoCD sincronizan automáticamente el estado deseado en Git con el estado real en Kubernetes.
## Instalación de ArgoCD
```bash
# Create namespace
kubectl create namespace argocd
# Install ArgoCD
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
# Wait for pods
kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=argocd-server -n argocd --timeout=300s
# Get initial admin password
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d
# Port forward to access UI
kubectl port-forward svc/argocd-server -n argocd 8080:443
# Access at: https://localhost:8080
# Username: admin
# Password: (from above command)
```
## Estructura de Repositorio GitOps
```
gitops-repo/
├── projects/
│ ├── backend/
│ │ ├── base/
│ │ │ ├── deployment.yaml
│ │ │ ├── service.yaml
│ │ │ └── kustomization.yaml
│ │ ├── dev/
│ │ │ ├── kustomization.yaml
│ │ │ └── patches.yaml
│ │ ├── staging/
│ │ │ ├── kustomization.yaml
│ │ │ └── patches.yaml
│ │ └── production/
│ │ ├── kustomization.yaml
│ │ └── patches.yaml
│ │
│ └── my-project/
│ ├── base/
│ ├── dev/
│ ├── staging/
│ └── production/
└── argocd/
├── applications/
│ ├── backend-dev.yaml
│ ├── backend-staging.yaml
│ ├── backend-production.yaml
│ └── my-project-production.yaml
└── app-of-apps.yaml
```
## Base Manifests con Kustomize
```yaml
# projects/backend/base/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend
spec:
replicas: 2
selector:
matchLabels:
app: backend
template:
metadata:
labels:
app: backend
spec:
containers:
- name: backend
image: aiworker/backend:latest
ports:
- containerPort: 3000
env:
- name: NODE_ENV
value: production
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
cpu: 1
memory: 2Gi
---
# projects/backend/base/service.yaml
apiVersion: v1
kind: Service
metadata:
name: backend
spec:
selector:
app: backend
ports:
- port: 3000
targetPort: 3000
---
# projects/backend/base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
commonLabels:
app: backend
managed-by: argocd
```
## Environment Overlays
```yaml
# projects/backend/production/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: control-plane
bases:
- ../base
patchesStrategicMerge:
- patches.yaml
images:
- name: aiworker/backend
newTag: v1.2.3 # This gets updated automatically
replicas:
- name: backend
count: 3
configMapGenerator:
- name: backend-config
literals:
- NODE_ENV=production
- LOG_LEVEL=info
---
# projects/backend/production/patches.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend
spec:
template:
spec:
containers:
- name: backend
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: 2
memory: 4Gi
```
## ArgoCD Application
```yaml
# argocd/applications/backend-production.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: backend-production
namespace: argocd
spec:
project: default
source:
repoURL: https://git.aiworker.dev/aiworker/gitops
targetRevision: HEAD
path: projects/backend/production
destination:
server: https://kubernetes.default.svc
namespace: control-plane
syncPolicy:
automated:
prune: true
selfHeal: true
allowEmpty: false
syncOptions:
- CreateNamespace=false
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m
revisionHistoryLimit: 10
```
## App of Apps Pattern
```yaml
# argocd/app-of-apps.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: aiworker-apps
namespace: argocd
spec:
project: default
source:
repoURL: https://git.aiworker.dev/aiworker/gitops
targetRevision: HEAD
path: argocd/applications
destination:
server: https://kubernetes.default.svc
namespace: argocd
syncPolicy:
automated:
prune: true
selfHeal: true
```
## Actualización de Imagen desde Backend
```typescript
// services/gitops/updater.ts
import { Octokit } from '@octokit/rest'
import yaml from 'js-yaml'
import { logger } from '../../utils/logger'
export class GitOpsUpdater {
private octokit: Octokit
private repo: string
private owner: string
constructor() {
this.octokit = new Octokit({
baseUrl: process.env.GITEA_URL,
auth: process.env.GITEA_TOKEN,
})
this.repo = 'gitops'
this.owner = 'aiworker'
}
async updateImage(params: {
project: string
environment: string
imageTag: string
}) {
const { project, environment, imageTag } = params
const path = `projects/${project}/${environment}/kustomization.yaml`
logger.info(`Updating GitOps: ${project}/${environment}${imageTag}`)
try {
// 1. Get current file
const { data: fileData } = await this.octokit.repos.getContent({
owner: this.owner,
repo: this.repo,
path,
})
if (Array.isArray(fileData) || fileData.type !== 'file') {
throw new Error('Invalid file')
}
// 2. Decode content
const content = Buffer.from(fileData.content, 'base64').toString('utf-8')
const kustomization = yaml.load(content) as any
// 3. Update image tag
if (!kustomization.images) {
kustomization.images = []
}
const imageIndex = kustomization.images.findIndex(
(img: any) => img.name === `aiworker/${project}`
)
if (imageIndex >= 0) {
kustomization.images[imageIndex].newTag = imageTag
} else {
kustomization.images.push({
name: `aiworker/${project}`,
newTag: imageTag,
})
}
// 4. Encode new content
const newContent = yaml.dump(kustomization)
const newContentBase64 = Buffer.from(newContent).toString('base64')
// 5. Commit changes
await this.octokit.repos.createOrUpdateFileContents({
owner: this.owner,
repo: this.repo,
path,
message: `Update ${project} ${environment} to ${imageTag}`,
content: newContentBase64,
sha: fileData.sha,
})
logger.info(`GitOps updated: ${project}/${environment}`)
return { success: true }
} catch (error: any) {
logger.error('Failed to update GitOps:', error)
throw error
}
}
}
```
## Integración con CI/CD
```typescript
// services/queue/deploy-worker.ts
import { GitOpsUpdater } from '../gitops/updater'
const gitopsUpdater = new GitOpsUpdater()
export const deployWorker = new Worker('deploys', async (job) => {
const { deploymentId, projectId, environment, commitHash } = job.data
// ... deployment logic ...
// Update GitOps repo
await gitopsUpdater.updateImage({
project: project.name,
environment,
imageTag: commitHash.slice(0, 7),
})
// ArgoCD will automatically sync within 3 minutes
// Or trigger manual sync:
await triggerArgoCDSync(project.name, environment)
logger.info('GitOps updated, ArgoCD will sync')
})
```
## Trigger ArgoCD Sync
```typescript
// services/gitops/argocd.ts
export async function triggerArgoCDSync(project: string, environment: string) {
const appName = `${project}-${environment}`
const argoCDUrl = process.env.ARGOCD_URL || 'https://argocd.aiworker.dev'
const token = process.env.ARGOCD_TOKEN
const response = await fetch(`${argoCDUrl}/api/v1/applications/${appName}/sync`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
prune: false,
dryRun: false,
strategy: {
hook: {},
},
}),
})
if (!response.ok) {
throw new Error(`ArgoCD sync failed: ${response.statusText}`)
}
logger.info(`Triggered ArgoCD sync: ${appName}`)
}
```
## Health Status from ArgoCD
```typescript
// services/gitops/argocd.ts
export async function getApplicationStatus(appName: string) {
const argoCDUrl = process.env.ARGOCD_URL
const token = process.env.ARGOCD_TOKEN
const response = await fetch(`${argoCDUrl}/api/v1/applications/${appName}`, {
headers: {
'Authorization': `Bearer ${token}`,
},
})
const app = await response.json()
return {
syncStatus: app.status.sync.status, // Synced, OutOfSync
healthStatus: app.status.health.status, // Healthy, Progressing, Degraded
lastSyncedAt: app.status.operationState?.finishedAt,
}
}
```
## Monitoring Dashboard
```typescript
// api/routes/gitops.ts
router.get('/gitops/status', async (req, res) => {
const apps = ['backend-production', 'backend-staging', 'backend-dev']
const statuses = await Promise.all(
apps.map(async (app) => {
const status = await getApplicationStatus(app)
return {
name: app,
...status,
}
})
)
res.json({ applications: statuses })
})
```
## Benefits of GitOps
### 1. Declarative
Todo el estado deseado está en Git, versionado y auditable.
### 2. Auditabilidad
Cada cambio tiene un commit con autor, timestamp y descripción.
### 3. Rollback Fácil
```bash
# Rollback to previous version
git revert HEAD
git push
# ArgoCD automatically syncs back
```
### 4. Disaster Recovery
Cluster destruido? Simplemente:
```bash
# Reinstall ArgoCD
kubectl apply -f argocd-install.yaml
# Deploy app-of-apps
kubectl apply -f app-of-apps.yaml
# Todo vuelve al estado en Git
```
### 5. Multi-Cluster
```yaml
# Deploy same app to multiple clusters
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: backend-cluster-2
spec:
destination:
server: https://cluster-2.example.com
namespace: control-plane
# ... same source
```
## Best Practices
### 1. Separate Repo
Mantener GitOps separado del código de aplicación:
- **App repo**: Código fuente
- **GitOps repo**: Manifests de K8s
### 2. Environment Branches (Optional)
```
main → production
staging → staging environment
dev → dev environment
```
### 3. Secrets Management
No commitear secrets en Git. Usar:
- **Sealed Secrets**
- **External Secrets Operator**
- **Vault**
### 4. Progressive Rollout
```yaml
# Use Argo Rollouts for canary/blue-green
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: backend
spec:
strategy:
canary:
steps:
- setWeight: 20
- pause: {duration: 5m}
- setWeight: 50
- pause: {duration: 5m}
- setWeight: 100
```
## Troubleshooting
```bash
# Ver estado de aplicación
argocd app get backend-production
# Ver diferencias
argocd app diff backend-production
# Sync manual
argocd app sync backend-production
# Ver logs
kubectl logs -n argocd deployment/argocd-application-controller
# Refresh (fetch latest from Git)
argocd app refresh backend-production
```