> ## Documentation Index
> Fetch the complete documentation index at: https://docs.formae.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Deploy a Helm chart

> Manage a Helm release as formae resources: reference a chart by name and version, set values inline, and apply it alongside your other Kubernetes resources.

Use Helm charts inside a forma. Reference a chart by name and version, set values
inline, and apply it alongside the rest of your Kubernetes resources, with no
separate `helm install` step. formae expands the chart into individual managed
resources, so a release reconciles, drifts, and tears down like everything else.

```mermaid theme={"languages":{"custom":["/languages/pkl.json"]}}
flowchart LR
  target["k8s-local"]:::tgt

  subgraph stack["helm-nginx"]
    direction LR
    ns["Namespace"]:::res
    chart["HelmChart bitnami/nginx"]:::res
    deploy["Deployment"]:::res
    svc["Service"]:::res
    cfg["ConfigMaps / secrets"]:::res

    chart --> deploy
    chart --> svc
    chart --> cfg
    ns --> chart
  end

  target --> stack

  classDef tgt fill:#FF8201,stroke:#B25900,color:#ffffff
  classDef res fill:#FFF3E6,stroke:#FF8201,color:#02024B
  style stack fill:#ffffff,stroke:#02024B,stroke-width:2px,stroke-dasharray:6 4,color:#02024B
```

## Scope and limitations

formae **renders** a chart (`helm template`, via `pkl-reader-helm`) into individual
Kubernetes resources at evaluation time, then manages those resources like any
other forma. It does **not** run `helm install`, so there is no Helm release: the
chart's manifests become plain formae-managed objects, reconciled and
drift-corrected by the agent.

That model means several Helm features are **not supported**:

* **Helm hooks are not honored.** Resources annotated with `helm.sh/hook`
  (`pre-install`, `post-install`, `pre-upgrade`, `post-upgrade`, `pre-delete`,
  `post-delete`) are rendered as ordinary resources and applied together with
  everything else. There is no hook ordering, no run-once-then-forget behavior, and
  no post-completion cleanup; `helm.sh/hook-weight` and `helm.sh/hook-delete-policy`
  are ignored. A chart that depends on a hook Job to initialize state (migrations,
  CRD installation, secret generation) may not behave as it does under `helm install`.
* **Chart tests are not run.** A `helm.sh/hook: test` pod is treated as a normal
  resource (created and left in place), not executed as a test. There is no
  `helm test` equivalent.
* **No Helm release lifecycle.** There is no release history, no `helm rollback`, no
  `helm list`, and no release-tracking Secret/ConfigMap. Rollback, waiting for
  readiness, and atomic apply are handled by formae's own reconcile and drift
  correction; the Helm flags `--wait`, `--timeout`, and `--atomic` have no analog.
* **No Helm ownership metadata.** Resources are not stamped with
  `app.kubernetes.io/managed-by: Helm` or Helm release annotations; `releaseName` is
  used only as a formae label prefix.
* **`helm.sh/resource-policy: keep` is ignored.** formae's reconcile owns deletion,
  so a resource the chart marks `keep` is still removed if a reconcile no longer
  declares it.
* **CRDs shipped under a chart's `crds/` directory may not render.** `helm template`
  does not emit `crds/` by default, and there is no `--include-crds` toggle. CRDs a
  chart templates under `templates/` do render (mapped to a custom resource); CRDs in
  `crds/` should be applied separately.
