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 serveis running. Destructive operations (helm delete,helm rollback) carrydestructiveHint: true; read-only ones (helm list,helm history,helm values,helm template) carryreadOnlyHint: 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) andvalues(free-form, exposed to the template as@values).- Adds
:helmto thedeploys.<d>.type:enum viaMarea.Plugins.Base.marea_deploy_types/1. With only this plugin loaded,:helmis the default —type:can be omitted. - A deploy-level refinement (
validate_helm) that enforces atype: helmdeploy has eitherreleases:orhelm.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 fromhelm.chartor the deploy name)..helmignore,README.md,values.yaml(empty).- One
templates/<release>_md.yamlper release, rendered from the template named inreleases.<r>.helm.template. The template is resolved against<marea_dir>/templates/<name>first, then Marea's built-in templates.
Available assigns inside a template:
| Assign | Type | Notes |
|---|---|---|
@deploy | string | Deploy name. |
@deploy_name | string | deploys.<deploy>.name if set, otherwise the deploy name. |
@name | string | Release name. |
@namespace | string | helm.namespace. |
@values | any | releases.<r>.helm.values from marea.yaml. |
@config_files | map | Filename → contents from <marea_dir>/configs/. |
@secret_files | map | Filename → 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@valuesentry) in a ConfigMap.secret_env.yaml— same asconfigmap_env.yamlbut base64-encoded into a Kubernetes Secret.secret_files.yaml— same asconfigmap_files.yamlbut 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:
- Run
Marea.Plugins.Build.build/2(=mix releasefor the chosen release). - Run
Marea.Plugins.Docker.make_env/2andMarea.Plugins.Docker.build_image/2(rsync the build context anddocker build). The same flags apply:--platform,--template,--no-cache. store_image/1resolves the registry image name fromaws.<id>.ecr.image_headerand the deploy's release name, mergesservices.<release>.image: <uri>:<vsn>into<marea_dir>/deploys/<deploy>/values.yaml, and commits that file.docker tag <release>:<vsn> <image>:<vsn>and:latest, thendocker push <image>:<vsn>.- If
--upgradeis passed, callsrun_upgrade/1with--nopauseset 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:
<marea_dir>/templates/foo.yaml— your project-local templates.- 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:
@valuesarrives in the template with the same key types you wrote inmarea.yaml. Because the schema declareshelm.values: Zoi.any(), Marea does not coerce the inner keys — values parsed byYamlElixirkeep string keys, soMap.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:
| Helper | Purpose |
|---|---|
to_dashes/1 | "my_release" → "my-release" for K8s names. |
indent/2 | Indent every line of a string by N spaces (for embedding files). |
yaml!/1 | Parse 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: 10Gimarea.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.yamlThese 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:
- Computes the full registry URL from the AWS plugin
(
aws.<id>.ecr.image_header) and the release name. - Merges
services.<release>.image: <uri>:<vsn>into the deploy'svalues.yaml. git adds andgit 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-…-abcdef1This 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
lib/marea/plugins/helm.ex- Built-in templates:
priv/templates/helm/