[DevOpsプラットフォームの取り組み #5] Cloud Native CI/CDを実現するTektonの紹介

はじめに

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は、上記で説明した TaskPipelineTaskRunPipelineRun を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では、そのようなケースで利用できる機能として resultsworkspaces が提供されています。

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における改題解決のアプローチについて紹介します。お楽しみに!

© NTT Communications Corporation All Rights Reserved.