Jenkins

Jenkinsfileの書き方

ほとんど情報がなかったので調べながら書いたメモ。基本的に、公式情報が最もまとまっている。

また、古い情報らしいけど、現状こちらにしか書かれていない項目もある。

Declarative Pipelineの1.2.8から、ジェネレータが追加されたようです。

基本

現状のJenkinsfileは2通り書き方があって、pipelineがルートになっている場合はDeclarative Pipelineという。この場合は、Groovyスクリプトを直接書くことができず、Groovyを書きたければscriptディレクティブを使う必要がある。この記事では主にこちらを扱う。

pipelineから始まらない場合、Scripted Pipelineといって、この場合は直接Groovyスクリプトも書けるし、node()stage()などの、Pipeline Stepsメソッドも書くことができる。便利そうにみえるけど自由度が高すぎて職人コードになりがち。

Declarative Pipelineの書き方

Jenkinsfileは特定の条件ごとにディレクティブを書いて実装する。ディレクティブの出現順は以下の順番になっている。例えばstagesのすぐ下にagentを書くことはできない。

pipeline {
    // 1行コメント
    /*
     * 複数行コメント
     */
    agent { ... }
    environment { ... }
    options {
        buildDiscarder(...)
        disableConcurrentBuilds(...)
        skipDefaultCheckout(...)
        timeout(...)
        retry(...)
        timestamps()
    }
    parameters {
        string(...)
        booleanParam(...)
        choice(...)
        file(...)
        text(...)
        password(...)
        run(...) // ??
    }
    tools {
        maven '...'
        jdk '...'
        gradle '...'
    }
    triggers {
        cron(...)
        pollSCM(...)
    }
    stages {
        stage {
            agent { ... }
            environment { ... }
            tools { ... }
            when { ... }
            steps {
                //Pipeline Steps Reference参照
                echo 'help'
                sh 'ls'
                script {
                    // 任意のGroovyスクリプト
                }
            }
        }
    }
    steps {
        //Pipeline Steps Reference参照
    }
    post {
        always { ... }
        success { ... }
        failure { ... }
        ...
    }
}

stepsだけの場合は、直接stepsの中に書くものを記述できる。

echo 'help'
sh 'ls'

node()のようにstepsディレクティブの中で子ブロックが現れる場合、そのブロックの中はstepsディレクティブと同じものが書ける。

steps {
    node('slave1'){
        sh 'ls'
        dir('output'){
            sh 'ls'
        }
    }
}

個別の事例

stageの書き方

stage()を使うと、進捗を分けて表示することができるが、インターネットでは2種類の書き方がある。stage単体で書かれているものは古く、推奨されない。

stage 'first'
sh 'ls'

stage 'second'
sh 'pwd'

今はブロックを取るようになっていて、こちらが推奨される。

stage('first'){
    sh 'ls'
}
stage('second'){
    sh 'pwd'
}

ビルドするノードを制限する

agentディレクティブでラベルを指定する。

agent {
    label 'slave1'
}

または、stepsディレクティブでnodeブロックを使う。

steps {
    node('slave1'){
    }
}

環境変数をセットする

environmentディレクティブが使える場合はその中で書く。ここでは、定数またはcredentials()しか使えない。また、credentials()はSSHユーザ名と秘密鍵を扱えない。

environment {
    GOPATH = '/home/jenkins/go'

    // Jenkinsの資格情報に登録されている値を取り出す
    TOKEN = credentials('credential_id')
}

credentials()で取り出したものは、シークレットテキストの場合はそのまま使える。ユーザ名とパスワードの場合、TOKENはユーザ名とパスワードを:で区切った文字列になっている。個別に使いたい場合は、TOKEN_USRTOKEN_PSWのようにサフィックスを付けて扱うと良い。

または、stepsディレクティブの中でwithEnvブロックやwithCredentialsブロックを使う。withEnvの中でだけ環境変数が有効になる。

