helm의 최대 강적이자, 때어낼 수 없는 template문법이다. 하나하나 알고보면 별게 아니지만, 모아놓고 복잡하게 쓰면 정말 답없이 쓸 수 있기 때문에, 최대한 basic한 내용만 가져다 쓰기로 하자.
이전에도 말했지만 helm은 golang으로 만들어졌기 때문에 template문법도 golang이다. 다행이도 golang의 template문법은 다른 언어들보다는 괜찮다는 평가를 받는다. 그러나, 애시당초 template문법 자체가 너무 피로도가 높기 때문에 최대한 simple하게 쓰는 것을 추천한다.
template 문법을 시작할 때는 항상 {{
으로 열어주고 }}
으로 닫아야 한다. 이것을 actions라고 한다.
actions 안에 앞 뒤에 -
을 넣어줄 수 있는데, -
는 value의 맨 앞 또는 맨 뒤의 white space를 지우겠다는 것이다.
{{ "Hello" -}} , {{- "World" }}
다음의 결과는 "Hello,World"가 나온다. 이는
-`을 사용하여 앞 뒤의 white space를 지웠기 때문이다.
한 가지 조심해야할 점은 -
와 나머지 action 사이에 ASCII whitespace가 있어야 한다는 것이다. 만약 숫자의 경우 문제가 된다. {{-12}}
다음의 값은 -12
가 나온다는 것이다.
.Values
를 통해서 전달된 value를 받아 template를 렌더링 할 수 있다는 사실을 알 수 있었다.
이외에 release에 대한 정보를 넘겨주기도 하는데, 정해진 틀이 있다. 먼저 .Release
로 시작해야하는데, 다음과 같다.
.Release.Name
: release의 이름.Release.Namespace
: chart가 release된 namespace.Release.IsInstall
: release workload가 설치되면 true
로 나온다..Release.IsUpgrade
: release가 upgrade되거나 rollback되면 true
로 나온다..Release.Service
: release를 수행하는 service들에 대한 list가 나온다. helm install을 통해 설치할 때 Helm
으로 설정된다.Chart.yaml
file에 있는 chart metadata도 template에 전달이 가능하다.
.Chart.Name
: chart의 이름.Chart.Version
: chart의 version.Chart.AppVersion
: application version.Chart.Annotations
: key-value annotation list재밌는 건 Chart.yaml
파일에 가보면 소문자로 시작하는 이름들이 여기서는 대문자로 시작한다. 이건 golang을 해본 사람이라면 쉽게 이해할 수 있는데, golang에서 public 접근 지시자는 대문자로 시작하기 때문이다. 더불어 Chart.yaml
에 custom metadata를 넣어서 .Chart
로 접근하는 방법은 쓰지말도록 하자. 적용되지 않을 것이다. 차라리 annotation
에 추가하는 것이 좋다.
kubernetes version에 따라서 helm의 동작을 바꿔야 하는 경우들도 있다. 가령, kubernetes version에 따라 지원하는 manifest 정의가 다를 수 있다. 이러한 데이터를 helm이 kubernetes를 통해 받아와서 template에 전달해줄 수 있다. 이 경우 .Capabilities
를 사용한다.
.Capabilities.APIVersions
: cluster에서 사용 가능하 API version과 resource type을 전달한다. .Capabilities.KubeVersion.Version
: kubernetes version이다..Capabilities.KubeVersion.Minor
: kubenetes version의 minor 값을 가져온다. 어차피 major는 1에서 바뀐 적이 없기 때문에 minor가 중요하다..Capabilities
을 사용할 때 helm template
를 실행하면 helm install
이나 helm package
처럼 직접 kubernete cluster에 정보들을 묻지 않는다. 단지, helm을 통하여 이전에 이미 알고있던 kubernetes cluster 정보를 바탕으로 template를 렌더링 해줄 뿐이다. 이는 helm template
가 kubernetes cluster와 상호작용하지 않고 template 작업을 하기 때문에 이렇게 구현된 것이다.
마지막으로 template 자체에 대한 정보는 .Template
를 통해서 얻을 수 있다.
.Template.Name
: template 파일의 path와 이름이 나온다. anvil/templates/deployment.yaml.
.Template.BasePath
: 현재 chart의 template 디렉터리 path가 나온다. anvil/templates
pipeline은 command의 연속으로 하나의 체인을 이룬 거라고 생가하면 된다. linux pipeline와 같이 이전 command의 output이 다음 command의 입력으로 들어가서 command가 연속으로 실행된다.
character: {{ .Values.character | default "Sylvester" | quote }}
linux pipeline과 마찬가지로 |
로 표현한다. 첫번째 .Values.character
은 character
값을 가져오고, 이 결과가 두 번째 연산자인 default
함수에 들어가는 것이다. default
함수는 입력받은 값이 비어있으면 default값을 반환해주는데, 여기서는 Sylvester
이다. 다음으로 default
함수를 거친 결과가 quote
함수의 입력으로 들어가는 것이다. quote
함수는 입력으로 들어간 문자열에 quote
를 씌워준다.
가령 다음의 id
값이 있다고 하자.
id: 12345e2
겉보기에는 문자열 같지만 yaml은 문자와 숫자를 구분하기 때문에 숫자이다. 때문에 문자열이라는 표시를 해주기 위해서 quote
를 사용하여 표현해주어야 한다.
id: "12345e2"
quote
로 wrapping되면서 yaml parser는 이 id값을 string값으로 생각할 것이다.
helm에서 기본적으로 제공해주는 template function들이 있는데, 자주 쓰이는 것들이 있다. 가령 indent
나 nindent
같은 것들이 있다.
다음의 예제를 보도록 하자.
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
.Values.podSecurityContext
은 하나의 data인데, yaml file로 받기 위해서 toYaml
을 통해 yaml 파일로 만든 것이다. 잘보면 action 맨 앞에 -
가 있는 것을 볼 수 있다. 따라서 output의 맨 앞 쪽 whitespace가 모두 지워질 것이라는 것을 예상할 수 있다. 심지어 securityContext:
앞까지 지워질텐데, 이렇게 만들면 securityContext
계위에 안맞기 때문에 indent가 필요하다. nindent
는 new line을 하나만들고 입력한 숫자만큼 indent를 실행한다. 이렇게 하면 계위가 맞게되는 것이다.
indent
는 nindent
와 달리 new line을 만들지 않지만 indent를 하는 것은 똑같다.
toYaml
말고도 toJson
, toToml
도 있다. toYaml
은 kubernetes manifest를 만들 때 사용하는 것이고, toJson
은 주로 configmap과 같은 설정값 데이터를 넣을 때 쓴다.
참고로 pipeline으로 앞 command의 output이 뒤 command의 input에 들어갈 때는 맨 뒤의 argument로 들어가는 것이다. 가령 toYaml
의 결과가 nindent
의 맨 뒤 argument에 들어가는 것이다.
https://helm.sh/docs/chart_template_guide/function_list/
여기 여러 template 함수들이 있다.
앞서 보았던 .Capabilities.APIVersions
에도 method가 있는데 .Capabilities.APIVersions.Has
가 method이다. 이는 argument로 kubernetes API나 type을 받아 cluster가 해당 kubernetes API나 type을 호환할 수 있는 지 결과를 true
, or false
로 내보낸다. 가령 batch/v1
과 같은 group, version을 check받을 수 있다.
file을 다루는 .Files
역시도 여러 method들을 가지고 있다. 다음을 보도록 하자.
.Files.Get name
: path를 포함한 file name
을 받아서 file의 content를 string으로 가져와 준다..Files.GetBytes
: .Files.Get
과 유사하지만 string이 아니라 bytes 배열로 가져온다. 사실 golang 개발자 입장에서 이게 더 익숙하다..Files.Glob
: glob
pattern을 받아 매칭된 file정보를 담은 Files
object를 반환한다..Files.AsConfig
: file group을 입력으로 받아서 flatten된 YAML파일을 반환한다. 이 yaml file은 kubernetes configmap manifest의 data section에 딱 알맞게 나온다. 이는 .Files.Glob
과 함께 사용할 때 좋다..Files.AsSecrets
: .Files.AsConfig
와 비슷하게 flatten된 yaml을 반환하되, secret의 data section에 딱 맞게 전달해준다. 즉, base64 인코딩이 되어있다.Files.Lines
: filename을 입력으로 주면 newline으로 분리된 file의 content 배열을 전달해준다.다음의 예시는 config
directory안에 file들을 모두 읽어서 secret으로 전달해주는 것이다.
apiVersion: v1
kind: Secret
metadata:
name: {{ include "example.fullname" . }}
type: Opaque
data:
{{ (.Files.Glob "config/*").AsSecrets | indent 2 }}
결과로 다음과 같이 나온다.
apiVersion: v1
kind: Secret
metadata:
name: myapp
type: Opaque
data:
jetpack.ini: ZW5hYmxlZCA9IHRydWU=
rocket.yaml: ZW5hYmxlZDogdHJ1ZQ==
file자체가 secret안에 data
section으로 들어가는 것이다.
helm은 kubernetes cluster내의 resource들을 볼 수 있도록 하는 template function을 포함한다. lookup
template function은 개별 object 또는 list object를 반환할 수 있다. 만약 command가 cluster와 상호작용할 수 없는 상태에 실행되면 빈 응답이 전달된다.
다음의 예제는 anvil
namespace의 Deployment
인 runner
를 loop up하고 runner
deployment의 metadata.annotations
를 가져오는 것이다.
{{ (lookup "apps/v1" "Deployment" "anvil" "runner").metadata.annotations }}
lookup
함수의 인자는 다음과 같다.
v1
, apps/v1
이 있다.Deployment
이다.anvil
이다.만약 결과가 list로 나온다면 .items
를 통해서 loop가 가능하다.
{{ (lookup "v1" "ConfigMap" "anvil" "").items }}
한 가지 조심해야할 것은 lookup
을 사용할 때, helm template
와 실제 배포 결과가 다를 수 있다는 것이다. 이는 kubernetes api server와 상호작용을 해야하기 때문에 발생한 문제이다.
`if-else
문법은 기존의 programming 문법과 별반 다를 바 없다.
가령, Ingess
기능을 on/off 한다고 할 때 다음과 같이 할 수 있다.
ingress:
enabled: false
해당 value를 이용하여 Ingress.yaml
file에 기능을 on/off할 수 있다.
{{- if .Values.ingress.enabled -}}
...
{{- end }}
if
만에 있는 조건문이 true
라면 안에 있는 template문법들이 평가되어 렌더링된다. 재밌는 것은 shell을 쓰듯이 {{- end -}}
가 있다는 것이다.
if
문은 else
와 함께 쓰여 실행될 수 있다.
{{- if .Values.ingress.enabled -}}
...
{{- else -}}
# Ingress not enabled
{{- end }}
그런데 and
와 or
조건을 사용하는 것이 조금 재밌는데, 다음과 같이 사용해야한다.
{{- if and .Values.characters .Values.products -}}
...
{{- end }}
and
는 두 item 앞에 나와 사용되어야 한다. 이유는 간단한데, and
라는 함수를 실행하는 것이기 때문에 argument로 두 식을 받는 것이다. 그래서 and
가 앞에 있는 것이다. 이러한 이유로 or
도 마찬가지의 로직을 가진다.
{{- if or (eq .Values.character "Wile E. Coyote") .Values.products -}}
...
{{- end }}
or
에 넘기기 전에 eq
명령어로 true
or fasle
검사를 하고 전달하는 것을 볼 수 있다. 참고로 eq
는 eqaulity check로 두 argument가 동일한지 확인해준다. 이렇게 먼저 실행되어야할 부분에 대해서 ()
으로 감싸주는 작업이 필요하다.
with
은 if
와 유사한데, if
는 안에 조건문이 있었다면, with
은 안에 특정 값이 비어있는 것인지 아닌지 확인하도록 하고, 비어있지 않다면 해당 값을 .
에 가져와 사용할 수 있도록 한다.
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
다음의 예제를 보면 with
을 통해서 .Values.ingress.annotations
값이 empty인지 아닌지 확인하고, 비어있지 않다면 해당 값을 .
에 넣어준다. 따라서 annotations:
안에 있는 .
은 .Values.ingress.annotations
가 된다.
with
역시도 else
와 함께 사용이 가능하다.
변수도 선언할 수 있는데 $
을 앞에 붙여주면 된다. 단, 한 번 선언된 변수는 타입을 절대 바꿀 수 없다는 점을 알도록 하자.
변수를 만들 때 special syntax로 :=
을 사용하여 선언과 동시에 초기화를 해줄 수 있다. 타입은 자동으로 들어간다. golang을 했던 사람이라면 쉽게 이해할 수 있다.
{{ $var := .Values.character }}
var
이라는 변수를 선언하였고 .Values.character
값이 초기화되었다. 이제 해당 변수를 어디에서든 사용할 수 있다.
character: {{ $var | default "Sylvester" | quote }}
$var
이 전달되어 default
함수로 전달된다. 이때 $var
은 .Values.character
로 전달된 것이다.
만약 변수에 할당된 값을 바꾸고 싶다면 :=
이 아니라 =
을 사용하면 된다. 앞에서도 말했지만 :=
은 변수 선언과 초기화를 동시에 할 때 사용하는 것이지, 할당을 할 때는 사용하는 것이 아니다.
{{ $var := .Values.character }}
{{ $var = "Tweety" }}
$var
변수가 "Tweety"
로 바뀌게 된다.
참고로 변수의 생명주기는 template가 실행되는 동안으로, 같은 action에서는 무조건 사용이 가능하며 template 내에 다른 곳에서도 사용이 가능하다.
for
loop 대신에 range
loop를 사용하여 list와 dict를 순회할 수 있다. dict는 참고로 golang에서 map과 같다.
다음의 yaml dict와 list를 보도록 하자.
# An example list in YAML
characters:
- Sylvester
- Tweety
- Road Runner
- Wile E. Coyote
# An example map in YAML
products:
anvil: They ring like a bell
grease: 50% slippery
boomerang: Guaranteed to return
range
함수는 순회하는 방식이 두 가지있다. 아래는 element를 받아내는 방법으로 characters
처럼 list의 경우 range
로 list를 받아내고 .
가 각 iteration item이 된다.
characters:
{{- range .Values.characters }}
- {{ . | quote }}
{{- end }}
range
와 end
가 같이 쓰이는 것을 볼 수 있다. 그리고 각 item들은 .
으로 들어오는 것을 알 수 있다.
결과를 확인하면 다음과 같다.
characters:
- "Sylvester"
- "Tweety"
- "Road Runner"
- "Wile E. Coyote"
다음은 key와 value를 받아내는 방법이다. dict에 매우 유용한데, list에도 사용이 가능하다. list의 경우는 index와 value가 된다.
products:
{{- range $key, $value := .Values.products }}
- {{ $key }}: {{ $value | quote }}
{{- end }}
kubernetes manifest template안에 우리가 정의한 template를 중첩되게 넣고 싶을 때가 있을 것이다. 가령, label의 경우 application pod의 metadata로도 들어가지만, selector에도 들어가기 때문에 이를 중복으로 쓰는 것보다, 어디에 정의를 해놓고 불러내는 것이 더 좋다.
다음은 anvil
의 selector
label을 template로 만든 것이다.
{{/*
Selector labels
*/}}
{{- define "anvil.selectorLabels" -}}
app.kubernetes.io/name: {{ include "anvil.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}
Selector labels
: template function을 정의하기 이전에 comment로 주석을 써주는 것이다.{{- define "anvil.selectorLabels" -}}
: define
문을 통해서 template function을 만들어준다. 이름은 anvi.selectorLabels
이다.define
부터 end
까지 안의 내용이 바로 template function의 code들이 된다고 생각하면 된다.
잘보면 {{- define ... -}}
과 {{- end -}}
에는 -
가 앞뒤로 있는 것을 볼 수 있다. 이는 공백을 삭제하여 추가적인 line이 없도록 한 것으로 볼 수 있다.
이제 해당 template function을 사용해보도록 하자. 다음은 anvil
chart의 Deployment
부분이다.
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "anvil.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "anvil.selectorLabels" . | nindent 8 }}
include
를 통해서 helm template function을 호출하면 된다. 재밌는 것은 whitespace를 모두 제거했었기 때문에 nindent
를 통해서 indentation을 해주어야한다.
include
는 두 가지 인자로 호출되는 것을 볼 수 있다. 첫번째는 실행하려는 template이고, 두 번재는 template에 전달하려는 argument이다. 여기서는 .
을 전달하여 전체 global object를 전달하였다.
이제, anvil.selectorLabels
template function을 다른 helm template function에도 중첩되게 사용해보도록 하자. 다음은 anvil.labels
로 anvil의 label을 정의하는 template function이다.
{{/*
Common labels
*/}}
{{- define "anvil.labels" -}}
helm.sh/chart: {{ include "anvil.chart" . }}
{{ include "anvil.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end -}}
내부적으로 anvil.selectorLabels
이 사용되고, if
문을 통해 몇 가지 label들이 추가되는 것을 볼 수 있다. 이처럼 다른 helm template function에 중첩되게 helm template function을 넣을 수 있다는 것을 알 수 있다.
helm template function을 사용하여 복잡한 logic을 단순화하여 제공할 수 있는데, 다음을 보도록 하자.
{{- define "anvil.getImage" -}}
{{- if .Values.image.digest -}}
{{ .Values.image.repository }}@{{ .Values.image.digest }}
{{- else -}}
{{ .Values.image.repository }}:
{{- .Values.image.tag | default .Chart.AppVersion }}
{{- end -}}
{{- end -}}
위는 anvil.getImage
라는 helm template function으로 digest
가 있을 때와 없을 때의 image이름을 결정해주는 함수이다. 이렇게 복잡한 image
생성 로직을 helm template function으로 감싸주어 만들면, 좀 더 편하게 사용할 수 있다.
image: "{{ include "anvil.getImage" . }}"
다음과 같이 image
처리 로직을 간단하게 만들어 줄 수 있다. 이는 마치 software program을 만드는 것과 같다.
이처럼 helm template function은 복잡한 logic을 분해하여 단순화하거나, 공유하여 사용해 여러 곳에서 같은 code에 대한 redundancy를 없애는 기능을 한다.
helm template를 개발하는 일은 쉽지 않은 일이다. 따라서, 이를 위한 debugging이 필요한데, helm에서는 3개의 debug기능을 제공한다.
install, upgrade, roll back, uninstall에 관한 모든 helm chart command들은 dry run
명령어를 가지고 있다. 이는 실제 process를 실행하진 않지만, 해당 command의 결과가 무엇으로 나올 지 simulation하는 것이라고 생각하면 된다. 이는 모두 --dry-run
flag로 실행된다. 가령 install
명령어에서 --dry-run
을 실행하고 싶다면 helm install myanvil anvil --dry-run
을 쓰면 된다. --dry-run
을 쓰면 helm install
명령어의 결과를 kubernetes api-server에 보내는 것이 아니라, stdout으로 보낸다. 이를 통해 잘 만들어진 것인지 아닌지 확인할 수 있는 것이다.
NAME: myanvil
LAST DEPLOYED: Mon Jul 8 13:12:48 2024
NAMESPACE: default
STATUS: pending-install
REVISION: 1
HOOKS:
---
...
MANIFEST:
...
NOTES:
1. Get the application URL by running these commands:
export POD_NAME=$(kubectl get pods --namespace default -l "app.kubernetes.io/name=anvil,app.kubernetes.io/instance=myanvil" -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod --namespace default $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl --namespace default port-forward $POD_NAME 8080:$CONTAINER_PORT
HOOKS
와 MANIFEST
부분이 바로 kubernetes에 전달되는 yaml파일이다. 너무 길기 때문에 위에서는 생략했다.
만약, template에서 에러가 존재하면 결과가 조금 다르게 나온다. 가령 anvil
chart의 deployment.yaml
file에 첫부분 }
을 지워버리면 parsing error가 발생하게 된다. 이러한 부분을 --dry-run
을 통해서 캐치할 수 있는 것이다.
Error: parse error at (anvil/templates/deployment.yaml:4): unexpected "}" in
operand
위 error msg만 보고도 어디에서 어떻게 문제가 발생했는 지 알 수 있다.
helm은 또한 문법 check까지도 해주는데, deployment.yaml
file에 apiVersion
을 삭제해보도록 하자. --dry-run
을 실행하면 다음과 같은 에러가 발생한다.
Error: YAML parse error on anvil/templates/deployment.yaml: error converting
YAML to JSON: yaml: line 2: mapping values are not allowed in this context
error msg를 보면 yaml to json
이라는 문구가 있는데, 이는 helm과 kubernetes에서 사용하는 yaml 파싱 라이브러리에서 yaml을 json으로 convert하여 사용하기 때문이다. error문구를 보면 두번째 라인에 잘못된 syntax가 사용하여 error가 발생했다는 것을 알 수 있다.
--dry-run
이 실제 install된 manifest와 다른 결과를 낼 수 있다고 하였다. 이는 --dry-run
을 사용하면 kubernetes cluster에 요청이 가지 않기 때문이다. 따라서, 실제로 helm chart를 배포해보도록 하고, 그 결과가 내가 예상한 결과와 동일한 지 확인하는 process가 필요하다.
가령 istio
와 같은 service mesh를 사용한다고 하자. 이 경우 pod에 sidecar로 service mesh container를 주입해주는데, --dry-run
으로는 확인이 불가능하다. 왜냐면 kubernetes
와 상호작용하여 istio
가 런타임에 sidecar container를 주입해주기 때문이다. 설치된 manifest를 확인하는 방법 밖에 없는 것이다.
helm get manifest
명령어를 사용하면 현재 설치된 helm chart의 manifest를 보여준다.
helm get manifest myanvil
다음의 결과를 볼 수 있다.
---
# Source: anvil/templates/serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: myanvil-anvil
labels:
helm.sh/chart: anvil-0.1.0
app.kubernetes.io/name: anvil
app.kubernetes.io/instance: myanvil
app.kubernetes.io/version: "9.17.49"
app.kubernetes.io/managed-by: Helm
---
# Source: anvil/templates/service.yaml
apiVersion: v1
kind: Service
...
이 처럼 실제 배포된 helm chart의 manifest를 보고 싶다면 helm get manifest
를 사용하면 된다.
chart를 만드는 중에 발생하는 문제점 중 하나는 lint
이다. 가령, 문자로 대문자가 허용되지 않은 경우가 있거나, 들여쓰기가 실패한 경우들이 존재하는데, 이러한 문제점들은 helm
을 통해서 쉽사리 알아차리기 쉽지 않다.
다음의 경우를 보면, 마지막 문자에 Wile
이라는 글이 있는 것을 볼 수 있다. 이렇게 대문자가 있는 경우 error가 발생할 수 있는데, helm
자체에서는 이를 catch하기 쉽지 않다. 왜냐하면 template 자체의 error는 아니기 때문이다. 단지 kubernetes 자체 spec에서의 문제인 것이다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "anvil.fullname" . }}-Wile
helm lint
를 사용하면 이러한 문제를 조기에 catch할 수 있다.
helm lint anvil
==> Linting anvil
[ERROR] templates/deployment.yaml: object name does not conform to Kubernetes
naming requirements: "test-release-anvil-Wile"
Error: 1 chart(s) linted, 1 chart(s) failed
helm lint
를 통해서 어디서 문법 문제가 발생했는 지 catch할 수 있는 것이다.