Skip to content

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 → Azure

How 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 → Azure

How it works:

  • Helm templates render real Crossplane MRs from provider-family-azure sub-providers
  • Every Azure resource is visible in the chart templates
  • Cross-resource references use Crossplane's native *Ref fields
  • values.yaml parameterizes resource specs, environment overlays handle dev/staging/prod

Why this wins:

BenefitExplanation
Full visibilityEvery Azure resource is a template file you can read
One systemOnly Helm + Crossplane, no Composition layer to maintain
Real CRDsYou learn the actual provider-family-azure API shapes
Simple debugginghelm template shows exactly what gets applied
No premature abstractionYou don't build an abstraction layer until you need one

What you accept:

Trade-offMitigation
Charts are Azure-specificAcceptable — 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 detailsFine until you have non-platform consumers

Approach C: Helm Renders Everything (XRDs + Compositions + Claims)

Helm chart → XRDs + Compositions + Claims → MRs → Azure

Avoid This

This sounds tidy but creates real problems.

Problems:

  1. Lifecycle couplinghelm upgrade would update both the platform schema (XRD/Composition) AND the instances (Claims) simultaneously. If the schema change breaks existing instances, you have a cascading failure.

  2. Version entanglement — Compositions should be versioned independently of consumers. Packaging them in the same Helm release removes that boundary.

  3. 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:

FeatureTerraform ModulesHelm Charts
OutputsYes — output blocks wire modules togetherNo — Helm has no output mechanism
StateYes — tracks resource IDs, dependenciesNo — Helm has no state
PlanYes — terraform plan shows changesNo — helm diff is a plugin, not native
DAGYes — automatic dependency orderingNo — 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

PatternWhy Rejected
Compositions inside Helm chartsTies platform schema to instance lifecycle
One chart per Azure resourceOver-modularization — a VNet alone is useless
Umbrella charts with sub-dependenciesAdds scoping complexity without value
global: for cross-chart valuesInvisible coupling, hard to debug
Abstract resource names (XNetwork)Hides the real CRDs you need to understand
Terraform-module analogyHelm has no outputs, no state, no plan
Cloud-agnostic abstractionRarely worth the cost, always leaks

Released under the MIT License.