steps {
    withEnv(["GOPATH=${env.WORKSPACE}"]) {
    }
    withCredentials(bindings: [
        // シークレットテキスト
        string(credentialsId: 'credential_id', variable: 'TOKEN'),

        // ユーザ名とパスワード
        usernamePassword(credentialsId: 'credential_id',
            passwordVariable:'PASS',
            usernameVariable:'USER'),

        // ファイル
        file(credentialsId: 'credential_id', variable: 'FILE'),

        // SSH秘密鍵: passphraseVariable, usernameVariableは省略可
        sshUserPrivateKey(credentialsId: 'credential_id',
            keyFileVariable: 'KEY_FILE',
            passphraseVariable: '',
            usernameVariable: '')
    ]){
    }
}

withCredentials()の詳細はCredentials Binding Pluginを読むと良い。

Groovyを使ってもっと細かく制御したい場合。stepsディレクティブに直接Groovyは書けないのでscriptディレクティブを使う。

steps {
    script {
        // ここからGroovy
        env.GOPATH = env.WORKSPACE
    }
}

ツールをインストールする

Jenkinsの管理メニューにGlobal Tools Configuration があり、そこで事前にMavenやGoコンパイラなどのバージョンと名前を定義しておくと、toolsディレクティブで、自動でインストールすることができる。

pipeline {
    tools {
        go 'go1.10'
    }
}

Pipeline Syntaxで自動生成したコードはtool name: 'go1.9', type: 'go'のように出力されるが、このままDeclarative Pipelineとして記述するとエラーになる。$type '$name'のように置き換えて書く必要がある。

パラメータ付きビルドにする

parametersに何か書くと、パラメータ付きビルドとして扱われる。params.nameで参照する。

pipeline {
    parameters {
        string(name: 'TARGET', defaultValue: 'Tests', description: '説明')
        choice(name: 'STAGE', choices: 'staging\nproduction', description: '説明')
    }
    stages {
        stage('build'){
            sh "./make.bash ${params.TARGET}"
        }
    }
}

実行条件を設定する

stage()ブロックでwhenディレクティブを使う。以下の例は、ブランチ名がrelease-*にマッチした場合のみstageを実行する。

stage('stage'){
    when {
        branch 'release-*'
    }
}

他にも、環境変数の値を条件としたり、全て一致またはどれかが一致などの条件を作ることができる。

複数行のシェルスクリプトを実行

