- 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>
532 lines
11 KiB
Markdown
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
|
|
```
|