はじめに
DevOpsプラットフォームの取り組みを紹介する5回目の記事です。
Qmonus Value Stream開発チームの杉野です。
連載第5回では、Qmonus Value StreamでCI/CD機能を実現するための要素技術として用いている、OSSのTektonについて紹介します。
これまでの記事をまだ見ていないという方は、Qmonus Value StreamというプラットフォームがどのようにTektonを利用しているかを過去の記事で述べていますので、覗いてみてください。
また、本記事ではKubernetes(以下、k8s)に関する知識がある前提で記述していますので、ご了承ください。
Tekton とは
Tektonは一言でいうと、CI/CDシステムを作成するためのKubernetes Nativeなオープンソースフレームワークです。さらに噛み砕いて表現すると、k8s上で動作してCI/CDの機能を実現するソフトウェアです。
Tektonは、基盤となるTekton Pipelinesと呼ばれるコンポーネントと、Tektonをエコシステムにするための複数のサポートコンポーネントで構成されます。現在は次のようなコンポーネントが存在しています。
- Tekton Pipelines: パイプラインを構築するためのビルディングブロックを提供するTektonの基盤コンポーネント
- Tekton Triggers: パイプラインをイベントトリガーで実行する機能を提供
- Tekton CLI: パイプラインを管理するコマンドラインインタフェースを提供
- Tekton Dashboard: パイプラインの確認や実行が可能なWeb UIを提供
- Tekton Catalog: コミュニティが提供するビルディングブロックのリポジトリ
- Tekton Hub: Tekton CatalogのWeb UIを提供
- Tekton Operator: Tektonのコンポーネントを管理する機能を提供
- Tekton Results: パイプラインの実行履歴のストレージ機能を提供
- Tekton Chains: パイプラインでサプライチェーンセキュリティの機能を提供
上記コンポーネントの中でもPipelines、Triggers、CLI、Dashboardは主要コンポーネントと呼ばれます。本記事では、Tekton Pipelinesに焦点をあてて紹介します。
Tekton Pipelines
コンセプト
TektonにはStep、Task、Pipelineと呼ばれる概念があります。
Step: Tektonでパイプラインを実行する際の一番小さな処理の単位です。例えば、ソースコードを取得する・コンパイルする・テストするといった処理に相当します。
Task: 順番に実行されるStepの集まりです。シーケンシャルに実行される一連の処理に相当します。
Pipeline: Taskの集まりです。Taskの順序関係の設定および直列・並列実行の制御、Taskの実行条件の設定などが可能です。
これらの概念の関係性を図示すると、以下の通り表現されます。
上記の3つの概念を用いてCI/CDパイプラインを記述し実行することで、k8sのワークロードリソースが生成されて、タスク処理が実行されます。前述の図にk8sリソースとの関係性を記述してみます。
上図にあるように、StepはContainerに、TaskはPodに対して1対1に対応しています。k8sの目線で考えると、Taskの実行はPodを起動すること、Stepを順番に処理することはPod内のコンテナを順番に処理することに相当します。データ共有に関しては、Step間ならばコンテナ間で共有、Task間ならPod 間でデータ共有することのように言い換えられます。
TektonにはTask/Pipelineの実行の概念であるTaskRun/PipelineRunが存在します。TaskRunによってTaskの内容をもとにPodが起動され、Stepであるコンテナの処理が順次実行されます。PipelineRunはPipelineの内容を元に各TaskをTaskRunを利用して実行します。
Tekton Pipelinesは、上記で説明した Task、Pipeline、 TaskRun、 PipelineRun をk8sのカスタムリソースとして提供しています。ユーザはそれらのリソースを組み合わせてパイプラインを構築します。
パイプラインの作成と実行例
ここでは、具体的なカスタムリソースの定義と実行例を紹介します。
以下の実行環境で動作を確認しています。
Component | Version |
---|---|
Kubernetes | v1.24.1 |
Tekton Pipelines | v0.38.0 |
Tekton CLI | v0.25.0 |
Task の定義と実行
サンプルとして、次の仕様を満たすTaskのカスタムリソースを定義します。
- パラメータ名
message
として文字列を受け取る。受け取らない場合は、"Hello, Tekton Task"をデフォルト値とする。 - step-1、step-2のstepが順番に実行され、受け取ったパラメータをechoする。
apiVersion: tekton.dev/v1beta1 kind: Task metadata: name: echo namespace: sample-tekton spec: params: - name: message type: string description: echo this message default: Hello, Tekton Task steps: - name: step-1 image: ubuntu command: - echo args: - $(params.message) - name: step-2 image: ubuntu command: - echo args: - $(params.message)
サンプル中の以下の部分では、実行の際にTaskが受け取るパラメータを定義しています。パラメータは配列形式で複数定義でき、パラメータ名、型、デフォルト値を指定できます。パラメータの型として、stringかarrayが指定可能です。
params: - name: message type: string description: echo this message default: Hello, Tekton Task
Task内でStepなどを定義する際に $(params.<パラメータ名>)
の形式で記述することで、Taskが実行される際に受け取ったパラメータの値で置換できます。
次に、Taskが実行するStepについてです。Stepは次のように配列として定義できます。コンセプトで説明した通り、Stepの1つ1つがコンテナに相当します。
steps: - name: step-1 image: ubuntu command: - echo args: - $(params.message)
steps配列に定義される各stepの定義は、Podで設定するコンテナの指定に似ています。今回の場合だと、step-1
という名前のstep(コンテナ)はubuntu
コンテナイメージを使い、echo
コマンドを実行し、$(params.message)
を置換した値が引数として渡されるという意味になります。また、後ほどの例にでてきますが、commandの代わりにscriptフィールドを指定することで、コンテナで実行するスクリプトを記述するような方法もあります。
TaskRunリソースを作成することで、上記で定義したTaskを実行します。以下にTaskRunのサンプルを示します。
apiVersion: tekton.dev/v1beta1 kind: TaskRun metadata: generateName: echo- spec: params: - name: message value: Hello taskRef: name: echo
spec.params
には、Taskのパラメータ名と渡したい値のペアを配列で指定します。
spec.taskRef
には、このTaskRunで実行するTaskを指定します。TaskRunリソースではTaskリソースを参照する他にインラインで定義することも可能ですが、ここでは割愛します。
metadata.generateName
が書かれたマニフェストを作成すると、生成されたリソースの名前にランダムなサフィックスが付与されます。これにより、生成されたTaskRunリソースの名前が重複しなくなるため、同じTaskRunマニフェストを用いて複数個のTaskRunリソースを作成し、何回でもTaskを実行できるようになります。
これらの設定をk8sクラスタに適用しCLIで確認すると、以下のような結果を得ることができます。ここでtkn
はTekton CLIをインストールすることで利用可能になるバイナリです。
> tkn taskrun logs -n sample-tekton -f [step-1] Hello [step-2] Hello
以上のように、Taskリソースを定義することで、複数のコンテナを組み合わせて任意の処理を実行するCI/CDタスクを組み上げることができます。
Pipeline の定義と実行
サンプルとして、次の仕様を満たすPipelineのカスタムリソースを定義します。
- 前述のecho Taskに異なるパラメータを与えて、2つのTaskを実行する
- 2つのTaskは並列に実行する
apiVersion: tekton.dev/v1beta1 kind: Pipeline metadata: name: hello namespace: sample-tekton spec: params: - name: task1-message type: string default: Hello, Task1 - name: task2-message type: string default: Hello, Task2 tasks: - name: task1 taskRef: name: echo params: - name: message value: $(params.task1-message) - name: task2 taskRef: name: echo params: - name: message value: $(params.task2-message)
params
ではTaskと同様に、実行の際にPipelineが受け取るパラメータを定義します。
Pipelineが実行するTaskは、次のように配列として定義します。
tasks: - name: task1 taskRef: name: echo params: - name: message value: $(params.task1-message)
PipelineでのTaskの指定の仕方は、前述のTaskRunリソースでの指定の仕方と同じです。Taskを直接実行する際にTaskRunを生成するのと同様に、PipelineがTaskを実行する際にもTaskRunが生成されるためです。ここで指定したTask定義は、PipelineがTaskRunを生成する際に使用されます。列挙したTaskは、後述のrunAfterが宣言されていない限り、すべて並列に実行されます。
PipelineRunリソースを作成することで、上記で定義したPipelineを実行します。以下にPipelineRunのサンプルを示します。
apiVersion: tekton.dev/v1beta1 kind: PipelineRun metadata: generateName: hello- namespace: sample-tekton spec: pipelineRef: name: hello params: - name: task1-message value: Hello, Task1 - name: task2-message value: Hello, Task2
spec.pipelineRef
には、このPipelineRunで実行するPipelineを指定します。
spec.params
には、Pipelineのパラメータ名と渡したい値のペアを配列で指定します。
これらの設定をk8sクラスタに適用しCLIで確認すると、以下のような結果を得ることができます。
> tkn pipelinerun logs -n sample-tekton -f [task2 : step-1] Hello, Task2 [task2 : step-2] Hello, Task2 [task1 : step-1] Hello, Task1 [task1 : step-2] Hello, Task1
以上のように、Pipelineリソースを定義することで、複数のTaskを組み合わせてより複合的なCI/CDタスクを組み上げることができます。
Task の実行順序制御
コンセプトの項目でも触れましたが、PipelineはTaskの実行順序を制御できます。
あるTaskを別のTaskの後で実行したいときには、PipelineリソースでrunAfter
を指定します。前述のPipelineにおいてtask2をtask1の後で実行したい場合は、パイプラインで次のように指定します。
tasks: - name: task1 taskRef: name: echo params: - name: message value: $(params.task1-message) - name: task2 taskRef: name: echo params: - name: message value: $(params.task2-message) runAfter: # task1 との依存関係を定義 - task1
上記の指定にあるように、runAfter
は配列の形式で指定できます。そのため、複数のTaskとの依存関係を記述でき、 ファンインとファンアウトの両方を構成できます。 runAfter
をうまく活用することで、並列実行と直列実行を組み合わせて複雑なPipelineを組むことが可能です。
データの共有
複数の処理で構成されたパイプラインを実行した際、処理間でデータの共有を必要とする場合があります。Tektonでは、そのようなケースで利用できる機能として results
とworkspaces
が提供されています。
results: resultsは、Pipelineを介してTask間でTask実行結果を共有できる機能です。例えば、ビルドしたコンテナイメージの Tagやハッシュ値を後続のTaskで利用するなどの使い方があります。
workspaces: workspacesは、Task実行時に1つ以上のボリュームをマウント可能とする機能です。Step間のみの共有ならばemptyDir、Task間共有が必要ならばPersistentVolumeと使い分けが可能です。例えば、ソースコードをcloneしてきたあと、後続の処理で使うといった使い方があります。
上記機能のうち、resultsの例を紹介します。
サンプルのパイプラインを次の仕様で作成します。
- パラメータとして与えられた文字列を連結するTaskと文字列をechoするTaskの2つが実行される
- 文字列を連結するTaskの実行結果をresultsとして保存し、echo Taskでresultsをecho する
文字列を連結するTaskを次のように定義します。
apiVersion: tekton.dev/v1beta1 kind: Task metadata: name: concat namespace: sample-tekton spec: results: - name: concat-msg description: A string that is a concatenation of two strings params: - name: msg1 type: string - name: msg2 type: string steps: - name: concat image: ubuntu script: | #!/usr/bin/env bash echo -n $(params.msg1) $(params.msg2) > $(results.concat-msg.path)
resultsを出力するTaskは、Task内で以下のように spec.results
を定義する必要があります。
results: - name: concat-msg description: A string that is a concatenation of two strings
その上で、Step内の実装に、resultsを書き出す処理を追加します。resultsとして出力したい文字列を $(results.<results名>.path)
に対して書き出すことで、書き出した値をTaskのresultとして扱えるようになります。
2つのTaskを順番に実行するPipelineは、以下のように記述されます。
apiVersion: tekton.dev/v1beta1 kind: Pipeline metadata: name: concat-msg namespace: sample-tekton spec: params: - name: msg1 type: string - name: msg2 type: string tasks: - name: concat taskRef: name: concat params: - name: msg1 value: $(params.msg1) - name: msg2 value: $(params.msg2) - name: echo taskRef: name: echo params: - name: message value: $(tasks.concat.results.concat-msg)
ポイントは、後続のecho Taskのパラメータに指定している$(tasks.concat.results.concat-msg)
の記述です。Pipeline上で$(tasks.<task名>.results.<result名>)
のフォーマットで記述することで、task名で指定したTaskが出力したresultsの値で置換できます。
以下のPipelineRunリソースを作成して、Pipelineを実行します。
apiVersion: tekton.dev/v1beta1 kind: PipelineRun metadata: generateName: concat-msg- namespace: sample-tekton spec: pipelineRef: name: concat-msg params: - name: msg1 value: Hello - name: msg2 value: World
実行した後の確認結果は次のようになりました。連結された文字列が適切にecho Taskに渡ったのが確認できます。
> tkn pipelinerun logs -n sample-tekton -f [echo : step-1] Hello World [echo : step-2] Hello World
ここで1点補足です。 runAfter
を指定しない場合、Taskは並列実行されると前述しました。その場合、適切にresultsの値がとれるのか?と疑問を覚えると思います。この点に関しては、task間でresultsの参照関係がある場合には実行順序制御が自動で行われるため、明示的にrunAfterを記述する必要はありません。ただ、明示的に記述した方がわかりやすいとの意見もありますので、チームメンバーと方針を決めるのがよいかと思います。
Tekton Pipelinesの機能紹介はここまでとなります。本記事では紹介していない機能もありますので、興味を持たれた方はぜひ試してみてください。
何故、Tekton を選んだか
最後に、Qmonus Value Streamの要素技術としてTektonを選択した理由について紹介します。
まず、CI/CD機能そのものを提供するOSSやSaaSを選択しなかった理由について説明します。当初、私達がDevOpsの取り組みの中でCI/CDパイプラインの作成や改善を続けていくにあたり、SaaSやOSSの検証も行いました。どのサービス・ソフトウェアもよくできていて学ぶことはとても多いのですが、私達のユースケースを実現するためには課題もありました。
例えば、以下のような課題がありました。
- 構築したいCI/CDのサイクルが複雑で、シンプルな機能だけでは構築が難しい。例えば、複数のコンポーネントを複数の異なるバージョンで組み合わせて動作させたいケースなど。
- k8sだけでなく、各パブリッククラウド特有の挙動を考慮したロジックを組み立てる必要がある。
このような背景があり、ある程度決まった枠組みが作られているサービス・ソフトウェアよりも、ワークフローのエンジンとなるような技術の方が私達に適しているのではないか、という結論に至りました。
Tektonには、ほかのプロダクトにはない次の特徴がありました。これらは私達のユースケースに合致するものでした。
- Kubernetes Nativeなソフトウェアのため、宣言的記述・スケーリングなどのk8sが持つメリットを活用できる。
- 最小の処理単位(step)をコンテナとして作るため処理を再利用しやすく、また既存のコンテナ資産を流用できる。
- 一連のシーケンシャルな処理の集合(Task)の順序関係を定義して、複雑なパイプラインを構築できる。
- TaskをPodとして実現するため、各処理間(コンテナ間)でリソースをシェアしやすい。
これらの特徴を踏まえて、私達が目指すDevOpsの仕組みを実現するにはTektonを利用するのが良いだろうと総合的に判断し、選択しました。
おわりに
Tektonのプロジェクトは開発が活発であり、どのコンポーネントも進化スピードが早いです。今回はTekton Pipelinesの基本的な機能の紹介のみでしたが、今後、新機能や変更点に関するキャッチアップ、運用してみての知見などを紹介していきたいと考えています。
Qmonus Value Streamチームは、メンバ一人一人が、利用者であるアプリケーション開発者のために信念を持って活動を続けています。 プラットフォームを内製開発するポジション、プラットフォームを使ってNTTグループのクラウド基盤やCI/CDを構築支援するポジションそれぞれで、一緒に働く仲間を募集しています。興味を持たれた方は応募いただけると嬉しいです。
次回は、CI/CDパイプライン作成時のパラメータバインディングの課題と、Qmonus Value Streamにおける改題解決のアプローチについて紹介します。お楽しみに!