サーバレスなCIツール Tektonでスケーラブルなビルドを試す
はじめに
以前、K8s上で動くサーバレスなCIツールとしてTekton Pipelineを紹介しました。あれから随分間が空いてしまいましたが、今回はGitやDockerとの連携など基本的なビルドを実際に行い、最後にスケーラビリティに関して確認したいと思います。
TL;DR
- TektonはサーバレスなCIツール
- ビルダーがコンテナなのでJavaやGoなんでもビルド可能
- k8sへのビルド/デプロイのみに限定したCIツールではない
- Autopilotではdind(Docker)よりKanikoでビルド
- とりあえず21ノード(200 vCPU)までは雑にスケール出来た
Tekton環境の準備
前回と同様にGKE Autopilotを利用して、Tektonの環境を構築します。k8sクラスタの作り方にはTekton自体はあまり依存しないので、GKE StandardでもEKSでもRancherでも好きな環境を使えると思います。
PROJECT_ID=$(gcloud config configurations list | grep True | awk '{print $4}')
CLUSTER=mycluster
REGION=us-central1
gcloud container clusters create-auto mycluster --create-subnetwork name=gke --region us-central1
gcloud container clusters get-credentials $CLUSTER --region $REGION --project $PROJECT_ID
gcloud auth configure-docker
続いてTekton Pipeline, Tekton Dashboardをインストール。
kubectl apply --filename https://storage.googleapis.com/tekton-releases/pipeline/latest/release.yaml
kubectl apply --filename https://github.com/tektoncd/dashboard/releases/latest/download/tekton-dashboard-release.yaml
Tekton CLIは以下の開発環境用コンテナに入れているのでこちらを使います。
$ docker run -it -v $HOME:/home/user -v /var/run/docker.sock:/var/run/docker.sock koduki/env-dev4tekton
user@5297fd802e46:~$ tkn version
Client version: 0.22.0
Pipeline version: v0.32.1
DashboardにアクセスするためのProxyの立ち上げなどはこちらを参考にしてください。
始める前に少しアーキテクチャの話
これからTektonでのビルドを行っていきますが、いくつか新しい概念/単語もでるので、まずはTektonの基本的な構造を少し整理したいと思います。
Tektonではビルドの基本的な単位はTaskです。このYAMLファイルにStepという形で実際のビルドスクリプトを記載していきます。Stepは ひとつひとつが別のコンテナ になっておりJavaでもGoでも任意のビルダーを利用することが出来ます。GCPのCloudBuildをはじめよくある奴ですね。最近のCIっぽい。Taskは実態としてはPodにマッピングされていて、同じTaskの中であれば/workspace
を使ってファイルを共有出来ます。gitで取得したソースコードやビルドの成果物をここに置くことでStep間でデータのやり取りを出来ます。
複数のTaskを束ね複雑な処理を実行するのがPipelineです。いわゆるワークフローエンジンでね。PipelineはTaskを超えて、つまりPodを超えて実行されます。よってファイル連携をするにはPersistentVolumeなどを使う必要がありますが、TektonにはWorkspacesという機能があり、ビルドに紐付いた形で自動的にボリュームを確保、マウントが出来ます。
最後にRun
という概念があります。RunはTaskやPipelineにコンテキストを与え実行をします。プログラミング言語っぽく言うなら、TaskやPipelineがクラスでTaskRunやPipelineRunがインスタンスですね。Runではビルドを実行するサービスアカウントを指定したり、SpotVMの利用の有無、各種パラメータを指定することが出来ます。
まとめると関係性は以下のようになります。
Entity | Description |
---|---|
Step | Builder。実態としてはコンテナとなっており任意のイメージを使ってビルド処理を実行する |
Task | Tektonのビルドの基本単位。実態としてはPod.一つ以上のStepの集合 |
Pipeline | ワークフロー。複数のTaskを束ねて順序や条件に基づいて実行 |
TaskRun | Taskを直接実行する際の実行コンテキストを与える。実行ユーザやリソース、パラメータなどを指定 |
PipelineRun | Pipelineを実行する場合の実行コンテキスト。実行ユーザやリソース、パラメータなどを指定 |
ちなみに今回の記事の中ではWorkspacesやPipelineは扱いません。
Git clone & Build
まずは最も基本的なシナリオであるGitHubからcloneしてBuildをするケースを作成します。Taskは以下のようなマニフェストを書いてtask_sample-java-build.yaml
で保存します。
apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
name: sample-java-build
description: An example of Java Build with maven
spec:
params:
- name: repo-url
type: string
description: The git repo URL to clone from.
steps:
- name: git-clone
image: alpine/git
workingDir: /workspace
script: |
#!/usr/bin/env sh
echo "repos-url: "$(params.repo-url)
git clone $(params.repo-url)
- name: java-build
image: openjdk
workingDir: /workspace
script: |
#!/usr/bin/env sh
cd quarkus-quickstarts/getting-started
./mvnw package
params.repo-url
でパラメータとして渡されたURLをcloneして、Mavenでビルドしています。stepsの中のimage
を変える事で任意のビルダーが利用出来ます。TaskをTektonに反映するにはマニフェストをApplyします。
$ kubectl apply -f task_sample-java-build.yaml
taskrun.tekton.dev/echo-hello-world-task-run created
$ $ tkn task describe sample-java-build
Name: sample-java-build
Namespace: default
⚓ Params
NAME TYPE DESCRIPTION DEFAULT VALUE
∙ repo-url string The git repo URL to... ---
🦶 Steps
∙ git-clone
∙ java-build
🗂 Taskruns
つづいて実行のコンテキストであるTaskRunは以下のようになります。
apiVersion: tekton.dev/v1beta1
kind: TaskRun
metadata:
name: run-sample-java-build
spec:
taskRef:
name: sample-java-build
params:
- name: repo-url
value: https://github.com/quarkusio/quarkus-quickstarts.git
taskRef
で対応するTaskを記載しparams
で先程Taskの中で宣言したパラメータの具体的な値を渡しています。Taskと同じく以下のようにマニフェストをApplyしますが このタイミングでビルドジョブが走る という点に注意をしてください。クラスを初期化するイメージ。
$ kubectl apply -f taskrun_sample-java-build.yaml
taskrun.tekton.dev/run-sample-java-build created
$ tkn taskrun logs run-sample-java-build -f
さて、本来であればここでただちに実行されるのですが、GKE Autopilotの場合は少し時間がかかるケースが多いかと思います。
user@d2908acc5680:~/git/example-tekton$ kubectl get pods
NAME READY STATUS RESTARTS AGE
run-sample-java-build-pod 0/4 Pending 0 9s
というのもノード数が既存のPod数に最適化されているのでTaskを実行するのに必要なリソースが足りないんですよね。ExceededNodeResources
と以下のように出るはず。
ただ、心配は不要で以下のようにPodのイベントログを見るとTriggeredScaleUp
が発生しているはずです。
ここから数分でノードが自動で追加され正常にビルドが完了するかと思います。この辺はAutoScaleの設定をしてる環境なら似たような動きになると思いますが、クイックに実行したいなら最低ノード数に余裕を持たせると良いのかもしれません。
コンテナイメージを作成してDockerHubにPush
GitHubからコードを取得してMavenでビルドするという事が出来ました。ただ、先程のサンプルだと最終的に生成されたJARファイルもコンテナの中に置き去りで実用的ではありません。適当なMavenリポジトリやS3やGCSに格納しても良いのですが、せっかくなのでコンテナイメージとしてDockerHubにPushをしてみます。
Kanikoによるコンテナイメージの作成
コンテナイメージを作るならビルダーはDockerで! と言いたいところですが、ちょっと注意が必要です。というのもビルダー自体がコンテナなのでDockerコマンドを使おうとすると Docker in Docker (dind) になるんですね。TektonにもSidecarでdindを起動してDockerをTekton Taskの中で実行する方法はあります。しかしながら、その場合でも当然privileged
を取る必要が出てきます。残念ながらGKE Autopilotでは2022/06現在ではprivilegedを有効に出来ません。そもそも、buildやpushのためだけにdindするのもちょっとToo Muchな感じがしますよね? そんな時に便利なのがKanikoです。検索候補に某プロレタリア文学が出てくる感じのツールですが、Googleが開発したCI環境に特化したコンテナビルドツールです。DockerlessでBuildとPushを実施できるため、dindを考慮する必要がなく、Tektonのようなコンテナを基盤とするビルド環境に非常にマッチしています。
DockerHubへのクレデンシャルの準備
今回、DockerHubへのPushを想定しているので、例えばPublicなレジストリだとしても認証情報が必要です。Tektonではk8sのSecret
とServiceAccount
を利用することでセキュアに認証情報を運用出来ます。
まず、以下のようにDockerHubの認証情報をSecretに登録します。
kubectl create secret docker-registry docker-credential
--docker-server=https://index.docker.io/v1/
--docker-username={ID}
--docker-password={パスワード}
パスワードはDokerHubのセキュリティ設定からTekton向けにアクセストークンを払い出すのがお勧めです。公式ページにあるようにYAMLにすることも出来ますが、オペミスでGitに登録してしまうのも怖いのであえてコマンドに留めています。また、公式ではdocker-server
がhttps://gcr.io
になっているのですが、今回試した限りでは実際にアクセスしているのはindex.docker.io
なので修正してやる必要がありました。
続いて上記を使うサービスアカウントを作成します。sa-tektonbot.yaml
の名前で保存します。
apiVersion: v1
kind: ServiceAccount
metadata:
name: build-bot
secrets:
- name: docker-credential
以下のように反映します。先程作成したdocker-credentialもマッピングされていることが分かりますね。
$ kubectl apply -f sa-tektonbot.yaml
$ kubectl describe sa build-bot
kubernetes/kubectl-auth-changes-in-gke
Name: build-bot
Namespace: default
Labels: <none>
Annotations: <none>
Image pull secrets: <none>
Mountable secrets: build-bot-token-hp2g8
docker-credential
Tokens: build-bot-token-hp2g8
Events: <none>
ここで作ったサービスアカウントをTaskRunやPipelineRunに指定することで認証情報を紐づける事が出来ます。
DockerHub:PushをTaskとTaskRunへ追加
それではTaskとTaskRunを修正してコンテナのBuildとPushに対応させましょう。まずはTaskです。
Javaのビルドまでは変わらないのですが一応全部を載せています。またcontainer-pre
は今回アドホックにDockerfileを作ったので追加したStepですが通常はDockerfileはコードと同じ場所に格納済みだと思うので不要なステップとなります。container-build-and-push
のみが本質的な追加部分です。
apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
name: sample-java-build
description: An example of Java Build with maven
spec:
params:
- name: repo-url
type: string
description: The git repo URL to clone from.
steps:
- name: git-clone
image: alpine/git
workingDir: /workspace
script: |
#!/usr/bin/env sh
echo "repos-url: "$(params.repo-url)
git clone $(params.repo-url)
- name: java-build
image: openjdk
workingDir: /workspace
script: |
#!/usr/bin/env sh
cd quarkus-quickstarts/getting-started
./mvnw package
- name: container-pre
image: debian
workingDir: /workspace
script: |
#!/usr/bin/env sh
cd quarkus-quickstarts/getting-started
cat << EOF > ./Dockerfile
FROM openjdk
RUN mkdir /app
ADD target/quarkus-app /app
CMD [ "java", "-jar", "/app/quarkus-run.jar"]
EOF
- name: container-build-and-push
image: gcr.io/kaniko-project/executor:latest
workingDir: /workspace
env:
- name: "DOCKER_CONFIG"
value: "/root/.docker"
command:
- /kaniko/executor
args:
- --dockerfile=Dockerfile
- --destination=koduki/getting-started
- --context=quarkus-quickstarts/getting-started/
ビルダーにはgcr.io/kaniko-project/executor:latest
を利用しています。今までとは違いstep
でscript
を書くのではなくcommand
とargs
を指定しています。すでに整ったビルダーであればこちらの方がブレがなくて良いと思います。
注意したいのがenv
でDOCKER_CONFIG
を指定しているところです。TaskRunに先程作成したサービスアカウントをマッピングすると、step毎に$HOME/.docker/config.json
が作成され、認証情報が書き込まれます。しかしKaniko自体はデフォルトでは/kaniko/.docker/config.json
を見に行こうとするため認証エラーになります。なのでTektonによって作られたconfig.jsonを見るように環境変数のDOCKER_CONFIG
を変更してやる必要があります。
この辺りKaniko自体の挙動に慣れてないと分かりづらいのでね。個人的にはちょっとハマりました。以下のように手元の環境でKanikoのデバックイメージを実行して中身を確認するのも良いと思います。
$ docker run -it -v `pwd`:/workspace --entrypoint=/busybox/sh gcr.io/kaniko-project/executor:debug
TaskRunは以下のように修正します。先程との違いはサービスアカウントを指定してるだけなので、修正は簡単ですね。
apiVersion: tekton.dev/v1beta1
kind: TaskRun
metadata:
name: run-sample-java-build
spec:
serviceAccountName: build-bot
taskRef:
name: sample-java-build
params:
- name: repo-url
value: https://github.com/quarkusio/quarkus-quickstarts.git
それでは実行してみます。確実に反映されるように念のため先ほどのTaskRunを削除しておきます。
tkn taskrun delete run-sample-java-build
kubectl apply -f task_sample-java-build.yaml -f taskrun_sample-java-build.yaml
Dashboadで実行完了が確認できます。
DockerHubにも登録されていることが確認できるかと思います。
スケーラビリティの実験
Tektonの特徴はサーバレスでスケーラブルな所です。GKE Autopilotでノード周りのスケールもお任せなので、その辺の挙動を試してみたいと思います。
今まで、ビルドジョブを1ジョブずつ流していましたがもっと大量に同時実行してみます。とりあえず100ジョブくらい同時に流してみましょう。
実行前にnodeの状態を確認します。どうやら5ノード起動しているようです。
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
gk3-mycluster-default-pool-adc39573-6w23 Ready <none> 14h v1.22.8-gke.202
gk3-mycluster-default-pool-adc39573-kh42 Ready <none> 14h v1.22.8-gke.202
gk3-mycluster-default-pool-afaf435e-l1zq Ready <none> 14h v1.22.8-gke.202
gk3-mycluster-default-pool-afaf435e-zkd6 Ready <none> 14h v1.22.8-gke.202
gk3-mycluster-nap-sa06pmyq-92873cab-fxl5 Ready <none> 11h v1.22.8-gke.202
それでは実行をします。流石に手でポチポチ作るのは面倒なので、既存のTaskRunの名前をスクリプトで変換して100個ほど投げます。
$ echo {1..100}| tr ' ' '\n'|awk '{print "cat taskrun_sample-java-build.yaml | sed q;qs/run-sample-java-build/run-sample-java-build"$1"/gq;q > taskrun_sample-java-build$1".yaml"}'|sed 's/q;q/"/g'|sh
$ ls -1 *.yaml|xargs -n1 -I {} kubectl apply -f {}
ノードの状態を見てみましょう。流石に100ノードは出来ませんでしたが21ノード程作られました。203.25 vCPUを作ったせいで何かのリミットに引っかかっているようですね。この辺はGKEの設定しだいかな、と。
Pendding中のものも多いですが、以下のように一気に実行されています。
user@d2908acc5680:~/git/example-tekton/tmp$ tkn taskrun list
NAME STARTED DURATION STATUS
run-sample-java-build99 6 minutes ago --- Running
run-sample-java-build98 6 minutes ago --- Running
run-sample-java-build97 6 minutes ago --- Running
run-sample-java-build96 6 minutes ago 5 minutes Succeeded
run-sample-java-build95 6 minutes ago 5 minutes Succeeded
run-sample-java-build94 6 minutes ago 5 minutes Succeeded
run-sample-java-build92 6 minutes ago 4 minutes Succeeded
run-sample-java-build93 6 minutes ago 5 minutes Succeeded
run-sample-java-build91 6 minutes ago 3 minutes Succeeded
run-sample-java-build90 6 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build9 6 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build89 6 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build87 6 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build88 6 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build86 6 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build85 6 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build84 6 minutes ago 3 minutes Succeeded
run-sample-java-build83 6 minutes ago 3 minutes Succeeded
run-sample-java-build82 6 minutes ago 3 minutes Succeeded
run-sample-java-build81 6 minutes ago 4 minutes Succeeded
run-sample-java-build80 6 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build8 6 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build79 6 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build78 6 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build77 6 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build76 6 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build75 6 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build74 6 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build73 6 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build72 6 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build71 6 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build70 6 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build7 6 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build69 6 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build68 6 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build67 6 minutes ago 4 minutes Succeeded
run-sample-java-build66 6 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build65 6 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build64 6 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build63 6 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build62 6 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build61 6 minutes ago 4 minutes Succeeded
run-sample-java-build60 6 minutes ago 3 minutes Succeeded
run-sample-java-build6 6 minutes ago 3 minutes Succeeded
run-sample-java-build59 6 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build58 6 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build57 6 minutes ago --- Running
run-sample-java-build56 6 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build55 6 minutes ago 4 minutes Succeeded
run-sample-java-build54 6 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build53 6 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build52 6 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build51 6 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build50 6 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build5 6 minutes ago --- Running
run-sample-java-build49 6 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build48 6 minutes ago --- Running
run-sample-java-build47 6 minutes ago --- Running
run-sample-java-build46 6 minutes ago --- Running
run-sample-java-build45 6 minutes ago --- Running
run-sample-java-build44 6 minutes ago --- Running
run-sample-java-build43 6 minutes ago 4 minutes Succeeded
run-sample-java-build42 6 minutes ago --- Running
run-sample-java-build41 6 minutes ago --- Running
run-sample-java-build40 6 minutes ago --- Running
run-sample-java-build4 6 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build39 6 minutes ago --- Running
run-sample-java-build38 6 minutes ago --- Running
run-sample-java-build37 6 minutes ago --- Running
run-sample-java-build36 7 minutes ago --- Running
run-sample-java-build35 7 minutes ago 4 minutes Succeeded
run-sample-java-build34 7 minutes ago 4 minutes Succeeded
run-sample-java-build33 7 minutes ago 4 minutes Succeeded
run-sample-java-build32 7 minutes ago 4 minutes Succeeded
run-sample-java-build31 7 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build30 7 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build3 7 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build29 7 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build28 7 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build27 7 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build26 7 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build25 7 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build24 7 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build23 7 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build22 7 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build21 7 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build20 7 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build2 7 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build19 7 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build18 7 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build17 7 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build16 7 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build15 7 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build13 7 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build14 7 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build12 7 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build11 7 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build100 7 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build10 7 minutes ago --- Pending(ExceededNodeResources)
run-sample-java-build1 7 minutes ago 5 minutes Succeeded
run-sample-java-build 7 minutes ago 3 minutes Succeeded
最終的には無事にすべての実行完了が確認できました。100並列とはさすがにいきませんでしたが、10や15の同時実行でも実際には十分でしょうし、GKEの設定次第ではもっと拡張出来そうですしね。
まとめ
今回はTektonを使ってGitHubからのClone、Javaのビルド、そしてコンテナイメージにビルドしてDockerHubへのPushを実施しました。GCPのCloudBuildと同じくビルダーをコンテナで作成 出来るので、JavaだろうがGoだろうがなんでもビルド出来るのが良いですね。k8sネイティブというとk8s以外へのビルドやデプロイが苦手そうなイメージですが、特にそんなことは無さそうです。
GKE Autopilotの相性もあって簡単にスケール出来るのも便利そうです。ただ、テストでコンテナでの実行を多用している場合はDocker in Dockerになってしまうので、AutopilotではなくStandardクラスタを利用するか、Dockerでは無くk8sでテストを実行するようにテストスクリプトの修正等が要りそうです。例えばJavaであれば TestContainerの代わりにk8sを使うコード とかもあるので、その辺を参考に対応するのもありかもです。
今度時間がある時にトリガーの挙動とかも見てみたいと思います。
それではHappy Hacking!
Discussion