* **Rendering has no cluster access.** Because the chart is templated at evaluation
  time (not against your cluster), `lookup()` returns empty and `.Capabilities`
  reflect Helm's defaults rather than your live cluster's API versions. Charts whose
  output branches on live cluster state render as if the cluster were empty. (Version
  alignment is instead enforced by the [version coupling](#version-coupling) below.)
* **The namespace is not created for you.** There is no `--create-namespace`; declare
  the target namespace as its own resource in the forma (as in [the forma](#the-forma)
  below).

If your chart relies on any of the above, prefer breaking it into explicit formae
resources, or run those pieces (hook Jobs, CRD installation) as a separate step.

## Prerequisites

* `pkl` 0.30 or newer.
* `pkl-reader-helm` on your `PATH`, from the [apple/pkl-readers releases](https://github.com/apple/pkl-readers/releases) (look for `helm@<ver>` tags).
* `helm` 3 or newer, with the chart repositories you reference added:
  ```bash theme={"languages":{"custom":["/languages/pkl.json"]}}
  helm repo add bitnami https://charts.bitnami.com/bitnami
  helm repo update
  ```
* A `@formae-helm/v<X.Y>` import that lines up with your target's `kubernetesVersion` and the `@k8s/v<X.Y>` imports in the same forma. See [Version coupling](#version-coupling).

## The forma

```pkl theme={"languages":{"custom":["/languages/pkl.json"]}}
amends "@formae/forma.pkl"

import "@formae/formae.pkl"
import "@k8s/k8s.pkl" as k8s
import "@k8s/v1.31/core/Namespace.pkl" as ns
import "@formae-helm/v1.31/HelmChart.pkl"

local chart = new HelmChart {
  chart       = "bitnami/nginx"
  version     = "22.4.7"
  releaseName = "my-nginx"
  namespace   = "demo"
  values = new Dynamic {
    replicaCount = 2
    service { type = "ClusterIP" }
  }
}

forma {
  new formae.Stack { label = "helm-nginx" }
  new formae.Target {
    label = "k8s-local"
    namespace = "K8S"
    config = new k8s.Config {
      kubernetesVersion = "1.31"
      auth = new k8s.KubeconfigAuth {}
    }
  }
  new ns.Namespace {
    label = "demo-namespace"
    metadata = new ns.NamespaceMetadata { name = "demo" }
  }
  ...chart.resources
}
```

**Deploy and tear down.** `...chart.resources` spreads the chart's rendered
objects into the forma, so they apply and reconcile with everything else:

```bash theme={"languages":{"custom":["/languages/pkl.json"]}}
formae apply --mode reconcile --yes helm-nginx.pkl
formae destroy --yes helm-nginx.pkl
```

## HelmChart fields

| Field             | Type       | Default       | Purpose                                                                 |
| ----------------- | ---------- | ------------- | ----------------------------------------------------------------------- |
| `chart`           | `String`   | required      | Chart reference (`<repo>/<chart>` or OCI URL)                           |
| `version`         | `String`   | required      | Chart version                                                           |
| `releaseName`     | `String`   | required      | Helm release name; used as the label prefix                             |
| `namespace`       | `String`   | `"default"`   | Target namespace for namespaced resources                               |
| `values`          | `Dynamic?` | `null`        | Values overrides, as `new Dynamic { ... }`                              |
| `labelPrefix`     | `String`   | `releaseName` | Prefix on formae resource labels                                        |
| `skipUnsupported` | `Boolean`  | `true`        | Skip resource kinds the K8s minor does not ship; `false` throws instead |

## Version coupling

Three things must agree, or `pkl eval` fails before any cluster call:

* the `@formae-helm/v<X.Y>` import,
* the `@k8s/v<X.Y>` imports, and
* `Config.kubernetesVersion = "<X.Y>"`.

To run the same chart against several Kubernetes minors, write one forma per minor
or parameterize the file with Pkl `properties`.

If a chart emits a kind your Kubernetes minor does not have (for example
`FlowSchema` against a 1.28 cluster), the integration drops it by default. Set
`skipUnsupported = false` to fail at eval time instead.

<Note>
  This page is about using formae to manage **other** Helm charts. Installing
  **formae itself** through a Helm chart is a different task, covered in
  [Install the agent with Helm](/documentation/guides/install-agent-helm).
</Note>

## Source

The `HelmChart` type ships as the `formae-helm` Pkl package on the hub. See
[formae-plugin-k8s/helm](https://github.com/platform-engineering-labs/formae-plugin-k8s/tree/main/helm).
