ローカル環境に簡易 CI/CD 環境を構築して試す tekton 編
この記事は ローカル環境に簡易 CI/CD 環境を構築して試す の続きになります。
記事内で使用するリソースは Github に置いてあります。
Motivation
前回の記事ではローカル CI/CD 環境を構築し、concourse を使ってコンテナイメージのビルド等の CI 処理を自動化していました。しかし、課題 で上げた git ブランチを分けて開発を進めていくことを考えた際に、現在の構成では処理を自動化することが難しい部分があります。というのも、現時点において concourse は webhook の payload の中身を見て処理を反映する機能が実装されていないためです。
Note that the request payload sent to this API endpoint is entirely ignored. You should configure the resource as if you're not using web hooks, as the resource config is still the "source of truth."
Github issue でも webhook payload に関する機能リエクストが上がっていますが、現時点では未対応となっています。
Gitlab の webhook に基づいて CI 処理を自動化することを考える場合、例えば以下のように payload の中身に基づいて処理を分けたい場面は多くあります。
- push, merge 等のイベントに基づいてそれぞれ別の pipeline を実行する
- payload の中身を見てイベントをフィルタリングする。
- payload から値を抽出して pipeline 実行時のパラメータに設定する
よって、今回は webhook にも対応している tekton を使って既存の CI/CD 環境を改良していきます。
目標としては、concourse の部分を tekton で置き換え。 Gitlab の webhook イベントを処理して CI/CD によるビルドやデプロイの自動化を目指します。
また、実際にブランチを分けた開発を実践することで構築した tekton pipeline が正しく動作することを確認します。
今回目指す CI/CD 構成。concourse を tekton で置き換える。
tekton について
tekton は concourse と同じようにクラウドネイティブな CI を実現するためのプラットフォームです。tekton について検索すると既に概要や機能について詳しく解説している記事が出てくるので(例えば Tekton 徹底解説 など)、ここでは基本的な概要や手順の説明は割愛します。
tekton では concourse で行っていた処理をそのまま実装できる他、以下のようなメリット・デメリットがあります(私見を含む)。
- メリット
- 公式のドキュメントが充実しており、実際の運用に必要なことはドキュメントを読めばだいたいわかる。
- webhook を含む機能が充実している。
- tekton hub にユーザやコミュニティが作ったタスクが公開されており自由に使える。
- task, pipeline が yaml で記述できる。
- 開発が盛ん。
- デメリット
- concourse よりもコンポーネントが多く、はじめに覚えることが多い。
- kubernetes の知識があることが前提。
concourse よりも機能は充実していますが、はじめの内はやや取っ掛かりづらさを感じるといったところでしょうか。
concourse からの移行
tekton としては以下のリソースをインストール済みとします。
task
ローカル CI/CD 環境で concourse から tekton に移行するにあたって、まずは concourse で行っていた以下の処理を tekton における Task オブジェクトで置き換えます。
- gitlab からアプリケーションコードを git clone
- コンテナイメージのビルド & 作成したイメージを harbor レジストリに push
- argocd デプロイをトリガー
tekton では Tekton Hub に様々なタスクが公開されており、上記の処理も Tekton Hub で公開されているタスクがほぼそのまま使用できます。
Git clone
git clone がそのまま使えます。
イメージのビルド・プッシュ
task 内でビルドを実行するため候補はいくつかありますが、kaniko を使ってイメージをビルド & プッシュするタスク Build and upload container image using Kaniko を使います。
argocd デプロイをトリガー
こちらも argocd があるので内容を少し修正して使用します。
task
apiVersion: tekton.dev/v1
kind: Task
metadata:
name: argocd-sync
namespace: flask-app
labels:
app.kubernetes.io/version: "0.9"
annotations:
tekton.dev/pipelines.minVersion: "0.38.0"
tekton.dev/categories: Deployment
tekton.dev/tags: deploy
tekton.dev/displayName: "argocd"
tekton.dev/platforms: "linux/amd64"
spec:
description: >-
This task syncs (deploys) an Argo CD application and waits for it to be healthy.
To do so, it requires the address of the Argo CD server and some form of
authentication either a username/password or an authentication token.
workspaces:
- name: argocd-basic-auth
description: |
A workspace containing username and password to log in to argocd server.
params:
- name: applicationName
description: name of the application to sync
type: string
- name: revision
description: the revision to sync to
default: HEAD
type: string
- name: flags
default: --
type: string
- name: argocd-version
default: v2.2.2
type: string
- name: server
description: Argocd server name
type: string
default: argocd-dev.ops.com
- name: timeout
description: timeout until
default: 60
- name: imageName
description: Image name
type: string
default: harbor.centre.com/k8s/argocd-cli
- name: imageTag
description: Image tag
type: string
default: latest
steps:
- name: sync
image: $(params.imageName):$(params.imageTag)
env:
- name: ARGOCD_SERVER
value: $(params.server)
- name: ARGOCD_APPNAME
value: $(params.applicationName)
- name: ARGOCD_AUTH_PATH
value: $(workspaces.argocd-basic-auth.path)
- name: ARGOCD_TIMEOUT
value: $(params.timeout)
script: |
#!/usr/bin/env sh
export ARGOCD_USERNAME=$(cat ${ARGOCD_AUTH_PATH}/username)
export ARGOCD_PASSWORD=$(cat ${ARGOCD_AUTH_PATH}/password)
if [ -z "$ARGOCD_AUTH_TOKEN" ]; then
yes | argocd login "$ARGOCD_SERVER" --username="${ARGOCD_USERNAME}" --password="${ARGOCD_PASSWORD}" --insecure;
fi
argocd app sync ${ARGOCD_APPNAME} --revision "$(params.revision)" "$(params.flags)" --replace
pipeline
concourse では pipeline のリソース内に個々のタスク処理を直接書いていましたが、tekton では一連の処理を行うために独立した pipeline オブジェクトを作成し、その中で個々の task を実行するように記述します。pipeline の中身は後で作成する際に書きます。
webhook の対応
tekton では eventlistener, triggers というオブジェクトを用いて webhook に対応することができます。こちらも後半で実際に使う際に詳細を書きます。
準備
tekton を使えば webhook を処理して pipeline を実行したりすることができるので、実際に前回記事で作成したアプリケーションの開発に活用してみます。
やりたいこと
前回の記事ではアプリケーションやマニフェストを修正する際 main ブランチに直接 push していましたが、今回は本格的な運用を想定し、以下のように本番用・開発用でブランチを分けます。
- 開発中のコード変更は開発中ブランチ dev に push する。
- 本番用の変更は dev -> main ブランチに PR を立てる。
- PR で承認を受けた後 main マージし、新しいバージョンタグを追加する。
- 新しいバージョンに対応するコンテナイメージをビルドし、本番環境用のクラスタにデプロイする
前回の記事で作成したアプリケーションのバージョンを v1.0.0
とします。
今回の記事で機能を追加したアプリケーションをバージョン v1.1.0
として作成し、最終的に本番環境クラスタにデプロイすることを目標とします。
これらを踏まえると開発の流れは以下のようになります。
- マニフェストレポジトリに develop ブランチを作成
- アプリケーションレポジトリに develop ブランチを作成
- アプリケーションレポジトリ develop ブランチに機能追加したコードを push
- tekton pipeline でイメージビルド、開発用クラスタにデプロイ
- アプリケーションレポジトリで Pull Request を作成
- PR をマージ
- マージしたアプリケーションのバージョンを更新してタグ登録
- tekton pipeline でイメージビルド
- マニフェストレポジトリ develop ブランチにコードを push
- アプリケーションレポジトリで Pull Request を作成
- PR をマージ
- tekton pipeline で本番用クラスタにデプロイ
環境の定義
次に今回の開発に向けて環境の整理を行います。
Gitlab や k8s クラスタなど開発に必要なリソースは 前回の環境構築で作成したもの をそのまま使いまわします。
ただし今回は本番用、開発用で gitlab のブランチを分けるため、ブランチやイメージ、用語の設定を以下のように定義します。
アプリケーションレポジトリ
- git レポジトリ名:
https://gitlab.centre.com/gitlab/flask-k8s-app.git
- ブランチ: 以下の 2 ブランチを用意
- main: 本番環境用のコードを保管する。
- dev: 開発環境用のコードを保管する。
マニフェストレポジトリ
- git レポジトリ名:
https://gitlab.centre.com/gitlab/flask-k8s-manifest.git
- ブランチ: 以下の 2 ブランチを用意
- main: 本番環境用のコードを保管する。
- dev: 開発環境用のコードを保管する。
Git タグ
- 前回作成したアプリケーションのバージョンを git tag
v1.0.0
とする。 - 今回作成するバージョンを
v1.1.0
とする。
イメージ
- 本番環境と開発環境でイメージ名、タグを以下のように分ける。
- 本番用:
harbor.centre.com/k8s/flask-k8s-example:1.0.0
- イメージタグはアプリケーションのバージョンに一致させる。ただし
v
は除く - 今回作成するバージョンに対応するイメージタグは
1.1.0
となる。
- イメージタグはアプリケーションのバージョンに一致させる。ただし
- 開発用:
harbor.centre.com/k8s/flask-k8s-example-dev:latest
- イメージ名に
-dev
をつけて本番用と区別する。 - 開発用イメージのタグは常に latest とする。
- イメージ名に
k8s クラスタ
-
flask-prod
: 本番用クラスタ -
flask-dev
: 開発用クラスタ -
kube-argo
: argocd, tekton インストール済みの作業用クラスタ
pipeline の起動タイミングと処理
今回も前回と同様に Gitlab の webhook を起点として tekton pipeline をトリガーする構成にします。
また、pipeline で行う処理について以下の様な要求があるとします。
- develop ブランチでは何度もコードの更新やビルド・テストを繰り返すことが想定されるので、アプリケーションコードが push されたタイミングでコードの更新・ビルド・クラスタの展開まで自動でやりたい。
- 本番用のアプリケーションは develop -> main のマージが完了し、新しいバージョンのタグが完了したタイミングでイメージビルドしたい。
- 本番環境へのアプリケーションの展開はマニフェストのタグが変更されたタイミングで行いたい。
これを実現するためには、pipeline の起動タイミングと pipeline 内で処理する内容を以下のように作れば良いことがわかります。
番号 | 起動タイミング | pipeline の処理 |
---|---|---|
1 | アプリケーション dev ブランチへの push 時 | git clone -> build -> argocd デプロイ |
2 | アプリケーション main ブランチへの tag push 時 | git clone -> build |
3 | マニフェスト main ブランチへの merge 時 | argocd デプロイ |
リソースの作成
やりたいことが決まったので、次にこれを実現するための k8s および tekton リソースを作成していきます。
namespace
今回は作成するリソースが多いので flask-app
という namespace に必要なリソースを作っていきます。
kubectl create namespace flask-app
Secret
tekton pipeline 内で git clone の実行、harbor レジストリへのイメージ push, argocd へのログインを行うための認証情報をそれぞれ secret として作成します。
ここでは簡単のためいずれも basic auth としていますが、セキュリティ面を考慮すると他の認証方法でも良いでしょう。
apiVersion: v1
kind: Secret
metadata:
name: argocd-basic-auth
namespace: flask-app
type: kubernetes.io/basic-auth
data:
username: xxxx
password: xxxx
---
kind: Secret
apiVersion: v1
metadata:
name: gitlab-basic-auth
namespace: flask-app
type: Opaque
stringData:
.gitconfig: |
[credential "https://<hostname>"]
helper = store
.git-credentials: |
https://gitlab:password@gitlab.centre.com
---
apiVersion: v1
kind: Secret
metadata:
name: harbor-basic-auth
namespace: flask-app
data:
config.json: |
xxxx
serviceaccount
tekton pipeline を実行するための serviceaccount を作成します。この serviceaccount には eventlistener や trigger を参照するための権限、gitlab からの webhook を受け取って pipeline を実行するために pipelinerun, taskrun の作成権限を付加します。
権限は clusterrole として作成し、clusterrolebinding で serviceaccount に紐づけます。
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: webhook
namespace: flask-app
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: gitlab-webhook
namespace: flask-app
rules:
- apiGroups:
- "triggers.tekton.dev"
resources:
- "clustertriggerbindings"
- "clusterinterceptors"
- "eventlisteners"
- "triggerbindings"
- "triggertemplates"
- "triggers"
- "interceptors"
verbs:
- "get"
- "list"
- "watch"
- apiGroups:
- "tekton.dev"
resources:
- "pipelineruns"
- "taskruns"
verbs:
- "create"
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: gitlab-webhook
namespace: flask-app
subjects:
- kind: ServiceAccount
name: webhook
namespace: flask-app
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: gitlab-webhook
webhook の対応
pipeline の起動タイミングと処理 で決めたように、今回の範囲では 3 つの異なるタイミング発生する webhook をもとに pipeline をトリガーする必要があります。eventlistener の構成によっては複数の webhook を処理するようにできないこともないですが、ここでは素直にそれぞれの webhook イベントを処理する eventlistener を 1 つずつ作成します。
eventlistener
まず 1 つめの webhook に対応する eventlistener を作るためのマニフェストを準備します。
eventlistener では interceptor を使用することで event のフィルタリングや検証が可能となっています。interceptor にもいくつか種類がありユーザ独自の interceptor も作成可能ですが、今回は CEL interceptor を利用して webhook event のフィルタリングと payload の処理を行います。
フィルタリングでは、payload 内の特定のフィールドの値を確認し、条件に合ったときのみ指定した pipeline を実行する等の処理が記述できます。gitlab webhook が発生した時に payload 内にどのような値が設定されるかはGitlab ドキュメントの webhook events に一覧が表示されているので、これを確認しながらフィルタリング条件を決めていきます。
commit が特定のブランチに push された時に発生するイベントは Push event であるため、アプリケーションの dev ブランチへの push イベントを一意に定めるために以下のような条件でフィルタリングします 。
- commit が push された時のイベントを補足するため、
body.event_name == 'push'
とする。 - main ブランチ以外 (dev ブランチ) への push のみを補足するため、
body.ref != 'refs/heads/main'
とする。 - アプリケーションレポジトリのみの push イベントを補足するため、
body.project.path_with_namespace == 'gitlab/flask-k8s-example'
とする。
interceptors:
- ref:
name: cel
params:
- name: "filter"
value: body.ref != 'refs/heads/main'
- name: "filter"
value: body.project.path_with_namespace == 'gitlab/flask-k8s-example'
- name: "filter"
value: body.event_name == 'push'
CEL interceptors では overlays
を使うことで payload に追加のフィールドを設定して後続の処理に渡すことができます。
ここでは pipeline 内で以下の情報を使いたいので文字列を payload に追加します。
- branchName
- pipeline 内で git clone を実行する際の対象レポジトリのブランチ名
- imageName
- pipeline 内でイメージをビルドする際のイメージ名
- imageTag
- 上記のイメージタグ名
- argocdApplicationName
- pipeline 内で
argocd sync
を実行する際の Application 名
- pipeline 内で
interceptors:
- ref:
name: cel
params:
...
- key: branchName
expression: "string('dev')"
- key: imageName
expression: "string('harbor.centre.com/k8s/flask-k8s-example-dev')"
- key: imageTag
expression: "string('latest')"
- key: argocdApplicationName
expression: "string('flask-dev')"
bindings, template ではこの eventlistener に紐付ける triggerbinding, triggertemplate のリソース名を指定します。これらは次で作成します。
bindings:
- ref: build-dev-image
template:
ref: build-dev-image
まとめると eventlistener マニフェストの中身は以下のようになります。
eventlistener
---
apiVersion: triggers.tekton.dev/v1alpha1
kind: EventListener
metadata:
name: flask-app-dev
namespace: flask-app
spec:
serviceAccountName: webhook
triggers:
- name: event-dev-branch
interceptors:
- ref:
name: cel
params:
- name: "filter"
value: body.ref != 'refs/heads/main'
- name: "filter"
value: body.project.path_with_namespace == 'python/flask-k8s-example'
- name: "filter"
value: body.event_name == 'push'
- name: "overlays"
value:
- key: branchName
expression: "string('dev')"
- key: imageName
expression: "string('harbor.centre.com/k8s/flask-k8s-example-dev')"
- key: imageTag
expression: "string('latest')"
- key: argocdApplicationName
expression: "string('flask-dev')"
bindings:
- ref: build-dev-image
template:
ref: build-dev-image
pipeline
pipeline リソースでは push イベントを受け取って実際に実行する tekton pipeline を定義します。
以下 3 つのタスクを順次実行するように記述します。
git-clone
アプリケーションレポジトリからコードを clone するための git-clone
タスクを実行するように指定します。
tekton workspace としては以下を指定。
- 後続のタスクで clone したコードを共有するための persistentVolume を
shared-data
で指定。 - git clone するための git 認証情報を
git-credentials
で指定。
git clone するレポジトリの URL, ブランチはパラメータとして外部から与える形式にします。
tasks:
- name: git-clone
taskRef:
name: git-clone
workspaces:
- name: output
workspace: shared-data
- name: basic-auth
workspace: git-credentials
params:
- name: url
value: $(params.repositoryUrl)
- name: revision
value: $(params.branch)
build
次に clone したアプリケーションをビルドするタスクを定義します。
tekton workspace としては以下を指定。
- clone したコードを共有するための persistentVolume を
shared-data
で指定。 - ビルドしたイメージを harbor レジストリに push する際の認証情報を
harbor-credentials
で指定。
git clone した後にこのタスクが実行されるように runAfter
で前のタスクを指定。
- name: build
taskRef:
name: kaniko
runAfter:
- git-clone
workspaces:
- name: source
workspace: shared-data
- name: dockerconfig
workspace: harbor-credentials
params:
- name: IMAGE
value: $(params.imageName):$(params.imageTag)
- name: CONTEXT
value: $(params.context)
deploy
最後にアプリケーションをクラスタにデプロイするタスクを実行するように指定します。
tekton workspace としては以下を指定。
- argocd にログインし、
argocd sync
を実行するための認証情報をargocd-credentials
で指定。 - このタスクでは clone したコードを共有する必要はないため、
shared-data
の指定は不要。
デプロイ先のクラスタ(開発用 or 本番用)はパラメータとして外部から指定するようにします。
- name: deploy
taskRef:
name: argocd-sync
runAfter:
- git-clone
- build
workspaces:
- name: argocd-basic-auth
workspace: argocd-credentials
params:
- name: applicationName
value: $(params.argocdApplicationName)
これらを spec.tasks
で定義することで一連の task を pipeline として実行できるようになります。また、各タスクで使用するパラメータと workspace の定義はそれぞれ spec.params
, spec.workspaces
で行います。pipeline が実行される際にこれらの値をどのように設定するかはこのリソース内ではなく pipelineRun リソースで指定します。
最終的な pipeline リソースのマニフェストは以下のようになります。この pipeline は build-dev-image
という名前を付けます。
pipeline
apiVersion: tekton.dev/v1
kind: Pipeline
metadata:
name: build-dev-image
namespace: flask-app
spec:
description: >-
This pipeline run the sequence of tasks as a pipeline.
1. Clone the application source from a git repository.
2. Build an image from Dockerfile and push it to a registry.
3. Deploy application by argocd sync.
params:
- name: repositoryUrl
type: string
description: The git repository URL to clone the source.
- name: branch
type: string
description: The branch of the repository.
- name: context
description: The path to the build context, used by Kaniko - within the workspace
default: "./"
- name: imageName
description: Image name
- name: imageTag
description: Image tag
default: "latest"
- name: argocdApplicationName
description: The name of Argocd application to be synced.
workspaces:
- name: shared-data
description: >-
This workspace contains the cloned repo files, so they can be read by the
next task.
- name: git-credentials
description: Name of the secret to log in to gitlab.
- name: harbor-credentials
description: Name of the secret to log in to harbor.
- name: argocd-credentials
description: Name of the secret to log in to argocd.
tasks:
- name: git-clone
taskRef:
name: git-clone
workspaces:
- name: output
workspace: shared-data
- name: basic-auth
workspace: git-credentials
params:
- name: url
value: $(params.repositoryUrl)
- name: revision
value: $(params.branch)
- name: sslVerify
value: "false"
- name: build
taskRef:
name: kaniko
runAfter:
- git-clone
workspaces:
- name: source
workspace: shared-data
- name: dockerconfig
workspace: harbor-credentials
params:
- name: IMAGE
value: $(params.imageName):$(params.imageTag)
- name: CONTEXT
value: $(params.context)
- name: deploy
taskRef:
name: argocd-sync
runAfter:
- git-clone
- build
workspaces:
- name: argocd-basic-auth
workspace: argocd-credentials
params:
- name: applicationName
value: $(params.argocdApplicationName)
TriggerBinding
TriggerBinding にはこの eventlistener からパラメータを受け取って triggertemplate に渡すパラメータを定義します。
- repositoryUrl
- git clone するレポジトリの URL
- push イベントの payload では元から
body.project.http_url
に設定されているため、その値を抽出する。
それ以外の値は eventlistener の cel interceptor payload に追加した値を抽出して変数にセットしています。
---
apiVersion: triggers.tekton.dev/v1beta1
kind: TriggerBinding
metadata:
name: build-dev-image
namespace: flask-app
spec:
params:
- name: repositoryUrl
value: $(body.project.http_url)
- name: branch
value: $(extensions.branchName)
- name: imageName
value: $(extensions.imageName)
- name: imageTag
value: $(extensions.imageTag)
- name: argocdApplicationName
value: $(extensions.argocdApplicationName)
triggertemplate
TriggerTemplate は templateBinding からパラメータを受け取って pipeline を実行する部分を定義します。pipeline は pipelineRun リソースを作成することでトリガーされるため、spec.resourcetemplates
以下で pipelineRun の中身を定義するような構成になっています。
まず、pipelineRef
に実行する pipeline のリソース名を指定します。ここでは先ほど作成した pipeline 名を指定します。
spec:
pipelineRef:
name: build-dev-image
podTemplate には pipeline 実行用の pod の設定を書きます。
pipeline の中で使っている git-clone task では実行ユーザの指定があるため、securityContext
で指定してます。
spec:
podTemplate:
securityContext:
fsGroup: 65532
imagePullSecrets:
- name: harbor-basic-auth
workspaces には各 task で使用する workspace の定義を行います。
- clone したコードを各タスクで共有するための
shared-data
には persistentVolume を使用するため、ここで PVC の定義を書いています。PV は事前に作成しておくか、openebs などの dynamic provisioner を使って動的に作成します。 - 残りの credentials 系は事前に作成した secret のリソース名を指定します。
spec:
workspaces:
- name: shared-data
volumeClaimTemplate:
spec:
storageClassName: openebs-hostpath
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
- name: git-credentials
secret:
secretName: gitlab-basic-auth
- name: harbor-credentials
secret:
secretName: harbor-basic-auth
- name: argocd-credentials
secret:
secretName: argocd-basic-auth
タスクに渡すパラメータは params に設定します。まず spec.params
でパラメータの定義を行い、pipeline に渡すパラメータは spec.resourcetemplates[].spec.params
で記述します。
実際のパラメータの値は payload -> eventlistener -> triggertemplate の順で処理され、spec.params
で定義した変数名に代入されています。格納されたパラメータは $(tt.params.[変数名])
で取得できます。
spec:
params:
- name: repositoryUrl
description: The git repo URL to clone from.
- name: branch
description: git branch to be pulled from.
- name: imageName
description: Image name including repository
- name: imageTag
description: Image tag
- name: argocdApplicationName
description: applicationName
resourcetemplates:
- apiVersion: tekton.dev/v1beta1
spec:
params:
- name: repositoryUrl
value: $(tt.params.repositoryUrl)
- name: branch
value: $(tt.params.branch)
- name: imageName
value: $(tt.params.imageName)
- name: imageTag
value: $(tt.params.imageTag)
- name: argocdApplicationName
value: $(tt.params.argocdApplicationName)
まとめると、triggertemplate のマニフェストは以下のようになります。
triggertemplate
apiVersion: triggers.tekton.dev/v1beta1
kind: TriggerTemplate
metadata:
name: build-dev-image
namespace: flask-app
spec:
params:
- name: repositoryUrl
description: The git repo URL to clone from.
- name: branch
description: git branch to be pulled from.
- name: imageName
description: Image name including repository
- name: imageTag
description: Image tag
- name: argocdApplicationName
description: applicationName
resourcetemplates:
- apiVersion: tekton.dev/v1beta1
kind: PipelineRun
metadata:
generateName: build-dev-image-
spec:
pipelineRef:
name: build-dev-image
podTemplate:
securityContext:
fsGroup: 65532
imagePullSecrets:
- name: harbor-basic-auth
workspaces:
- name: shared-data
volumeClaimTemplate:
spec:
storageClassName: openebs-hostpath
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
- name: git-credentials
secret:
secretName: gitlab-basic-auth
- name: harbor-credentials
secret:
secretName: harbor-basic-auth
- name: argocd-credentials
secret:
secretName: argocd-basic-auth
params:
- name: repositoryUrl
value: $(tt.params.repositoryUrl)
- name: branch
value: $(tt.params.branch)
- name: imageName
value: $(tt.params.imageName)
- name: imageTag
value: $(tt.params.imageTag)
- name: argocdApplicationName
value: $(tt.params.argocdApplicationName)
その他の webhook 対応
以上のように、以下のリソースを作成することで pipeline の起動タイミングと処理 の 1 番目の pipeline を実行するためのリソースが準備できました。
- pipeline
- eventlistener
- triggerbinding
- triggertemplate
残りの 2, 3 番目に対応するリソースも同様の手順で作成していきます。作り方は大体同じなので必要な部分だけ説明します。
2 番目の webhook 対応
2 番目の pipeline は tag の push イベント を受け取るための eventlistener を作成します。push イベントにある v1.1.0 の数字を抽出してイメージタグに設定するため、cel intercept で以下のような表現を使います。
- key: imageTag
expression: "body.ref.split('/')[2].replace('v', '')"
これで "ref": "refs/tags/v1.1.0"
から 1.1.0
の数字部分のみを抽出できます。
eventlistener
---
apiVersion: triggers.tekton.dev/v1alpha1
kind: EventListener
metadata:
name: flask-app-prod
namespace: flask-app
spec:
serviceAccountName: webhook
triggers:
- name: event-tag-push
interceptors:
- ref:
name: cel
params:
- name: "filter"
value: body.event_name == "tag_push"
- name: "filter"
value: body.project.path_with_namespace == 'gitlab/flask-k8s-example'
- name: "overlays"
value:
- key: branchName
expression: "string('main')"
- key: imageName
expression: "string('harbor.centre.com/k8s/flask-k8s-example')"
- key: imageTag
expression: "body.ref.split('/')[2].replace('v', '')"
bindings:
- ref: build-prod-image
template:
ref: build-prod-image
この pipeline ではイメージの build, push を行いますが、本番用クラスタへのデプロイはマニフェスト変更後に行います。そのため、pipeline リソースでは argocd sync 以外の 2 タスクを実行するように記述します。
pipeline
apiVersion: tekton.dev/v1
kind: Pipeline
metadata:
name: build-prod-image
namespace: flask-app
spec:
description: >-
This pipeline run the sequence of tasks as a pipeline.
1. Clone the application source from a git repository.
2. Build an image from Dockerfile and push it to a registry.
params:
- name: repositoryUrl
type: string
description: The git repository URL to clone the source.
- name: branch
type: string
description: The branch of the repository.
- name: context
description: The path to the build context, used by Kaniko - within the workspace
default: "./"
- name: imageName
description: Image name
- name: imageTag
description: Image tag
default: "latest"
workspaces:
- name: shared-data
description: >-
This workspace contains the cloned repo files, so they can be read by the
next task.
- name: git-credentials
description: Name of the secret to log in to gitlab.
- name: harbor-credentials
description: Name of the secret to log in to harbor.
tasks:
- name: git-clone
taskRef:
name: git-clone
workspaces:
- name: output
workspace: shared-data
- name: basic-auth
workspace: git-credentials
params:
- name: url
value: $(params.repositoryUrl)
- name: revision
value: $(params.branch)
- name: sslVerify
value: "false"
- name: build
taskRef:
name: kaniko
runAfter:
- git-clone
workspaces:
- name: source
workspace: shared-data
- name: dockerconfig
workspace: harbor-credentials
params:
- name: IMAGE
value: $(params.imageName):$(params.imageTag)
- name: CONTEXT
value: $(params.context)
3 番目の webhook 対応
最後の pipeline では マニフェストの変更を main ブランチにマージした際に実行するように設定します。
gitlab では PR に関するイベントは Merge request events となっており、PR がマージされた際のイベントは object_attributes.action = merge
が設定されるのでそれをフィルタリング条件に指定します。
eventlistener
---
apiVersion: triggers.tekton.dev/v1alpha1
kind: EventListener
metadata:
name: merge-manifest
namespace: flask-app
spec:
serviceAccountName: webhook
triggers:
- name: deploy-prod-manifest
interceptors:
- ref:
name: cel
params:
- name: "filter"
value: body.event_type == 'merge_request'
- name: "filter"
value: body.project.path_with_namespace == 'gitlab/flask-k8s-manifest'
- name: "filter"
value: body.object_attributes.target_branch == 'main'
- name: "filter"
value: body.object_attributes.action == 'merge'
- name: "overlays"
value:
- key: argocdApplicationName
expression: "string('flask-prod')"
bindings:
- ref: deploy-prod-manifest
template:
ref: deploy-prod-manifest
この pipeline では本番用クラスタのデプロイのみを行うため、argocd sync タスクだけを実行すれば良く clone や build は必要ありません。
そのため新規に pipeline は作成せず、triggertemplate では argocd sync タスクを起動するように pipelineRun の代わりに taskRun を記述します。
TriggerBinding,TriggerTemplate
---
apiVersion: triggers.tekton.dev/v1beta1
kind: TriggerBinding
metadata:
name: deploy-prod-manifest
namespace: flask-app
spec:
params:
- name: argocdApplicationName
value: $(extensions.argocdApplicationName)
---
apiVersion: triggers.tekton.dev/v1beta1
kind: TriggerTemplate
metadata:
name: deploy-prod-manifest
namespace: flask-app
spec:
params:
- name: argocdApplicationName
description: Argocd application name to be deployed
resourcetemplates:
- apiVersion: tekton.dev/v1beta1
kind: TaskRun
metadata:
generateName: deploy-prod-manifest-
spec:
taskRef:
name: argocd-sync
workspaces:
- name: argocd-basic-auth
secret:
secretName: argocd-basic-auth
params:
- name: applicationName
value: $(tt.params.argocdApplicationName)
Ingress
いままで説明しませんでしたが、eventlistener を作成すると event を処理するための pod と service が作成されます。この service に対して webhook を飛ばすことでイベントを処理できます。
$ kubectl get pod -l app.kubernetes.io/managed-by=EventListener
NAME READY STATUS RESTARTS AGE
el-flask-app-dev-68f6b74978-dnxvt 1/1 Running 0 12d
el-flask-app-prod-cdcb5d64b-jndnf 1/1 Running 0 12d
el-merge-manifest-749cbf9985-qx78w 1/1 Running 0 12d
$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
el-flask-app-dev ClusterIP 10.104.27.131 <none> 8080/TCP,9000/TCP 12d
el-flask-app-prod ClusterIP 10.100.124.225 <none> 8080/TCP,9000/TCP 12d
el-merge-manifest ClusterIP 10.98.62.105 <none> 8080/TCP,9000/TCP 12d
ここで作成した service は clusterIP であるため、クラスタ外部にある gitlab から service に webhook を送信できるように ingress リソースを作成します。
ingress では path prefix を使い、リクエスト URL のパスに基づいてそれぞれ別の service にルーティングするように設定します。
ingress
spec:
ingressClassName: nginx
rules:
- host: tekton-webhook.ops.com
http:
paths:
- path: /dev
pathType: Prefix
backend:
service:
name: el-flask-app-dev
port:
number: 8080
- path: /prod
pathType: Prefix
backend:
service:
name: el-flask-app-prod
port:
number: 8080
- path: /manifest
pathType: Prefix
backend:
service:
name: el-merge-manifest
port:
number: 8080
実践編
pipeline の作成と webhook の処理を準備したので、ここからは実際にアプリケーションの変更を行い、gitlab webhook に基づいて pipeline が実行される様子を見ていきます。
初期状態の確認
はじめに最初のクラスタ上のアプリケーションを確認しておきます。
確認方法は前回記事と同様、各クラスタ上で稼働しているアプリケーションに対して curl を実行した際のレスポンスを見て確認します。
各クラスタには既に ingress を展開しているため、kube-argo クラスタ上で以下の URL にリクエストを送信することで対象のアプリケーションにアクセスできます。
- 本番用クラスタ:
flask-prod.ops.com
- 開発用クラスタ:
flask-dev.ops.com
$ curl https://flask-dev.ops.com
{"cluster":"k8s-dev","message":"This is dev"}
$ curl https://flask-prod.ops.com
{"cluster":"k8s-prod","message":"This is prod"}
この時点では前回記事で作成したアプリケーション v1.0.0 が本番・開発用クラスタにデプロイされています。また、マニフェストで本番環境・開発環境で異なる環境変数を設定しているため、message
の内容がそれぞれ異なっています。
開発用クラスタへのデプロイ
アプリケーションの v1.1.0 を作成するために既存のコードを改修していきます。
変更箇所は前回と同様なんでも良いですが、今回もレスポンスに対して新しいフィールドを追加していきます。
環境変数 VALUE1
を読み取ってレスポンスに value1
として追加します。
+ value1 = os.getenv("VALUE1", "undefined")
@app.route("/")
def hello_world():
- ret = {"message": f"This is {region}", "cluster": cluster}
+ ret = {"message": f"This is {region}", "cluster": cluster, "value1": value1}
これをアプリケーションレポジトリ gitlab/flask-k8s-app.git
の dev ブランチに push します。
dev ブランチに push したことによって webhook イベントが発動し、イメージのビルド、開発用クラスタへのデプロイを実行する tekton pipeline がトリガーされます。
pipeline の実行状況は tkn pipelinerun list
で確認できます。
$ tkn pipelinerun list
NAME STARTED DURATION STATUS
build-dev-image-zf6k4 1 week ago 51s Succeeded
pipeline が完了すると、変更したアプリケーションが開発用クラスタにデプロイされたためレスポンスが変更されています。ただしこの時点ではマニフェスト側に環境変数を設定していないため、value1 の値は undefined となっています。
$ curl https://flask-dev.ops.com
{"cluster":"k8s-dev","message":"This is dev","value1":"undefined"}
本番用クラスタに変更は適用されないためレスポンスは変化していません。
$ curl https://flask-prod.ops.com
{"cluster":"k8s-prod","message":"This is prod"}
アプリケーションにもう一つ変更を加えてみます。先程と同様にレスポンスに value2 というフィールドを追加します。
+ value2 = os.getenv("VALUE2", "undefined")
@app.route("/")
def hello_world():
- ret = {"message": f"This is {region}", "cluster": cluster, "value1": value1}
+ ret = {"message": f"This is {region}", "cluster": cluster, "value1": value1, "value2": value2}
今回はフィールドに値を設定するためマニフェストの方に環境変数をセットします。
spec:
template:
spec:
containers:
- name: flask-k8s-example
env:
+ - name: VALUE1
+ value: "dev-1"
+ - name: VALUE2
+ value: "dev-2"
変更をマニフェストレポジトリ gitlab/flask-k8s-manifest.git
dev ブランチに push した後、アプリケーションの変更点を push します。
pipeline 完了後にレスポンスを確認すると、value2 のフィールドと環境変数に設定した値が追加されていることが確認できます。
もちろん本番環境の方は何も更新されていないためレスポンスは変化していません。
$ curl https://flask-dev.ops.com
{"cluster":"k8s-dev","message":"This is dev","value1":"dev-1","value2":"dev-2"}
$ curl https://flask-prod.ops.com
{"cluster":"k8s-prod","message":"This is prod"}
本番用イメージのビルド
アプリケーションの変更点が問題なく動作することが確認できたので、次はこの変更点をアプリケーションの新しいバージョン v1.1.0
としてデプロイするための準備を行います。
まず、変更点を main ブランチにマージするために gitlab 上で PR を作成します。
実際の運用ではこの段階でコードレビュー等を受けますが、今回はこのまま merge します。
これで main ブランチに変更点が適用されたので、これをバージョン v1.1.0
として公開するため tag を追加します。
$ git tag v1.1.0
$ git push origin v1.1.0
warning: redirecting to https://gitlab.centre.com/gitlab/flask-k8s-example.git/
Total 0 (delta 0), reused 0 (delta 0), pack-reused 0
To https://gitlab.centre.com/gitlab/flask-k8s-example
* [new tag] v1.1.0 -> v1.1.0
tag を push すると gitlab の tag push イベントが webhook として送信され、イメージをビルドする pipeline がトリガーされます。
これにより アプリケーション v1.1.0 に対応するコンテナイメージがタグ 1.1.0
としてビルドされ、harbor レジストリに push されます。
harbor の docker registry API を使って harbor 上のイメージタグを確認してみます。
$ curl -Ssu "admin:xxxx" https://harbor.centre.com/v2/k8s/flask-k8s-example/tags/list | jq
{
"name": "k8s/flask-k8s-example",
"tags": [
"1.0.0",
"1.1.0"
]
}
もともとあったイメージタグ 1.0.0 に加えて pipeline によって作成された 1.1.0 が追加されていることが確認できます。
本番用クラスタへのデプロイ
最後に、1.1.0 として作成したイメージを使ったアプリケーションを本番用クラスタにデプロイします。
マニフェスト内で使用するイメージタグを更新し、環境変数もセットしておきます。
spec:
containers:
- name: flask-k8s-example
- image: harbor.centre.com/k8s/flask-k8s-example:1.0.0
+ image: harbor.centre.com/k8s/flask-k8s-example:1.1.0
env:
- name: REGION
value: prod
- name: CLUSTER
value: "k8s-prod"
+ - name: VALUE1
+ value: "prod-1"
+ - name: VALUE2
+ value: "prod-2"
これをマニフェストレポジトリの dev ブランチに push し、dev -> main のマージを行う PR を作成します。
先程と同様にマージを完了させると、PR のマージに対応する webhook イベントが発生し、最後の pipeline がトリガーされます。この pipeline では上記マニフェストを本番用クラスタに適用します。
アプリケーションが本番用クラスタに適用されたことを確認するためレスポンスを見てみます。
$ curl https://flask-dev.ops.com
{"cluster":"k8s-dev","message":"This is dev","value1":"dev-1","value2":"dev-2"}
$ curl https://flask-prod.ops.com
{"cluster":"k8s-prod","message":"This is prod","value1":"prod-1","value2":"prod-2"}
本番用クラスタでも無事にアプリケーションの新しいバージョンが適用され、マニフェストに設定した環境変数がセットされていることが確認できました。
まとめ
tekton を使うことで gitlab webhook のイベントを区別し、CI 処理を自動化することが達成できました。
始めはやや取っ掛かりづらさを感じますが、機能が豊富であり本番環境での動作のも十分であることから、tekton は CI 実行プラットフォームとしては有力な選択肢となり得ると感じました。
おまけ
tekton pipeline で pipeline の成功/失敗を slack 等で通知できる機能があればいいなと思ったのですが、現時点ではなさそうでした。
機能リクエストとしては https://github.com/tektoncd/pipeline/issues/1740 があり、2021 年の roadmap に記載されています。
また、pipeline の定義で全タスクが終了した後に実行される finally というのがあるので、こちらで代用はできそうです。
Discussion