こんにちは。テクマトリックスの橘です。

最近の CI ツールでは、パイプライン定義に YAML が採用されることが増えています。CI ツール以外でも Docker や Helm など、さまざまなツールが YAML 形式の設定ファイルをサポートしています。Configuration as Code の観点でも、YAML は主流になりつつあると感じます。一方で「なぜJenkinsのパイプラインはYAMLではなくGroovyなんだろう」という疑問も生じてきました。また、最近は GitHub Actions に触れる機会が増え、YAML でのワークフロー定義に慣れてきたこともあって、この疑問は一層強くなっていきました。

Jenkinのパイプラインについて調査を進める中で、JenkinsパイプラインをYAML形式で定義できるPipeline As YAMLというプラグインを見つけたため、本記事ではその概要をご紹介します。

はじめに:Jenkinsパイプラインの簡単なおさらい

Jenkinsパイプラインは、継続的インテグレーションと継続的デリバリー(CI/CD)を実現するための強力なフレームワークです。以前のJenkinsでは、フリースタイルジョブというGUIでの設定を中心とした設定方法が主流でした。しかし、フリースタイルジョブには設定が属人化・複雑化しやすい、複雑なCI/CDの表現が困難、耐障害性が低い、などの課題がありました。

このような課題に対応するために、Pipeline as Codeという考え方が導入され、GroovyベースのDomain Specific Language(DSL)Jenkinsfileでパイプラインを実現するという手段が採用されました。Gitでの差分管理やレビュー、耐障害性(再開可能)、可視化、マルチブランチ連携などを実現し、2016年のJenkins 2.0(パイプラインの正式公開バージョン)誕生以降、宣言的パイプライン(Declarative Pipeline)を軸にCI/CDの標準手段として定着しています。

そもそもなぜGroovy形式なのか?

現在、多くのCIツールではYAML形式によるパイプラインコードが採用されています。また、CIツールに限らず、Configuration As Codeの概念における設定ファイルとしては、YAMLが採用されるケースが非常に多い一方で、なぜJenkinsパイプラインはGroovy形式なのでしょうか?

最大の理由は、JenkinsやそのプラグインがJVM上で動く、という点だと考えられます。Groovy は JVM 言語であり、Java とシームレスに相互運用することが可能で、既存の Java ライブラリ・Jenkins プラグインと容易に連携でき、型やクラスの橋渡しがスムーズな点もGroovyを採用する理由になりました。他にもJVM言語はありますが(Kotlin、Scalaなど)、当時の言語成熟度や、Jenkinsの最大の売りの一つであるカスタマイズ性などを考慮すると、Groovyが最善の選択肢だったと言わざるを得ないでしょう。

本題:Jenkins Pipeline As YAMLプラグインとは?

Pipeline As YAMLプラグインは、Jenkinsのパイプラインジョブおよびマルチブランチパイプラインジョブの定義を、従来のGroovy DSLの代わりにYAML形式で行えるようにするものです。このプラグインは、既存のJenkinsのステップや宣言型パイプラインの機能を活用しつつ、YAMLという別の構文でパイプラインを定義する手段を提供するものであり、Jenkinsのパイプラインエンジン自体を置き換えるものではありません。
要するに、パイプライン自体はYMAL形式で記述できますが、パイプラインの実行時にはGroovyに変換されて実行される、というわけです。

このプラグインの具体的な説明に入っていく前に、お伝えしなければいけない重要な点が一つあります。
このプラグインは現在インキュベーション段階にあります。これは、プラグインがまだ発展途上であり、将来的に互換性のない変更が加えられる可能性があることを意味します。そのため、本番環境の重要なパイプラインへの適用には慎重な検討が必要です。このプラグインの開発チームはユーザーからのフィードバックやコントリビューションを期待していると思いますので、試験的に利用し、その感想を伝えてみてもよいかもしれません。

主な機能と構文の概要

Pipeline As YAMLプラグインは、宣言型パイプラインの主要なディレクティブや構造をYAML形式で表現できるように設計されています。YAMLで定義された内容は、実行時に宣言型パイプラインのスクリプトに変換されます。

設定方法

本プラグインを追加することで、設定項目が追加されます。ジョブの種類により以下のいずれかで設定が可能です。

Pipelineジョブ:

以下の選択肢がパイプラインの設定箇所に追加されます。

  • Pipeline As Yaml: Jenkinsジョブ設定画面の埋め込みエディタで直接YAMLを記述します。

  • Pipeline As Yaml from SCM: ソースコード管理システム(SCM)から jenkinsfile.yml ファイルを読み込みます。ファイルには任意の名前を設定可能です。

