最近 Continuous Delivery Foundation の発表で Tekton Pipelines というものがあるのを知り試してみました。 試してみて面白かったので、忘れないためにもやったことをまとめてみました。
Tekton Pipelines とは
すごく雑に説明すると Kubernetes 上で動く CI/CD パイプラインを提供してくれるようです。 元は Knative の一部で knative/build-pipeline というリポジトリだったのが、 Knative から外れて Tekton Pipelines になり Continuous Delivery Foundation にホストされるようになったみたいです。
試してみる
公式のチュートリアル を参考にしつつ、そのまま動かしても面白くないのでいろいろと変えて試します。
次のことを実行する Pipeline を組んでみます。
- gofmt でフォーマット済みかチェック
- kaniko で Docker イメージをビルドして
gcr.io
に push - Tekton Pipelines が動いているのと同じ Kubernetes クラスタ内にビルドしたアプリケーションをデプロイ
公式のチュートリアルでも kaniko でビルドしてデプロイまでをやっていますが、 gcr.io
に push する部分や、 gofmt でチェックしたりする部分は無いので触りつつ組みました。
Kubernetes クラスタを作る
試すためのクラスタを新規に作成します。 今回は GKE を使って n1-standard-1 を2台のクラスタを作成しました。 (最初1台で試したところ Insufficient cpu になってしまい、リソースを調整するのも面倒だったため)
gcloud container clusters create tekton-test \
--cluster-version=1.12.5-gke.5 \
--machine-type=n1-standard-1 \
--num-nodes=2 \
--zone=asia-northeast1-b
kubectl
コマンドを使って正常に動いているのを確認する。
$ kubectl get cs
NAME STATUS MESSAGE ERROR
etcd-1 Healthy {"health": "true"}
etcd-0 Healthy {"health": "true"}
controller-manager Healthy ok
scheduler Healthy ok
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
gke-tekton-test-default-pool-bd21f2eb-g54q Ready <none> 67s v1.12.5-gke.5
gke-tekton-test-default-pool-bd21f2eb-qnqg Ready <none> 67s v1.12.5-gke.5
また、あとで必要になると思うので cluster-admin になっておきます。
kubectl create clusterrolebinding cluster-admin-binding \
--clusterrole=cluster-admin \
--user=$(gcloud config get-value core/account)
Tekton Pipelines をインストールする
公式ドキュメント を参考にしてインストールするとリリース版(現時点では v0.1.0) がインストールされますが、 v0.1.0 以降に実装された機能も試したかったので master のをビルドしてインストールしました。
まずはリポジトリを clone してきます。
試した時点での最新は 3527be31e3cab81356532dc77e726c368d8769c4
でした。
git clone https://github.com/tektoncd/pipeline.git
cd pipeline
#git checkout 3527be31e3cab81356532dc77e726c368d8769c4
Tekton Pipelines は ko というツールを使っているようなので、インストールします。 (ko は Go アプリケーションをビルドして Kubernetes にデプロイするためのツールらしいです)
DEVELOPMENT.md によると github.com/google/go-containerregistry/cmd/ko
になっていますが、数日前に github.com/google/ko/cmd/ko
ができていたので、そっちをインストールしました。
go get -u github.com/google/ko/cmd/ko
ko を使って Tekton をビルドし apply します。 (clone してきた Tekton Pipelines のディレクトリで実行します)
KO_DOCKER_REPO=gcr.io/$(gcloud config get-value project) ko apply -f config/
少し待てば controller などが動いているのが確認できるはず。
$ kubectl get pods -n tekton-pipelines
NAME READY STATUS RESTARTS AGE
tekton-pipelines-controller-5b5b47ff99-lfh2m 1/1 Running 0 76s
tekton-pipelines-webhook-7d9cc5d77b-rfvcm 1/1 Running 0 76s
gcr.io
へ push するための credentials を準備
今回はビルドした Docker イメージを gcr.io
に push するので、そのために必要な GCP の Service account などを作成します。
# Service account の作成
gcloud iam service-accounts create tekton-test
# Service account が gcr.io に push できるように GCS への権限を追加
gcloud projects add-iam-policy-binding $(gcloud config get-value project) \
--member serviceAccount:tekton-test@$(gcloud config get-value project).iam.gserviceaccount.com \
--role roles/storage.admin
# Service account の key を作成
gcloud iam service-accounts keys create tekton-test-key.json \
--iam-account tekton-test@$(gcloud config get-value project).iam.gserviceaccount.com
Service account を作成した後は、 kaniko から使うために Secret を作成します。
kubectl create secret generic gcr-credentials --from-file=tekton-test-key.json
Task を定義する
今回は以下の4つの Task を作成します。
- gofmt (アプリに gofmt を実行し、 diff が発生した場合に fail する)
- kaniko (アプリをビルドして
gcr.io
に push します) - jsonnet (jsonnet から extVar でビルドした Docker イメージの URL をもらって json に変換します)
- deploy (3で生成した json ファイルを
kubectl apply
します)
gofmt
apiVersion: tekton.dev/v1alpha1
kind: Task
metadata:
name: gofmt
spec:
inputs:
resources:
- name: source-repo
type: git
params:
- name: path
description: Path to .go files
default: /workspace/source-repo
steps:
- name: gofmt
image: golang:1.12.1-alpine
command: ["sh"]
args: ["-c", 'test -z "$(gofmt -l ${inputs.params.path})"']
この Task では、 source-repo
(git リポジトリ)を渡され、 golang 公式イメージを使って指定された path
(デフォルトではリポジトリへのパスである /workspace/source-repo
) に対して gofmt を実行して diff があった場合には exit code 1 で終了します。 Task 定義内で ${inputs.params.path}
のように書くと、 Task を実行する側から入力された値に置き換えてくれるようです。
kaniko
apiVersion: tekton.dev/v1alpha1
kind: Task
metadata:
name: kaniko
spec:
inputs:
resources:
- name: source-repo
type: git
params:
- name: dockerfile
description: Path to Dockerfile
default: /workspace/source-repo/Dockerfile
- name: context
description: Path to build context
default: /workspace/source-repo
- name: credentialsSecretName
description: Secret resource name for gcr.io
- name: credentialsSecretKey
description: Secret resource key for gcr.io
outputs:
resources:
- name: destImage
type: image
steps:
- name: build-and-push
image: gcr.io/kaniko-project/executor
args:
- --dockerfile=${inputs.params.dockerfile}
- --context=${inputs.params.context}
- --destination=${outputs.resources.destImage.url}
volumeMounts:
- name: kaniko-secret
mountPath: /secret
env:
- name: GOOGLE_APPLICATION_CREDENTIALS
value: /secret/${inputs.params.credentialsSecretKey}
volumes:
- name: kaniko-secret
secret:
secretName: ${inputs.params.credentialsSecretName}
この Task では source-repo
を受け取って deskImage
(Docker イメージ)をビルドします。 gcr.io
に push するために GCP の Service account の鍵を Secret で受け取る必要がありますが、 Tekton では Secret は resource として扱えないみたいなので、 Secret の名前とキーを params として受け取り、普通の Pod などと同じように volumeMounts しています。 Tekton では Secret 以外にも、 ConfigMap や PVC なども問題なく使うことができるようです。
jsonnet
workspace
を受け取り、 jsonnet コマンドを使って json ファイルを生成し保存します。
次の deploy タスクでは、その生成したファイルを使うために workspace
を outputs としています。
Task 自体は gofmt や kaniko で書いたのと同じような感じなので、実際の定義は省略します。 (GitHub に置いてあるので、みたい場合はそちらへ)
deploy
jsonnet で生成した json ファイルを kubectl apply
で apply します。
Task の実行時に ServiceAccount を指定するため、認証などの設定は必要ありません。
(こちらも実際の定義は GitHub で)
Pipeline を定義する
Task は特定のアプリに依存せず、汎用的に使えるように定義しました。 次はその Task を組み合わせてアプリをビルドしてデプロイするまでの Pipeline を作ります。
今回は Go で作ったシンプルな Hello World アプリ (github.com/takonomura/tekton-test/test-app) の Pipeline を作ります。
kaniko などの Task はリポジトリ直下に Dockerfile などがあるのを想定してデフォルト値を指定しましたが、今回のアプリはリポジトリの中の /test-app
にあるので、それも指定します。
apiVersion: tekton.dev/v1alpha1
kind: Pipeline
metadata:
name: test-app
spec:
params:
- name: credentialsSecretName
description: Secret resource name for gcr.io
- name: credentialsSecretKey
description: Secret resource key for gcr.io
resources:
- name: test-repo
type: git
- name: image
type: image
tasks:
- name: gofmt
taskRef:
name: gofmt
params:
- name: path
value: /workspace/source-repo/test-app
resources:
inputs:
- name: source-repo
resource: test-repo
- name: kaniko
taskRef:
name: kaniko
params:
- name: dockerfile
value: /workspace/source-repo/test-app/Dockerfile
- name: context
value: /workspace/source-repo/test-app
- name: credentialsSecretName
value: "${params.credentialsSecretName}"
- name: credentialsSecretKey
value: "${params.credentialsSecretKey}"
resources:
inputs:
- name: source-repo
resource: test-repo
outputs:
- name: destImage
resource: image
- name: jsonnet
taskRef:
name: jsonnet
resources:
inputs:
- name: workspace
resource: test-repo
- name: image
resource: image
from: [kaniko]
outputs:
- name: workspace
resource: test-repo
params:
- name: path
value: /workspace/test-app/manifest.jsonnet
- name: outputPath
value: /workspace/test-app/manifest.json
- name: deploy
taskRef:
name: deploy
resources:
inputs:
- name: workspace
resource: test-repo
from: [jsonnet]
params:
- name: path
value: /workspace/test-app/manifest.json
- リポジトリやイメージなど実行環境によって変わる resources は Pipeline を実行時に指定してもらうことになります
- 同じく
gcr.io
に push するための Secret も params として受け取ります (受け取った値は kaniko Task に渡します) - 各 Task には必要になる resources や params を指定しています
- kaniko でビルドしたイメージは jsonnet で使うので、 jsonnet では
from: [kaniko]
で受け取っています - 同じように jsonnet で書き換えたワークスペースも deploy に
from: [jsonnet]
で渡しています
- kaniko でビルドしたイメージは jsonnet で使うので、 jsonnet では
PipelineResource を定義する
今まで何度も登場してきた git リポジトリや Docker イメージなどは、 Tekton では PipelineResource
として扱われます。
Pipeline の実行には要求される PipelineResource が必要なので定義します。
apiVersion: tekton.dev/v1alpha1
kind: PipelineResource
metadata:
name: test-git-repo
spec:
type: git
params:
- name: url
value: https://github.com/takonomura/tekton-test
- name: revision
value: master
git リポジトリでは url と revision を指定します。今回は master ブランチを選択しました。
revision に pull/100/head
などを指定することで、 PR を指定することも可能なようです。
apiVersion: tekton.dev/v1alpha1
kind: PipelineResource
metadata:
name: test-app-image
spec:
type: image
params:
- name: url
value: gcr.io/YOUR_PROJECT_ID/tekton-test/test-app
Docker イメージでも url を指定します。 (YOUR_PROJECT_ID
は GCP の ID に置き換えてください)
tektoncd/pipeline#639 によれば、 outputs にイメージを指定した場合には、ビルドされたイメージの digest がその後の Task で使えるようになる仕様が計画されているようですが、現時点ではまだ無いので url のみを使います。 (なので現状では Task 間の依存関係を表す以上の意味はなさそうです)
Pipeline の実行
Task, Pipeline, PipelineResource を定義したので、実際に実行してみます。
PipelineRun
を作成することで実行されるようです。
apiVersion: tekton.dev/v1alpha1
kind: PipelineRun
metadata:
name: test-app
spec:
serviceAccount: deploy-test-app
pipelineRef:
name: test-app
trigger:
type: manual
params:
- name: credentialsSecretName
value: gcr-credentials
- name: credentialsSecretKey
value: tekton-test-key.json
resources:
- name: test-repo
resourceRef:
name: test-git-repo
- name: image
resourceRef:
name: test-app-image
kubectl で作成されたことを確認します。
$ kubectl get pipelineruns
NAME AGE
test-app 7s
gofmt と kaniko の TaskRun が作成され、実行されていることがわかります。
resource の関係などが無いため、 gofmt と kaniko は同時に実行されるようです。 (ドキュメントによると、 resource の from
や Pipeline での runAfter
の指定を元に組まれているようです)
$ kubectl get taskruns
NAME AGE
test-app-kaniko-zdm66 9s
test-app-gofmt-zctfj 9s
しばらくすると、 deploy まですべて作成されることがわかります。 (kaniko, jsonnet, deploy は順番に実行されています)
$ kubectl get taskruns
NAME AGE
test-app-kaniko-zdm66 5m
test-app-deploy-t5vt4 11s
test-app-gofmt-zctfj 5m
test-app-jsonnet-wrv25 25s
Pod を確認すると、アプリがデプロイされていることも確認できます。 各 TaskRun が 1 Pod になっているのもわかります。
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
test-app-7c8869cd5-4j922 1/1 Running 0 2m33s
test-app-kaniko-zdm66-pod-7a86ab 0/3 Completed 0 6m22s
test-app-deploy-t5vt4-pod-bd18d1 0/4 Completed 0 40s
test-app-gofmt-zctfj-pod-9d670a 0/3 Completed 0 6m24s
test-app-jsonnet-wrv25-pod-11ff6c 0/5 Completed 0 54s
gofmt で失敗させてみる
gofmt に失敗すると、それ以上 Task を実行しなくなりました。
kaniko は gofmt と同時に開始されているため実行されました。特に fail しても実行中の他の Task を止めるようなことはせず、最後まで実行されるようです。
kubectl describe
で確認すると、 Failed になっているのが確認できました。
$ kubectl get taskruns
NAME AGE
test-app-fail-kaniko-pbw8g 1m
test-app-fail-gofmt-jbvj2 1m
$ kubectl describe pipelinerun/test-app-fail
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning Failed 109s pipeline-controller TaskRun test-app-fail-gofmt-jbvj2 has failed
Normal PipelineRunSucceeded 32s (x5 over 109s) pipeline-controller PipelineRun completed successfully.
Pod を見てみる
TaskRun ごとに Pod が作成されているようですが、その Pod では何が行われているのか生成された Pod のspec を軽く見てみました。 実際の spec などを貼ると長くなってしまうので省略しますが、以下のことがわかりました。 (実際の実装までは確認しなかったので間違っているかもしれませんが)
- initContainers で entrypoint を emptyDir 内にコピーし、各コンテナはそれを使っていました
- その entrypoint は、実行が終わった後に emptyDir にファイルを作成しているようです
- ファイルが作成されるまで entrypoint が待つことで、 Task 内の step が順番に実行されるようになっているようです
- git なども Pod 内で clone されていました (revision もそのまま master で commit hash などにはなっていませんでした)
- PipelineRun 単位で PVC を作成されており、 jsonnet から deploy へのワークスペースの共有などにはその PVC にコピーすることで実現されていました
感想
PipelineResource や params を使って汎用的な Task を作り、柔軟に組み合わせて Pipeline を作れることがわかりました。 約1ヶ月前にリリースされた v0.1.0 では gofmt と kaniko が同時に実行されるようなことはなく順番に実行されたのが今の master では変わっているように、まだまだ機能などが増えていくことに期待しています。 現時点では「master ブランチに push されたら実行」のようなことができなさそうなため Tekton 単体では CI/CD として使うのは難しそうですが、そのうち PipelineTrigger のような機能が増えて、 push や cron などをトリガーに実行できるようになったら良さそうだなと思っています。