Jenkins宣言型パイプライン matrix 構文の紹介


この記事の初出Liam NewmanによるJeknins.ioへの投稿です。

複数の構成に対して同じアクションを実行しなければならないことがよくあります。そんなとき、これまではパイプラインで同一のステージを何回もコピーするしかありませんでした。変更が必要になると、パイプラインのあちこちに同じ変更を加えなければなりません。パイプラインが大きいと、ほんのいくつかの構成をメンテナンスするだけでも大変でした。

Declarative Pipeline 1.5.0-beta1(Jenkins Experimental Update siteからダウンロードできます。補注:本記事で紹介するmatrix構文は2019年12月リリースの Declarative Pipeline 1.5.0 にてオフィシャルリリースされました)では、新しくmatrixセクションが追加され、一度ステージのリストを指定するだけで、複数の構成でそのステージのリストを並行実行できるようになりました。どんなものか、さっそく見てみましょう。

単一構成のパイプライン

まず、ビルドステージとテストステージからなるシンプルなパイプラインから始めます。ビルドおよびテストアクションの代わりとしてechoステップを使用しています。

Jenkinsfile

pipeline {
    agent none
    stages {
        stage('BuildAndTest') {
            agent any
            stages {
                stage('Build') {
                    steps {
                        echo 'Do Build'
                    }
                }
                stage('Test') {
                    steps {
                        echo 'Do Test'
                    }
                }
            }
        }
    }
}

複数のプラットフォームとブラウザーに対応したパイプライン

つぎに、プラットフォームとブラウザーの組み合わせに対してビルドとテストを実行したいと思います。新しいmatrix ディレクティブを使うと、軸の集まりであるaxes(訳注:axesはaxisの複数形)を指定できます。個々のaxisにはnameと1つまたはそれ以上の値を含むvaluesリストがあります。パイプラインが実行されると、Jenkinsはこれらに基づいて、軸の値の可能な組み合わせすべてに対してステージを実行します。マトリクスのすべてのセルは並行して実行されます(唯一の制限は、利用可能なエージェントの数です)。各セル内のステージは順次実行されます。

私のマトリクスにはPLATFORMBROWSERという2つの軸があります。PLATFORMには3つの値があり、BROWSERには4つの値があるため、結果として12の異なる組み合わせでステージが実行されます。各セルで軸の値を出力するようechoステップを変更しました。

Jenkinsfile

pipeline {
    agent none
    stages {
        stage('BuildAndTest') {
            matrix {
                agent any
                axes {
                    axis {
                        name 'PLATFORM'
                        values 'linux', 'windows', 'mac'
                    }
                    axis {
                        name 'BROWSER'
                        values 'firefox', 'chrome', 'safari', 'edge'
                    }
                }
                stages {
                    stage('Build') {
                        steps {
                            echo "Do Build for ${PLATFORM} - ${BROWSER}"
                        }
                    }
                    stage('Test') {
                        steps {
                            echo "Do Test for ${PLATFORM} - ${BROWSER}"
                        }
                    }
                }
            }
        }
    }
}

ログ出力(抜粋)