MultiBranch Pipelineジョブ:

以下の選択肢がパイプラインの設定箇所に追加されます。

  • by Jenkinsfile As Yaml: ソースコード管理システム(SCM)から jenkinsfile.yml ファイルを読み込みます。ファイルには任意の名前を設定可能です。

サポートされる主なディレクティブ

宣言型パイプラインで利用可能な多くのディレクティブが、YAML形式でもサポートされています。以下に主要なものとその基本的なYAML構文例を示します。これらのディレクティブは、多くの場合、ステージ定義内でも使用可能です。

agent: パイプライン全体または特定のステージを実行するエージェント(ノード)を指定します。

pipeline:
  agent: any
pipeline:
  agent:
    node:
      label: 'my-agent-label'

environment: 環境変数を定義します。

pipeline:
  environment:
    MAVEN_OPTS: "-Xmx1024m"
    VERBOSE: "true"

tools: パイプラインで使用するツール(例: Maven, JDK)を指定します。

pipeline:
  tools:
    maven: "Maven3"
    jdk: "JDK11"

options: パイプライン固有のオプションを設定します。

pipeline:
  options:
    - "timeout(time: 1, unit: 'HOURS')" # 一時間後に強制終了

parameters: パイプライン実行時にユーザーが入力するパラメータを定義します。

pipeline:
  parameters:
    - "string(name: 'BRANCH_NAME', defaultValue: 'main', description: 'Build target branch')"
    - "booleanParam(name: 'RUN_TESTS', defaultValue: true, description: 'Execute tests?')"

triggers: パイプラインを自動的に実行するトリガーを設定します。

pipeline:
  triggers:
    - "cron('H */4 * * 1-5')" # 平日の4時間ごと

stages: パイプラインの主要な実行単位であるステージを定義します。

pipeline:
  stages:
    - stage: "Build"

steps: ステージ内で実行される具体的なアクション(ステップ)を定義します。

pipeline:
  agent: any
  stages:
    - stage: "Build"
      steps:
        - echo "Building..."
        - sh "./build.sh"
    - stage: "Test"
      steps:
        - echo "Testing..."
        - sh "./test.sh"

post: パイプラインまたはステージの実行結果に応じて実行されるアクションを定義します。

pipeline:
  agent: any
  stages:
    - stage: "Deploy"
      steps:
        - echo "Deploying..."
        - sh "./deploy.sh"
  post:
    always:
      - echo "Pipeline finished."
    success:
      - mail to: 'dev-team@example.com', subject: 'Deployment Successful'
    failure:
      - mail to: 'ops-team@example.com', subject: 'Deployment Failed!'

when: ステージを実行するための条件を定義します。

pipeline:
  stages:
    - stage: "Deploy to Production"
      when:
        "branch 'main'" # 'main'ブランチの場合のみ実行
      steps:
        - echo "Deploying to production environment..."

library: 共有ライブラリを読み込みます。共有ライブラリについては、別途共有ライブラリのドキュメントを参照してください。

pipeline:
  library: "my-shared-library@1.0"
  agent: any
  stages:
    - stage: "Use Shared Library"
      steps:
        script:
          - "myCustomStepFromLibrary()" # 共有ライブラリのカスタムステップ


実際にYAMLでパイプラインを書いてみた

それでは、ここまで紹介してきたディレクティブを利用して、YAML形式の pipelineコードを作成してみましょう。

まず、従来のGroovy形式のパイプラインはこちらです。

Groovy形式の宣言型パイプライン(クリックで展開されます)
pipeline {
  agent none

  parameters {
    string(name: 'DEPLOY_TO', defaultValue: 'dev', description: '')
  }

  stages {
    stage('Build') {
      parallel {
        stage('Build Java 8') {
          agent {
            node {
              label 'java8'
            }
          }
          steps {
            bat 'echo [Build Java 8] build would run here'
          }
        }
        stage('Build Java 7') {
          agent {
            node {
              label 'java7'
            }
          }
          steps {
            bat 'echo [Build Java 7] build would run here'
          }
        }
      }
    }

    stage('Test') {
      parallel {
        stage('Backend Java 8') {
          agent {
            node {
              label 'java8'
            }
          }
          steps {
            bat 'echo [Backend Java 8] backend tests would run here'
          }
          post {
            always {
              bat 'echo [Backend Java 8] test results summary: PASSED (demo)'
            }
          }
        }
        stage('Backend Java 7') {
          agent {
            node {
              label 'java7'
            }
          }
          steps {
            bat 'echo [Backend Java 7] backend tests would run here'
          }
          post {
            always {
              bat 'echo [Backend Java 7] test results summary: PASSED (demo)'
            }
          }
        }
      }
    }

    stage('Confirm Deploy') {
      when {
        branch 'main'
      }
      steps {
        timeout(time: 3, unit: 'MINUTES') {
          input(message: 'Okay to Deploy to Staging?', ok: "Let's Do it!")
        }
      }
    }

    stage('Deploy') {
      when {
        branch 'main'
      }
      agent {
        node {
          label 'java8'
        }
      }
      steps {
        bat "echo [Deploy] would deploy to %DEPLOY_TO%"
      }
    }
  }
}


