LabHub
배우기 러닝패스 코스

Helm 차트 제작과 배포 · 범위와 공백 · 이론

점이 바뀌고 줄이 붙는다 — 템플릿 사고의 두 가지 뿌리

LabHub 에서 이어서 보기

한 줄 요약

템플릿 사고의 대부분은 문법이 아니라 두 가지에서 온다 — with·range 가 점이 가리키는 대상을 바꾸는 것, 그리고 공백 다듬기가 줄바꿈을 지워 버리는 것.

왜 이게 문제가 되나

헬름 템플릿에서 나는 오류는 대개 이런 모양이다.

Error: YAML parse error on frontier/templates/portal.yaml:error converting YAML to JSON: yaml: line 5: mapping values are not allowed in this context

이 메시지는 YAML 파서가 한 말이다. 템플릿을 다 렌더한 뒤에 그 결과를 YAML 로 읽으려다 실패한 것이라, 메시지에 적힌 줄 번호는 렌더 결과의 줄 번호지 템플릿 파일의 줄 번호가 아니다. 그래서 템플릿 5번째 줄을 아무리 봐도 아무 문제가 없다. 범인은 세 줄 위의 태그 끝에 붙은 -}} 두 글자인데, 그 글자는 오류 어디에도 나오지 않는다.

여기서 빠져나오는 길은 하나다. 깨진 결과를 직접 보는 것. helm template --debug 는 YAML 로 파싱되지 않는 결과도 그대로 출력한다. 출력을 보면 annotations: 뒤에 주석 블록이 같은 줄에 붙어 있고, 그다음 labels: 까지 앞줄 끝에 달라붙어 있는 것이 한눈에 보인다. 원인을 추측하는 일이 관찰하는 일로 바뀐다.

점은 블록마다 바뀐다

withrange 는 편의 문법이 아니라 컨텍스트를 갈아 끼우는 블록이다.

{{- with .Values.app }}data:  team: {{ .team }}  release: {{ .Release.Name }}{{- end }}

블록 안에서 . 는 더 이상 뿌리가 아니라 .Values.app 이다. 그래서 .team 은 찾아지고 .Release 는 없다. 헬름은 이렇게 말한다.

nil pointer evaluating interface {}.Name

이 메시지도 원인을 가리키지 않는다. .Release 가 nil 이라는 뜻인데, 사람이 보기에 .Release 는 언제나 있는 것이라 의심하지 않는다. 해법은 뿌리를 가리키는 변수 $ 다. $ 는 템플릿이 시작될 때의 컨텍스트에 묶여 있고 어떤 블록 안에서도 바뀌지 않는다. {{ $.Release.Name }} 이라고 쓰면 된다.

range 도 같다. range $i, $e := .Values.envs 처럼 변수를 받아 두면 인덱스와 원소를 안전하게 쓸 수 있고, 뿌리의 값은 $.Values... 로 꺼낸다. 변수를 받지 않고 . 만 쓰다가 그 안에서 다시 뿌리 값이 필요해지는 순간 막힌다.

indent 와 nindent, 그리고 공백 표시

{{-태그 앞의 공백과 줄바꿈을 지우고, -}}태그 뒤의 공백과 줄바꿈을 지운다. 대부분의 사고는 뒤쪽에서 난다. 다음 줄이 통째로 앞줄에 붙기 때문이다.

indentnindent 의 차이도 같은 축에 있다.

| 함수 | 하는 일 | 쓰는 자리 |
| --- | --- | --- |
| indent 4 | 각 줄 앞에 네 칸을 넣는다 | 이미 줄이 바뀐 자리 |
| nindent 4 | 줄바꿈을 먼저 넣고 네 칸을 넣는다 | 키 바로 아래에 블록을 꽂을 때 |

annotations: 아래에 맵 하나를 꽂는 자리는 거의 언제나 nindent 다. 여기서 indent 를 쓰면 블록의 첫 줄이 annotations: 와 같은 줄에 붙어 annotations: owner: sre 같은 것이 만들어지고, YAML 파서는 이것을 "매핑 값이 올 자리가 아니다" 라고 거절한다.

따옴표를 빼면 값의 타입이 바뀐다

세 번째 함정은 렌더가 성공한 뒤에 터진다.

data:  tag: {{ .Values.release.tag }}       # values 에는 "1.10"  enabled: {{ .Values.release.enabled }}  # values 에는 "no"

렌더 결과는 tag: 1.10enabled: no 다. 따옴표가 사라졌으니 이제 이 값들은 문자열이 아니다. YAML 1.1 규칙을 쓰는 파서는 no 를 거짓으로 읽고, 숫자로 읽힌 1.10 은 끝의 0 을 잃어 1.1 이 된다. ConfigMap 의 data 는 문자열만 받으므로 API 서버가 거절한다.

cannot unmarshal bool into Go struct field ConfigMap.data of type string

이미지 태그, 버전, 전화번호, 국가 코드처럼 사람이 문자열로 다루는 값은 템플릿에서 quote 를 거치는 것이 기본이다. 반대로 숫자여야 하는 자리(replicas, port)에 quote 를 붙이면 그쪽에서 같은 종류의 거절이 난다.

현장에서 만나는 모습

이 세 가지는 대개 배포 파이프라인의 서로 다른 지점에서 잡힌다. 범위와 공백 사고는 렌더에서 즉시 잡히지만, 따옴표 사고는 클러스터가 받아 볼 때까지 살아남는다. 그래서 helm template 만 돌려 보고 "렌더 되네" 하고 넘어가면 가장 늦은 지점에서 터진다. CI 에 helm template | kubectl apply --dry-run=server 를 한 줄 넣어 두는 팀이 많은 이유가 이것이다 — 스키마와 타입을 진짜 API 서버가 봐 준다.

배포 전에 값 자체를 막는 장치도 함께 쓴다. required 는 값이 없을 때, fail 은 값이 말이 안 될 때 렌더를 세운다. 둘 다 메시지를 사람이 쓰므로, 여기에 "무엇을 어떻게 고쳐라" 를 적어 두면 장애 시간에 읽는 사람이 바로 움직인다. 그리고 helm lint --strict 는 기본 린트가 경고로만 알리는 것들(대문자가 든 오브젝트 이름 같은)을 실패로 바꿔 준다. 같은 차트가 helm lint 로는 0 으로 끝나고 --strict 로는 1 로 끝나는 것을 한 번 보면, CI 에 어느 쪽을 걸어야 하는지가 분명해진다.

다음 실습에서 할 것

with 안에서 .Release 를 찾지 못하는 오류를 직접 내고 $ 로 고친다. range 에 변수를 받아 인덱스와 뿌리 값을 함께 쓴다. 공백 다듬기를 일부러 틀려 렌더를 무너뜨린 뒤 --debug 로 깨진 결과를 읽고 nindent 로 고친다. 따옴표 없는 값이 API 서버에 거절당하는 것을 확인하고, 마지막에 required·fail·helm lint --strict 로 같은 사고들이 배포까지 가지 않게 막는다.