LabHub
学习 学习路径 课程

Helm Chart 的制作与发布

渲染结果不是 YAML——作用域与空白

在 LabHub 中继续学习

한국어 원문으로 표시합니다.

목표

with·range 가 점을 바꾸는 것, 공백 다듬기가 줄을 붙여 버리는 것, 따옴표를 뺀 값이 숫자와 불리언이 되는 것을 각각 일부러 일으켜 보고, --debug·required·fail·helm lint --strict 로 막는 법까지 간다.

왜 중요한가

템플릿에서 나는 사고의 대부분은 함수를 몰라서가 아니라 결과가 YAML 이 아니게 되어서 난다. 그래서 오류 메시지가 원인을 가리키지 않는다. mapping values are not allowed in this context 는 YAML 파서의 말이지 템플릿의 말이 아니라서, 그 줄을 아무리 들여다봐도 -}} 하나가 범인이라는 것은 보이지 않는다. 같은 이유로 with 안에서 .Release 가 nil 이라는 메시지도 '점이 바뀌었다' 는 사실을 알기 전에는 읽히지 않는다. 여기에 따옴표 문제가 겹치면 렌더까지는 멀쩡히 되고 클러스터가 거절한다 — 배포 파이프라인의 가장 늦은 지점에서 터지는 종류다. 이 넷을 한 번씩 직접 만들어 보면 다음부터는 메시지 두 줄만 보고 어디를 볼지 정할 수 있다.

단계

  1. /root/hc-scope/frontier 차트(이름 frontier, 버전 0.1.0)를 만들고 values.yamlapp(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 에 저장하세요.
  2. /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 에 저장하세요.
  3. /root/hc-scope/frontier/templates/scoped.yaml 에서 with 블록은 그대로 두고 data.release 만 고쳐 릴리스 이름이 렌더되게 하세요. 렌더 결과를 /root/hc-scope/out/scoped.yaml 에 저장합니다 (릴리스 이름 web). data 에는 release: webteam: core 두 줄이 나와야 합니다.
  4. /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 에 저장하세요.
  5. /root/hc-scope/broken 차트(이름 broken)를 따로 만들고 values.yamlapp(name portal, team core)과 notes(owner sre, runbook wiki/portal)를 두세요. templates/portal.yamlannotations: 아래 줄에서 {{- toYaml .Values.notes | indent 4 -}} 로 주석을 꽂고 그 뒤에 labels: 가 오는 모양입니다. 렌더하면 실패합니다 — 보통 출력은 /root/hc-scope/out/ws-error.txt 에, --debug 를 붙인 출력은 /root/hc-scope/out/ws-debug.txt 에 저장하세요. 이 차트는 고치지 말고 그대로 둡니다.
  6. /root/hc-scope/broken/root/hc-scope/fixed 로 복사하고(차트 이름은 fixed 로 바꿉니다) templates/portal.yaml 만 고쳐 렌더가 성공하게 만드세요. 주석 블록은 annotations: 아래에 네 칸 들여써서 두 줄로, labels.teamcore 로 나와야 합니다. 결과를 /root/hc-scope/out/fixed.yaml 에 저장하세요.
  7. /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 에 저장하세요.
  8. /root/hc-scope/frontier/templates/guard.yaml 을 만드세요. 포트가 1024 미만이면 fail 로 멈추고, data.teamrequired 로 값이 없을 때 멈춥니다. 정상 값으로는 이름이 <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 에 저장하세요.
  9. /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=<종료 코드> 를 덧붙입니다. 같은 차트인데 통과 여부가 갈리는 것이 이 단계의 답입니다.

참고

값 구조를 먼저 깔고 한 번 렌더한다

/root/hc-scope/frontier 차트(이름 frontier, 버전 0.1.0)를 만들고 values.yamlapp(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: webteam: core 두 줄이 나와야 합니다.

템플릿이 시작될 때의 뿌리 컨텍스트는 $ 에 묶여 있고, withrange 안에서도 바뀌지 않습니다. .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.yamlapp(name portal, team core)과 notes(owner sre, runbook wiki/portal)를 두세요. templates/portal.yamlannotations: 아래 줄에서 {{- 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.teamcore 로 나와야 합니다. 결과를 /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.teamrequired 로 값이 없을 때 멈춥니다. 정상 값으로는 이름이 <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 에서 엄격 모드를 쓰면 이런 이름이 배포까지 가지 않습니다. 종료 코드는 명령 바로 뒤에서 $? 로 읽습니다.