コンテナイメージの更新契機でCartographer経由でTektonパイプラインを実行する。


コンテナイメージを作り直した際、その更新契機で自動でスキャンやらデプロイをしたくなることがある。\
今回は、イメージのbuildをk8sリソースとして扱える[kpack](https://github.com/pivotal/kpack)と、k8sのリソースの状態を入出力情報として別リソースに橋渡しできる[Cartographer](https://cartographer.sh/)を組み合わせて、コンテナイメージの更新契機で[Tekton](https://tekton.dev/)のPipelineを実行し、スキャンやらデプロイが出来そうなところを確認する。
なお、Feasibility確認のためのメモなので、実際にスキャンとかデプロイは実施しない。

## 前提ソフトウェアのインストール
CartographerとTektonをk8sクラスタにインストールする。\
今回は後で削除が楽にできるよう、[kapp](https://carvel.dev/kapp/)を使ってインストールしているが、kubectlで代用してもよい。

Cartographerのインストールは以下の手順となる。
```
kapp deploy -a cartographer -f https://github.com/vmware-tanzu/cartographer/releases/download/v0.4.2/cartographer.yaml -n default -y
```
Tektonのインストールは以下の手順となる。
```
kapp deploy -a tekton -f https://storage.googleapis.com/tekton-releases/pipeline/latest/release.yaml -n default -y
```

## リソースの作成
Cartographerは[テンプレート](https://cartographer.sh/docs/v0.4.0/reference/template/)と呼ばれるManifestを生成する雛形と、テンプレートを組み合わせた`ClusterSupplyChain`、`ClusterSupplyChain`を呼び出す`Workload`から構成される。\
（それ以外もあるけど、今回はCartographerの説明が主ではないので割愛）

最終的には`kind: Workload`をapplyすることで、以下を自動で実行するものを作成する。
```
ClusterSupplyChainを呼び出し、テンプレートから kind:Image と kind:Pipeline を作成
　　├──> Imageのビルド
　　└──> Pipelineの呼び出し
　　　　　  ├─-> Task1の実行 (Scan相当)
　　　　　  └─-> Task2の実行 (Deploy相当)
```

今回は以下の手順で作成していく。
1. テンプレートの作成
2. ClusterSupplyChainの作成
3. Workloadの作成

### テンプレートの作成
最初に、Gitのソースを元に自動でコンテナイメージを作成してくれる`kind: Image`を生成するためのテンプレート(`ClusterImageTemplate`)を作成する。
```yaml
apiVersion: carto.run/v1alpha1
kind: ClusterImageTemplate
metadata:
  name: image
spec:
  imagePath: .status.latestImage
  template:
    apiVersion: kpack.io/v1alpha1
    kind: Image
    metadata:
      name: $(workload.metadata.name)$
    spec:
      tag: myharbor.hoge/myapp/node-hello-world
      serviceAccount: default
      builder:
        kind: ClusterBuilder
        name: default
      source:
        git:
          revision: main
          url: https://mygitlab.hoge/myapp/node-hello-world.git
```
なお、ビルド元のコードは[こちら](https://github.com/IBM/node-hello-world)からForkしたものを利用している。

metadataの`$(workload.metadata.name)$`はCartographer固有のsimple templateという書き方である。詳細は[こちら](https://cartographer.sh/docs/v0.4.0/templating/)を参照のこと。\
親リソースである`Workload`の`metadata.name`から値を引っ張っていると考えておくとよい。\
本当はtagやgitのurlもsimple templateまたはyttで置換する方がテンプレートっぽい使い方になるのだが、今回は検証が主たる目的なので時短のためにハードコーディングした。\
実際に複数ユーザに使ってもらう場合は、これらの値は外から渡せるようにテンプレート化しよう。

また、ClusterBuilderは事前に作成している。作り方が分からない人は[kpackのチュートリアル](https://github.com/pivotal/kpack/blob/main/docs/tutorial.md)の`Builder`を`ClusterBuilder`に変更して作成する、もしくは[Tanzu Build Service](https://docs.vmware.com/en/VMware-Tanzu-Build-Service/index.html)を購入するとよい。

次に、TektonのPipelineを実行するためのテンプレートを作成する。
```yaml
apiVersion: carto.run/v1alpha1
kind: ClusterTemplate
metadata:
  name: app-deploy
spec:
  template:
    apiVersion: carto.run/v1alpha1
    kind: Runnable
    metadata:
      name: $(workload.metadata.name)$
    spec:
      serviceAccountName: default
      runTemplateRef:
        name: tekton-pipeline
      selector:
        resource:
          apiVersion: tekton.dev/v1beta1
          kind: Pipeline
        matchingLabels:
          pipeline: scan-and-deploy
      inputs:
        image-with-hash: $(image)$
---
apiVersion: carto.run/v1alpha1
kind: ClusterRunTemplate
metadata:
  name: tekton-pipeline
spec:
  template:
    apiVersion: tekton.dev/v1beta1
    kind: PipelineRun
    metadata:
      generateName: scan-and-deploy-
    spec:
      pipelineRef:
        name: scan-and-deploy
      params:
        - name: last-image
          value: $(runnable.spec.inputs.image-with-hash)$
```

`ClusterTemplate`はリソースのテンプレートであり、`spec.template`でどういうk8sリソースのテンプレートかを定義する。\
今回は[Runnable](https://cartographer.sh/docs/v0.4.0/tutorials/runnable/)というリソースのテンプレートとして定義する。\
`Runnable`はリソースのアップデートを検知して、リソースを再デプロイしなおすためのリソースで、`inputs`のフィールドで`ClusterImageTemplate`の`.status.latestImage`を受け取って、`runTemplateRef`で定義した参照元リソースに渡している（`image-with-hash`の部分は好きな名前でOK）。\
`runTemplateRef`のselectorは`kind: Pipeline`であり、すなわちTektonのPipelineにkpackで生成したImageの名前(`.status.latestImage`)を渡している。\
また、`PipelineRun`に`ClusterRunTemplate`で受け取ったimageを`params.value`で渡している。\
これにより以下のようなフローが実現できるようになる（イメージスキャン以降はTekton Pipelineで実装）。
```mermaid
graph TD;
    E[kpackによりイメージビルド]-->A;
    A["ビルドした際のImage名(Hash値込)"を元にイメージスキャン]-->B[ビルドしたImage名をManifestに反映];
    B-->C["更新イメージをデプロイ"];
```
最後に、スキャンとデプロイ(になる予定)のPipelineを定義する。
```yaml
apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
  name: scan-task
spec:
  params:
    - name: last-image
  steps:
    - name: scan-image
      image: centos
      command:
        - bash
        - -cxe
        - |-
          set -o pipefail
          echo "Scan $(params.last-image)"
          exit 0
---
apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
  name: deploy-task
spec:
  params:
    - name: last-image
  steps:
    - name: deploy-image
      image: centos
      command:
        - bash
        - -cxe
        - |-
          set -o pipefail
          echo "Deploy $(params.last-image)"
          exit 0
---
apiVersion: tekton.dev/v1beta1
kind: Pipeline
metadata:
  name: scan-and-deploy
  labels:
    pipeline: scan-and-deploy
spec:
  params:
  - name: last-image
  tasks:
  - name: scan
    taskRef:
      name: scan-task
    params:
    - name: last-image
      value: $(params.last-image)
  - name: deploy
    runAfter:
    - scan
    taskRef:
      name: deploy-task
    params:
    - name: last-image
      value: $(params.last-image)
```
`kind: Pipeline`のnameは`ClusterRunTemplate`の`PipelineRun`で記載した`pipelineRef`とあわせること。\
あと、一応Pipeline風に動作させるため、deployステージ相当の箇所には`runAfter`でscan後に動くよう設定した。

以上がテンプレート関連箇所のManifestとなる。

### ClusterSupplyChainの作成
ここでは、先ほど作成したテンプレートを使った、
1. イメージのビルド
2. パイプラインの呼び出し
の2つのみを呼び出す、シンプルなClusterSupplyChainを作成する。\
作成した`kind: ClusterSupplyChain`は以下となる。
```yaml
apiVersion: carto.run/v1alpha1
kind: ClusterSupplyChain
metadata:
  name: supply-chain
spec:
  selector:
    app.tanzu.vmware.com/workload-type: myapp
  resources:
    - name: image-builder
      templateRef:
        kind: ClusterImageTemplate
        name: image

    - name: deployer
      templateRef:
        kind: ClusterTemplate
        name: app-deploy
      images:
        - resource: image-builder
          name: image
```
selectorは後で作るworkloadのラベルと一致する必要がある。\
一致するworkloadが作成されると、このClusterSupplyChainが動作する。
deployer部分の`images`でimage-builderの結果をapp-deployという`ClusterTemplate`に渡している。

### Workloadの作成
最後にWorkloadリソースを作成する。
```yaml
apiVersion: carto.run/v1alpha1
kind: Workload
metadata:
  name: node-hello-world
  labels:
    app.tanzu.vmware.com/workload-type: myapp
spec:
  image: hogehoge
```
実は今回はWorkloadの入力を使わないので`spec.image`の箇所は何でもよい。\
なので、実際に確認した際もhogehogeとしている。\
`ClusterSupplyChain`をキックするのにラベルが必要なので、ここだけ間違いないようにする。

### 実行結果
テンプレート関連をapplyして、`ClusterSupplyChain`がreadyになったところで、`ClusterSupplyChain`をapplyする。\
これでk8sクラスタとしては準備OKなので、満を持して`Workload`をapplyして、リソースの状態を観察する。\
なお、今回は`Workload`から派生して作成されるk8sリソースをまとめて観察するために、kubectl pluginの[tree](https://github.com/ahmetb/kubectl-tree)を利用する。\
実行直後は以下ようになる。
```
$ kubectl tree workload node-hello-world
NAMESPACE  NAME                                              READY    REASON              AGE
default    Workload/node-hello-world                         Unknown  MissingValueAtPath  97s
default    └─Image/node-hello-world                          Unknown                      94s
default      ├─PersistentVolumeClaim/node-hello-world-cache  -                            94s
default      └─SourceResolver/node-hello-world-source        -                            94s
```
これはkpackによりImage作成中であり、ImageのlatestImageが空になっているため、`MissingValueAtPath`が表示されている。\
[kpコマンド](https://github.com/vmware-tanzu/kpack-cli)でもImage作成中なのが確認できる。
```
$ kp image list
NAME                READY      LATEST REASON    LATEST IMAGE    NAMESPACE
node-hello-world    Unknown    CONFIG                           default
```
少し待って`kp build logs`を実行すると、コンテナイメージのビルドの様子を見ることが出来る。
```
$ kp build logs node-hello-world
===> PREPARE
Build reason(s): CONFIG
CONFIG:
	resources: {}
	- source: {}
	+ source:
	+   git:
	+     revision: main
	+     url: https://gitlab.gitlab.hogeeee.info/imurata/node-hello-world.git
：（省略）
```
イメージの作成が終わった頃に再度workloadを取得すると、Pipelineに処理が遷移している事がわかる。
```
$ kubectl tree workload node-hello-world
NAMESPACE  NAME                                              READY  REASON        AGE
default    Workload/node-hello-world                         True   Ready         6m40s
default    ├─Image/node-hello-world                          True                 6m37s
default    │ ├─Build/node-hello-world-build-1                -                    2m16s
default    │ │ └─Pod/node-hello-world-build-1-build-pod      False  PodCompleted  2m15s
default    │ ├─PersistentVolumeClaim/node-hello-world-cache  -                    6m37s
default    │ └─SourceResolver/node-hello-world-source        True                 6m37s
default    └─Runnable/node-hello-world                       True   Ready         80s
default      └─PipelineRun/scan-and-deploy-48zk2             -                    75s
default        ├─TaskRun/scan-and-deploy-48zk2-deploy        -                    49s
default        │ └─Pod/scan-and-deploy-48zk2-deploy-pod      False  PodCompleted  49s
default        └─TaskRun/scan-and-deploy-48zk2-scan          -                    75s
default          └─Pod/scan-and-deploy-48zk2-scan-pod        False  PodCompleted  74s
```
Podの状態がFalseになっているが、これはJobのように動作してPod自体が終了しているためで、状態としては正しい。Podのログを見てみると、先程実装したTaskの処理が実行されていることが分かる。\
また、AGEを見るとscanの方が生存時間が長いため、scan->deployの順で動作したことが分かる。

実際の出力結果はこちら。
```
$ kubectl logs Pod/scan-and-deploy-48zk2-scan-pod
+ set -o pipefail
+ echo 'Scan myharbor.hoge/myapp/node-hello-world@sha256:5ee4288116e32ead948c1922295f1f66a81c8d4556c3bff5c53a6af6e3b946ef'
+ exit 0
Scan myharbor.hoge/myapp/node-hello-world@sha256:5ee4288116e32ead948c1922295f1f66a81c8d4556c3bff5c53a6af6e3b946ef
$ kubectl logs Pod/scan-and-deploy-48zk2-deploy-pod
+ set -o pipefail
+ echo 'Deploy myharbor.hoge/myapp/node-hello-world@sha256:5ee4288116e32ead948c1922295f1f66a81c8d4556c3bff5c53a6af6e3b946ef'
+ exit 0
Deploy myharbor.hoge/myapp/node-hello-world@sha256:5ee4288116e32ead948c1922295f1f66a81c8d4556c3bff5c53a6af6e3b946ef
```
無事イメージの更新契機でTektonのPipelineを走らせることが出来た。\
なお、この状態で`kp image trigger node-hello-world`で再度イメージを更新すると、パイプラインが再実行されることも確認できる。

今回はCartographerの肝である、yttやsimple templateを活用したテンプレートの汎用化までは触れなかったが、このようにテンプレートとClusterSupplyChainを作成することで、k8sのワークフローを作成できる、という1つのCartographerのサンプルは紹介できたかと思う。