一键导入
twelve-factor
Build 12-factor PaaS charms with paas-charm — fit, rock, charm, build, push, deploy, verify.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Build 12-factor PaaS charms with paas-charm — fit, rock, charm, build, push, deploy, verify.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Known-good Juju bundle shapes (COS Lite, 12-Factor + COS, Identity Platform, Charmed Kubeflow) — canonical app lists and relation edges
Implementing Juju actions for operational tasks in charms. WHEN: add Juju actions to a charm, implement backup / rotate-credentials / restore actions, declare actions in charmcraft.yaml, write action handlers, test actions with Scenario, run actions with juju run.
Adding and validating charm configuration options. WHEN: add charm configuration options, declare config in charmcraft.yaml, validate config values in config-changed, apply config to a Pebble layer, apply config to a machine charm, write Scenario tests for config.
End-to-end workflow for building ops-framework charms for custom applications (K8s and machine). WHEN: build a custom Juju charm, write src/charm.py with Pebble, write a machine charm with systemd, decide K8s vs machine substrate, scaffold a non-paas-charm with charmcraft init, customise the ops framework charm lifecycle.
Workflow for charming infrastructure software (databases, caches, message brokers, proxies, monitoring). WHEN: charm infrastructure software, charm a database / cache / message broker / proxy / monitoring system, implement peer relations, leader election, primary/replica failover, backup and restore actions, clustering.
Expert guidance for developing, building, testing, and publishing Juju charms using charmcraft. WHEN: edit charmcraft.yaml, run charmcraft pack, run charmcraft init, manage charm libraries, publish a charm to Charmhub, set up multi-base builds, configure relations / config options, set up storage or containers.
| name | twelve-factor |
| description | Build 12-factor PaaS charms with paas-charm — fit, rock, charm, build, push, deploy, verify. |
This skill covers the complete workflow for building a Juju charm from a 12-factor web application using the paas-charm base. Inspect first — generate nothing until the fit verdict is clear. The structure here is adapted from canonical/skills PR #4 (Apache-2.0); the per-framework rules and the handoff-payload shape come from upstream.
rockcraft init with the framework profile, validate the rock contract, stay inside the extension boundary.charmcraft init with the framework profile, mirror env contracts, never re-declare extension-embedded relations, keep src/charm.py stock.juju status.The rock and charm phases can run independently once the fit verdict is in.
The "experimental" status differs between charmcraft and rockcraft. Cantrip's rockcraft_init and rockcraft_pack tools set ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true unconditionally — every rockcraft framework extension is still experimental upstream as of April 2026, even for the long-stable Flask and Django profiles.
| Framework | Charmcraft Profile | Rockcraft Profile | Charmcraft Experimental? | Rockcraft Experimental? |
|---|---|---|---|---|
| Flask | flask-framework | flask-framework | No | Yes |
| Django | django-framework | django-framework | No | Yes |
| FastAPI | fastapi-framework | fastapi-framework | Yes | Yes |
| Go | go-framework | go-framework | Yes | Yes |
| Express | express-framework | express-framework | Yes | Yes |
| Spring Boot | spring-boot-framework | spring-boot-framework | Yes | Yes |
To use a charmcraft-experimental extension you also need CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true for charmcraft init and charmcraft pack — Cantrip's charmcraft_init sets this for the relevant profiles.
analyse_framework to get a framework verdict with signals, candidates, and a web-app-fit guess. If multiple candidates score similarly, surface the alternatives to the user before committing.frontend_build on your own.migrate.sh.-worker or -scheduler services alongside the main web service, and capture the intended commands.preflight_targets to verify the chosen Kubernetes context, Juju controller, registry, and required local tools.Ask these unless the answer is already explicit and reliable in the conversation.
Fit and intent:
rockcraft and charmcraft?Deployment contract:
Rock and registry:
Cluster selection:
Runtime behaviour:
collectstatic, …)?migrate.sh?-worker or -scheduler services? If yes, what commands?optional: true or optional: false in charm metadata?Layout adaptation:
<expected-path>/. If the repo does not match, is a small trial-copy layout adaptation acceptable?Stop and explain why if any of these are true:
Once the fit checks pass, record the following structure (in conversation, in a comment, or in a scratch file) so the rock and charm phases do not have to re-derive the answers. Adapted from the upstream's handoff contract.
framework: <flask | django | fastapi | go | express | spring-boot>
repo_path: <absolute path>
k8s_context: <string>
juju_controller: <string>
juju_model: <string or null>
registry: <string>
charm_publication: local | charmhub
deployment_mode: provider-first | hybrid-local-artifact
relations:
- name: <relation name>
optional: true | false
config_options_needed:
- <option name>
frontend_build: none | embedded-in-backend-image | separate-deployment
migrations:
mode: none | framework-managed | migrate-sh
tool: <string or null>
background_services:
workers:
- <service name or command>
schedulers:
- <service name or command>
experimental_extensions_accepted: true | false
minimal_change_policy: true
ingress:
needed: true | false
external_hostname: <string or null>
Stay inside the extension. If the repo only works after replacing the extension, stop.
A chiselled rock contains only the filesystem slices the workload actually needs — no shell, no apt, no unneeded OS utilities. The result is a smaller image with a reduced attack surface and faster pulls. Because the 12-factor framework extensions default to base: bare, most 12-factor workloads already produce a de facto chiselled image through the extension's staged slice selection.
When to consider a chiselled base:
apt-get, dpkg, or a shell interpreter at runtime.curl | bash, npm preinstall/postinstall with wget) is needed at build or run time.Eligibility check — always run before generating:
check_chisel_eligibility(path=repo_path, framework=framework)
This returns an eligible flag, any blocking reasons, and a short rationale you should include as a comment in the generated rockcraft.yaml. Run it after check_rock_contract — the two checks are orthogonal.
When chiselled output is safe:
Keep the default base: bare emitted by rockcraft init. The extension's slice selection is already chiselled. Add a comment near the top of rockcraft.yaml explaining why the smaller base is safe:
# base: bare — this workload passes the chisel eligibility check.
# <rationale from check_chisel_eligibility>
# To fall back to a fuller Ubuntu base: replace `bare` with `ubuntu@24.04`.
base: bare
Escape hatch — when to use a fuller Ubuntu base:
Use base: ubuntu@24.04 whenever:
check_chisel_eligibility returns blockers (shell-at-runtime, apt-at-runtime, opaque vendor scripts).rockcraft pack with base: bare fails with a missing-slice error.strace, curl, bash for debugging).Switching to a fuller base requires only one line change — this is intentional:
base: ubuntu@24.04 # was: bare
Pebble plans, health checks, and 12-factor composition:
A chiselled base does not change how Pebble interacts with the workload. The Pebble binary and its health-check HTTP endpoint are both staged by the extension regardless of base:. Health check configuration in rockcraft.yaml applies identically:
services:
app:
override: merge
command: gunicorn -w 4 myapp:app
checks:
- name: http-ready
http:
url: http://localhost:8080/healthz
Worker and scheduler services defined in rockcraft.yaml compose with chiselled bases exactly as with ubuntu@24.04 — the extension stages the interpreter slices required by all defined services, not just the main one.
check_rock_contract with the chosen framework before generating anything. It returns blocking issues, advisory warnings, and the supported base: list. Resolve every blocking issue before continuing.
2a. Run check_chisel_eligibility with the same framework. If eligible, keep the default base: bare and add the eligibility rationale as a comment. If blockers are present, switch to base: ubuntu@24.04.rockcraft is on an edge channel (preflight_targets reports the snap channel).rockcraft init.migrate.sh for Flask / FastAPI / Express / Go when the repo does not already provide a supported migration entrypoint. For Django, paas-charm uses manage.py migrate whenever manage.py exists, so a new migrate.sh will not replace that path. For Spring Boot, prefer framework-managed migrations (Flyway / Liquibase) unless the repo already exposes a compatible wrapper.rockcraft.yaml services whose names end in -worker or -scheduler. -worker services run on every unit; -scheduler services run on exactly one unit — name deliberately.rockcraft_init with the exact framework profile. Always use the tool — never copy from templates, previously generated files, or example rocks.rockcraft.yaml._daemon_), verify any path the app may write to at startup is writable by that user, or relocate that mutable state.server, worker, …) rather than the bare top-level binary.rockcraft_pack. Never use --destructive-mode.rockcraft pack fails on base: bare, first try a supported Ubuntu base from the contract table below before deeper dependency surgery.build-packages / stage-packages the app actually needs;migrate.sh when the app needs migrations and the repo does not already expose a supported entrypoint;-worker / -scheduler services with repo-backed or user-confirmed commands;paas-charm env names to its existing runtime contract;base: bare fails.rockcraft expand-extensions output;rockcraft.yaml by copying from templates;build-base for Python frameworks;-worker / -scheduler commands without repo evidence or a user-confirmed handoff;rockcraft pack --destructive-mode.| Framework | Runtime | Bases | Required Evidence |
|---|---|---|---|
| Flask | Gunicorn | bare, ubuntu@22.04, ubuntu@24.04 | requirements.txt or pyproject.toml with flask; discoverable WSGI entrypoint |
| Django | Gunicorn | bare, ubuntu@22.04, ubuntu@24.04 | root requirements.txt; runtime app under <repo>/<project-name>/; supported wsgi.py with application |
| FastAPI | Uvicorn | bare, ubuntu@24.04 | root requirements.txt with fastapi or starlette; discoverable ASGI app object |
| Express | npm start | bare, ubuntu@24.04 | app/package.json with name and scripts.start |
| Go | compiled binary | bare, ubuntu@24.04 | root go.mod; service command defaults to the rock name; extra assets via go-framework/assets |
| Spring Boot | fat jar | bare, ubuntu@24.04 | exactly one active build system (pom.xml XOR build.gradle); mvnw XOR gradlew; wrapper scripts executable |
Flask — realistic apps may need extra staged runtime files (config.py, migrations/, migrate.sh). -r requirements/*.txt includes are handled by the dep parser in check_rock_contract.
Django — realistic repos often need a trial-copy layout adaptation to match the extension's directory expectations. If Python deps compile native code, add only the missing build-packages to django-framework/dependencies. If pyproject.toml plus charm/ causes pip install . to fail, treat it as a build-tooling interaction and make the smallest explicit workaround. collectstatic belongs in the rock build, not the charm.
FastAPI — on base: bare, Python comes from build-base; ubuntu@22.04 gives Python 3.10, ubuntu@24.04 gives Python 3.12.
Express — root-level Express repos are valid fit candidates but the rock contract requires a move into app/. On base: bare the extension already stages Node and npm. Be careful with code that forces TLS just because a database URL is set.
Go — if you override services.go.command, the extension stops auto-adding the default binary organize rule; when the main package directory name differs from the rock name, add an explicit go-framework/install-app.organize mapping rather than manualising the rock. Multi-command CLIs need an explicit subcommand in the Pebble service command.
Spring Boot — wrapper scripts (mvnw, gradlew) must be executable before build. Dual-build-system repos need explicit user confirmation of which build path to keep. application.properties / application.yml is the natural place to bridge env-driven runtime config.
Always build the charm in charm/. Treat paas-charm as part of the contract.
charm/ subdirectory. Do not generate the charm at project root.charmcraft is on an edge channel with CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true.charmcraft_init with the exact framework profile inside charm/. Always use the tool — never copy from templates, previously generated files, or example charms.charmcraft.yaml, requirements.txt, and src/charm.py.inspect_env_keys with the framework to inventory application env expectations before adding config options or workload-side env bridges. The tool returns the framework's expected env contract (built-in vars, user-config prefix, relation env families) alongside the detected keys so you can spot keys the workload reads that paas-charm will not deliver.rockcraft.yaml, inspect services.*.environment for the main app service and any worker / scheduler services. Assume those rock-defined env vars will not survive automatically once paas-charm renders the workload Pebble layer; mirror the required keys.optional from the fit handoff or the user's explicit answer. If that choice is missing, stop and ask — never default.app-port while current paas-charm aliases still expect port. If so, add a non-conflicting port config option rather than rewriting charm logic.paas-charm env output as the baseline contract. When rockcraft.yaml defines service env, prefer non-conflicting charm config defaults for deploy-time values, or a tiny workload-side defaulting bridge for static app-facing defaults, before editing charm Python.<endpoint>-redirect-path over charm subclassing or relation-hook overrides.permission denied on workload paths, treat that as a rock / runtime issue first. Inspect the rock service command, runtime user, and writable paths before considering charm Python changes.src/charm.py changes still seem necessary after exhausting workload-side adaptations, stop and ask before modifying it.charmcraft_pack. Never use --destructive-mode.The charmcraft framework extensions already declare these relations in the generated charmcraft.yaml with fixed optionality. Do not re-declare them. Do not ask the user whether they should be optional — the extension owns that decision.
| Relation | Interface | Optionality |
|---|---|---|
| ingress | ingress | required |
| grafana-dashboard | grafana_dashboard | optional |
| metrics-endpoint | prometheus_scrape | optional |
| logging | loki_push_api | optional |
paas-charm Runtime ContractGenerated 12-factor charms subclass paas-charm framework classes. paas-charm owns:
Migration precedence (current paas-charm checks in this order, last match wins):
manage.pymigrate.pymigrate.shmigrateBecause Django repos normally have manage.py, adding a repo migrate.sh will not replace the default migration path — paas-charm will still prefer manage.py migrate.
Rock-defined service env: env set under services.*.environment in rockcraft.yaml should not be assumed to survive unchanged once the app runs under the charm. paas-charm owns the final Pebble layer; mirror required keys into the charm-managed contract.
Flask — Charmcraft extension is stable. Built-in config surface includes Gunicorn options plus Flask-specific settings; built-in env shape is mostly FLASK_*.
Django — Charmcraft extension is stable. Built-in config surface includes Gunicorn options, django-debug, django-secret-key, django-allowed-hosts. Built-in action surface includes create-superuser. Built-in env shape is mostly DJANGO_*. Prefer the built-in config for django-secret-key and django-allowed-hosts over inventing parallel charm options. manage.py migrate is always preferred — do not rely on a repo migrate.sh to force static asset work. collectstatic belongs in the rock build.
FastAPI — experimental. Built-in config surface includes webserver-workers, webserver-port, webserver-log-level, metrics-*, app-secret-key. Built-in env shape is mostly unprefixed framework variables plus APP_* for user config.
Express — experimental. Built-in config surface includes app-port, metrics-*, app-secret-key. Known mismatch: current paas-charm Express aliases may expect port while the charm injects app-port. Adding a non-conflicting port config option is the preferred bridge.
Go — experimental. Built-in config surface includes app-port, metrics-*, app-secret-key. Built-in env shape is APP_*. Relation-provided database URLs often need a small workload-side alias to the repo's existing config key.
Spring Boot — experimental. Built-in config surface includes app-port, app-profiles, metrics-*, app-secret-key. Default metrics path is /actuator/prometheus. Relation env is heavily translated into Spring property names: spring.datasource.url, spring.data.redis.url, spring.security.oauth2.*, management.endpoints.web.*.
permission denied on workload paths (/app, …) → rock filesystem-layout or runtime-user problem first. Inspect writable paths before charm changes.The scaffolded requirements.txt for any PaaS profile contains both ops and paas-charm — these are mandatory. src/charm.py imports paas_charm.<framework>, so removing paas-charm makes the charm crash at install time with ModuleNotFoundError: No module named 'paas_charm'.
If you bring the application's own requirements.txt into the charm directory, merge — never overwrite. The charm's requirements.txt must end up with both the app's runtime deps (e.g. flask) AND ops + paas-charm. A cp requirements.txt <charm-dir>/ is a bug.
rockcraft pack
Use rockcraft_pack. As of rockcraft 1.18, the Flask, Django, and FastAPI extensions default to a bare base — the resulting rock contains only the application and its runtime dependencies, no Ubuntu shell or apt. Smaller image, faster pulls, smaller attack surface. Override with an explicit base: only when the workload genuinely needs system packages.
skopeo copy --insecure-policy --dest-tls-verify=false \
oci-archive:my-app_0.1_amd64.rock \
docker://localhost:32000/my-app:latest
Use skopeo_registry_push. The default registry is localhost:32000 — but call local_registry_status first to confirm a local registry is reachable. The canonical k8s snap (the preferred substrate) does not ship a registry, so the typical first-run on a fresh k8s dev box looks like:
local_registry_status → reports "no registry, k8s snap detected"setup_local_registry → deploys a registry charm into the dev model and prints the sudo block for containerd trustskopeo_registry_push to the URL setup_local_registry returnedOnly fall back to MicroK8s (microk8s enable registry) if the user has explicitly asked for that substrate. Avoid public registries (ghcr.io, Docker Hub) unless the image is already there or the user has pointed you at one — registry_mirror handles the rate-limit case where copying a public image into the local registry once saves repeated round-trips.
charmcraft pack
Use charmcraft_pack. Produces a .charm file ready for deployment.
juju deploy ./my-app_amd64.charm \
--resource oci-image=localhost:32000/my-app:latest
Use juju_deploy with resources={"oci-image": "localhost:32000/my-app:latest"}. For charms that access cloud APIs (e.g. ingress), also pass trust=True.
juju status --watch 5s
Use juju_status. The application should reach active/idle within a couple of minutes. If it stays in waiting or blocked, check the status message and logs.
Most 12-factor apps need a database. The paas-charm profiles support PostgreSQL via a standard requires relation. Add to charmcraft.yaml (set optional from the user's explicit answer in the fit phase):
requires:
postgresql:
interface: postgresql_client
optional: false
Deploy and relate:
juju deploy postgresql-k8s --trust
juju integrate my-app:postgresql postgresql-k8s:database
The framework automatically exposes database connection details as environment variables (POSTGRESQL_DB_CONNECT_STRING, etc.).
Charmcraft 4.2+ ships an http-proxy integration for 12-factor charms that automatically wires charm config (or a related proxy charm) into the workload's http_proxy / https_proxy / no_proxy environment variables. Add to charmcraft.yaml:
requires:
http-proxy:
interface: http_proxy
optional: true
Useful in restricted environments where the workload needs to reach external HTTP services through a corporate proxy.
For 12-factor charms that need authentication, the OIDC integration adds an oidc relation that exposes the OpenID Connect provider's issuer URL, client ID, and client secret to the workload as environment variables. Add to charmcraft.yaml:
requires:
oidc:
interface: oauth
optional: true
limit: 1
Deploy alongside an OIDC provider charm (Hydra, Keycloak, …) and integrate; the paas-charm base populates the env vars on the workload's behalf.
For Canonical Identity Platform integration (Hydra + Kratos + login-ui as a bundle), load the identity-platform skill — it covers the bundle-based default topology, the four other relation interfaces (oauth-cli, oidc-info, hydra-token-introspect, kratos-external-idp), and the worked example for a 12-factor charm.
The charmcraft_init tool automatically adds the tracing relation to charmcraft.yaml for all profiles (including PaaS framework profiles). Once COS is deployed, wire up tracing:
juju integrate my-app:tracing cos.tempo:tracing
For standard (non-PaaS) charms, charmcraft_init also injects ops-tracing into requirements.txt and src/charm.py — no manual setup needed. For full observability (metrics, logs, dashboards), load the observability skill.
12-factor apps are configured through environment variables. The paas-charm base auto-converts charm config options to environment variables with a framework-specific prefix.
| Framework | User-Config Prefix |
|---|---|
| Flask | FLASK_ |
| Django | DJANGO_ |
| FastAPI | APP_ |
| Go | APP_ |
| Express | APP_ |
| Spring Boot | APP_ (also spring.* properties) |
Example — adding a config option:
# charmcraft.yaml
config:
options:
log-level:
type: string
default: "info"
description: Application log level
For Flask this becomes FLASK_LOG_LEVEL on the workload. Use inspect_env_keys to surface the env vars the application actually reads, then map them through charm config.
When updating the application:
version in rockcraft.yaml.rockcraft pack → new .rock.0.2).charmcraft pack if charm metadata changed.juju refresh my-app --path=./my-app.charm --resource oci-image=localhost:32000/my-app:0.2.ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true (and CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true for the matching charmcraft path). preflight_targets reports both gates.charmcraft.yaml.optional field must come from the fit handoff or an explicit user answer. Defaulting to optional: false because "it feels required" is wrong.resources section in charmcraft.yaml — the charm cannot pull the OCI image without a declared oci-image resource.juju deploy pulls the image from the registry; if it is not there, the unit will enter error state.localhost:32000 does not use TLS. Always pass --dest-tls-verify=false to skopeo.rockcraft pack + push, the K8s node may cache the old image. Use a unique tag (not just latest) or delete the pod to force a re-pull.kubernetes profile instead of a framework profile gives a bare ops charm without the paas-charm base. Always match the profile to the detected framework.migrate.sh for Django — paas-charm always prefers manage.py migrate when manage.py exists. A new migrate.sh will not replace that path.pom.xml and build.gradle (or build.gradle.kts). check_rock_contract flags this before rockcraft pack.src/charm.py to fix workload issues — Pebble service-command failures, permission denied on /app, missing env vars are rock / runtime issues first. Fix the rock before touching charm Python.base: bare is already the framework-extension default and is de facto chiselled, but a chiselled base is not always safe. Always run check_chisel_eligibility first; if it returns blockers, use base: ubuntu@24.04. Do not treat chisel as an invisible optimisation — explain the tradeoff to the user.| Symptom | Likely Cause | Fix |
|---|---|---|
rockcraft init fails with "unknown extension" | Experimental flag not set | Set ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true |
check_rock_contract reports issues | Framework contract violated | Resolve every issue before rockcraft pack |
rockcraft pack fails with "slice not found" | Package has no chisel slice | Switch to base: ubuntu@24.04; no other changes needed |
check_chisel_eligibility returns blockers | Shell / apt / vendor-script at runtime | Use base: ubuntu@24.04; explain to user why smaller base is unsafe |
Unit stuck in waiting | Image not in registry | Push the rock with skopeo_registry_push |
Unit in error | Workload crash | Check juju debug-log for container errors |
blocked: missing relation | Database or ingress not related | Run juju_relate to add the integration |
| Config change has no effect | Wrong env var prefix | Check the framework prefix table above |
| Pebble exit loop with CLI usage | Wrong service command | Fix rockcraft.yaml services.*.command, repack, repush |
permission denied on /app | Non-root runtime user, unwritable path | Fix ownership in the rock, repack |
| Spring Boot rock build refused | Both Maven and Gradle present | Pick one in the trial copy; check_rock_contract reports this |