Generate Helm charts from marea.yaml, install/upgrade them on a Kubernetes cluster, inspect history, render templates, roll back, and delete releases. Also owns the combined Docker-build + push + chart-update pipeline as marea build helm.

Optional. Enable by listing Marea.Plugins.Helm under plugins: in marea.yaml. Pulls in Marea.Plugins.Docker (and Marea.Plugins.Build) transitively via plugin_deps:.

Every command below is also exposed as an MCP tool when marea mcp serve is running. Destructive operations (helm delete, helm rollback) carry destructiveHint: true; read-only ones (helm list, helm history, helm values, helm template) carry readOnlyHint: true, so MCP clients can auto-approve the safe ones and require confirmation for the rest.

Schema contributions

  • deploys.<d>.helm:chart, namespace, kube_context, secret_values_files, chart_dir. See 04-marea-yaml.md for the full key list.
  • deploys.<d>.releases.<r>.helm:template (required) and values (free-form, exposed to the template as @values).
  • Adds :helm to the deploys.<d>.type: enum via Marea.Plugins.Base.marea_deploy_types/1. With only this plugin loaded, :helm is the default — type: can be omitted.
  • A deploy-level refinement (validate_helm) that enforces a type: helm deploy has either releases: or helm.chart_dir:, not both.

Commands

marea helm
 chart      --deploy <d>
 list       --deploy <d>
 history    --deploy <d>
 values     --deploy <d>
 template   --deploy <d>
 upgrade    --deploy <d>          [--debug]
 delete     --deploy <d>
 rollback   --deploy <d>          --pos <n>

marea build
 helm       --deploy <d> --release <r> --mix-env <env>
              [--template <name>] [--platform <p>] [--upgrade] [--no-cache]

--deploy and --release default to the most recently used values.

The plugin also exposes run_upgrade/1 and store_image/1 as public functions so other plugins or your own code can chain into the upgrade pipeline.

marea helm chart

Generates the Helm chart for the deploy at <marea_dir>/charts/<deploy>/.

If helm.chart_dir is set in the deploy spec, Marea copies that directory verbatim. Otherwise it generates a chart from scratch:

  • Chart.yaml (apiVersion v2, with the chart name from helm.chart or the deploy name).
  • .helmignore, README.md, values.yaml (empty).
  • One templates/<release>_md.yaml per release, rendered from the template named in releases.<r>.helm.template. The template is resolved against <marea_dir>/templates/<name> first, then Marea's built-in templates.

Available assigns inside a template:

AssignTypeNotes
@deploystringDeploy name.
@deploy_namestringdeploys.<deploy>.name if set, otherwise the deploy name.
@namestringRelease name.
@namespacestringhelm.namespace.
@valuesanyreleases.<r>.helm.values from marea.yaml.
@config_filesmapFilename → contents from <marea_dir>/configs/.
@secret_filesmapFilename → contents from <marea_dir>/secrets/.

Marea ships four useful base templates that cover most ConfigMap / Secret cases out of the box (see priv/templates/helm/). On disk the files use the .yaml.eex extension (so editors treat them as templated files, not pure YAML); you reference them from marea.yaml by their rendered name (.yaml):

  • configmap_env.yaml — turn a flat YAML map into a ConfigMap with upper-cased env-style keys.
  • configmap_files.yaml — wrap arbitrary files (one per @values entry) in a ConfigMap.
  • secret_env.yaml — same as configmap_env.yaml but base64-encoded into a Kubernetes Secret.
  • secret_files.yaml — same as configmap_files.yaml but base64-encoded into a Secret.

marea helm template

Renders the chart locally with helm template <chart> <build_dir>, including --values <marea_dir>/deploys/<deploy>/values.yaml if it exists, plus any helm.secret_values_files. Equivalent to marea helm chart && helm template … — useful for inspecting what upgrade would apply.

marea helm upgrade

