最近 Continuous Delivery Foundation の発表で Tekton Pipelines というものがあるのを知り試してみました。 試してみて面白かったので、忘れないためにもやったことをまとめてみました。

Tekton Pipelines とは

すごく雑に説明すると Kubernetes 上で動く CI/CD パイプラインを提供してくれるようです。 元は Knative の一部で knative/build-pipeline というリポジトリだったのが、 Knative から外れて Tekton Pipelines になり Continuous Delivery Foundation にホストされるようになったみたいです。

試してみる

公式のチュートリアル を参考にしつつ、そのまま動かしても面白くないのでいろいろと変えて試します。

次のことを実行する Pipeline を組んでみます。

  1. gofmt でフォーマット済みかチェック
  2. kaniko で Docker イメージをビルドして gcr.io に push
  3. 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 を作成します。

  1. gofmt (アプリに gofmt を実行し、 diff が発生した場合に fail する)
  2. kaniko (アプリをビルドして gcr.io に push します)
  3. jsonnet (jsonnet から extVar でビルドした Docker イメージの URL をもらって json に変換します)
  4. 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] で渡しています

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 などをトリガーに実行できるようになったら良さそうだなと思っています。