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:
531
docs/06-deployment/gitops.md
Normal file
531
docs/06-deployment/gitops.md
Normal file
@@ -0,0 +1,531 @@
|
||||
# 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
|
||||
```
|
||||
Reference in New Issue
Block a user