⎈ Chart Scaffolded — cultivaia-api
/helm:create
Chart.yaml
YAML
# Helm 3 only — never apiVersion: v1
apiVersion: v2
name: cultivaia-api
description: "CULTIVA IA Platform API — REST service for AI content generation and automation"
type: application
version: 0.1.0 # chart semver — increment on chart changes
appVersion: "1.4.2" # application image tag
maintainers:
- name: Álvaro Gimeno
email: alvarogimeno2002@gmail.com
keywords: [cultivaia, api, ai, automation]
home: https://cultivaia.com
values.yaml
YAML — documentado
# -- Number of replicas. Overridden by HPA when autoscaling.enabled=true.
replicaCount: 2
image:
# -- Container registry and image name
repository: registry.hetzner.com/cultivaia/api
# -- Image pull policy: IfNotPresent | Always | Never
pullPolicy: IfNotPresent
# -- Image tag. Defaults to .Chart.AppVersion when empty.
tag: ""
# -- List of imagePullSecrets names if registry requires authentication
imagePullSecrets: []
service:
type: ClusterIP
port: 3000
ingress:
enabled: true
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/proxy-body-size: "10m"
hosts:
- host: api.cultivaia.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: cultivaia-api-tls
hosts: [api.cultivaia.com]
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 6
targetCPUUtilizationPercentage: 70
env:
NODE_ENV: production
PORT: "3000"
LOG_LEVEL: info
# -- External Secrets Operator: name of the SecretStore in the namespace
externalSecrets:
enabled: true
refreshInterval: 1h
secretStoreName: hetzner-vault
secretStoreKind: ClusterSecretStore
remoteRef: cultivaia/prod/api
serviceAccount:
create: true
automountServiceAccountToken: false
name: ""
podDisruptionBudget:
enabled: true
minAvailable: 1
networkPolicy:
enabled: true
ingressNamespace: ingress-nginx
probes:
liveness:
path: /health
initialDelaySeconds: 15
periodSeconds: 30
readiness:
path: /ready
initialDelaySeconds: 5
periodSeconds: 10
templates/deployment.yaml
YAML — security-hardened
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "cultivaia-api.fullname" . }}
labels:
{{- include "cultivaia-api.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "cultivaia-api.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "cultivaia-api.selectorLabels" . | nindent 8 }}
spec:
serviceAccountName: {{ include "cultivaia-api.serviceAccountName" . }}
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: {{ .Chart.Name }}
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: [ALL]
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: 3000
envFrom:
- secretRef:
name: {{ include "cultivaia-api.fullname" . }}-secrets
env:
{{- range $k, $v := .Values.env }}
- name: {{ $k }}
value: {{ $v | quote }}
{{- end }}
livenessProbe:
httpGet: { path: {{ .Values.probes.liveness.path }}, port: http }
initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }}
periodSeconds: {{ .Values.probes.liveness.periodSeconds }}
readinessProbe:
httpGet: { path: {{ .Values.probes.readiness.path }}, port: http }
initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }}
periodSeconds: {{ .Values.probes.readiness.periodSeconds }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
{{- include "cultivaia-api.selectorLabels" . | nindent 20 }}
topologyKey: kubernetes.io/hostname
templates/networkpolicy.yaml
YAML
{{- if .Values.networkPolicy.enabled }}
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: {{ include "cultivaia-api.fullname" . }}
labels:
{{- include "cultivaia-api.labels" . | nindent 4 }}
spec:
podSelector:
matchLabels:
{{- include "cultivaia-api.selectorLabels" . | nindent 6 }}
policyTypes: [Ingress, Egress]
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: {{ .Values.networkPolicy.ingressNamespace }}
ports:
- protocol: TCP
port: 3000
egress:
- # Allow DNS
ports: [{ protocol: UDP, port: 53 }, { protocol: TCP, port: 53 }]
- # Allow HTTPS outbound (Anthropic, OpenAI, Supabase, Lemon Squeezy)
ports: [{ protocol: TCP, port: 443 }]
{{- end }}
templates/externalsecret.yaml
YAML — External Secrets Operator
{{- if .Values.externalSecrets.enabled }}
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: {{ include "cultivaia-api.fullname" . }}-secrets
labels:
{{- include "cultivaia-api.labels" . | nindent 4 }}
spec:
refreshInterval: {{ .Values.externalSecrets.refreshInterval }}
secretStoreRef:
name: {{ .Values.externalSecrets.secretStoreName }}
kind: {{ .Values.externalSecrets.secretStoreKind }}
target:
name: {{ include "cultivaia-api.fullname" . }}-secrets
creationPolicy: Owner
dataFrom:
- extract:
key: {{ .Values.externalSecrets.remoteRef }}
# Pulls: OPENAI_API_KEY, ANTHROPIC_API_KEY, SUPABASE_URL,
# SUPABASE_KEY, LEMON_SQUEEZY_WEBHOOK_SECRET
{{- end }}
helm lint cultivaia-api/
CLI Output
$helm lint cultivaia-api/ --strict
==> Linting cultivaia-api/
[INFO] Chart.yaml: Found Chart.yaml
[INFO] Chart.yaml: apiVersion is v2
[INFO] values.yaml: Found values.yaml
[INFO] templates/: Found templates/
[INFO] templates/_helpers.tpl: Parsed OK
[INFO] templates/deployment.yaml: Rendered OK
[INFO] templates/networkpolicy.yaml: Rendered OK
[INFO] templates/externalsecret.yaml: Rendered OK
[INFO] templates/hpa.yaml: Rendered OK
[INFO] templates/pdb.yaml: Rendered OK
✓ 1 chart(s) linted, 0 chart(s) failed
🔒 Security Audit — cultivaia-api
/helm:security0
Critical
0
High
0
Medium
1
Low (info)
✓ PASS
securityContext — runAsNonRoot + runAsUser: 1000
Pod y contenedor con contexto de seguridad explícito. No se ejecuta como root en ningún caso.
✓ PASS
readOnlyRootFilesystem: true + emptyDir /tmp
Sistema de archivos raíz montado como solo lectura. Directorio /tmp provisto por emptyDir efímero.
✓ PASS
capabilities.drop: [ALL]
Todas las Linux capabilities eliminadas. El proceso no puede escalar privilegios.
✓ PASS
allowPrivilegeEscalation: false
Evita que el proceso hijo adquiera más privilegios que el padre. Correctamente seteado.
✓ PASS
Secrets gestionados por External Secrets Operator
OPENAI_API_KEY, ANTHROPIC_API_KEY, SUPABASE_URL/KEY y LEMON_SQUEEZY_WEBHOOK_SECRET se sincronizan desde Vault. Ningún secreto hardcodeado en values.yaml.
✓ PASS
NetworkPolicy — default deny + allow ingress-nginx only
Ingress permitido únicamente desde namespace ingress-nginx. Egress limitado a DNS (53) y HTTPS (443).
✓ PASS
ServiceAccount dedicada + automountServiceAccountToken: false
SA específica creada. Token de API de Kubernetes no montado automáticamente (el servicio no necesita acceder a la API).
✓ PASS
PodDisruptionBudget — minAvailable: 1
Garantiza alta disponibilidad durante mantenimiento de nodos. Con 2 réplicas mínimo, siempre hay un pod activo.
✓ PASS
seccompProfile: RuntimeDefault
Perfil seccomp del runtime activo a nivel de pod. Filtra syscalls peligrosas por defecto.
ℹ LOW
AppArmor profile no declarado (opcional)
No hay anotación container.apparmor.security.beta.kubernetes.io. No es crítico si el cluster tiene el perfil runtime/default activo por defecto.
annotations: { container.apparmor.security.beta.kubernetes.io/api: "runtime/default" }
📋 Review Checklist
/helm:review
CULTIVA IA SLU · Constructor de Helm Charts para Kubernetes · skill id: d02c1eff · generado 2026-06-12