mirror of
https://github.com/helmfile/helmfile.git
synced 2026-09-30 17:31:00 +02:00
* feat(state): add mergeStrategy field to EnvironmentSpec Introduces a per-environment mergeStrategy with valid values "override" (default, current behavior) and "fallback". This commit only adds the field, the constants, and a parse-time validator; the loader still ignores the value, so behavior is unchanged. Subsequent commits thread the value through the values loader and implement the fallback semantics. Signed-off-by: Dominik Schmidt <dev@dominik-schmidt.de> * refactor(state): thread mergeStrategy through values loader Adds a mergeStrategy string parameter to LoadEnvironmentValues, loadValuesEntries, and mapMerge so the value can flow from EnvironmentSpec down to the merge call site. Behavior is unchanged in this commit; mapMerge ignores the strategy and the next commit implements the fallback semantics. Top-level state.DefaultValues and the --state-values-file/-set loaders are passed an empty strategy ("") since they have no per-environment spec to consult and stay on the default override behavior. Signed-off-by: Dominik Schmidt <dev@dominik-schmidt.de> * feat(state): implement fallback merge strategy Adds a hand-rolled fallbackDeepMerge that, unlike mergo, preserves keys present in the destination even when their value is the zero value (false, 0, "", nil, empty list/map). mapMerge dispatches to it when mergeStrategy == "fallback"; "override" and the empty default keep using mergo with WithOverride so existing behaviour is unchanged. Validation lives at the entry of LoadEnvironmentValues so a single chokepoint guards the field. Invalid values produce an error naming both the offending value and the valid options. Tests cover: first-file-wins precedence, gap filling, deep nested merge, three-file chains, explicit zero-value preservation (the case naïve mergo gets wrong), explicit nil preservation, inline map entries, override regression, default-equals-override equivalence, and invalid-strategy errors. Signed-off-by: Dominik Schmidt <dev@dominik-schmidt.de> * feat(state): expose prior-file values in fallback template context Under mergeStrategy: fallback, .gotmpl values files can now reference values from earlier files in the same `values:` list via .Values (e.g. `service.domain: "service.{{ .Values.cluster.domain }}"`). The accumulated result is layered under env.GetMergedValues so env defaults, env values, and CLI overrides still win on overlap. Override mode keeps the historical template context — unchanged — so this is strictly opt-in via the mergeStrategy field. Together with the precedence flip from the previous commit, this lets users replace the brittle two-stage `merged-values.yaml.gotmpl` workaround with native helmfile syntax. Tests cover the headline cross-file template reference case and pin the override-mode contract that prior-file values stay invisible. Signed-off-by: Dominik Schmidt <dev@dominik-schmidt.de> * docs: document mergeStrategy and fallback semantics Adds a new section to values-and-merging.md describing the override vs fallback strategies, the explicit-zero-value preservation guarantee, and the cross-file template reference behavior. Adds a brief pointer to environments.md so users land on the new field from the environment values discussion. Signed-off-by: Dominik Schmidt <dev@dominik-schmidt.de> * refactor(state): reuse maputil.MergeMaps for fallback merge Replaces the hand-rolled fallbackDeepMerge with a single call to maputil.MergeMaps, swapping its arguments so the accumulated dest wins over the new src file. Same first-file-wins semantic, fewer lines, and the fallback path now inherits the same slice merge strategies the rest of helmfile already uses. The one observable behavior shift is for explicit nil values: under fallback, nil in an earlier file no longer 'wins' over a non-nil value in a later file — instead it falls through (matching MergeMaps' rule that nil from the override side only fills missing keys). This is internally consistent: nil-overwrites is an mergo.WithOverride quirk that lives only in the override path. The renamed test NilFallsThroughToFallback pins the new behavior with a comment referencing the contrast with override mode (Issue1154). Signed-off-by: Dominik Schmidt <dev@dominik-schmidt.de> --------- Signed-off-by: Dominik Schmidt <dev@dominik-schmidt.de>
348 lines
12 KiB
Markdown
348 lines
12 KiB
Markdown
# Environments
|
|
|
|
## Environment
|
|
|
|
When you want to customize the contents of `helmfile.yaml` or `values.yaml` files per environment, use this feature.
|
|
|
|
You can define as many environments as you want under `environments` in `helmfile.yaml`.
|
|
`environments` section should be separated from `releases` with `---`.
|
|
|
|
The environment name defaults to `default`, that is, `helmfile sync` implies the `default` environment.
|
|
The selected environment name can be referenced from `helmfile.yaml` and `values.yaml.gotmpl` by `{{ .Environment.Name }}`.
|
|
|
|
If you want to specify a non-default environment, provide a `--environment NAME` flag to `helmfile` like `helmfile --environment production sync`.
|
|
|
|
The below example shows how to define a production-only release:
|
|
|
|
```yaml
|
|
environments:
|
|
default:
|
|
production:
|
|
|
|
---
|
|
|
|
releases:
|
|
- name: newrelic-agent
|
|
installed: {{ eq .Environment.Name "production" | toYaml }}
|
|
# snip
|
|
- name: myapp
|
|
# snip
|
|
```
|
|
|
|
### Environment Values
|
|
Helmfile supports 3 values languages :
|
|
- Straight yaml
|
|
- Go templates to generate straight yaml
|
|
- HCL
|
|
|
|
Environment Values allows you to inject a set of values specific to the selected environment, into `values.yaml` templates.
|
|
Use it to inject common values from the environment to multiple values files, to make your configuration DRY.
|
|
|
|
Suppose you have three files `helmfile.yaml`, `production.yaml` and `values.yaml.gotmpl`:
|
|
|
|
`helmfile.yaml`
|
|
|
|
```yaml
|
|
environments:
|
|
production:
|
|
values:
|
|
- production.yaml
|
|
|
|
---
|
|
|
|
releases:
|
|
- name: myapp
|
|
values:
|
|
- values.yaml.gotmpl
|
|
```
|
|
|
|
`production.yaml`
|
|
|
|
```yaml
|
|
domain: prod.example.com
|
|
releaseName: prod
|
|
```
|
|
|
|
`values.yaml.gotmpl`
|
|
|
|
```yaml
|
|
domain: {{ .Values | get "domain" "dev.example.com" }}
|
|
```
|
|
|
|
`helmfile sync` installs `myapp` with the value `domain=dev.example.com`,
|
|
whereas `helmfile --environment production sync` installs the app with the value `domain=prod.example.com`.
|
|
|
|
For even more flexibility, you can now use values declared in the `environments:` section in other parts of your helmfiles:
|
|
|
|
consider:
|
|
`default.yaml`
|
|
|
|
```yaml
|
|
domain: dev.example.com
|
|
releaseName: dev
|
|
```
|
|
|
|
```yaml
|
|
environments:
|
|
default:
|
|
values:
|
|
- default.yaml
|
|
production:
|
|
values:
|
|
- production.yaml # bare .yaml file, content will be used verbatim
|
|
- other.yaml.gotmpl # template directives with potential side-effects like `exec` and `readFile` will be honoured
|
|
|
|
---
|
|
|
|
releases:
|
|
- name: myapp-{{ .Values.releaseName }} # release name will be one of `dev` or `prod` depending on selected environment
|
|
values:
|
|
- values.yaml.gotmpl
|
|
- name: production-specific-release
|
|
# this release would be installed only if selected environment is `production`
|
|
installed: {{ eq .Values.releaseName "prod" | toYaml }}
|
|
...
|
|
```
|
|
|
|
#### Merge strategy
|
|
|
|
By default, when several files are listed under `values:`, later files override earlier files. Set `mergeStrategy: fallback` on the environment to flip the precedence so earlier files win and later files only fill missing keys:
|
|
|
|
```yaml
|
|
environments:
|
|
production:
|
|
mergeStrategy: fallback
|
|
values:
|
|
- cluster-specific.yaml # wins on every key it defines
|
|
- shared-defaults.yaml.gotmpl
|
|
```
|
|
|
|
Under `fallback`, explicit non-nil values in the earlier file (including zero values like `false`, `0`, `""`, and empty list) are preserved against any later file, while maps are deep-merged so later files may still add nested keys. A later `.gotmpl` file can also reference values from earlier files via `.Values`. See [Merge Strategy: override vs fallback](values-and-merging.md#4a-merge-strategy-override-vs-fallback) for the full semantics, including how explicit `null` is handled.
|
|
|
|
#### HCL specifications
|
|
|
|
Since Helmfile v0.164.0, HCL language is supported for environment values only.
|
|
HCL values supports interpolations and sharing values across files
|
|
|
|
* Only `.hcl` suffixed files will be interpreted as is
|
|
* Helmfile supports 2 different blocks: `values` and `locals`
|
|
* `values` block is a shared block where all values are accessible everywhere in all loaded files
|
|
* `locals` block can't reference external values apart from the ones in the block itself, and where its defined values are only accessible in its local file
|
|
* Only values in `values` blocks are made available to the final root `.Values` (e.g : ` values { myvar = "var" }` is accessed through `{{ .Values.myvar }}`)
|
|
* There can only be 1 `locals` block per file
|
|
* Helmfile hcl `values` are referenced using the `hv` accessor.
|
|
* Helmfile hcl `locals` are referenced using the `local` accessor.
|
|
* When the same key is defined multiple times across imported `.hcl` files in `values` blocks, values from later files override those from earlier files (last file loaded wins). Map values are merged per key, while list values are replaced as a whole (i.e. not deep-merged). Mixed-types overrides (e.g. bool -> string) are supported (latest value/type wins).
|
|
* All cty [standard library functions](`https://pkg.go.dev/github.com/zclconf/go-cty@v1.14.3/cty/function/stdlib`) are available and custom functions could be created in the future
|
|
|
|
Consider the following example :
|
|
|
|
```terraform
|
|
# values1.hcl
|
|
locals {
|
|
hostname = "host1"
|
|
}
|
|
values {
|
|
domain = "DEV.EXAMPLE.COM"
|
|
hostnameV1 = "${local.hostname}.${lower(hv.domain)}" # "host1.dev.example.com"
|
|
}
|
|
```
|
|
```terraform
|
|
# values2.hcl
|
|
locals {
|
|
hostname = "host2"
|
|
}
|
|
|
|
values {
|
|
hostnameV2 = "${local.hostname}.${hv.domain}" # "host2.DEV.EXAMPLE.COM"
|
|
}
|
|
```
|
|
#### Note on Environment.Values vs Values
|
|
|
|
The `{{ .Values.foo }}` syntax is the recommended way of using environment values.
|
|
|
|
Prior to this [pull request](https://github.com/roboll/helmfile/pull/647), environment values were made available through the `{{ .Environment.Values.foo }}` syntax.
|
|
This is still working but is **deprecated** and the new `{{ .Values.foo }}` syntax should be used instead.
|
|
|
|
You can read more infos about the feature proposal [here](https://github.com/roboll/helmfile/issues/640).
|
|
|
|
### Environment Secrets
|
|
|
|
Environment Secrets *(not to be confused with Kubernetes Secrets)* are encrypted versions of `Environment Values`.
|
|
You can list any number of `secrets.yaml` files created using `helm secrets` or `sops`, so that
|
|
Helmfile could automatically decrypt and merge the secrets into the environment values.
|
|
|
|
First you must have the [helm-secrets](https://github.com/jkroepke/helm-secrets) plugin installed along with a
|
|
`.sops.yaml` file to configure the method of encryption (this can be in the same directory as your helmfile or
|
|
in the subdirectory containing your secrets files).
|
|
|
|
Then suppose you have a secret `foo.bar` defined in `environments/production/secrets.yaml`:
|
|
|
|
```yaml
|
|
foo.bar: "mysupersecretstring"
|
|
```
|
|
|
|
You can then encrypt it with `helm secrets enc environments/production/secrets.yaml`
|
|
|
|
Then reference that encrypted file in `helmfile.yaml`:
|
|
|
|
```yaml
|
|
environments:
|
|
production:
|
|
secrets:
|
|
- environments/production/secrets.yaml
|
|
|
|
---
|
|
|
|
releases:
|
|
- name: myapp
|
|
chart: mychart
|
|
values:
|
|
- values.yaml.gotmpl
|
|
```
|
|
|
|
Then the environment secret `foo.bar` can be referenced by the below template expression in your `values.yaml.gotmpl`:
|
|
|
|
```yaml
|
|
{{ .Values.foo.bar }}
|
|
```
|
|
|
|
#### Loading remote Environment secrets files
|
|
|
|
Since Helmfile v0.149.0, you can use `go-getter`-style URLs to refer to remote secrets files, the same way as in values files:
|
|
```yaml
|
|
environments:
|
|
staging:
|
|
secrets:
|
|
- git::https://{{ env "GITHUB_PAT" }}@github.com/org/repo.git@/environments/staging.secret.yaml?ref=main
|
|
- http://$HOSTNAME/artifactory/example-repo-local/test.tgz@environments/staging.secret.yaml
|
|
production:
|
|
secrets:
|
|
- git::https://{{ env "GITHUB_PAT" }}@github.com/org/repo.git@/environments/production.secret.yaml?ref=main
|
|
- http://$HOSTNAME/artifactory/example-repo-local/test.tgz@environments/production.secret.yaml
|
|
```
|
|
|
|
### Loading remote Environment values files
|
|
|
|
Since Helmfile v0.118.8, you can use `go-getter`-style URLs to refer to remote values files:
|
|
|
|
```yaml
|
|
environments:
|
|
cluster-azure-us-west:
|
|
values:
|
|
- git::https://git.company.org/helmfiles/global/azure.yaml?ref=master
|
|
- git::https://git.company.org/helmfiles/global/us-west.yaml?ref=master
|
|
- git::https://gitlab.com/org/repository-name.git@/config/config.test.yaml?ref=main # Public Gilab Repo
|
|
cluster-gcp-europe-west:
|
|
values:
|
|
- git::https://git.company.org/helmfiles/global/gcp.yaml?ref=master
|
|
- git::https://git.company.org/helmfiles/global/europe-west.yaml?ref=master
|
|
- git::https://ci:{{ env "CI_JOB_TOKEN" }}@gitlab.com/org/repository-name.git@/config.dev.yaml?ref={{ env "APP_COMMIT_SHA" }} # Private Gitlab Repo
|
|
staging:
|
|
values:
|
|
- git::https://{{ env "GITHUB_PAT" }}@github.com/[$GITHUB_ORGorGITHUB_USER]/repository-name.git@/values.dev.yaml?ref=main #Github Private repo
|
|
- http://$HOSTNAME/artifactory/example-repo-local/test.tgz@values.yaml #Artifactory url
|
|
---
|
|
|
|
releases:
|
|
- ...
|
|
```
|
|
|
|
Since Helmfile v0.158.0, support more protocols, such as: s3, https, http
|
|
```
|
|
values:
|
|
- s3::https://helm-s3-values-example.s3.us-east-2.amazonaws.com/values.yaml
|
|
- s3://helm-s3-values-example/subdir/values.yaml
|
|
- https://john:doe@helm-s3-values-example.s3.us-east-2.amazonaws.com/values.yaml
|
|
- http://helm-s3-values-example.s3.us-east-2.amazonaws.com/values.yaml
|
|
```
|
|
|
|
For more information about the supported protocols see: [go-getter Protocol-Specific Options](https://github.com/hashicorp/go-getter#protocol-specific-options-1).
|
|
|
|
This is particularly useful when you co-locate helmfiles within your project repo but want to reuse the definitions in a global repo.
|
|
|
|
### Environment values precedence
|
|
With the introduction of HCL, a new value precedence was introduced over environment values.
|
|
Here is the order of precedence from least to greatest (the last one overrides all others)
|
|
1. `yaml` / `yaml.gotmpl`
|
|
2. `hcl`
|
|
3. `yaml` secrets
|
|
|
|
Example:
|
|
|
|
---
|
|
|
|
```yaml
|
|
# values1.yaml
|
|
domain: "dev.example.com"
|
|
```
|
|
|
|
```terraform
|
|
# values2.hcl
|
|
values {
|
|
domain = "overdev.example.com"
|
|
env = "dev"
|
|
willBeOverridden = "override_me"
|
|
}
|
|
```
|
|
|
|
```terraform
|
|
# values3.hcl
|
|
values {
|
|
env = "local"
|
|
}
|
|
```
|
|
|
|
```yaml
|
|
# secrets.yml (assuming this one has been encrypted)
|
|
willBeOverridden: overridden
|
|
```
|
|
|
|
```
|
|
# helmfile.yaml.gotmpl
|
|
environments:
|
|
default:
|
|
values:
|
|
- values1.yaml
|
|
- values2.hcl
|
|
- values3.hcl
|
|
secrets:
|
|
- secrets.yml
|
|
---
|
|
releases:
|
|
- name: random-release
|
|
[...]
|
|
values:
|
|
domain: "{{ .Values.domain }}" # == "overdev.example.com"
|
|
env: "{{ .Values.env }}" # == "local"
|
|
willBeOverridden: "{{ .Values.willBeOverridden }}" # == "overridden"
|
|
```
|
|
|
|
### Environment defaults
|
|
|
|
In addition to `values`, environments support a `defaults` block that provides a separate layer of default values. These are merged **before** `values`, giving `values` higher priority:
|
|
|
|
```yaml
|
|
environments:
|
|
default:
|
|
defaults:
|
|
- cluster: dev
|
|
replicas: 1
|
|
values:
|
|
- replicas: 3
|
|
```
|
|
|
|
The merge order for environment values is:
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ 1. Environment defaults (merged first, lowest priority) │
|
|
│ 2. Environment values (yaml/yaml.gotmpl) │
|
|
│ 3. Environment values (HCL) │
|
|
│ 4. Environment secrets (non-HCL, decrypted) │
|
|
│ 5. CLI overrides (--state-values-set, --state-values-file) │
|
|
└─────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
In the example above, `{{ .Values.replicas }}` would be `3` (values overrides defaults) and `{{ .Values.cluster }}` would be `dev` (only defined in defaults).
|