レンダリング結果が YAML ではなかった — スコープと空白
한국어 원문으로 표시합니다.
목표
with·range 가 점을 바꾸는 것, 공백 다듬기가 줄을 붙여 버리는 것, 따옴표를 뺀 값이 숫자와 불리언이 되는 것을 각각 일부러 일으켜 보고, --debug·required·fail·helm lint --strict 로 막는 법까지 간다.
왜 중요한가
템플릿에서 나는 사고의 대부분은 함수를 몰라서가 아니라 결과가 YAML 이 아니게 되어서 난다. 그래서 오류 메시지가 원인을 가리키지 않는다. mapping values are not allowed in this context 는 YAML 파서의 말이지 템플릿의 말이 아니라서, 그 줄을 아무리 들여다봐도 -}} 하나가 범인이라는 것은 보이지 않는다. 같은 이유로 with 안에서 .Release 가 nil 이라는 메시지도 '점이 바뀌었다' 는 사실을 알기 전에는 읽히지 않는다. 여기에 따옴표 문제가 겹치면 렌더까지는 멀쩡히 되고 클러스터가 거절한다 — 배포 파이프라인의 가장 늦은 지점에서 터지는 종류다. 이 넷을 한 번씩 직접 만들어 보면 다음부터는 메시지 두 줄만 보고 어디를 볼지 정할 수 있다.
단계
/root/hc-scope/frontier차트(이름frontier, 버전0.1.0)를 만들고values.yaml에app(nameportal, teamcore, port8080),notes(ownersre, runbookwiki/portal),envs(STAGE,PROD),release(tag"1.10", enabled"no") 를 두세요.templates/base.yaml은 이름이.Values.app.name,data.port가 따옴표로 감싼 포트인 ConfigMap 입니다.helm template web /root/hc-scope/frontier결과를/root/hc-scope/out/base.yaml에 저장하세요./root/hc-scope/frontier/templates/scoped.yaml을 만드세요. 이름은<app.name>-scoped, 그 아래를{{- with .Values.app }}블록으로 감싸고 그 안에서data.release를{{ .Release.Name }},data.team을{{ .team }}로 씁니다. 렌더하면 실패합니다 — 그 출력을/root/hc-scope/out/with-error.txt에 저장하세요./root/hc-scope/frontier/templates/scoped.yaml에서with블록은 그대로 두고data.release만 고쳐 릴리스 이름이 렌더되게 하세요. 렌더 결과를/root/hc-scope/out/scoped.yaml에 저장합니다 (릴리스 이름web). data 에는release: web과team: core두 줄이 나와야 합니다./root/hc-scope/frontier/templates/envs.yaml을 만드세요. 이름은<app.name>-envs이고,.Values.envs를 인덱스와 값 두 변수로 받아 돌면서data에<소문자 환경이름>: "<인덱스>-<app.port>"한 줄씩 냅니다. 렌더 결과에stage: "0-8080"과prod: "1-8080"이 나와야 합니다. 결과를/root/hc-scope/out/envs.yaml에 저장하세요./root/hc-scope/broken차트(이름broken)를 따로 만들고values.yaml에app(nameportal, teamcore)과notes(ownersre, runbookwiki/portal)를 두세요.templates/portal.yaml은annotations:아래 줄에서{{- toYaml .Values.notes | indent 4 -}}로 주석을 꽂고 그 뒤에labels:가 오는 모양입니다. 렌더하면 실패합니다 — 보통 출력은/root/hc-scope/out/ws-error.txt에,--debug를 붙인 출력은/root/hc-scope/out/ws-debug.txt에 저장하세요. 이 차트는 고치지 말고 그대로 둡니다./root/hc-scope/broken을/root/hc-scope/fixed로 복사하고(차트 이름은fixed로 바꿉니다)templates/portal.yaml만 고쳐 렌더가 성공하게 만드세요. 주석 블록은annotations:아래에 네 칸 들여써서 두 줄로,labels.team은core로 나와야 합니다. 결과를/root/hc-scope/out/fixed.yaml에 저장하세요./root/hc-scope/frontier/templates/release.yaml을 만드세요. 이름은<릴리스이름>-release,data.tag는.Values.release.tag,data.enabled는.Values.release.enabled를 따옴표 없이 꽂습니다. 이 템플릿만 렌더해yq -o=json을 거친 결과를/root/hc-scope/out/quote-yaml12.json에, 그 렌더를kubectl apply --dry-run=server에 넣은 출력을/root/hc-scope/out/quote-error.txt에 저장하세요. 그다음 두 값에quote를 걸어 고치고, 다시 서버 검증을 통과한 출력을/root/hc-scope/out/quote-ok.txt에 저장하세요./root/hc-scope/frontier/templates/guard.yaml을 만드세요. 포트가 1024 미만이면fail로 멈추고,data.team은required로 값이 없을 때 멈춥니다. 정상 값으로는 이름이<app.name>-guard인 ConfigMap 이 나와야 합니다.--set app.port=80으로 렌더한 출력을/root/hc-scope/out/guard-fail.txt에,--set app.team=null로 렌더한 출력을/root/hc-scope/out/guard-required.txt에 저장하고, 옵션 없이 렌더한 전체 결과를/root/hc-scope/out/guard-ok.yaml에 저장하세요./root/hc-scope/lintbad차트(이름lintbad)를 만들고templates/cm.yaml의 ConfigMap 이름을Bad_Name으로 두세요.helm lint의 출력과 종료 코드를/root/hc-scope/out/lint-plain.txt에,helm lint --strict의 출력과 종료 코드를/root/hc-scope/out/lint-strict.txt에 저장하세요. 두 파일의 마지막 줄에exit=<종료 코드>를 덧붙입니다. 같은 차트인데 통과 여부가 갈리는 것이 이 단계의 답입니다.
참고
helm template --debug는 YAML 로 파싱되지 않는 결과도 그대로 보여 준다helm template <릴리스> <차트> -s templates/<파일>로 한 템플릿만 렌더한다- 키 바로 아래에 블록을 꽂을 때는
indent가 아니라nindent - 흔한 실수:
with블록 안에서.Release·.Chart를 그대로 쓴다 — 뿌리는$다 - 흔한 실수: 태그 끝의
-}}가 다음 줄을 앞줄에 붙인다 - 공식 문서: https://helm.sh/docs/chart_template_guide/control_structures/ · https://helm.sh/docs/chart_template_guide/variables/ · https://helm.sh/docs/chart_template_guide/yaml_techniques/
값 구조를 먼저 깔고 한 번 렌더한다
/root/hc-scope/frontier 차트(이름 frontier, 버전 0.1.0)를 만들고 values.yaml 에 app(name portal, team core, port 8080), notes(owner sre, runbook wiki/portal), envs(STAGE, PROD), release(tag "1.10", enabled "no") 를 두세요. templates/base.yaml 은 이름이 .Values.app.name, data.port 가 따옴표로 감싼 포트인 ConfigMap 입니다. helm template web /root/hc-scope/frontier 결과를 /root/hc-scope/out/base.yaml 에 저장하세요.
helm create 로 만들면 뼈대 템플릿이 함께 딸려 오니, 이 실습에서는 디렉터리와 파일을 직접 만드는 편이 깔끔합니다. 필요한 것은 Chart.yaml, values.yaml, templates/ 세 가지뿐입니다. 릴리스 이름은 이 실습 내내 web 으로 씁니다.
with 안에서 .Release 를 찾지 못한다
/root/hc-scope/frontier/templates/scoped.yaml 을 만드세요. 이름은 <app.name>-scoped, 그 아래를 {{- with .Values.app }} 블록으로 감싸고 그 안에서 data.release 를 {{ .Release.Name }}, data.team 을 {{ .team }} 로 씁니다. 렌더하면 실패합니다 — 그 출력을 /root/hc-scope/out/with-error.txt 에 저장하세요.
with 는 조건이 참일 때 점(.)이 가리키는 대상을 바꿉니다. 블록 안에서 점은 더 이상 뿌리가 아니라 .Values.app 입니다. 그래서 .team 은 찾아지고 .Release 는 없습니다. 오류는 표준오류로 나가므로 2>&1 로 함께 받으세요. 메시지의 '무엇이 nil 인지' 를 잘 읽어 보세요.
달러 기호로 뿌리를 다시 잡는다
/root/hc-scope/frontier/templates/scoped.yaml 에서 with 블록은 그대로 두고 data.release 만 고쳐 릴리스 이름이 렌더되게 하세요. 렌더 결과를 /root/hc-scope/out/scoped.yaml 에 저장합니다 (릴리스 이름 web). data 에는 release: web 과 team: core 두 줄이 나와야 합니다.
템플릿이 시작될 때의 뿌리 컨텍스트는 $ 에 묶여 있고, with 나 range 안에서도 바뀌지 않습니다. .Values.app 로 좁혀 놓은 편의는 유지하면서 뿌리의 것만 꺼내 쓰고 싶을 때 쓰는 손잡이입니다.
range 안에서도 뿌리 값을 함께 쓴다
/root/hc-scope/frontier/templates/envs.yaml 을 만드세요. 이름은 <app.name>-envs 이고, .Values.envs 를 인덱스와 값 두 변수로 받아 돌면서 data 에 <소문자 환경이름>: "<인덱스>-<app.port>" 한 줄씩 냅니다. 렌더 결과에 stage: "0-8080" 과 prod: "1-8080" 이 나와야 합니다. 결과를 /root/hc-scope/out/envs.yaml 에 저장하세요.
range $i, $e := .Values.envs 처럼 변수를 두 개 받으면 인덱스와 원소를 함께 쓸 수 있고, 점이 바뀌어도 $i·$e 는 그대로 살아 있습니다. 뿌리의 포트는 $.Values.app.port 로 꺼냅니다. 소문자로 바꾸는 함수는 lower 입니다.
공백 다듬기 하나가 YAML 을 무너뜨린다
/root/hc-scope/broken 차트(이름 broken)를 따로 만들고 values.yaml 에 app(name portal, team core)과 notes(owner sre, runbook wiki/portal)를 두세요. templates/portal.yaml 은 annotations: 아래 줄에서 {{- toYaml .Values.notes | indent 4 -}} 로 주석을 꽂고 그 뒤에 labels: 가 오는 모양입니다. 렌더하면 실패합니다 — 보통 출력은 /root/hc-scope/out/ws-error.txt 에, --debug 를 붙인 출력은 /root/hc-scope/out/ws-debug.txt 에 저장하세요. 이 차트는 고치지 말고 그대로 둡니다.
indent 4 는 줄바꿈 없이 네 칸만 넣고, 끝의 -}} 는 뒤따르는 줄바꿈과 공백을 지웁니다. 그래서 주석 블록은 annotations: 와 같은 줄에 붙고, 다음 줄의 labels: 도 앞줄 끝에 붙습니다. 오류 메시지는 YAML 파서의 말이라 원인을 가리키지 않습니다 — --debug 를 붙이면 헬름이 깨진 결과를 그대로 보여 줍니다. 거기서 어느 줄이 붙었는지 눈으로 확인하세요.
nindent 로 줄바꿈까지 넘겨 준다
/root/hc-scope/broken 을 /root/hc-scope/fixed 로 복사하고(차트 이름은 fixed 로 바꿉니다) templates/portal.yaml 만 고쳐 렌더가 성공하게 만드세요. 주석 블록은 annotations: 아래에 네 칸 들여써서 두 줄로, labels.team 은 core 로 나와야 합니다. 결과를 /root/hc-scope/out/fixed.yaml 에 저장하세요.
nindent 4 는 줄바꿈을 먼저 넣고 네 칸을 들여씁니다. 앞의 {{- 는 템플릿 태그 앞의 공백을 지워 주는 역할만 하면 되고, 끝에는 -}} 를 쓰지 않습니다. 규칙으로 외우면 편합니다 — 키 바로 아래 줄에 블록을 꽂을 때는 언제나 nindent 입니다.
따옴표를 빼먹자 클러스터가 거절했다
/root/hc-scope/frontier/templates/release.yaml 을 만드세요. 이름은 <릴리스이름>-release, data.tag 는 .Values.release.tag, data.enabled 는 .Values.release.enabled 를 따옴표 없이 꽂습니다. 이 템플릿만 렌더해 yq -o=json 을 거친 결과를 /root/hc-scope/out/quote-yaml12.json 에, 그 렌더를 kubectl apply --dry-run=server 에 넣은 출력을 /root/hc-scope/out/quote-error.txt 에 저장하세요. 그다음 두 값에 quote 를 걸어 고치고, 다시 서버 검증을 통과한 출력을 /root/hc-scope/out/quote-ok.txt 에 저장하세요.
helm template <릴리스> <차트> -s templates/release.yaml 로 한 템플릿만 렌더할 수 있습니다. 따옴표가 없으면 1.10 은 숫자가 되어 끝의 0 이 사라지고, no 는 YAML 1.1 규칙에서 거짓이 됩니다. ConfigMap 의 data 는 문자열만 받으므로 API 서버가 형 변환 오류로 거절합니다. 값이 사람이 읽는 문자열이면 quote 를 붙이는 것이 기본이라고 생각하세요.
잘못된 값이면 렌더 단계에서 멈춘다
/root/hc-scope/frontier/templates/guard.yaml 을 만드세요. 포트가 1024 미만이면 fail 로 멈추고, data.team 은 required 로 값이 없을 때 멈춥니다. 정상 값으로는 이름이 <app.name>-guard 인 ConfigMap 이 나와야 합니다. --set app.port=80 으로 렌더한 출력을 /root/hc-scope/out/guard-fail.txt 에, --set app.team=null 로 렌더한 출력을 /root/hc-scope/out/guard-required.txt 에 저장하고, 옵션 없이 렌더한 전체 결과를 /root/hc-scope/out/guard-ok.yaml 에 저장하세요.
fail 은 조건을 직접 쓸 수 있어서 '값은 있는데 말이 안 되는 값' 을 막을 때 쓰고, required 는 '값 자체가 없을 때' 를 막습니다. 두 메시지 모두 사람이 읽고 바로 고칠 수 있게 씁니다. --set app.team=null 은 그 키를 지웁니다 — 빈 문자열과는 다릅니다. 숫자 비교에는 lt (int .Values.app.port) 1024 처럼 int 를 한 번 거치세요.
경고를 오류로 다루게 만든다
/root/hc-scope/lintbad 차트(이름 lintbad)를 만들고 templates/cm.yaml 의 ConfigMap 이름을 Bad_Name 으로 두세요. helm lint 의 출력과 종료 코드를 /root/hc-scope/out/lint-plain.txt 에, helm lint --strict 의 출력과 종료 코드를 /root/hc-scope/out/lint-strict.txt 에 저장하세요. 두 파일의 마지막 줄에 exit=<종료 코드> 를 덧붙입니다. 같은 차트인데 통과 여부가 갈리는 것이 이 단계의 답입니다.
쿠버네티스 오브젝트 이름은 소문자 RFC 1123 규칙을 따라야 해서 대문자와 밑줄은 경고가 됩니다. 기본 린트는 경고를 내고도 0 으로 끝나지만, 엄격 모드는 경고를 실패로 셉니다. CI 에서 엄격 모드를 쓰면 이런 이름이 배포까지 가지 않습니다. 종료 코드는 명령 바로 뒤에서 $? 로 읽습니다.