Runs helm upgrade <chart> <build_dir> --install, plus the same flags as template. The command is wrapped in Lib.pause_cmd!/3, so unless --nopause is set, you'll be prompted to confirm before the roll-out.

--debug adds --debug to the helm invocation.

marea helm list / history / values

Thin wrappers around helm list, helm history <chart>, and helm get values <chart> respectively, with --namespace and --kube-context from the deploy spec automatically added.

marea helm delete

helm delete <chart>, gated behind Lib.pause_cmd!/3.

marea helm rollback --pos <n>

helm rollback <chart> <n>, gated behind pause_cmd!. Use helm history first to find the revision number to roll back to.

marea build helm

The end-to-end "publish a release" command. Combines the Docker build pipeline (from Marea.Plugins.Docker), an ECR push, an update to the deploy's values.yaml, and an optional helm upgrade:

  1. Run Marea.Plugins.Build.build/2 (= mix release for the chosen release).
  2. Run Marea.Plugins.Docker.make_env/2 and Marea.Plugins.Docker.build_image/2 (rsync the build context and docker build). The same flags apply: --platform, --template, --no-cache.
  3. store_image/1 resolves the registry image name from aws.<id>.ecr.image_header and the deploy's release name, merges services.<release>.image: <uri>:<vsn> into <marea_dir>/deploys/<deploy>/values.yaml, and commits that file.
  4. docker tag <release>:<vsn> <image>:<vsn> and :latest, then docker push <image>:<vsn>.
  5. If --upgrade is passed, calls run_upgrade/1 with --nopause set so the roll-out happens without an extra interactive prompt.

That last step is why the plugin advertises store_image/1 and run_upgrade/1 as public functions: a marea build helm --upgrade goes through the same code paths a stand-alone marea helm chart && marea helm upgrade would, but back-to-back with no intermediate prompts.

Writing your own templates

The four shipped templates only cover ConfigMaps and Secrets. For everything else — Deployment, Service, Ingress, StatefulSet, CronJob, custom CRDs — you provide the template yourself. Marea has no opinion about its content; whatever you write is rendered into <marea_dir>/charts/<deploy>/templates/<release>_md.yaml and handed straight to helm.

Where templates are found

When a release sets releases.<r>.helm.template: foo.yaml, Marea looks for foo.yaml in this order, taking the first match:

  1. <marea_dir>/templates/foo.yaml — your project-local templates.
  2. Marea's built-in templates (the four listed above), embedded into the binary at compile time from priv/templates/.

If neither exists, marea helm chart aborts with a clear error. The upshot is that you can drop a file into <marea_dir>/templates/ to:

  • Add a brand-new template name (e.g. deployment.yaml, cronjob.yaml).
  • Override one of the built-ins for a specific project — same name, different content.

Starting from a built-in

The fastest way to write a new template is to copy a built-in and adapt it. The four built-ins live in priv/templates/helm/ in the Marea source, and you can also dump them locally:

mkdir -p marea.d/templates
# from a checkout of the marea source
cp ../marea/priv/templates/helm/configmap_env.yaml.eex \
   marea.d/templates/configmap_env.yaml

Now reference the local copy from marea.yaml:

deploys:
  staging:
    releases:
      api:
        helm:
          template: configmap_env.yaml      # resolves to marea.d/templates/...

Run marea helm template --deploy staging to render the chart locally and inspect the result before any helm upgrade.

Template anatomy

A Marea template is a regular Helm chart YAML file with a single twist: Marea EEx-evaluates it before handing it to Helm. The convention the built-ins follow is to put the EEx setup inside a Helm comment block at the top, so editors syntax-highlight the rest as YAML:

{{/*
  <%
    import Marea.Templates, only: [to_dashes: 1, yaml!: 1]
    file = Map.fetch!(@values, "file")
    data = Map.fetch!(@config_files, file) |> yaml!()
  %>
*/}}

apiVersion: v1
kind: ConfigMap
metadata:
  name: <%= to_dashes(@name) %>
data:
  <%= for {key, value} <- data do %>
  <%= String.upcase(to_string(key)) %>: '<%= value %>'
  <% end %>