...
[Pipeline] stage
[Pipeline] { (BuildAndTest)
[Pipeline] parallel
[Pipeline] { (Branch: Matrix - OS = 'linux', BROWSER = 'firefox')
[Pipeline] { (Branch: Matrix - OS = 'windows', BROWSER = 'firefox')
[Pipeline] { (Branch: Matrix - OS = 'mac', BROWSER = 'firefox')
[Pipeline] { (Branch: Matrix - OS = 'linux', BROWSER = 'chrome')
[Pipeline] { (Branch: Matrix - OS = 'windows', BROWSER = 'chrome')
[Pipeline] { (Branch: Matrix - OS = 'mac', BROWSER = 'chrome')
[Pipeline] { (Branch: Matrix - OS = 'linux', BROWSER = 'safari')
[Pipeline] { (Branch: Matrix - OS = 'windows', BROWSER = 'safari')
[Pipeline] { (Branch: Matrix - OS = 'mac', BROWSER = 'safari')
[Pipeline] { (Branch: Matrix - OS = 'linux', BROWSER = 'edge') (hide)
[Pipeline] { (Branch: Matrix - OS = 'windows', BROWSER = 'edge')
[Pipeline] { (Branch: Matrix - OS = 'mac', BROWSER = 'edge')
...
Do Build for linux - safari
Do Build for linux - firefox
Do Build for windows - firefox
Do Test for linux - firefox
Do Build for mac - firefox
Do Build for linux - chrome
Do Test for windows - firefox
...

無効な組み合わせを除外する

基本的なマトリクスが作成できたところで、私は無効な組み合わせもあることに気づきました。Microsoft EdgeはWindowsでしか動きませんし、SafariのLinux版はありません。

excludeを使うと、無効なセルを除くことができます。各excludeには1つまたはそれ以上のaxisディレクティブをnameおよびvaluesとともに指定します。exclude内のaxisディレクティブは、(マトリクスのセルを生成するのと同じように)組み合わせのセットを生成します。excludeの組み合わせに含まれる値に一致するセルはマトリクスから除外されます。複数のexcludeディレクティブがある場合、個別に評価され、セルが除外されます。

除外する値のリストが長くなる場合、valuesではなくnotValuesを使うと、除外したくない軸の値を指定できます。そうです、二重否定になるため、ちょっとわかりにくいかもしれません。私は、notValueは本当に必要なときにだけ使用するようにしています。

次のサンプルパイプラインでは、 linux, safariという組み合わせだけを除外したうえで、さらにedgeブラウザーでwindows以外のプラットフォームを除外しています。

このパイプラインは2つの軸を使用していますが、axisディレクティブの数に制限はありません。

また、このパイプラインではどちらのexcludeも両方の軸の値を指定していますが、これは必須ではありません。linuxセルだけを実行したい場合、次のようなexcludeを使用します。

exclude {
    axis {
        name 'PLATFORM'
        notValues 'linux'
    }
}
pipeline {
    agent none
    stages {
        stage('BuildAndTest') {
            matrix {
                agent any
                axes {
                    axis {
                        name 'PLATFORM'
                        values 'linux', 'windows', 'mac'
                    }
                    axis {
                        name 'BROWSER'
                        values 'firefox', 'chrome', 'safari', 'edge'
                    }
                }
                excludes {
                    exclude {
                        axis {
                            name 'PLATFORM'
                            values 'linux'
                        }
                        axis {
                            name 'BROWSER'
                            values 'safari'
                        }
                    }
                    exclude {
                        axis {
                            name 'PLATFORM'
                            notValues 'windows'
                        }
                        axis {
                            name 'BROWSER'
                            values 'edge'
                        }
                    }
                }
                stages {
                    stage('Build') {
                        steps {
                            echo "Do Build for ${PLATFORM} - ${BROWSER}"
                        }
                    }
                    stage('Test') {
                        steps {
                            echo "Do Test for ${PLATFORM} - ${BROWSER}"
                        }
                    }
                }
            }
        }
    }
}

ログ出力(抜粋)

...
[Pipeline] stage
[Pipeline] { (BuildAndTest)
[Pipeline] parallel
[Pipeline] { (Branch: Matrix - OS = 'linux', BROWSER = 'firefox')
[Pipeline] { (Branch: Matrix - OS = 'windows', BROWSER = 'firefox')
[Pipeline] { (Branch: Matrix - OS = 'mac', BROWSER = 'firefox')
[Pipeline] { (Branch: Matrix - OS = 'linux', BROWSER = 'chrome')
[Pipeline] { (Branch: Matrix - OS = 'windows', BROWSER = 'chrome')
[Pipeline] { (Branch: Matrix - OS = 'mac', BROWSER = 'chrome')
[Pipeline] { (Branch: Matrix - OS = 'windows', BROWSER = 'safari')
[Pipeline] { (Branch: Matrix - OS = 'mac', BROWSER = 'safari')
[Pipeline] { (Branch: Matrix - OS = 'windows', BROWSER = 'edge')
...
Do Build for linux - firefox
...

実行時にセルの動作を制御する

matrixディレクティブの内側には、「セルごと」のディレクティブも追加できます。stageに追加するのと同じディレクティブでマトリクスの各セルの動作を制御できます。セルごとのディレクティブは、入力としてセルの軸の値を使用できるので、軸の値に合わせて個々のセルの動作をカスタマイズできます。

私のJenkinsサーバーには、対応するOSに合わせてラベル付けしたエージェント(“linux-agent”、“windows-agent”、“mac-agent”)が構成してあります。マトリクスの各セルを適切なOSで実行するため、Groovy文字列テンプレートを使ってセルにラベルを設定します。

matrix {
    axes { ... }
    excludes { ... }
    agent {
        label "${PLATFORM}-agent"
    }
    stages { ... }
    // ...
}

私はときどき、Jenkins Web UIから手動でパイプラインを実行することがあります。その場合、1つのプラットフォームだけを選択して実行できるとよいでしょう。axisおよびexcludeディレクティブはマトリクスを構成するセルの静的なセットを定義します。組み合わせは実行が始まる前に生成され、このときまだパラメーターは処理されていません。つまり、ジョブが開始された後にマトリクスのセルを追加または削除することはできません。

いっぽう、「セルごと」のディレクティブは実行時に評価されます。 matrixの内側で「セルごと」にwhenディレクティブを指定して実行するマトリクスのセルを制御できます。そこで、プラットフォームのリストを指定したchoiceパラメーターを追加し、whenディレクティブに条件を追加します。すると、すべてのプラットフォームが実行されるか、選択したプラットフォームに一致するセルだけが実行されます。

pipeline {
    parameters {
        choice(name: 'PLATFORM_FILTER', choices: ['all', 'linux', 'windows', 'mac'], description: 'Run on specific platform')
    }
    agent none
    stages {
        stage('BuildAndTest') {
            matrix {
                agent {
                    label "${PLATFORM}-agent"
                }
                when { anyOf {
                    expression { params.PLATFORM_FILTER == 'all' }
                    expression { params.PLATFORM_FILTER == env.PLATFORM }
                } }
                axes {
                    axis {
                        name 'PLATFORM'
                        values 'linux', 'windows', 'mac'
                    }
                    axis {
                        name 'BROWSER'
                        values 'firefox', 'chrome', 'safari', 'edge'
                    }
                }
                excludes {
                    exclude {
                        axis {
                            name 'PLATFORM'
                            values 'linux'
                        }
                        axis {
                            name 'BROWSER'
                            values 'safari'
                        }
                    }
                    exclude {
                        axis {
                            name 'PLATFORM'
                            notValues 'windows'
                        }
                        axis {
                            name 'BROWSER'
                            values 'edge'
                        }
                    }
                }
                stages {
                    stage('Build') {
                        steps {
                            echo "Do Build for ${PLATFORM} - ${BROWSER}"
                        }
                    }
                    stage('Test') {
                        steps {
                            echo "Do Test for ${PLATFORM} - ${BROWSER}"
                        }
                    }
                }
            }
        }
    }
}

Jenkins UIからパイプラインを実行し、PLATFORM_FILTERパラメーターにmacを指定すると、次のような結果が出力されます。

ログ出力 (抜粋 – PLATFORM_FILTER = ‘mac’ )

...
[Pipeline] stage
[Pipeline] { (BuildAndTest)
[Pipeline] parallel
[Pipeline] { (Branch: Matrix - OS = 'linux', BROWSER = 'firefox')
[Pipeline] { (Branch: Matrix - OS = 'windows', BROWSER = 'firefox')
[Pipeline] { (Branch: Matrix - OS = 'mac', BROWSER = 'firefox')
[Pipeline] { (Branch: Matrix - OS = 'linux', BROWSER = 'chrome')
[Pipeline] { (Branch: Matrix - OS = 'windows', BROWSER = 'chrome')
[Pipeline] { (Branch: Matrix - OS = 'mac', BROWSER = 'chrome')
[Pipeline] { (Branch: Matrix - OS = 'windows', BROWSER = 'safari')
[Pipeline] { (Branch: Matrix - OS = 'mac', BROWSER = 'safari')
[Pipeline] { (Branch: Matrix - OS = 'windows', BROWSER = 'edge')
...
Stage "Matrix - OS = 'linux', BROWSER = 'chrome'" skipped due to when conditional
Stage "Matrix - OS = 'linux', BROWSER = 'firefox'" skipped due to when conditional
...
Do Build for mac - firefox
Do Build for mac - chrome
Do Build for mac - safari
...
Stage "Matrix - OS = 'windows', BROWSER = 'chrome'" skipped due to when conditional
Stage "Matrix - OS = 'windows', BROWSER = 'edge'" skipped due to when conditional
...
Do Test for mac - safari
Do Test for mac - firefox
Do Test for mac - chrome

まとめ

この記事では、 matrixディレクティブを使い、簡潔で強力な宣言型パイプラインを作成する方法を紹介しました。matrixを使わずに同等のパイプラインを作成しようとすると、すぐに何倍もの長さになり、理解もメンテナンスもはるかに困難になるでしょう。

現在、マトリクスは試験的アップデートセンターから入手できます。ドキュメントやオンラインヘルプの更新が終わり次第、メインのアップデートセンターにリリースされる予定です。

その他のリソース

(この記事は、CloudBees社 Blog 「Welcome to the Matrix」2020年2月18日 Liam Newman 投稿記事の翻訳です。)