akua / examples / 02-webapp-postgres
Example 02 — webapp-postgres
Two Helm charts composed into one Package. A webapp consumes a Postgres connection URL from a Secret the CloudNativePG operator creates — entirely by convention, no runtime late-binding required, all
Two Helm charts composed into one Package. A webapp consumes a Postgres connection URL from a Secret the CloudNativePG operator creates — entirely by convention, no runtime late-binding required, all deterministic at CI time. Adds a list-comprehension overlay to label every rendered resource with the owning team. Demonstrates a test_package.k unit-test file.
Layout
02-webapp-postgres/
├── akua.toml declares charts.cnpg and charts.webapp
├── akua.lock digest + signature ledger
├── package.k the Package — two `<chart>.template()` calls (alias-method form) + aggregation
├── test_package.k KCL unit tests for the schema + defaults
├── inputs.yaml sample inputs
└── README.md
What's new vs 01
- Two sources in one Package. Both are
<chart>.template(...)calls (alias-method form, dispatched via the synthesizedcharts.<name>stub) returning resource lists;resources = [*_pg, *_labeled]aggregates them. - Cross-source wiring by convention. The webapp references the Postgres Secret by its predictable CloudNativePG name (
${appName}-pg-app). No value needs to flow between the two source calls — both derive their values frominput. - List-comprehension overlay.
[r | {metadata.labels |= {"team": ...}} for r in _app]stamps ateamlabel onto everything the app chart emits — same effect a Helm post-renderer would have, expressed in plain KCL after the typed list returns. - Unit tests.
test_package.kasserts schema defaults and validates thatcheck:blocks catch the invariant violations.
Run
akua add # resolve cnpg + webapp charts
akua render --inputs inputs.yaml # render both into ./rendered/
akua test # run test_package.k
The cross-source convention pattern
CloudNativePG (and most mature Kubernetes operators) publishes contracts on resource naming — cluster foo creates Secret foo-app with key uri. That's a runtime contract. The webapp references it by the same convention at render time:
env = [{
name = "DATABASE_URL"
valueFrom.secretKeyRef = {
name = "${input.appName}-pg-app" # CNPG convention
key = "uri"
}
}]
If CNPG ever changed its naming convention, this is the one place we'd update — still at CI time, still deterministic. No cluster.get() runtime call ever needed.
What's disallowed
- Source A cannot reference Source B's output. Both derive from
input; cross-source late-binding is the RGD case. If you genuinely need it, route that source to aResourceGraphDefinitionoutput and let kro reconcile. See 06-multi-engine/ for the pattern. - No runtime cluster reads from KCL. Determinism is load-bearing (design-notes.md §2.2).
See also
- package-format.md §4 Body — engine calls, postRenderer, aggregation
- package-format.md §8 What's disallowed — cross-source wiring rules
- 03-multi-env-app/ — next example: Package + App + Environment in one workspace
package.k
# Example 02 — webapp-postgres
#
# Cross-source wiring. A webapp consumes a Postgres connection URL from a
# Secret created by the CloudNativePG operator — entirely by convention,
# no runtime late-binding required, all deterministic at CI time.
#
# Demonstrates:
# - two Helm sources in one package
# - per-source input mapping (`cluster.name`, `ingress.hostname`, etc.)
# - cross-source references via predictable operator naming
# - optional postRenderer for cross-cutting mutation (team label)
#
# Render:
# akua render --inputs inputs.yaml --out ./rendered
import akua.ctx
import charts.cnpg as cnpg
import charts.webapp as webapp
schema Input:
"""Customer-facing contract. Six fields; four have defaults."""
appName: str
hostname: str
replicas: int = 3
team: str = "platform"
database: {
user: str = "app"
}
input: Input = ctx.input()
# Each `charts.<name>` import lands a synthesized stub with a
# `template` lambda + typed `Values` schema. The engine
# (`akua.helm`) doesn't appear at the call site — the stub dispatches.
_pg = cnpg.template(cnpg.TemplateOpts {
values = cnpg.Values {
cluster = {
name: "${input.appName}-pg"
instances: 3
}
bootstrap.initdb = {
database: input.appName
owner: input.database.user
}
monitoring.enabled: True
}
})
# Application. References the conventional secret name above.
# If CNPG's operator changes its naming convention, this is the one
# place we'd update — still at CI time, still deterministic.
_app = webapp.template(webapp.TemplateOpts {
values = webapp.Values {
replicaCount = input.replicas
ingress.hostname = input.hostname
ingress.tls.enabled = True
fullnameOverride = input.appName
env = [
{
name = "DATABASE_URL"
valueFrom.secretKeyRef = {
name = "${input.appName}-pg-app" # CNPG convention
key = "uri"
}
}
]
}
})
# Cross-cutting label applied to the webapp resources — same effect
# as a chart postRenderer, expressed as a list comprehension after
# the alias-method call returns the typed list. Mirrors example 11's
# overlay pattern.
_labeled = [r | {metadata.labels |= {"team": input.team}} for r in _app]
resources = [*_pg, *_labeled]
Source: examples/02-webapp-postgres/