Search code examples
jsondeploymentyamlazure-pipelineskubernetes-helm

Helm: Overwrite configuration from json files, is there a better way?


We use helm to deploy a microservice on different systems.

Among other things, we have a ConfigMap template and of course a value file with the default values in the repo of the service. Some of these values are JSON and so far stored as JSON string:

apiVersion: v1
data:
  Configuration.json: {{ toYaml .Values.config | indent 4 }}
kind: ConfigMap
metadata:
  name: service-cm
config: |-
  {
    "val1": "key1",
    "val2": "key2"
  }

We also have a deployment repo where the different systems are defined. Here we overwrote the values with json strings as well.

Since the usability of these json strings is not so good, we want to move them to json files.

We use AKS and Azure Pipelines to deploy the service. We create the chart with:

helm chart save Chart $(acr.name).azurecr.io/$(acr.repo.name):$(BUILD_VERSION)

and push it with:

helm chart push $(acr.name).azurecr.io/$(acr.repo.name):$(BUILD_VERSION)

and upgrade after pull and export in another job:

helm upgrade --install --set image-name --wait -f demo-values.yaml service service-chart

What we have already done is to set the json config in the upgrade command with --set-file:

upgrade --install --set image-name --wait -f demo-values.yaml --set-file config=demo-config.json service service-chart

What works though only for the values, of the different systems, not for the default values. But we also want to outsource these and also do not want to do without them. Therefore at this point the first question, is there a way to inject the default values already per file, so that they are in the saved chart?

We know that you can read files in the templates with the following syntax:

  Configuration.json: |-
{{ .Files.Get "default-config.json" | indent 4 }}

But we can't override that. Another idea was to inject the path from the values:

 Configuration.json: |-
{{ .Files.Get (printf "%s" .Values.config.filename) | indent 4 }}

But the path seems to be relative to the chart folder. So there is no path to the deployment repo.

We now have the following solution with conditional templates:

data:
  {{ if .Values.config.overwrite }}
  Configuration.json: {{ toYaml .Values.config.value | indent 4 }}
  {{ else }}
  Configuration.json: |-
{{ .Files.Get "default-config" | indent 4 }}
  {{ end }}

In the deployment repo the value file then looks like this:

config:
  overwrite: true
  value: will_be_replaced_by_file_content

And demo-config.json is set with the upgrade command in the pipeline.

This works, but seems a bit fiddly to us. So the question: Do you know a better way?


Solution

  • In your very first setup, .Values.config is a string. The key: |- syntax creates a YAML block scalar that contains an indented text block that happens to be JSON. helm install --set-file also sets a value to a string, and .Files.Get returns a string.

    All of these things being strings means you can simplify the logic around them. For example, consider the Helm default template function: if its parameter is an empty string, it is logically false, and so default falls back to its default value.

    In your final layout you want to keep the default configuration in a separate file, but use it only if an override configuration isn't provided. So you can go with an approach where:

    1. In values.yaml, config is an empty string. (null or just not defining it at all will also work for this setup.)

      # config is a string containing JSON-format application configuration.
      config: ''
      
    2. As you already have it, .Files.Get "default-config.json" can be the fallback value.

    3. Use the default function to check if .Values.config is non-empty, and if not, fall back to that default.

    4. Use helm install --set-file config=demo-config.json to provide an alternate config at deploy time.

    The updated ConfigMap could look like:

    apiVersion: v1
    kind: ConfigMap
    metadata:
      name: {{ include "myapp.fullname" . }}
    data:
      Configuration.json:
    {{ .Values.config | default (.Files.Get "default-config.json") | indent 4 }}
    

    (Since any form of .Values.config is a string, it's not a complex structure and you don't need to call toYaml on it.)