The Decision: Helm + Direct Managed Resources
This page captures the architectural decision, the alternatives considered, and why this project uses Helm to render Crossplane Managed Resources directly instead of using Compositions, XRDs, and Claims.
The Decision Statement
Helm renders real provider-family-azure Managed Resources. No Compositions. No XRDs. No Claims. Crossplane reconciles the rendered YAML against Azure.
Three Approaches Evaluated
There are three legitimate ways to combine Helm with Crossplane. They are fundamentally different philosophies, not variations of one idea.
Approach A: Helm Renders Claims
Helm chart → Claims → XR → Composition → Managed Resources → AzureHow it works:
- Platform team maintains XRDs and Compositions separately (not in Helm)
- Helm charts render Claims — lightweight requests like "give me a network with this CIDR"
- Compositions map Claims to real Azure MRs
When it makes sense:
- 3+ teams consuming the same infrastructure patterns
- You want to hide Azure-specific details from consumers
- You have a dedicated platform team owning the Composition lifecycle
- You need to change Azure wiring without touching consumer charts
Why we skip it for now:
- Compositions are the hard part — writing XRDs, patching logic, testing
- You cannot write good Compositions without first understanding the MRs they abstract
- The abstraction layer adds indirection that makes debugging harder
- Premature until you have multiple consumers of the same pattern
Approach B: Helm Renders MRs Directly (The Decision)
Helm chart → Managed Resources → AzureHow it works:
- Helm templates render real Crossplane MRs from
provider-family-azuresub-providers - Every Azure resource is visible in the chart templates
- Cross-resource references use Crossplane's native
*Reffields values.yamlparameterizes resource specs, environment overlays handle dev/staging/prod
Why this wins:
| Benefit | Explanation |
|---|---|
| Full visibility | Every Azure resource is a template file you can read |
| One system | Only Helm + Crossplane, no Composition layer to maintain |
| Real CRDs | You learn the actual provider-family-azure API shapes |
| Simple debugging | helm template shows exactly what gets applied |
| No premature abstraction | You don't build an abstraction layer until you need one |
What you accept:
| Trade-off | Mitigation |
|---|---|
| Charts are Azure-specific | Acceptable — cloud abstraction is rarely worth the cost |
| Charts can get large (15+ templates) | Split into separate charts by lifecycle, not by resource |
| Every consumer sees Azure details | Fine until you have non-platform consumers |
Approach C: Helm Renders Everything (XRDs + Compositions + Claims)
Helm chart → XRDs + Compositions + Claims → MRs → AzureAvoid This
This sounds tidy but creates real problems.
Problems:
Lifecycle coupling —
helm upgradewould update both the platform schema (XRD/Composition) AND the instances (Claims) simultaneously. If the schema change breaks existing instances, you have a cascading failure.Version entanglement — Compositions should be versioned independently of consumers. Packaging them in the same Helm release removes that boundary.
Blast radius — A bad Composition change affects ALL instances of that type. Deploying it alongside one instance makes it look safe when it's actually cluster-wide.
Only valid for: Single-user demos or throwaway sandboxes. Not for any shared environment.
Why the raw.txt Proposal Falls Short
The proposal in raw.txt captures a reasonable mental model but has specific issues:
1. Fake CRD names
It uses XNetwork, XDatabase, XEnvironment — types that do not exist in any Crossplane provider. These are XR types that would need XRDs and Compositions to function. The proposal skips the hard part (writing those) while presenting the easy part (rendering templates).
2. The Terraform analogy is misleading
The proposal says "Charts are treated like Terraform modules." But:
| Feature | Terraform Modules | Helm Charts |
|---|---|---|
| Outputs | Yes — output blocks wire modules together | No — Helm has no output mechanism |
| State | Yes — tracks resource IDs, dependencies | No — Helm has no state |
| Plan | Yes — terraform plan shows changes | No — helm diff is a plugin, not native |
| DAG | Yes — automatic dependency ordering | No — Helm applies everything at once |
Crossplane's *Ref fields replace Terraform's output wiring, but this happens in the Crossplane controller, not in Helm. Structuring charts like Terraform modules creates a false sense of wiring that doesn't exist.
3. Sub-chart dependencies add complexity without value
The proposal uses file:// dependencies between network/database/environment charts. For infrastructure:
- Helm dependency scoping is fragile (rename a dep and overrides silently break)
global:creates invisible coupling between charts- The actual resource wiring happens via Crossplane
*Ref, not Helm values
A flat chart with multiple templates is simpler and equally capable.
4. It skips the hardest question
"Where do Compositions live and who maintains them?" is the question that determines your entire architecture. The proposal doesn't address it.
Helm's Actual Responsibilities
Key Insight
Helm's job is done after helm install. From that point, Crossplane owns the lifecycle. This is fundamentally different from Terraform, where the tool owns resources forever.
What This Decision Rejects
| Pattern | Why Rejected |
|---|---|
| Compositions inside Helm charts | Ties platform schema to instance lifecycle |
| One chart per Azure resource | Over-modularization — a VNet alone is useless |
| Umbrella charts with sub-dependencies | Adds scoping complexity without value |
global: for cross-chart values | Invisible coupling, hard to debug |
Abstract resource names (XNetwork) | Hides the real CRDs you need to understand |
| Terraform-module analogy | Helm has no outputs, no state, no plan |
| Cloud-agnostic abstraction | Rarely worth the cost, always leaks |