'''または"""で囲むと複数行のテキストを記述できる。

steps {
    sh '''
    date
    sleep 10
    date
    '''
}

'''の場合は変数展開などをせずにそのままシェルへ渡す。"""なら${env.PATH}のような展開を行う。

GitHub Branch Source Plugin環境でスレーブを使ってビルドする

基本的には何もしなくてもプラグインがcloneするが、デフォルトでは(おそらく)Jenkinsマスターのワークスペースへcloneされる。このため、ビルドノードがマスター以外の場合、ビルドするソースコードがスレーブのワークスペースにない状態となる。

stepsディレクティブでcheckoutを使うとslaveのワークスペースへcloneが行われる。

steps {
    checkout scm
}

または最初から、agentディレクティブでラベルを指定しておくと、対象となったスレーブでcloneされる。

pipeline {
    agent {
        label 'slave1'
    }
    steps {
        sh 'ls'
    }
}

特定のディレクトリへcloneする

optionsディレクティブで指定する。

options {
    checkoutToSubdirectory 'src/v2'
}

または、dir()を使うとカレントディレクトリを変更できるので、移動した後でcheckout scmを実行すればいい。移動するディレクトリが存在しない場合は自動的に作成される。

stage('chekout'){
    steps {
        dir("src/v2"){
            checkout scm
        }
    }
}

GitHub Branch Source Pluginでcheckoutの動作を変更する

How to Customize Checkout for Pipeline Multibranchによると、checkout scmは、

checkout([
    $class: 'GitSCM',
    branches: scm.branches,
    extensions: scm.extensions,
    userRemoteConfigs: scm.userRemoteConfigs
])

の省略形らしい。Shallow cloneとかcloneするディレクトリを変更する場合は、scm.extensionsに配列でオプションを追加する。

checkout([
    $class: 'GitSCM',
    branches: scm.branches,
    extensions: scm.extensions + [
        [ $class: 'CloneOption', noTags: false ],
        [ $class: 'RelativeTargetDirectory',
          relativeTargetDir: "src/v2"
        ],
    ],
    userRemoteConfigs: scm.userRemoteConfigs
])

Gitサブモジュールを使う

checkout scmSubmoduleOptionをセットします。

checkout([
    $class: 'GitSCM',
    branches: scm.branches,
    extensions: scm.extensions + [
        [ $class: 'SubmoduleOption',
          disableSubmodules: false,
          parentCredentials: true,
          recursiveSubmodules: true,
          reference: '',
          trackingSubmodules: false
        ],
    ],
    userRemoteConfigs: scm.userRemoteConfigs
])

ただし、GitHub Branch Source Pluginを使う場合、parentCredentialsはGitHub APIトークンが使われているため、サブモジュールの参照もHTTPで行う必要があります。

checkoutの前にワークスペースをクリアする

checkout scmの前にdeleteDirを使う。

stage('clean'){
    steps {
        deleteDir()
    }
}
stage('checkout'){
    steps {
        checkout scm
    }
}

Scripts not permitted to use methodエラーで動作しない

Jenkinsfileのコードを実行した時、

RejectedAccessException: Scripts not permitted to use method (メソッド名)

というエラーで停止する場合がある。これは、外部のコードを無条件に実行すると危険なので、Script Security Pluginによってsandbox実行されているため、らしい。

外部のコードを制限するための機能なので、Jenkinsfileで回避できるものではない。エラーが発生した後であれば、Jenkinsの管理画面にエラーとなったメソッドの許可を求めるログが出ているので、そこでApprovalボタンを押せば次からはエラーを回避できる。このファイルは$JENKINS_HOME/scriptApproval.xmlに置かれているので、これをコピーしてもいい。

成果物を保存する

postセクションでArchive Artifact Pluginを使えばよい。

pipeline {
    post {
        success {
            archiveArtifacts artifacts: bin/*, fingerprint: true
        }
    }
}

成果物の保存数を制限する

いろいろ書き方はあるが、おそらくoptionsディレクティブを使うのが簡単。

pipeline {
    options {
        buildDiscarder(logRotator(numToKeepStr: '5', artifactNumToKeepStr: '5'))
    }
}

並列ビルドする

stepsparallelを使う。

steps {
    parallel(
        linux: {
            sh './make.bash -t linux_amd64'
        },
        windows: {
            sh './make.bash -t windows_amd64'
        }
    )
}

この例では、./make.bash -t linux_amd64./make.bash -t windows_amd64が並列実行される。

他のジョブをビルドする

buildを使う。

引数のパラメータはJenkinsを置いているフォルダまでのパスを渡す。相対パスの場合は呼び出し元ジョブのディレクトリがカレントになり、絶対パスの場合は$JENKINS_HOME/jobsがルートになる。

steps {
    build '../jobName1'
}

マルチブランチ構成のプロジェクトを呼び出す場合は、内部の階層がブランチごとに切られているので、ブランチ名も必要。

steps {
    build '../jobName1/master'
}

GitHub Branch Sourceが管理しているジョブはマルチブランチに近くて、Organizationの下にジョブが作られるので、次のようになる。

steps {
    build '/organizationName/jobs/jobName/master'
}

Jenkinsが管理しているCredentialでssh接続したい

ssh-agentプラグインをインストールするとsshagentブロックが利用可能になる。このブロックの中に記述したコマンドは、Jenkinsが管理している秘密鍵が追加されたssh-agentが動作してる状態で実行される。なのでJenkinsのアカウントでgit pushしたい場合は、以下のように書く。

steps {
    // jenkins_credential_id_strという名前のCredentialを読み込む
    sshagent(['jenkins_credential_id_str']){
        sh 'git push'
    }
}