Chart
v1.0.0
cultivacore-api
App version
2.3.1
Node.js 18 · Express
Archivos template
9
+ 4 values files
Dependencias
2
PostgreSQL 15 + Redis 7
Arquitectura del despliegue
Internet
🌐 DNS
→
Ingress
🔒 TLS/NGINX
→
Service · ClusterIP
cultivacore-api
→
Deployment (HPA)
3–15 pods
→
Dep. PostgreSQL
🐘 pg 15
Dep. Redis
⚡ redis 7
cultivacore-api/Chart.yaml
YAML
# Chart metadata — Helm 3+ (apiVersion: v2) apiVersion: v2 name: cultivacore-api description: Helm chart para la API de agentes de IA de CULTIVA IA type: application version: 1.0.0 # version del chart (SemVer) appVersion: "2.3.1" # version de la aplicacion Node.js keywords: - ai-api - marketing-automation - backend - cultivaia home: https://cultivaia.com sources: - https://github.com/cultivaia/cultivacore-api maintainers: - name: DevOps · CULTIVA IA email: devops@cultivaia.com url: https://github.com/cultivaia kubeVersion: ">=1.25.0" dependencies: - name: postgresql version: "12.12.10" repository: "https://charts.bitnami.com/bitnami" condition: postgresql.enabled tags: - database - name: redis version: "18.6.1" repository: "https://charts.bitnami.com/bitnami" condition: redis.enabled tags: - cache annotations: artifacthub.io/license: MIT artifacthub.io/maintainers: | - user: cultivaia-devops
cultivacore-api/values.yaml — defaults
YAML
# ------------------------------------------------------------------- # cultivacore-api — valores por defecto (base) # Sobreescribe con values-dev/staging/prod.yaml segun entorno # ------------------------------------------------------------------- # Numero de replicas base replicaCount: 2 image: repository: eu.gcr.io/cultivaia/cultivacore-api tag: "" # vacio = usa .Chart.AppVersion (2.3.1) pullPolicy: IfNotPresent imagePullSecrets: - name: gcr-pullsecret nameOverride: "" fullnameOverride: "" serviceAccount: create: true annotations: # Workload Identity (GKE) iam.gke.io/gcp-service-account: cultivacore@cultivaia.iam.gserviceaccount.com name: "" podAnnotations: prometheus.io/scrape: "true" prometheus.io/port: "9100" podSecurityContext: runAsNonRoot: true runAsUser: 1001 fsGroup: 1001 securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: ["ALL"] service: type: ClusterIP port: 80 targetPort: 3000 ingress: enabled: false className: nginx annotations: {} hosts: - host: api.cultivaia.local paths: - path: / pathType: Prefix tls: [] resources: requests: memory: "128Mi" cpu: "100m" limits: memory: "256Mi" cpu: "250m" autoscaling: enabled: false minReplicas: 2 maxReplicas: 10 targetCPUUtilizationPercentage: 70 # Variables de entorno (no secretas) env: - name: NODE_ENV value: "development" - name: PORT value: "3000" - name: LOG_LEVEL value: "debug" - name: REDIS_URL value: "redis://cultivacore-api-redis-master:6379" # Variables secretas (referenciadas desde Secret) secretEnv: - name: DATABASE_URL secretName: cultivacore-api-secrets secretKey: database-url - name: OPENAI_API_KEY secretName: cultivacore-api-secrets secretKey: openai-api-key # Dependencia: PostgreSQL (Bitnami) postgresql: enabled: true auth: database: cultivacore username: cultivacore existingSecret: cultivacore-api-secrets secretKeys: userPasswordKey: db-password primary: persistence: enabled: true size: 10Gi # Dependencia: Redis (Bitnami) redis: enabled: true architecture: standalone auth: enabled: false master: persistence: enabled: false nodeSelector: {} tolerations: [] affinity: {}
cultivacore-api/values-prod.yaml — produccion
YAML
# ------------------------------------------------------------------- # Produccion: GKE eu-west1 · 3–15 pods · TLS · HPA activo # helm install cultivacore-api ./cultivacore-api \ # -f values-prod.yaml --namespace production # ------------------------------------------------------------------- replicaCount: 5 image: tag: "2.3.1" # pin explícito en prod pullPolicy: Always # siempre verificar imagen env: - name: NODE_ENV value: "production" - name: PORT value: "3000" - name: LOG_LEVEL value: "info" - name: REDIS_URL value: "redis://cultivacore-api-redis-master:6379" ingress: enabled: true className: nginx annotations: cert-manager.io/cluster-issuer: letsencrypt-prod nginx.ingress.kubernetes.io/rate-limit: "100" nginx.ingress.kubernetes.io/ssl-redirect: "true" hosts: - host: api.cultivaia.com paths: - path: / pathType: Prefix tls: - secretName: cultivacore-api-tls hosts: - api.cultivaia.com resources: requests: memory: "512Mi" cpu: "500m" limits: memory: "1Gi" cpu: "1000m" autoscaling: enabled: true minReplicas: 3 maxReplicas: 15 targetCPUUtilizationPercentage: 70 postgresql: enabled: true primary: persistence: size: 100Gi storageClass: premium-rwo redis: architecture: replication master: persistence: enabled: true size: 8Gi affinity: podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchLabels: app.kubernetes.io/name: cultivacore-api topologyKey: kubernetes.io/hostname podAnnotations: prometheus.io/scrape: "true" prometheus.io/port: "9100" cluster-autoscaler.kubernetes.io/safe-to-evict: "false"
cultivacore-api/values-dev.yaml — desarrollo
YAML
# Desarrollo: recursos minimos, 1 replica, sin TLS, sin HPA # helm install cultivacore-api ./cultivacore-api \ # -f values-dev.yaml --namespace development replicaCount: 1 image: pullPolicy: Always # agiliza ciclo dev env: - name: NODE_ENV value: "development" - name: LOG_LEVEL value: "debug" - name: PORT value: "3000" - name: REDIS_URL value: "redis://cultivacore-api-redis-master:6379" resources: requests: memory: "64Mi" cpu: "50m" limits: memory: "128Mi" cpu: "100m" postgresql: primary: persistence: size: 2Gi redis: master: persistence: enabled: false
cultivacore-api/values-staging.yaml — staging
YAML
# Staging: replica del setup prod con recursos reducidos # helm install cultivacore-api ./cultivacore-api \ # -f values-staging.yaml --namespace staging replicaCount: 2 env: - name: NODE_ENV value: "staging" - name: LOG_LEVEL value: "info" - name: PORT value: "3000" - name: REDIS_URL value: "redis://cultivacore-api-redis-master:6379" ingress: enabled: true hosts: - host: api-staging.cultivaia.com paths: - path: / pathType: Prefix resources: requests: memory: "256Mi" cpu: "200m" limits: memory: "512Mi" cpu: "500m" autoscaling: enabled: false postgresql: primary: persistence: size: 20Gi
cultivacore-api/templates/deployment.yaml
YAML · Helm
apiVersion: apps/v1 kind: Deployment metadata: name: {{ include "cultivacore-api.fullname" . }} labels: {{- include "cultivacore-api.labels" . | nindent 4 }} spec: {{- if not .Values.autoscaling.enabled }} replicas: {{ .Values.replicaCount }} {{- end }} revisionHistoryLimit: 5 selector: matchLabels: {{- include "cultivacore-api.selectorLabels" . | nindent 6 }} strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0 template: metadata: annotations: {{- toYaml .Values.podAnnotations | nindent 8 }} labels: {{- include "cultivacore-api.selectorLabels" . | nindent 8 }} spec: imagePullSecrets: {{- toYaml .Values.imagePullSecrets | nindent 8 }} serviceAccountName: {{ include "cultivacore-api.serviceAccountName" . }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} containers: - name: {{ .Chart.Name }} securityContext: {{- toYaml .Values.securityContext | nindent 12 }} image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.image.pullPolicy }} ports: - name: http containerPort: {{ .Values.service.targetPort }} protocol: TCP livenessProbe: httpGet: path: /health port: http initialDelaySeconds: 15 periodSeconds: 10 readinessProbe: httpGet: path: /ready port: http initialDelaySeconds: 5 periodSeconds: 5 resources: {{- toYaml .Values.resources | nindent 12 }} env: {{- toYaml .Values.env | nindent 12 }} # Variables desde Secret {{- range .Values.secretEnv }} - name: {{ .name }} valueFrom: secretKeyRef: name: {{ .secretName }} key: {{ .secretKey }} {{- end }} affinity: {{- toYaml .Values.affinity | nindent 8 }} nodeSelector: {{- toYaml .Values.nodeSelector | nindent 8 }} tolerations: {{- toYaml .Values.tolerations | nindent 8 }}
cultivacore-api/templates/hpa.yaml
YAML · Helm
{{- if .Values.autoscaling.enabled }} apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: {{ include "cultivacore-api.fullname" . }} labels: {{- include "cultivacore-api.labels" . | nindent 4 }} spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: {{ include "cultivacore-api.fullname" . }} minReplicas: {{ .Values.autoscaling.minReplicas }} maxReplicas: {{ .Values.autoscaling.maxReplicas }} metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} - type: Resource resource: name: memory target: type: Utilization averageUtilization: 80 behavior: scaleDown: stabilizationWindowSeconds: 300 # 5 min antes de reducir policies: - type: Pods value: 1 periodSeconds: 60 scaleUp: stabilizationWindowSeconds: 0 policies: - type: Pods value: 3 periodSeconds: 60 {{- end }}
cultivacore-api/templates/pre-upgrade-migration.yaml
YAML · Helm Hook
# Job de migracion de BD — se ejecuta antes de cada upgrade apiVersion: batch/v1 kind: Job metadata: name: {{ include "cultivacore-api.fullname" . }}-db-migration labels: {{- include "cultivacore-api.labels" . | nindent 4 }} annotations: "helm.sh/hook": pre-upgrade,pre-install "helm.sh/hook-weight": "-5" "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded spec: backoffLimit: 3 activeDeadlineSeconds: 300 template: spec: serviceAccountName: {{ include "cultivacore-api.serviceAccountName" . }} restartPolicy: OnFailure containers: - name: db-migrate image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" command: ["node", "scripts/migrate.js"] env: - name: NODE_ENV value: {{ (index .Values.env 0).value | quote }} - name: DATABASE_URL valueFrom: secretKeyRef: name: cultivacore-api-secrets key: database-url resources: requests: memory: "64Mi" cpu: "50m" limits: memory: "128Mi" cpu: "100m"
cultivacore-api/templates/_helpers.tpl
Go Template
{{/* Expand the name of the chart. */}} {{- define "cultivacore-api.name" -}} {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} {{- end }} {{/* Create a default fully qualified app name. */}} {{- define "cultivacore-api.fullname" -}} {{- if .Values.fullnameOverride }} {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} {{- else }} {{- $name := default .Chart.Name .Values.nameOverride }} {{- if contains $name .Release.Name }} {{- .Release.Name | trunc 63 | trimSuffix "-" }} {{- else }} {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} {{- end }} {{- end }} {{- end }} {{/* Chart label helper */}} {{- define "cultivacore-api.chart" -}} {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} {{- end }} {{/* Common labels */}} {{- define "cultivacore-api.labels" -}} helm.sh/chart: {{ include "cultivacore-api.chart" . }} {{ include "cultivacore-api.selectorLabels" . }} {{- if .Chart.AppVersion }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} {{- end }} app.kubernetes.io/managed-by: {{ .Release.Service }} {{- end }} {{/* Selector labels */}} {{- define "cultivacore-api.selectorLabels" -}} app.kubernetes.io/name: {{ include "cultivacore-api.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} {{/* ServiceAccount name */}} {{- define "cultivacore-api.serviceAccountName" -}} {{- if .Values.serviceAccount.create }} {{- default (include "cultivacore-api.fullname" .) .Values.serviceAccount.name }} {{- else }} {{- default "default" .Values.serviceAccount.name }} {{- end }} {{- end }} {{/* Full image reference helper */}} {{- define "cultivacore-api.image" -}} {{- printf "%s:%s" .Values.image.repository (.Values.image.tag | default .Chart.AppVersion) }} {{- end }}
cultivacore-api/templates/service.yaml
YAML · Helm
apiVersion: v1 kind: Service metadata: name: {{ include "cultivacore-api.fullname" . }} labels: {{- include "cultivacore-api.labels" . | nindent 4 }} {{- with .Values.service.annotations }} annotations: {{- toYaml . | nindent 4 }} {{- end }} spec: type: {{ .Values.service.type }} ports: - port: {{ .Values.service.port }} targetPort: http protocol: TCP name: http selector: {{- include "cultivacore-api.selectorLabels" . | nindent 4 }}
cultivacore-api/templates/ingress.yaml
YAML · Helm
{{- if .Values.ingress.enabled -}} apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: {{ include "cultivacore-api.fullname" . }} labels: {{- include "cultivacore-api.labels" . | nindent 4 }} {{- with .Values.ingress.annotations }} annotations: {{- toYaml . | nindent 4 }} {{- end }} spec: {{- if .Values.ingress.className }} ingressClassName: {{ .Values.ingress.className }} {{- end }} {{- if .Values.ingress.tls }} tls: {{- range .Values.ingress.tls }} - hosts: {{- range .hosts }} - {{ . | quote }} {{- end }} secretName: {{ .secretName }} {{- end }} {{- end }} rules: {{- range .Values.ingress.hosts }} - host: {{ .host | quote }} http: paths: {{- range .paths }} - path: {{ .path }} pathType: {{ .pathType }} backend: service: name: {{ include "cultivacore-api.fullname" $ }} port: number: {{ $.Values.service.port }} {{- end }} {{- end }} {{- end }}
cultivacore-api/templates/secret.yaml
YAML · Helm
# Secret de ejemplo — en produccion usar External Secrets Operator # o GCP Secret Manager con Workload Identity apiVersion: v1 kind: Secret metadata: name: {{ include "cultivacore-api.fullname" . }}-secrets labels: {{- include "cultivacore-api.labels" . | nindent 4 }} annotations: # Evitar que helm borre el secret al desinstalar "helm.sh/resource-policy": keep type: Opaque stringData: # ATENCION: reemplazar con valores reales via --set o Sealed Secrets database-url: {{ .Values.secrets.databaseUrl | default "postgresql://cultivacore:CHANGEME@cultivacore-api-postgresql:5432/cultivacore" | quote }} openai-api-key: {{ .Values.secrets.openaiApiKey | default "sk-REPLACE_WITH_REAL_KEY" | quote }} db-password: {{ .Values.secrets.dbPassword | default "CHANGEME" | quote }}
cultivacore-api/templates/tests/test-connection.yaml
YAML · Helm Test
apiVersion: v1 kind: Pod metadata: name: "{{ include "cultivacore-api.fullname" . }}-test-connection" labels: {{- include "cultivacore-api.labels" . | nindent 4 }} annotations: "helm.sh/hook": test "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded spec: containers: - name: curl-test image: curlimages/curl:8.5.0 command: - /bin/sh - -c - | set -e echo "Test 1: /health endpoint" curl -f -s http://{{ include "cultivacore-api.fullname" . }}:{{ .Values.service.port }}/health echo "Test 2: /ready endpoint" curl -f -s http://{{ include "cultivacore-api.fullname" . }}:{{ .Values.service.port }}/ready echo "Todos los tests pasaron" resources: requests: { memory: "16Mi", cpu: "10m" } limits: { memory: "32Mi", cpu: "50m" } restartPolicy: Never
cultivacore-api/templates/NOTES.txt
Template Text
{{/* Post-install instructions shown by helm install/upgrade */}} ✅ CULTIVA IA — cultivacore-api desplegado con exito Release: {{ .Release.Name }} Namespace: {{ .Release.Namespace }} Chart: {{ .Chart.Name }}-{{ .Chart.Version }} App: {{ .Chart.AppVersion }} {{- if .Values.ingress.enabled }} URL de acceso: {{- range .Values.ingress.hosts }} https://{{ .host }} {{- end }} {{- else }} Acceso via port-forward: kubectl port-forward svc/{{ include "cultivacore-api.fullname" . }} 3000:80 -n {{ .Release.Namespace }} curl http://localhost:3000/health {{- end }} Comandos utiles: helm status {{ .Release.Name }} -n {{ .Release.Namespace }} helm test {{ .Release.Name }} -n {{ .Release.Namespace }} kubectl logs -l app.kubernetes.io/name=cultivacore-api -n {{ .Release.Namespace }} {{- if .Values.autoscaling.enabled }} HPA activo: {{ .Values.autoscaling.minReplicas }}–{{ .Values.autoscaling.maxReplicas }} replicas {{- end }}
cultivacore-api/values.schema.json
JSON Schema
{
"$schema": "https://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["image", "service"],
"properties": {
"replicaCount": {
"type": "integer",
"minimum": 1,
"description": "Numero de replicas del Deployment"
},
"image": {
"type": "object",
"required": ["repository"],
"properties": {
"repository": { "type": "string" },
"tag": { "type": "string" },
"pullPolicy": {
"type": "string",
"enum": ["Always", "IfNotPresent", "Never"]
}
}
},
"service": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["ClusterIP", "NodePort", "LoadBalancer"]
},
"port": { "type": "integer", "minimum": 1 },
"targetPort": { "type": "integer", "minimum": 1 }
}
},
"autoscaling": {
"type": "object",
"properties": {
"enabled": { "type": "boolean" },
"minReplicas": { "type": "integer", "minimum": 1 },
"maxReplicas": { "type": "integer", "minimum": 1 },
"targetCPUUtilizationPercentage": {
"type": "integer", "minimum": 1, "maximum": 100
}
}
}
}
}
cultivacore-api/.helmignore
gitignore-style
# Excluir de helm package .git/ .gitignore .github/ # Dev *.md docs/ .env* .vscode/ .idea/ # Build artifacts *.swp *.bak *.tmp *.orig # CI/CD .gitlab-ci.yml .travis.yml Makefile # Tests locales test/ *.test **/__tests__/
Guia de despliegue — commandos completos
BASH
## 1. Anadir repos de dependencias Bitnami helm repo add bitnami https://charts.bitnami.com/bitnami helm repo update ## 2. Actualizar dependencias del chart helm dependency update ./cultivacore-api ## 3. Lint — verificar sintaxis helm lint ./cultivacore-api ## 4. Dry-run — preview sin desplegar helm install cultivacore-api ./cultivacore-api \ -f values-prod.yaml \ --namespace production \ --dry-run --debug ## 5. Instalar en PRODUCCION helm install cultivacore-api ./cultivacore-api \ -f values-prod.yaml \ --namespace production \ --create-namespace \ --set secrets.openaiApiKey="sk-..." \ --set secrets.dbPassword="SuperSecretPwd!" \ --set secrets.databaseUrl="postgresql://cultivacore:SuperSecretPwd!@..." \ --atomic --timeout 5m ## 6. Upgrade con migracion automatica helm upgrade cultivacore-api ./cultivacore-api \ -f values-prod.yaml \ --namespace production \ --atomic --timeout 10m ## 7. Verificar estado helm status cultivacore-api -n production kubectl get pods -n production -l app.kubernetes.io/name=cultivacore-api ## 8. Ejecutar tests Helm helm test cultivacore-api -n production --logs ## 9. Empaquetar para distribucion helm package ./cultivacore-api # Genera: cultivacore-api-1.0.0.tgz ## 10. Rollback si algo falla helm rollback cultivacore-api -n production
Comparativa de configuracion por entorno
Tabla
| Parametro | DEV | STAGING | PRODUCCION |
|---|---|---|---|
| replicaCount | 1 | 2 | 5 |
| image.pullPolicy | Always | IfNotPresent | Always |
| resources.requests.memory | 64Mi | 256Mi | 512Mi |
| resources.requests.cpu | 50m | 200m | 500m |
| resources.limits.memory | 128Mi | 512Mi | 1Gi |
| autoscaling.enabled | false | false | true |
| autoscaling.minReplicas | — | — | 3 |
| autoscaling.maxReplicas | — | — | 15 |
| ingress.enabled | false | true | true |
| ingress.tls | — | — | cultivacore-api-tls |
| ingress.host | api.cultivaia.local | api-staging.cultivaia.com | api.cultivaia.com |
| NODE_ENV | development | staging | production |
| LOG_LEVEL | debug | info | info |
| postgresql.persistence.size | 2Gi | 20Gi | 100Gi (premium-rwo) |
| redis.architecture | standalone | standalone | replication |
| podAntiAffinity | — | — | required (hostname) |
Checklist de validacion pre-release
Checklist
- ✓ Chart.yaml con apiVersion v2 y semver correctohelm show chart ./cultivacore-api
- ✓ Lint sin errores ni warnings criticoshelm lint ./cultivacore-api
- ✓ Dependencias ancladas a version exactahelm dependency list
- ✓ JSON Schema valida los values obligatorioshelm install --dry-run ...
- ✓ Template rendering sin errores en los 3 entornoshelm template . -f values-prod.yaml
- ✓ Secrets referenciados, no inline en valuesgrep -r "secretKeyRef" templates/
- ✓ SecurityContext con runAsNonRoot=truekubectl auth can-i ...
- ✓ Liveness y readiness probes configuradoskubectl describe pod ...
- ✓ HPA activo en prod con politicas de scale-down conservadoraskubectl get hpa -n production
- ✓ Hook de migracion con delete-policy y backoffLimitkubectl get jobs -n production
- ✓ PodAntiAffinity en prod para distribucion en nodoskubectl get pod -o wide
- ✓ Tests de conexion HTTP pasanhelm test cultivacore-api --logs
- ✓ NOTES.txt con URL e instrucciones post-installhelm install --dry-run ...
- ⚠ Sealed Secrets o External Secrets Operator para secret.yaml en prodpendiente
- ⚠ NetworkPolicy para aislar namespace de produccionpendiente v1.1.0