[零基礎入門] 使用最小 Helm chart,一步一步玩 `with` / `omit` / `toYaml` / `nindent`
===
###### tags: `K8s / Helm / template`
###### tags: `Kubernetes`, `k8s`, `Helm Chart`, `HelmChart`, `chart`, `template`, `Go template`, `with`, `omit`, `toYaml`, `indent`, `nindent`, `YAML`
<br>
[TOC]
<br>
:::info
## 0. 目標與前置需求
### 目標:
* 建一個**最小 Helm chart**,專門當「Template 實驗室」。
* 不用 K8s cluster,只靠 `helm template` 就能觀察:
* `with` vs `{{- with`(空白處理差異)
* `omit` 怎麼從 map 裡排除欄位
* `toYaml` + `nindent` 怎麼影響輸出 YAML 結構
### 前置需求:
* 已安裝 Helm CLI(`helm version` 能跑起來)
* 有一個工作目錄(例如:`~/work/tpl-lab`)
:::
---
## 1. 建立「超迷你 chart 實驗室」骨架
### Step 1-1. 建立目錄結構
```bash
mkdir -p tpl-lab/templates
cd tpl-lab
```
### Step 1-2. 建立最小 `Chart.yaml`
```bash=
cat > Chart.yaml << 'EOF'
apiVersion: v2
name: tpl-lab
description: Small lab chart to test Helm templates
type: application
version: 0.1.0
appVersion: "1.0.0"
EOF
```
> 這個 chart 不會真的部署,只是給 `helm template` 有東西可以渲染。
<br>
---
<br>
## 2. 準備第一份 `values.yaml`(persistence 測試資料)
建立 `values.yaml`:
```bash=
cat > values.yaml << 'EOF'
controller:
persistence:
existingClaim:
claimName: my-pvc
storageClassName: nfs-client
size: 10Gi
accessModes:
- ReadWriteOnce
EOF
```
這裡我們有:
* `existingClaim`:一個 map(裡面有 `claimName`)
* `storageClassName`:一個單純字串
* `size` / `accessModes`:一般欄位,待會會用 `omit` 測試
<br>
---
<br>
## 3. 建立 persistence 測試模板
### Step 3-1. 在 `templates/` 裡建 YAML 模板
```bash=
cat > templates/persistence.yaml << 'EOF'
apiVersion: v1
kind: ConfigMap
metadata:
name: tpl-lab-persistence-test
data:
config: |
persistence:
{{- with .Values.controller.persistence.existingClaim }}
# existingClaim block
{{- toYaml . | nindent 6 }}
{{- end }}{{- /* with existingClaim */}}
{{- with .Values.controller.persistence.storageClassName }}
# storageClassName (示範:直接對字串 toYaml)
{{- toYaml . | nindent 6 }}
{{- end }}{{- /* with storageClassName */}}
{{- with omit .Values.controller.persistence "existingClaim" "storageClassName" }}
# omit 之後剩下的欄位
{{- toYaml . | nindent 6 }}
{{- end }}{{- /* with omit ... */}}
EOF
```
> 注意:
>
> * `config: |` 底下多一層縮排,所以我用 `nindent 6`(2 個空白 + 再 4 格)。
> * `{{- ... }}` 前面的 `-` 是為了「吃掉左邊空白/換行」,避免多出空白行。
<br>
---
<br>
## 4. 第一次渲染:觀察整體輸出
在 `tpl-lab` 目錄內執行:
```bash
helm template demo .
```
你會看到其中一段像這樣(局部示意):
```yaml=
apiVersion: v1
kind: ConfigMap
metadata:
name: tpl-lab-persistence-test
data:
config: |
persistence:
# existingClaim block
claimName: my-pvc
# storageClassName (示範:直接對字串 toYaml)
nfs-client
# omit 之後剩下的欄位
size: 10Gi
accessModes:
- ReadWriteOnce
```
### 4-1. 「套用前 / 套用後」局部對照(existingClaim)
**Template(套用前)**
```gotemplate
{{- with .Values.controller.persistence.existingClaim }}
# existingClaim block
{{- toYaml . | nindent 6 }}
{{- end }}
```
**Values(輸入資料)**
```yaml
existingClaim:
claimName: my-pvc
```
**渲染後(套用後)**
```yaml
persistence:
# existingClaim block
claimName: my-pvc
```
* `with`:如果存在 → 進 block,把 `.` 換成 `existingClaim` 這個 map。
* `toYaml .`:把 `existingClaim` 轉成 YAML。
* `nindent 6`:前面加一個換行,再縮排 6 格,對齊到 `persistence:` 下面。
* `nindent` vs `indent`
* `nindent N`:控制「多一個換行+縮排 N 格」,讓輸出對齊到正確層級
* `indent N`:控制「縮排 N 格」,讓輸出對齊到正確層級
* 情境用途:
* 想要 **緊貼下一行** → 多半用 `indent`。
* 想要 **多一個空行再開始內容** → 用 `nindent`。
<br>
---
<br>
## 5. 深入理解 `{{ with }}` vs `{{- with }}`(吃不吃空白)
這裡玩一下空白差異。
### Step 5-1. 改 template:先拿掉 `-`(實驗版)
把 `templates/persistence.yaml` 中的其中一段改成**沒有 `-`** 的版本(先示範一段就好):
```gotemplate
{{ with .Values.controller.persistence.existingClaim }}
# existingClaim block
{{ toYaml . | nindent 6 }}
{{ end }}
```
再跑一次:
```bash
helm template demo .
```
你可能會看到類似(局部示意):
```yaml
persistence:
# existingClaim block
claimName: my-pvc
```
差異:
* 因為 `with` 那一行本身被渲染成「空白行」,沒有被 `-` 吃掉。
* 所以 `persistence:` 和 `# existingClaim block` 中間,多了一行空白。
### Step 5-2. 用 `{{- with` / `{{- end` 清空多餘空白
把它改回:
```gotemplate
{{- with .Values.controller.persistence.existingClaim }}
# existingClaim block
{{- toYaml . | nindent 6 }}
{{- end }}
```
再跑一次 `helm template demo .`,就回到比較緊實的版本:
```yaml
persistence:
# existingClaim block
claimName: my-pvc
```
**重點小抄:**
* `with`:**邏輯**一樣,不影響內容輸出。
* `{{- with` / `{{- end`:多了 `-` → 把左邊的空白/換行吃掉 → 避免多出空白行。
<br>
---
<br>
## 6. 深入理解 `omit`:把特定 key 排除掉
看這段:
```gotemplate
{{- with omit .Values.controller.persistence "existingClaim" "storageClassName" }}
# omit 之後剩下的欄位
{{- toYaml . | nindent 6 }}
{{- end }}
```
### 6-1. 套用前:原始 map
原始的 `controller.persistence` 是:
```yaml
existingClaim:
claimName: my-pvc
storageClassName: nfs-client
size: 10Gi
accessModes:
- ReadWriteOnce
```
### 6-2. `omit` 做了什麼?
```gotemplate
omit .Values.controller.persistence "existingClaim" "storageClassName"
```
概念上就是:
> 從這個 map 裡,**排除** `existingClaim` 和 `storageClassName` 這兩個 key。
所以 `omit` 後得到的 map 概念上變成:
```yaml
size: 10Gi
accessModes:
- ReadWriteOnce
```
### 6-3. 套用後:搭配 `with + toYaml + nindent`
整段:
```gotemplate
{{- with omit .Values.controller.persistence "existingClaim" "storageClassName" }}
# omit 之後剩下的欄位
{{- toYaml . | nindent 6 }}
{{- end }}
```
* `omit ...` → 得到只含 `size` / `accessModes` 的 map。
* `with` → 如果 map 非空 → 進 block,`.` 變成「那個 map」。
* `toYaml .` → 轉成 YAML:
```yaml
size: 10Gi
accessModes:
- ReadWriteOnce
```
* `nindent 6` → 對齊到 `persistence:` 下面。
**渲染結果(局部)**
```yaml
persistence:
# existingClaim block
claimName: my-pvc
# storageClassName (示範:直接對字串 toYaml)
nfs-client
# omit 之後剩下的欄位
size: 10Gi
accessModes:
- ReadWriteOnce
```
> 這招很適合:「某幾個欄位要特別處理,其餘全部交給 `toYaml` 一次吐出」。
<br>
---
<br>
## 7. 額外練習:values 不同情境(nil / 少欄位)
### Step 7-1. 建立「沒有 existingClaim」的 values
```bash
cat > values-no-claim.yaml << 'EOF'
controller:
persistence:
storageClassName: nfs-client
size: 5Gi
EOF
```
執行:
```bash
helm template demo . -f values-no-claim.yaml
```
你會看到:
* `with .Values.controller.persistence.existingClaim`
→ 因為 `existingClaim` 不存在 / 為空 → **整個 block 不輸出**。
* `omit .Values.controller.persistence "existingClaim" "storageClassName"`
→ 只會輸出剩下的欄位(這裡只剩 `size`)。
**渲染結果(局部示意):**
```yaml
persistence:
# storageClassName (示範:直接對字串 toYaml)
nfs-client
# omit 之後剩下的欄位
size: 5Gi
```
<br>
---
<br>
## 8. 額外實驗:`indent` vs `nindent`
### Step 8-1. 新增 `nindent-test.yaml`
```bash=
cat > templates/nindent-test.yaml << 'EOF'
apiVersion: v1
kind: ConfigMap
metadata:
name: nindent-test
data:
with-indent: |
{{ toYaml .Values | indent 4 }}
with-nindent: |
{{ toYaml .Values | nindent 4 }}
EOF
```
再跑:
```bash
helm template demo .
```
你會看到(局部示意):
```yaml=
data:
with-indent: |
controller:
persistence:
existingClaim:
claimName: my-pvc
storageClassName: nfs-client
size: 10Gi
accessModes:
- ReadWriteOnce
with-nindent: |
controller:
persistence:
existingClaim:
claimName: my-pvc
storageClassName: nfs-client
size: 10Gi
accessModes:
- ReadWriteOnce
```
**差異:**
* `indent 4`:只幫你縮排 4 個空白,不會自動加前置換行。
* `nindent 4`:會先加一個「換行」,再縮排 4 個空白。
→ 你會看到 `with-nindent` 底下多一行空白。
**小抄:**
* 用在 `key: |` 後面時:
* 想要 **緊貼下一行** → 多半用 `indent`。
* 想要 **多一個空行再開始內容** → 用 `nindent`。
<br>
---
<br>
## 9. 建議的練習路線(把這個實驗室當 REPL 用)
之後你要玩其他 Helm 函式(`include` / `tpl` / `default` / `required`…)時,可以照這套路:
1. **固定使用 `tpl-lab` 這個 chart** 當實驗場。
2. 新增或修改 `templates/xxx.yaml`,專門測某一個語法。
3. 用不同 `values-*.yaml` 搭配 `-f` 切換情境:
```bash
helm template demo . -f values.yaml
helm template demo . -f values-no-claim.yaml
```
4. 如果 chart 以後變大,可以用:
```bash
helm template demo . --show-only templates/persistence.yaml
```
只看單一模板的輸出。
---
如果你接下來想把 `storageClassName` 那段改成「更語意正確」(輸出 `storageClassName: nfs-client` 而不是孤零零 `nfs-client`),我也可以幫你再寫一個「原始寫法 vs 改良寫法」的對照版實驗章節。
<br>
---
<br>
## 討論
### `{{ with` 前面的數個空白會被吃掉?
- ### Question
```gotemplate
{{ with .Values.controller.persistence.existingClaim }}#這裡是註解
```
{{ 前面的空白會保留?還是整行是空白行?
- ### 結論:渲染結果
```
#這裡是註解
```
- ### gpt-5.1 (2025/11/28)
會保留,而且**會真的輸出那些空白**,只是因為 `with` 這行本身不產生任何文字,所以最後看起來像「只有空白的空行」。
我們拆開來看這一行:
```gotemplate
{{ with .Values.controller.persistence.existingClaim }}
```
* ` ` ← 這 6 個空白是「純文字」,Go template / Helm 不會幫你吃掉
* `{{ with ... }}` ← 這個動作本身不輸出任何內容(只是改變 `.` 和控制之後的區塊要不要執行)
### 實際效果示意
- 假設 template 是:
```gotemplate
persistence:
${{ with .Values.controller.persistence.existingClaim }}
{{ toYaml . | nindent 6 }} #comment
{{ end }}&
next-key: xxx
```
- `{{with}}` 和 `{{end}}` 前後用字元 `$`、`&` 來當作分隔符號,易於判斷空白部份
- 而 `.Values.controller.persistence.existingClaim` **不存在 / 為空** 時,整個 `with` block 都不輸出內容,結果會變成:
```yaml
persistence:
$&
next-key: xxx
```
- 中間那一行實際上是:
```text
<六個空白>$&<換行>
```
- `<換行>`: `&` 到下一行`next-key:`中間有 `\n`
- `#comment` 位於 `{{with}}` 和 `{{end}}` 區間,因 key 不存在而不輸出
- 也就是你問的那句:
> `{{` 前面的空白會保留?還是整行是空白行?
答案是:
* **空白會保留**,所以這一行會變成「只包含空白的行」。
* 在 YAML 看起來就像「空白行」,但技術上裡面是有空白字元的。
<br>
---
### 如果改成 `{{- with ... }}` 會怎樣?
若寫成:
```gotemplate
{{- with .Values.controller.persistence.existingClaim }}
```
那就不一樣了:
* `{{-` 會把「在 `{{` 左邊的空白+換行」全部吃掉
* 實際上會連同**上一行結尾的換行 + 這一行開頭的空白**一起清掉
常見 pattern 是讓這種「只有控制語法的行」完全不留下視覺上的空行,因此你會看到很多 Helm 寫法長這樣:
```gotemplate
persistence:
{{- with .Values.controller.persistence.existingClaim }}
{{- toYaml . | nindent 2 }}
{{- end }}
next-key: xxx
```
這樣如果 `with` 沒進去,就 **不會多出任何空白行**。
---
如果你願意,下一步我可以幫你把你現在那一段 `persistence` 全部改寫成一個「不會產生孤零零 value、也不會亂多空行」的版本,順便標註哪一行在「輸出前 / 輸出後」長什麼樣子。
<br>
{%hackmd vaaMgNRPS4KGJDSFG0ZE0w %}