---
title: ytt(YAML Templating Tool)入門 - Overlay編
tags: ["YAML", "ytt", "k14s", "Carvle"]
categories: ["Dev", "Carvel", "ytt"]
date: 2020-08-19T05:53:29Z
updated: 2020-08-19T15:47:44Z
---

[前の記事](/entries/544)では[ytt](https://get-ytt.io)のTemplating機能を見てきました。本記事ではYAMLの柔軟な加工に便利なOverlay機能を見ます。

* https://github.com/k14s/ytt/blob/develop/docs/lang-ref-ytt-overlay.md

**目次**
<!-- toc -->

### Mapの操作

まずは最もよく使うMapの操作方法を見ます。

#### 値の変更

次のKubernetesのmanifestファイルである`deployment.yml`の中の`Deployment`の`replicas`を`3`に変更したいとします。

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: demo
  template:
    metadata:
      labels:
        app: demo
    spec:
      containers:
      - image: making/hello-cnb
        name: hello-cnb
---
apiVersion: v1
kind: Service
metadata:
  name: demo
spec:
  ports:
  - name: 80-8080
    port: 80
    protocol: TCP
    targetPort: 8080
  selector:
    app: demo
  type: LoadBalancer
```

次のoverlay file(`scale.yml`)を作成します。

```yaml
#@ load("@ytt:overlay", "overlay")

#@overlay/match by=overlay.subset({"kind":"Deployment", "metadata": {"name": "demo"}})
---
spec:
  replicas: 3
```

`---`の上の`@overlay/match`アノテーションでマルチドキュメントなYAMLのうち、どのドキュメントを対象にするかを指定します。今回は`Kind`が`Deployment`で、`metadata.name`が`demo`なドキュメントのみをoverlayの対象とします。
指定方法は[こちら](https://github.com/k14s/ytt/blob/develop/docs/lang-ref-ytt-overlay.md#functions)を参照してください。

次のコマンドを実行して、`replicas`が変わっていることを確認してください。

```
$ ytt -f deployment.yml -f scale.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo
spec:
  replicas: 3
  selector:
    matchLabels:
      app: demo
  template:
    metadata:
      labels:
        app: demo
    spec:
      containers:
      - image: making/hello-cnb
        name: hello-cnb
---
apiVersion: v1
kind: Service
metadata:
  name: demo
spec:
  ports:
  - name: 80-8080
    port: 80
    protocol: TCP
    targetPort: 8080
  selector:
    app: demo
  type: LoadBalancer
```

同じように`Service`の`type`を`LoadBalancer`から`NodePort`に変更します。次のoverlay file(`nodeport.yml`)を作成します。

```yaml
#@ load("@ytt:overlay", "overlay")

#@overlay/match by=overlay.subset({"kind":"Service", "metadata": {"name": "demo"}})
---
spec:
  type: NodePort
```

次のコマンドを実行して、`type`が変わっていることを確認してください。

```
$ ytt -f deployment.yml -f nodeport.yml             
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: demo
  template:
    metadata:
      labels:
        app: demo
    spec:
      containers:
      - image: making/hello-cnb
        name: hello-cnb
---
apiVersion: v1
kind: Service
metadata:
  name: demo
spec:
  ports:
  - name: 80-8080
    port: 80
    protocol: TCP
    targetPort: 8080
  selector:
    app: demo
  type: NodePort
```

overlay filesは複数指定できるので、`replicas`の変更と`type`を同時に適用できます。

```
$ ytt -f deployment.yml -f scale.yml -f nodeport.yml 
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo
spec:
  replicas: 3
  selector:
    matchLabels:
      app: demo
  template:
    metadata:
      labels:
        app: demo
    spec:
      containers:
      - image: making/hello-cnb
        name: hello-cnb
---
apiVersion: v1
kind: Service
metadata:
  name: demo
spec:
  ports:
  - name: 80-8080
    port: 80
    protocol: TCP
    targetPort: 8080
  selector:
    app: demo
  type: NodePort
```

同じディクレトリ配下の全てのファイルを入力にしたい場合は

```
ytt -f .
```

で良いです。

#### 値の追加

次は元の`deployment.yml`にはない要素を追加します。

次の`prometheus-annotation.yml`で`Deployment`の`template`の`metadata`に`annotations`を追加します。
元のファイルにない要素には`#@overlay/match missing_ok=True`アノテーションを設定します。

```yaml
#@ load("@ytt:overlay", "overlay")

#@overlay/match by=overlay.subset({"kind":"Deployment", "metadata": {"name": "demo"}})
---
spec:
  template:
    metadata:
      #@overlay/match missing_ok=True
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "8080"
```

次のコマンドを実行して、`annotations`が追加されていることを確認してください。

```
$ ytt -f deployment.yml -f prometheus-annotation.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: demo
  template:
    metadata:
      labels:
        app: demo
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "8080"
    spec:
      containers:
      - image: making/hello-cnb
        name: hello-cnb
---
apiVersion: v1
kind: Service
metadata:
  name: demo
spec:
  ports:
  - name: 80-8080
    port: 80
    protocol: TCP
    targetPort: 8080
  selector:
    app: demo
  type: LoadBalancer
```

今回の場合は、元のファイルに`annotations`自体の定義がなかったため、上記のoverlay fileで問題ありませんが、
`annotations`の定義はあるけども`prometheus.io/****`アノテーションの定義はないケースにも対応するには次のように修正します。

```yaml
#@ load("@ytt:overlay", "overlay")

#@overlay/match by=overlay.subset({"kind":"Deployment", "metadata": {"name": "demo"}})
---
spec:
  template:
    metadata:
      #@overlay/match missing_ok=True
      annotations:
        #@overlay/match missing_ok=True
        prometheus.io/scrape: "true"
        #@overlay/match missing_ok=True
        prometheus.io/port: "8080"
```



もう一つ例を見ます。次の`namespace.yml`は**全てのYAMLドキュメント**の`metadata`に`namespace: demo`を追加します。
この場合、`#@overlay/match`の`by`に指定する関数は`overlay.all`です。
デフォルトでは1ドキュメントだけがヒットすることを期待しているのですが、今回は2ドキュメントがヒットするので、
`expects="1+"`もつけます。

```yaml
#@ load("@ytt:overlay", "overlay")

#@overlay/match by=overlay.all, expects="1+"
---
metadata:
  #@overlay/match missing_ok=True
  namespace: demo
```

次のコマンドを実行して、`namespace`が追加されていることを確認してください。

```
$ ytt -f deployment.yml -f namespace.yml            
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo
  namespace: demo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: demo
  template:
    metadata:
      labels:
        app: demo
    spec:
      containers:
      - image: making/hello-cnb
        name: hello-cnb
---
apiVersion: v1
kind: Service
metadata:
  name: demo
  namespace: demo
spec:
  ports:
  - name: 80-8080
    port: 80
    protocol: TCP
    targetPort: 8080
  selector:
    app: demo
  type: LoadBalancer
```

`namespace.yml`にYAMLドキュメントも追加できます。ここでは`Namespace`自体の定義も追加します。

```yaml
#@ load("@ytt:overlay", "overlay")
apiVersion: v1
kind: Namespace
metadata:
  name: demo
#@overlay/match by=overlay.all, expects="1+"
---
metadata:
  #@overlay/match missing_ok=True
  namespace: demo
```

次のコマンドで出力内容に`Namespace`も追加されることを確認してください。

```
$ ytt -f deployment.yml -f namespace.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo
  namespace: demo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: demo
  template:
    metadata:
      labels:
        app: demo
    spec:
      containers:
      - image: making/hello-cnb
        name: hello-cnb
---
apiVersion: v1
kind: Service
metadata:
  name: demo
  namespace: demo
spec:
  ports:
  - name: 80-8080
    port: 80
    protocol: TCP
    targetPort: 8080
  selector:
    app: demo
  type: LoadBalancer
---
apiVersion: v1
kind: Namespace
metadata:
  name: demo
  namespace: demo
```

`Namespace`の`metadata`に`namespace: demo`が含まれるのが変だと感じる場合は、次のように`overlay.all`ではなく、`overlay.not_op`を使い、`Kind`が`Namespace`の場合を除外します。

```yaml
#@ load("@ytt:overlay", "overlay")
apiVersion: v1
kind: Namespace
metadata:
  name: demo
#@overlay/match by=overlay.not_op(overlay.subset({"kind": "Namespace"})), expects="1+"
---
metadata:
  #@overlay/match missing_ok=True
  namespace: demo
```

次のコマンドで、`Namespace`の定義に`namespace: demo`が含まれていないことを確認してください。

```
$ ytt -f deployment.yml -f namespace.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo
  namespace: demo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: demo
  template:
    metadata:
      labels:
        app: demo
    spec:
      containers:
      - image: making/hello-cnb
        name: hello-cnb
---
apiVersion: v1
kind: Service
metadata:
  name: demo
  namespace: demo
spec:
  ports:
  - name: 80-8080
    port: 80
    protocol: TCP
    targetPort: 8080
  selector:
    app: demo
  type: LoadBalancer
---
apiVersion: v1
kind: Namespace
metadata:
  name: demo
```

#### 値の削除

次は削除します。次の`remove-service-type.yml`は`Service`の`type`を削除します。

削除したい要素には`#@overlay/remove`アノテーションをつけます。値はなんでも良いです。

```yaml
#@ load("@ytt:overlay", "overlay")

#@overlay/match by=overlay.subset({"kind":"Service", "metadata": {"name": "demo"}})
---
spec:
  #@overlay/remove
  type: 
```

次のコマンドで、`Service`の`type`が削除されていることを確認してください。

```
$ ytt -f deployment.yml -f remove-service-type.yml 
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: demo
  template:
    metadata:
      labels:
        app: demo
    spec:
      containers:
      - image: making/hello-cnb
        name: hello-cnb
---
apiVersion: v1
kind: Service
metadata:
  name: demo
spec:
  ports:
  - name: 80-8080
    port: 80
    protocol: TCP
    targetPort: 8080
  selector:
    app: demo
```

### List内のMapの操作

次にList内のMapを操作する例を見ます。

この場合Listの中のどの要素を操作するのかを`#@overlay/match`アノテーションで指定する必要があります。

#### 値の変更

次の`change-image.yml`では`image`の値を変更します。
`containers`がListなので、このうちのどの要素をoverlayの対象かを指定する必要があります。

`@overlay/match by="name"`アノテーションをつけると、`containers`のうち`name`が合致するものを対象とします。
次の例では`name`が`hello-cnb`な要素の`image`を`harbor.example.com/demo/hello-cnb`に変更します。

```
#@ load("@ytt:overlay", "overlay")

#@overlay/match by=overlay.subset({"kind":"Deployment", "metadata": {"name": "demo"}})
---
spec:
  template:
    spec:
      containers:
      #@overlay/match by="name"
      - name: hello-cnb
        image: harbor.example.com/demo/hello-cnb
```

次のコマンドを実行して、`image`の変更を確認してください。

```
$ ytt -f deployment.yml -f change-image.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: demo
  template:
    metadata:
      labels:
        app: demo
    spec:
      containers:
      - image: harbor.example.com/demo/hello-cnb
        name: hello-cnb
---
apiVersion: v1
kind: Service
metadata:
  name: demo
spec:
  ports:
  - name: 80-8080
    port: 80
    protocol: TCP
    targetPort: 8080
  selector:
    app: demo
  type: LoadBalancer
```

#### 値の追加

次にList内のMapに値を追加する例を見ます。
次の`demo-env.yml`は`containers`List中の`name`が`hello-cnb`なMapに`env`を追加します。
元のファイルに存在しない場合は、前述の例と同様に`#@overlay/match missing_ok=True`をつけます。

なお、追加する`env`もMapのListです。元のファイルに`env`が定義されており、
かつその中に`name`が`INFO_MESSAGE`の要素がない場合にエラーにならないように、`#@overlay/match by="name", missing_ok=True`を付けます。

```yaml
#@ load("@ytt:overlay", "overlay")

#@overlay/match by=overlay.subset({"kind":"Deployment", "metadata": {"name": "demo"}})
---
spec:
  template:
    spec:
      containers:
      #@overlay/match by="name"
      - name: hello-cnb
        #@overlay/match missing_ok=True
        env:
        #@overlay/match by="name", missing_ok=True
        - name: INFO_MESSAGE
          value: "Hello World!"
```

次のコマンドを実行して、`env`が追加されていることを確認してください。

```
$ ytt -f deployment.yml -f demo-env.yml 
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: demo
  template:
    metadata:
      labels:
        app: demo
    spec:
      containers:
      - image: making/hello-cnb
        name: hello-cnb
        env:
        - name: INFO_MESSAGE
          value: Hello World!
---
apiVersion: v1
kind: Service
metadata:
  name: demo
spec:
  ports:
  - name: 80-8080
    port: 80
    protocol: TCP
    targetPort: 8080
  selector:
    app: demo
  type: LoadBalancer
```

次の`sidecar.yml`では`containers`Listに値を追加します。これも同じく`#@overlay/match by="name" missing_ok=True`を付けています。

```yaml
#! sidecar.yml

#@ load("@ytt:overlay", "overlay")

#@overlay/match by=overlay.subset({"kind":"Deployment", "metadata": {"name": "demo"}})
---
spec:
  template:
    spec:
      containers:
      #@overlay/match by="name" missing_ok=True
      - name: hello-sidecar
        image: busybox
        command: ['sh', '-c', 'echo Hello ytt! && sleep 3600']
```

次のコマンドを実行して、`containers`が更新されていることを確認してください。

```
$ ytt -f deployment.yml -f sidecar.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: demo
  template:
    metadata:
      labels:
        app: demo
    spec:
      containers:
      - image: making/hello-cnb
        name: hello-cnb
      - name: hello-sidecar
        image: busybox
        command:
        - sh
        - -c
        - echo Hello ytt! && sleep 3600
---
apiVersion: v1
kind: Service
metadata:
  name: demo
spec:
  ports:
  - name: 80-8080
    port: 80
    protocol: TCP
    targetPort: 8080
  selector:
    app: demo
  type: LoadBalancer
```

#### 値の削除

次はList内のMapの値を削除します。次の`remove-service-targetport.yml`は`Service`の`ports`List内の`targetPort`を削除します。
前述の例と同じく、`@overlay/remove`アノテーションを付けます。

```yaml
#@ load("@ytt:overlay", "overlay")

#@overlay/match by=overlay.subset({"kind":"Service", "metadata": {"name": "demo"}})
---
spec:
  ports:
  #@overlay/match by="name"
  - name: 80-8080
    #@overlay/remove
    targetPort:
```

次のコマンドを実行して、`targetPort`が削除されていることを確認してください。

```
$ ytt -f deployment.yml -f remove-service-targetport.yml 
apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: demo
  template:
    metadata:
      labels:
        app: demo
    spec:
      containers:
      - image: making/hello-cnb
        name: hello-cnb
---
apiVersion: v1
kind: Service
metadata:
  name: demo
spec:
  ports:
  - name: 80-8080
    port: 80
    protocol: TCP
  selector:
    app: demo
  type: LoadBalancer
```

### Listの操作

最後に普通のListの操作を見ます。

#### 値の変更

ここではCloud Foundryのmanifest fileである、次の`manifest.yml`を例に使います。

```yaml
applications:
- name: hello
  memory: 1g
  path: hello.jar
  buildpacks:
  - java_buildpack
```

次の`replace-buildpack.yml`は`buildpacks` Listの中の`java_buildpack`を`java_buildpack_offline`に変えます。

MapのListの場合と異なり、普通のListを操作する場合は、どん要素が対象であるかを示すために、
`@overlay/match`の`by`でListの値そのものと比較するか、Listのindexを指定する必要があります。

値そのものを差し替えるために`@overlay/replace`を付けます。(デフォルトは`@overlay/merge`です。)

```yaml
#@ load("@ytt:overlay", "overlay")

#@overlay/match by=overlay.all
---
applications:
#@overlay/match by=overlay.all
- buildpacks:
  #@overlay/match by=overlay.subset("java_buildpack")
  #@overlay/replace
  - java_buildpack_offline
```

次のコマンドを実行して、`buildpacks`の値が変更されていることを確認してください。

```
$ ytt -f manifest.yml -f replace-buildpack.yml
applications:
- name: hello
  memory: 1g
  path: hello.jar
  buildpacks:
  - java_buildpack_offline
```

#### 値の追加

次の`insert-buildpack.yml`は`buildpacks` Listの中の先頭に`datadog_buildpack`を追加します。
`@overlay/insert before=True`アノテーションを使います。

```yaml
#@ load("@ytt:overlay", "overlay")

#@overlay/match by=overlay.all
---
applications:
#@overlay/match by=overlay.all
- buildpacks:
  #@overlay/match by=overlay.index(0)
  #@overlay/insert before=True
  - datadog_buildpack
```

次のコマンドを実行して、`buildpacks`に値が追加されていることを確認してください。

```
$ ytt -f manifest.yml -f insert-buildpack.yml
applications:
- name: hello
  memory: 1g
  path: hello.jar
  buildpacks:
  - datadog_buildpack
  - java_buildpack
```

#### 値の削除

次の`remove-buildpack.yml`は`buildpacks` Listの中の`java_buildpack`を削除します。
`@overlay/remove`アノテーションを使います。

```yaml
#@ load("@ytt:overlay", "overlay")

#@overlay/match by=overlay.all
---
applications:
#@overlay/match by=overlay.all
- buildpacks:
  #@overlay/match by=overlay.subset("java_buildpack")
  #@overlay/remove
  -
```

次のコマンドを実行して、`buildpacks`の値が削除されていることを確認してください。

```
$ ytt -f manifest.yml -f remove-buildpack.yml
applications:
- name: hello
  memory: 1g
  path: hello.jar
  buildpacks: []
```

---

yttのOverlayの色々なパターンを見てきました。

より実践的な利用例が見たい場合は、cf-for-k8s及びそのサブプロジェクトでyttが多用されているので、参考にすると良いです。

* https://github.com/cloudfoundry/cf-for-k8s/tree/master/config
* https://github.com/cloudfoundry/cf-k8s-networking/tree/develop/config
* https://github.com/cloudfoundry/cf-k8s-logging/tree/main/config
* https://github.com/cloudfoundry/capi-k8s-release/tree/master/templates

cf-for-k8sがなぜ広く採用されているHelmやKustomizeを使用しないかという[issue](https://github.com/cloudfoundry/cf-for-k8s/issues/265)があります。
これに対して、Joe Beda氏が[コメント](https://github.com/cloudfoundry/cf-for-k8s/issues/265#issuecomment-672371656)しているので一読の価値があります。
