コンテナイメージを作り直した際、その更新契機で自動でスキャンやらデプロイをしたくなることがある。
今回は、イメージのbuildをk8sリソースとして扱えるkpackと、k8sのリソースの状態を入出力情報として別リソースに橋渡しできるCartographerを組み合わせて、コンテナイメージの更新契機でTektonのPipelineを実行し、スキャンやらデプロイが出来そうなところを確認する。
なお、Feasibility確認のためのメモなので、実際にスキャンとかデプロイは実施しない。
前提ソフトウェアのインストール
CartographerとTektonをk8sクラスタにインストールする。
今回は後で削除が楽にできるよう、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はテンプレートと呼ばれるManifestを生成する雛形と、テンプレートを組み合わせたClusterSupplyChain
、ClusterSupplyChain
を呼び出すWorkload
から構成される。
(それ以外もあるけど、今回はCartographerの説明が主ではないので割愛)
最終的にはkind: Workload
をapplyすることで、以下を自動で実行するものを作成する。
ClusterSupplyChainを呼び出し、テンプレートから kind:Image と kind:Pipeline を作成
├──> Imageのビルド
└──> Pipelineの呼び出し
├─-> Task1の実行 (Scan相当)
└─-> Task2の実行 (Deploy相当)
今回は以下の手順で作成していく。
- テンプレートの作成
- ClusterSupplyChainの作成
- Workloadの作成
テンプレートの作成
最初に、Gitのソースを元に自動でコンテナイメージを作成してくれるkind: Image
を生成するためのテンプレート(ClusterImageTemplate
)を作成する。
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
なお、ビルド元のコードはこちらからForkしたものを利用している。
metadataの$(workload.metadata.name)$
はCartographer固有のsimple templateという書き方である。詳細はこちらを参照のこと。
親リソースであるWorkload
のmetadata.name
から値を引っ張っていると考えておくとよい。
本当はtagやgitのurlもsimple templateまたはyttで置換する方がテンプレートっぽい使い方になるのだが、今回は検証が主たる目的なので時短のためにハードコーディングした。
実際に複数ユーザに使ってもらう場合は、これらの値は外から渡せるようにテンプレート化しよう。
また、ClusterBuilderは事前に作成している。作り方が分からない人はkpackのチュートリアルのBuilder
をClusterBuilder
に変更して作成する、もしくはTanzu Build Serviceを購入するとよい。
次に、TektonのPipelineを実行するためのテンプレートを作成する。
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というリソースのテンプレートとして定義する。
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で実装)。
最後に、スキャンとデプロイ(になる予定)のPipelineを定義する。
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の作成
ここでは、先ほど作成したテンプレートを使った、
- イメージのビルド
- パイプラインの呼び出し
の2つのみを呼び出す、シンプルなClusterSupplyChainを作成する。
作成したkind: ClusterSupplyChain
は以下となる。
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リソースを作成する。
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を利用する。
実行直後は以下ようになる。
$ 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コマンドでも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のサンプルは紹介できたかと思う。