Note: @values arrives in the template with the same key types you wrote in marea.yaml. Because the schema declares helm.values: Zoi.any(), Marea does not coerce the inner keys — values parsed by YamlElixir keep string keys, so Map.fetch!(@values, "file") is the right call.

After EEx expansion the {{/* … */}} block is just a Helm comment that survives unchanged into the rendered chart, while the <%= … %> interpolations drive the actual content. The resulting file is then valid Helm — you can still use {{ .Values.foo }}, {{- if … }}, {{ template "…" . }} etc. for anything you want resolved at helm render time rather than at marea helm chart time.

Helpers available in templates

Marea.Templates exposes a few helpers you can import at the top of the EEx block:

HelperPurpose
to_dashes/1"my_release""my-release" for K8s names.
indent/2Indent every line of a string by N spaces (for embedding files).
yaml!/1Parse a YAML string into Elixir terms.

Together with the assigns listed under marea helm chart (@deploy, @name, @namespace, @values, @config_files, @secret_files), this is usually enough to shape arbitrary Kubernetes resources without reaching for helm templating itself.

One template per release, multiple resources per template

The chart Marea generates has exactly one file per release — templates/<release>_md.yaml. If a release needs more than one Kubernetes resource (e.g. a Deployment plus its Service), put them all in the same template, separated by ---:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: <%= to_dashes(@name) %>
# ...
---
apiVersion: v1
kind: Service
metadata:
  name: <%= to_dashes(@name) %>
# ...

If two releases share a deployment shape, share the file — set the same helm.template on both releases and let the differences flow through @values / @name.

Bundling external images and existing Helm charts

Most real deploys mix Marea-built releases with bits you do not build — third-party Docker images (Postgres, Redis, NGINX, …) or upstream Helm charts (bitnami/redis, an internal platform chart, something vendored from a registry). Two patterns cover the common cases.

One: external Docker image inside a Marea-managed chart

Stay on the releases: side and write a custom Helm template that references the upstream image directly. Marea still generates the chart for you (including the template for the external image), but marea build only touches the releases you actually ask it to.

deploys:
  staging:
    helm:
      namespace: my-app-staging
    releases:
      api:
        type: elixir          # built by Marea
        helm:
          template: deployment.yaml
      postgres:
        type: elixir          # default; never built unless you ask
        helm:
          template: postgres.yaml
          values:
            image: postgres:16-alpine
            storage: 10Gi

marea.d/templates/postgres.yaml references the upstream image straight from @values — there is no Marea-driven services.<release>.image substitution because no build runs:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: <%= to_dashes(@name) %>
spec:
  template:
    spec:
      containers:
        - name: postgres
          image: <%= Map.fetch!(@values, "image") %>

Workflow:

marea build helm --release api    # only api is built and pushed
marea helm upgrade                # the chart (api + postgres) is installed

marea build docker --release postgres would still attempt a build — just don't run that. There is no first-class "skip the build" type today; if you want to enforce it (so a CI pipeline can't accidentally build a non-Elixir release), wrap the rule in a small plugin that intercepts Marea.Plugins.Base.marea_cmd/2 matching [:build | _] for that release name. See 06-plugin-development.md.

Two: a vendored or pre-existing Helm chart

For a deploy that is entirely something you didn't build, drop releases: and point at the chart directory with helm.chart_dir:

deploys:
  cache:
    helm:
      namespace: cache
      chart_dir: charts/redis        # under <marea_dir>/

marea helm chart copies <marea_dir>/charts/redis/ verbatim into the build dir; marea helm upgrade / template / delete / rollback then operate on it like any Marea-generated chart, with the same --namespace and --kube-context plumbing applied automatically. You take on chart maintenance, but you keep Marea's invocation ergonomics, its last_values memory and its build/upgrade pipeline.

To vendor an upstream chart:

helm pull bitnami/redis --untar --untardir marea.d/charts/
git add marea.d/charts/redis

