Authoring and Shipping Helm Charts
--set Is Not a Shortcut, It Is a Small Language
Summary in one line
--set is not a shortcut for passing values but a small language with its own syntax and type rules, and its result can differ from passing the same value in a values file.
Why this is a problem
Suppose a deployment script has a line like this.
helm upgrade api ./api --set image.tag=8
The render result is image: registry.local/api:8 and nothing seems wrong. But the story changes if the chart's template looks like this.
image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
--set image.tag=8 puts the value in as the number 8. The template above is string concatenation, so the result looks the same, but in places that output the tag separately (annotations, labels, the data of a ConfigMap), the number goes out as it is and the API server rejects it. Conversely, if you write tag: "8" in a values file, a string goes in. The heart of this topic is that two ways of writing the same value create different types.
The rules by which --set decides types
The results confirmed directly on helm v3.16 are as follows.
| Value written | Type that goes in |
|---|---|
8 |
Number |
1.10 |
String (with a decimal point it is not read as an integer) |
0755 |
String (because the leading 0 must be kept) |
true |
Boolean |
null |
Deletes that key |
Misunderstandings often arise here. If you memorize it loosely as "values that look like versions are dangerous," you end up treating 1.10 and 8 as the same, but what actually causes trouble is only values read as integers and values read as booleans. That is also where --set-string is needed. Rather than memorizing the rules, it is faster to render a value dump once and check the types with your own eyes.
null needs particular care. Unlike --set key=, which puts in an empty string, --set key=null removes the key itself. If the chart branches only with if .Values.x, the two behave the same, but they differ in places that decide by the existence of the key.
Syntax — dots, commas, braces, brackets, backslashes
--set image.repository=registry.local/web,replicas=5 # 점은 깊이, 쉼표는 구분
--set 'args={alpha,beta,gamma}' # 중괄호는 리스트 통째로
--set 'args[0].name=first,args[0].value=1' # 대괄호는 원소 자리
--set 'nodeSelector.kubernetes\.io/os=linux' # 역슬래시는 점의 의미를 끈다
Dots inside key names are very common in Kubernetes. kubernetes.io/os, app.kubernetes.io/name, and prometheus.io/scrape are all like that. If you do not escape them, a nested map called io/os under kubernetes is quietly created, the render succeeds, but the label you wanted is not attached.
For complex values there are dedicated options. --set-json takes the value as JSON as it is and puts it in with the types as intended, and --set-file puts the content of a file in as the value. When you pass the body of a certificate or a configuration file whole, --set-file is the only realistic way. If you write --set config.ca=/path/ca.pem, the path string becomes the value.
Can you tell afterward what you deployed with?
The real cost of --set is neither syntax nor types but that no record is left. After a deployment is finished, when someone asks "what values is production running with now?", a team that deployed with -f values-prod.yaml opens one file in the repository and answers. A team that deployed with --set has to pull it out of the release.
helm get values api # 사용자가 준 값만
helm get values api --all # 차트 기본값까지 합쳐진 최종 값
It is easy to think it is fine because you can pull it out, but this value did not go through code review, nowhere does it say who decided it and why, and it disappears along with the cluster if the cluster disappears. So once the values go beyond two or three, it is better to move them to a file. The --set worth keeping is one or two things that must differ with every deployment — usually the image tag.
And that very tag is where type accidents happen. If you nail it down with --set-string image.tag=$TAG, or wrap it on the chart's template side as {{ .Values.image.tag | quote }}, it becomes safe whatever the side passing the value does. There are many people passing values and only one chart, so defending on the chart side costs less.
What it looks like in the field
When deployment scripts start to grow long with --set, two problems come together. First, no record is left of what was passed. You can pull it out with helm get values, but it is not in the repository, so it does not go through code review. Second, shell quoting and Helm syntax overlap and reading gets difficult. Braces and brackets are also treated specially by the shell, so you have to wrap them in single quotes, and if you forget this, what the shell expanded first is what gets passed to Helm.
So the boundary in practice is roughly this — structured values go in a values file, and only the one or two values that differ with each deployment go in --set. The image tag is a typical value that differs with each deployment, so it often remains in --set, and that spot is exactly where type accidents happen. For tags, use --set-string, or apply | quote on the chart side so it is safe whichever way it comes in. If there is a defense the chart author can provide, it is better to do that first — because there are many people passing values and only one chart.
What you will do in the next lab
You build a chart that exports the values it receives as JSON as they are, and apply the syntax of --set one piece at a time. You escape a dot inside a key, check how the types of integer and decimal-point values diverge, use --set-json and --set-file, and delete a key with null. Finally, you pass the same two values in three ways — a values file, --set, and --set-string — and compare the render results side by side.