bitnami retiro los tags semanticos minor (1.30/1.29/etc.) de Docker Hub 2026-05; solo quedan latest + digests. El InitContainer fallaba con ImagePullBackOff en el smoke E2E DO (run 1511/1512 del tenant 91c93c9c060a48febb7874c87c1ce993). Cambio: - image: bitnami/kubectl:1.30 -> curlimages/curl:8.20.0 (Docker Official, tag estable). - comando: kubectl get secret -> curl REST API kubernetes.default.svc con SA token + CA cert montados en /var/run/secrets/kubernetes.io/serviceaccount. - Misma logica: 48 iteraciones x 5s = 240s busy-wait. - El Role/RoleBinding (role-db-secret-reader.yaml) ya da el permiso get secrets al SA — sin cambios RBAC necesarios. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
APP_NAME — Web Backend Python (FastAPI + Alembic)
App generada desde el Golden Path web-backend/python de Coralware Cove IDP.
Este repositorio contiene el scaffold completo: aplicación FastAPI, migraciones
Alembic, frontend estático en /demo, Helm chart, workflow de CI y manifiesto
de contrato con la plataforma. Cuando se materializa este template para tu
tenant, los placeholders en mayúsculas se sustituyen automáticamente.
Contrato del shape: ver
coralware-shape.yamlen la raíz. Define los endpoints obligatorios, la BD esperada y la versión del shape. No elimines ese archivo — la plataforma lo lee.
Estructura
.
├── coralware-shape.yaml ← contrato con la plataforma
├── catalog-info.yaml ← entidad Backstage
├── code/
│ ├── app/ ← FastAPI app
│ ├── alembic/ ← migraciones DB
│ ├── tests/ ← pytest + cobertura
│ ├── pyproject.toml
│ ├── requirements.txt
│ └── Dockerfile
├── helm/__APP_NAME__/ ← Helm chart con AddonClaim + hook Alembic
└── .gitea/workflows/ ← CI: tests + build + deploy
Ciclo de vida
- Tú escribes código — endpoints en
code/app/routers/, modelos encode/app/models/, migraciones conalembic revision --autogenerate. git pushamaindispara el workflow Gitea Actions.- El CI corre
black,ruff,bandit,pytest --cov. Si algo falla, el pipeline aborta. - Si pasa, builda la imagen
linux/amd64con BuildKit y la publica en el proyecto Harbor de tu tenant. helm upgrade --installaplica el chart al cluster de tu tenant. Antes del rollout, un Job pre-upgrade correalembic upgrade head. Si Alembic falla, Helm aborta y la app vieja queda intacta.- Tras rollout exitoso, smoke check valida
/health200.
Orden de hooks Helm
El chart usa hooks pre-install,pre-upgrade con hook-weight para garantizar
el orden de creación. El helm upgrade --install funciona sin --no-hooks:
| Recurso | Template | hook-weight |
Motivo |
|---|---|---|---|
AddonClaim |
addonclaim.yaml |
-10 |
Declara la intención de la app de tener una BD — el primero en aplicarse para que addon-provisioner empiece a reconciliar lo antes posible |
Role + RoleBinding |
role-db-secret-reader.yaml |
-7 |
Permite al SA leer el Secret <APP_NAME>-db-creds (lo consume el InitContainer wait-for-secret del Job Alembic) |
ServiceAccount |
serviceaccount.yaml |
-5 |
Lo referencia el pod del Job Alembic — debe existir antes |
Job Alembic |
job-alembic-upgrade.yaml |
0 |
InitContainer wait-for-secret espera al Secret publicado por addon-provisioner (hasta 240s), luego corre alembic upgrade head |
El AddonClaim (hook weight -10) NO declara hook-delete-policy intencionalmente:
así sobrevive entre helm upgrade (re-apply idempotente) y se evita disparar el
finalizer cleanup-logical del controller en cada upgrade (que dropearía el
user de la BD y regeneraría el password, causando ventana de downtime).
Trade-off: helm uninstall deja el AddonClaim huérfano y MariaDB sigue
aprovisionada. Es coherente con deletionPolicy: Retain — borrar BD productiva
debe ser decisión explícita, no side-effect del uninstall. Cleanup explícito:
kubectl delete addonclaim <release>-db -n apps
El ServiceAccount se materializa como hook (no en la fase principal de Helm)
porque el Job Alembic, que también es hook, declara serviceAccountName. Si el
SA viviera en la fase principal se crearía después del Job → el pod del Job
nunca arrancaría (serviceaccount not found) → helm --wait haría timeout.
El SA-hook usa hook-delete-policy: before-hook-creation (no hook-succeeded):
el Deployment lo necesita vivo en runtime, así que solo se borra/recrea al
inicio del siguiente hook, manteniéndolo idempotente entre upgrades.
El imagePullSecret lo provee la plataforma (stack-deployer), no el chart.
El Secret <APP_NAME>-db-creds lo publica addon-provisioner tras reconciliar
el AddonClaim (ver §Base de datos).
Endpoints obligatorios
| Path | Auth | Propósito |
|---|---|---|
/health |
none | Liveness + readiness; devuelve {status: ok, db: up|down} |
/docs |
configurable (OPENAPI_REQUIRE_AUTH) |
Swagger UI |
/openapi.json |
igual que /docs |
Schema OpenAPI |
/demo |
none | Frontend estático servido desde code/app/static/demo/ |
/metrics |
restringido vía NetworkPolicy | Métricas Prometheus |
/health, /docs, /demo son contrato del shape — NO renombres.
Base de datos
Tu app espera un Secret __APP_NAME__-db-creds en su namespace con las
claves DATABASE_URL / DATABASE_HOST / DATABASE_PORT / DATABASE_NAME /
DATABASE_USER / DATABASE_PASSWORD (y opcionalmente MIGRATION_DATABASE_URL).
El Secret lo materializa automáticamente la plataforma vía el flujo
AddonClaim → addon-provisioner (ADR-052 + ADR-064):
- El chart aplica
helm/__APP_NAME__/templates/addonclaim.yamlcomo hookpre-install,pre-upgradeweight-10— declaraaddonType: mariadb,databaseNamederivado del Release name y elsecretNameesperado. addon-provisioner(corriendo en el plano de control IDP) reconcilia elAddonClaim: crea una BD lógica + user + grant en la instancia MariaDB compartida del tenant (nsaddonsdel cluster tenant) vía Job DDL efímero.- Publica el Secret
__APP_NAME__-db-credsen el nsapps. - El InitContainer
wait-for-secretdel Job Alembic espera al Secret (hasta 240s), Alembic ejecutaupgrade head, y el Deployment rola.
Opt-out — apps stateless
Si tu app no necesita BD, en values.yaml:
database:
enabled: false # no se aplica AddonClaim ni Role/RoleBinding
alembic:
enabled: false # no corre el Job Alembic
Con database.enabled=false la app se despliega sin BD ni migraciones.
Variables de entorno
| Variable | Origen | Default | Descripción |
|---|---|---|---|
DATABASE_URL |
Secret | — | Connection string runtime |
MIGRATION_DATABASE_URL |
Secret | = DATABASE_URL |
Connection para Alembic |
LOG_LEVEL |
ConfigMap | INFO |
DEBUG para troubleshooting |
CORRELATION_ID_HEADER |
ConfigMap | X-Correlation-ID |
Header de trazabilidad |
OPENAPI_REQUIRE_AUTH |
ConfigMap | false |
Activar JWT en /docs |
KEYCLOAK_ISSUER |
ConfigMap | — | URL del realm cuando auth activado |
STATIC_DEMO_PATH |
ConfigMap | /app/static/demo |
Path al frontend estático |
Migraciones Alembic
Migración base vacía en code/alembic/versions/0001_initial.py. Para crear
una nueva tras agregar modelos:
cd code
alembic revision --autogenerate -m "agregar tabla users"
Desarrollo local
cd code
pip install -r requirements-dev.txt
docker run --rm -d --name mariadb-dev \
-e MARIADB_ROOT_PASSWORD=dev -e MARIADB_DATABASE=app \
-p 3306:3306 mariadb:11
export DATABASE_URL="mysql+pymysql://root:dev@127.0.0.1:3306/app"
export MIGRATION_DATABASE_URL="$DATABASE_URL"
alembic upgrade head
uvicorn app.main:app --reload --port 8000
Plan de salida
Si decides migrar fuera de Cove, todo lo que necesitas vive en formato estándar:
| Componente | Cómo lo exportas |
|---|---|
| Código | git remote add github && git push github main |
| Imagen | docker pull desde Harbor + push a tu registry |
| Manifests | Ya están en helm/ — helm template genera YAML aplicable |
| Datos | Coralware ejecuta mariadb-dump y entrega .sql.gz |
Sin lock-in técnico.
Referencias
docs/adr-053-shape-web-backend-python.md— anatomía del shapedocs/adr-052-tenant-addon-claim-model.md— modelo de add-ons (BD)docs/adr-064-free-trial-allows-mariadb.md— free-trial permite MariaDBdocs/adr-038-ci-estandar-reusable-workflow.md— pipeline Python