Javaの開発を想定したパイプラインで、二つのエージェントを利用して、並列でビルド、テストを行い、ユーザーの承認を経て、デプロイをするというものになります。デプロイ先をパイプライン実行時に設定することができる、パラメータビルドも利用しています。

このパイプラインをYAML形式で表現すると、以下のようになります。

YAML形式のパイプライン(クリックで展開されます)

pipeline:
  agent: none

  parameters:
    - "string(name: 'DEPLOY_TO', defaultValue: 'dev', description: '')"

  stages:
    - stage: Build
      parallel:
        - stage: Build Java 8
          agent:
            node:
              label: java8
          steps:
            - bat "echo [Build Java 8] build would run here"

        - stage: Build Java 7
          agent:
            node:
              label: java7
          steps:
            - bat "echo [Build Java 7] build would run here"
            
    - stage: Test
      parallel:
        - stage: Backend Java 8
          agent:
            node:
              label: java8
          steps:
            - bat "echo [Backend Java 8] backend tests would run here"
        - stage: Backend Java 7
          agent:
            node:
              label: java7
          steps:
            - bat "echo [Backend Java 7] backend tests would run here"

    - stage: Confirm Deploy
      when:
        "branch 'main'"
      options:
        - "timeout(time: 3, unit: 'MINUTES')"
      steps:
        - "input(message: 'Okay to Deploy to Staging?', ok: \"Let's Do it!\")"

    - stage: Deploy
      when:
        "branch 'main'"
      agent:
        node:
          label: java8
      steps:
        - bat "echo [Deploy] would deploy to %DEPLOY_TO%"


インデントだけでネストを表現しているので、行数は半分近くまで圧縮されています(94行→54行)。

最後に:YAMLで定義してみての感想

どちらが記述しやすいか、読みやすいかは、人それぞれ異なる感想を持つと思いますが、私個人の意見としては、YAML形式の方が 波括弧の対応を考える必要がなく、読みやすさだけでなく、記述しやすさも高いように感じました。ただし、プラグイン自体がインキュベーション段階であることと、以下のような点が気になったため、強くお勧めできるとは言いづらい状況です。

参照できるリソースが豊富ではないため、記述方法が分からないことがあった

例えば、whenディレクティブの“branch ‘main'”は全体をダブルクォート、ブランチ名をシングルクォートで囲む必要があります。また、ユーザーからの入力を受け付けるinput()や、 optionsディレクティブのtimeout()もダブルクォートで囲む必要があります。慣れればよいだけなのかもしれませんが、Groovy形式のパイプラインでも、GitHub Actionsのworkflowでもこのような記述方法はないため、少し戸惑いました。
まだインキュベーション状態のプラグインのため、ドキュメントがそれほど充実していないことと、ブログなどでの試用報告なども見つけられず、トライ&エラーでの作業が求められました。
評価が一通り終わった後に気づいたのですが、実はこのPipeline as YAMLプラグインの機能として、Pipeline As Yaml Converterという、YAML形式のパイプラインをDeclarative Pipelineに変換してくれる機能があります。こちらを使うと作成したYAMLのパイプラインの構文が正しいかチェックしてくれるので、この機能を利用していれば、もう少しYAMLへの変換作業が楽になったかもしれません。

この機能は、いずれかのパイプラインジョブを開き、サイドメニュー内の Pipeline Syntax > Pipeline As Yaml Converter からアクセス可能です。

一部使えない機能がある

複数の因子の組み合わせのテストなどを行う際(例えば、プラットフォームとブラウザーの組み合わせなど)に利用するmatrixディレクティブは未サポートのため、利用することができません。もしYAMLで実現するとなるとその組み合わせ分のstageあるいはstepを用意することになります。
他にもDeclarative PipelineをYAML形式に変換する機能や、IntelliJ Idea用のプラグインなどの開発が計画されているようですが、リリース時期等は不明です。

今後、開発が進んでいき、上記の課題が解決された際には、再度評価をしてみたいと思います。