helm.chart_dir vs releases: — schema rules

The schema enforces that a deploy uses one or the other:

deploys.staging: helm deploy must have either 'releases' or 'helm.chart_dir'
deploys.staging: helm deploy cannot have both 'releases' and 'helm.chart_dir'

If a single namespace really needs both Marea-generated templates and a fully external chart, model them as two separate deploys (my-app and my-app-cache, both with the same helm.namespace) and run marea helm upgrade against each.

Multiple deploys: per-deploy values and image tracking

A typical marea.yaml declares several deploys side-by-side — one per environment — sharing the same releases: shape but pointing at different namespaces, clusters and image tags:

deploys:
  dev:
    helm:
      namespace: my-app-dev
      kube_context: dev-cluster
    releases:
      api: { type: elixir, helm: { template: api_deployment.yaml } }
      worker: { type: elixir, helm: { template: worker_deployment.yaml } }

  staging:
    helm:
      namespace: my-app-staging
      kube_context: staging-cluster
    releases:
      api: { type: elixir, helm: { template: api_deployment.yaml } }
      worker: { type: elixir, helm: { template: worker_deployment.yaml } }

  prod:
    helm:
      namespace: my-app-prod
      kube_context: prod-cluster
      secret_values_files:
        - prod_secrets.yaml
    releases:
      api: { type: elixir, helm: { template: api_deployment.yaml } }
      worker: { type: elixir, helm: { template: worker_deployment.yaml } }

Every marea command takes --deploy <name> (defaulting to the most recently used value, persisted in <state_dir>/last_values), so the same marea build helm / marea helm upgrade line works against any environment by changing one flag — no separate scripts, no environment-specific config files in disguise.

Per-deploy state files

Each deploy has its own values file under <marea_dir>/deploys/<deploy>/values.yaml, written and maintained by Marea:

marea.d/
  marea.yaml
  deploys/
    dev/values.yaml
    staging/values.yaml
    prod/values.yaml

These files are passed to helm as --values … on every template or upgrade, so they take effect alongside whatever is in the generated chart's own values.yaml. Anything you want to differ between environments — image tags, replica counts, resource limits, feature flags — naturally lives there.

How values.yaml gets populated

When marea build helm --deploy <d> --release <r> finishes, the plugin's store_image/1 step:

  1. Computes the full registry URL from the AWS plugin (aws.<id>.ecr.image_header) and the release name.
  2. Merges services.<release>.image: <uri>:<vsn> into the deploy's values.yaml.
  3. git adds and git commits the change with a message that records the image tag.

So every build helm leaves two artefacts: a new image in the registry, and a commit in the source repo. The values file on disk always reflects what was last built / deployed, and the file's git history is effectively a deploy log per environment.

# marea.d/deploys/staging/values.yaml
services:
  api:
    image: 123456789012.dkr.ecr.eu-west-1.amazonaws.com/acme/api:2026_05_06-…-abcdef1
  worker:
    image: 123456789012.dkr.ecr.eu-west-1.amazonaws.com/acme/worker:2026_05_06-…-abcdef1

This step is skipped (with a warning) if no AWS plugin is configured to provide a registry.

Seeing the image of each release

A few useful one-liners — the first three answer offline (just git and YAML), the last two reach the cluster:

# What's pinned in a deploy's values.yaml — current and pending images
cat marea.d/deploys/staging/values.yaml

# Compare two deploys side-by-side
diff -u marea.d/deploys/staging/values.yaml marea.d/deploys/prod/values.yaml

# Deploy log for a single environment, newest first
git log --oneline marea.d/deploys/prod/values.yaml

# What's actually running on the cluster (queries helm)
marea helm values --deploy prod

# Helm release history (revisions, timestamps, status)
marea helm history --deploy prod

The first one is the fastest sanity check on "what tag is each release pinned to in this environment?" — and because the file is always in git, it works the same on any clone and reviews cleanly in PRs whenever an image bump happens.

Source