Jenkinsfileの書き方 (Jenkins Pipeline)

# まずはじめに

つい先日、はじめてjenkins pipelineのためのJenkinsfileを作成し、PHPアプリケーションのワンクリックデプロイを実現しました。今回は振り返りの意味も込めて、その際に事前に知っておくと良かった点をまとめていきます。これから、Pipelineを構築したい方は参考にしてみてください。

環境は以下の通りです。
`CentOS: 7.3.1611`
`Jenkins Version: 2.73.2`

私の場合は、Pipelineを作成する前にRubyで書かれたCapistranoというデプロイツールでリリース用のスクリプトをすでに構築・運用済みでした。慣れてしまえば十分な環境でしたが、毎回コマンドを叩く手間と増加するチームメンバーへの共有コストを考えるとよりシンプルな手順が必要だと感じてきたため、Jenkinsへの移行を決断しました。

移行プロセスとして、ゼロからCapistranoのタスクを全てPipeline上の各ステージで書き直すことも考えましたが、Capistrano側の修正でそのまま再利用可能であると判断したためPipeline上でCapistranoの各タスクを実行する方針で進めました。

私のケースと同様にすでに別の方法である程度リリースを自動化されているのであれば、Pipelineの構築はそれほど難しい作業ではないので、ぜひ試してみることをオススメします。

# ポイント

それでは早速ポイントに入っていきましょう。

## 1. Declarative vs Scripted 2種類のSyntaxの違い

(公式)[Declarative versus Scripted Pipeline syntax](https://jenkins.io/doc/book/pipeline/#declarative-versus-scripted-pipeline-syntax)

こちらのドキュメントが参考になります。
要約すると、Jenkinsfileの記法はDeclarative記法とScripted記法の2種類あり、Declarative記法が後発です。Declarative記法の方がより柔軟でかつ読み書きしやすい記法です。
Jenkins Pipelineの投稿記事をググると記事によって記法が様々ですので、まずはじめにこの点を押さえて記法の違いを認識しておくことが大事です。

簡単な見分け方は開始タグを確認することです。

| 開始タグ | 記法 |
|:-----------|:------------|
|pipeline| Declarative|
|node| Scripted|

ちなみに私はDeclarative記法で記述したので、次のポイントからはその点にご注意ください。

## 2. 環境変数の設定方法
(公式)[Environment](https://jenkins.io/doc/book/pipeline/syntax/#environment)

こちらの公式ドキュメントに記載の通り、環境変数は`pipeline`直下または`stage`ディレクティブ内でのみ定義が可能です。_#8. Script実行結果をコマンドに渡す方法_ で紹介しますが、実行コマンドの結果を保存する際にも活用可能です。

```groovy
pipeline {
    agent any
    environment {
        work_dir='/home/jenkins/work'
        bundle='/home/jenkins/bin/bundle'
        deploy_dir='/home/jenkins/deploy'
    }
    stages { // 私の場合、特にステージごとの環境変数は必要なかったです。
        stage('Check Environment') {
            environment { 
                LOCAL_VAR='/home/jenkins/target_dir' 
            }
            steps {
                sh 'printenv'
            }
        }
    }
}
```

## 3. ビルド時のユーザー入力

(公式)[parameters](https://jenkins.io/doc/book/pipeline/syntax/#parameters)

こちらの`parameters`を設定すると、Jenkinsの「Build Now」が「Build with Parameters」へと変化し、ビルド開始時にユーザー入力を受け付けるようになります。私の場合、リリース用のgit情報とデプロイ対象サーバーを選択できるようにしました。

```groovy
pipeline {
    agent any
    environment {
        work_dir='/home/jenkins/work'
        bundle='/home/jenkins/bin/bundle'
        deploy_dir='/home/jenkins/deploy'
    }
    parameters {
        // 公式ドキュメントではchoiceの場合、choices: ['one', 'two', 'three']
        // のようにかけるそうですが、なぜか私の環境ではsyntax errorがでてしまったため、
        // 以下のように\n改行コードを入れることでセレクトボックス入力が可能になりました。
        choice(name: 'BRANCH_OR_TAG', choices: 'Branch\nTag\n', description: 'Select Checkout Type')
        string(name: 'CHECKOUT_POINT', defaultValue: 'develop / v1.0.0', description: 'Input Branch / Tag Name')
        choice(name: 'SERVERS', choices: 'hogehoge.com\nfugafuga.com\n', description: 'Select Deploying Servers')
    }
    stages {
        stage('Check Environment') {
            environment { 
                LOCAL_VAR='/home/jenkins/target_dir' 
            }
            steps {
                // 公式ドキュメントより
                // このように記述することで各ステージの途中で入力を受け付けるようになる
                input {
                    message "Should we continue?"
                    ok "Yes, we should."
                    submitter "alice,bob"
                    parameters {
                        string(name: 'PERSON', defaultValue: 'Mr Jenkins', description: 'Who should I say hello to?')
                    }
                }
                // 以下のようにsteps内からparametersの値へアクセス可能
                // 変数展開のためにダブルクオート
                sh """
                    printenv
                    echo ${params.BRANCH_OR_TAG}
                    echo ${params.CHECKOUT_POINT}
                    echo ${params.SERVERS}
                """
            }
        }
    }
}
```

## 4. 各Section/Directiveの階層関係

(公式)[Pipeline Syntax](https://jenkins.io/doc/book/pipeline/syntax/#pipeline-syntax)

書き進めていく際にSection/Directiveの階層構造を認識していくことが大事です。Declarative記法の場合、割と厳密な階層構造が指定されているので雰囲気で記述していくと頻繁にsyntax errorが発生します。

__Section定義位置・子要素早見表__
---
|Section|top-level|stage直下|指定可能な<br>子section|指定可能な<br>子directive|
|:--|:--|:--|:--|:--|
|agent|○|○||
|stages|○|○|stage|
|post|○|○|||
|steps||○|||

__Directive定義位置・子要素早見表__
---
|Directive|top-level|stage<br>直下|stages<br>直下|steps<br>直下|指定可能な<br>子section|指定可能な<br>子directive|
|:--|:--|:--|:--|:--|:--|:--|
|environment|○|○||||
|options|○|○<br>指定可能なオプションは限定||||
|parameters|○|○||||
|triggers|○|○||||
|stage|||○||steps<br>stages|parallel|
|tools|○|○|||
|input|○|||
|when|○|||
|parallel||○||||stage|
|script||||○||stage|


## 5. ディレクトリ移動してコマンドを実行する方法

(公式)[Pipeline: Basic Steps](https://jenkins.io/doc/pipeline/steps/workflow-basic-steps/#pipeline-basic-steps)

\#4で紹介した基本的なDeclarative記法のDirectiveの機能に加え、pluginという形で様々機能が提供されています。その中の一つ`dir()`を使うことで、ディレクトリを移動してからコマンドを叩くことが可能になります。`dir()`などのプラグインはstepsセクション内でのみ使用可能です。

```groovy
pipeline {
    agent any
    environment {
        work_dir='/home/jenkins/work'
        bundle='/home/jenkins/bin/bundle'
        deploy_dir='/home/jenkins/deploy'
    }
    stage('Change Directory And Echo') {
        steps {
            dir(work_dir) {
                 sh "echo ${deploy_dir}"
            }
        }
    }
}
```

## 6. 並列処理の方法

(公式) [Parallel](https://jenkins.io/doc/book/pipeline/syntax/#parallel)

```groovy
pipeline {
    agent any
    stages {
        stage('Parallel Stage') {
            parallel {
                stage('Branch A') {
                    agent {
                        label "for-branch-a"
                    }
                    steps {
                        echo "On Branch A"
                    }
                }
                stage('Branch B') {
                    agent {
                        label "for-branch-b"
                    }
                    steps {
                        echo "On Branch B"
                    }
                }
                stage('Branch C') {
                    agent {
                        label "for-branch-c"
                    }
                    stages {
                        stage('Nested 1') {
                            steps {
                                echo "In stage Nested 1 within Branch C"
                            }
                        }
                        stage('Nested 2') {
                            steps {
                                echo "In stage Nested 2 within Branch C"
                            }
                        }
                    }
                }
            }
        }
    }
}
```

上の例は公式ドキュメントからの引用です。#4で紹介した通り、`parallel`は`stage`直下にのみ指定可能です。私は感覚として`stages`直下にそのまま`parallel`を指定し複数の`stage`を実行しようと試みたのですが、うまくいきませんでした。

平行処理をする場合は、一度平行処理全体の`stage`を作成し、その配下に`parallel`+複数の`stage`を指定しなければいけません。

## 7. Bashでコマンド実行する方法

こちらのポイントに関しては、私自身もベストプラクティスを模索中です。
私の場合、すでにCapistranoでデプロイしていたため、サーバ上の`.bashrc`に環境変数がある程度指定してあり、かつCapistranoタスクがbashに依存していました。

理想的にはJenkinsfile一つで必要な環境変数の準備が完結することが望ましいですが、今回はbashの環境変数を使用するために以下のようにしました。

```groovy
pipeline {
    agent any
    environment {
        work_dir='/home/jenkins/work'
        git_dir='/home/jenkins/repository'
        cap_dir='/home/jenkins/capistrano'
        bundle='/home/jenkins/bin/bundle'
        deploy_dir='/home/jenkins/deploy'
    }
    stage('Change Directory And Echo') {
        steps {
            dir(work_dir) {
                 sh """
                     echo 'source ~/.bashrc' > composer.sh
                     echo 'cd ${cap_dir}' >> composer.sh
                     echo 'GIT_DIR=${git_dir} ${bundle} exec cap production composer:install' >> composer.sh
                     bash ./composer.sh
                     rm ./composer.sh
                 """
            }
        }
    }
}
```

## 8. Script実行結果をコマンドに渡す方法

最後に簡単なロジックを挟み、その結果をコマンド実行時に利用する方法です。
ポイントは以下の3点です。

1. `environment`ディレクティブ内に変数を定義(top-level or `stage`内)
2. `steps`セクション直下の`script`ディレクティブ内にgroovyで処理を記述
3. 変数展開して環境変数を`sh`内で展開して使用

以下の例は、ビルド開始時にユーザー入力からCapistranoへ渡す`roles`の指定を行う場面です。

```groovy
pipeline {
    agent any
    environment {
        work_dir='/home/jenkins/work'
        git_dir='/home/jenkins/repository'
        cap_dir='/home/jenkins/capistrano'
        bundle='/home/jenkins/bin/bundle'
        deploy_dir='/home/jenkins/deploy'
        // ポイント1
        role=''
    }
    parameters {
        choice(name: 'ROLE', choices: 'first\nsecondthird\n', description: 'Server Cluster')
    }
    stage('Change Directory And Echo') {
        steps {
            // ポイント2
            script {
                if (params.ROLE =~ /first/) {
                    role = "first"
                } else if(params.ROLE =~ /second/) {
                    role = "second"
                } else if(params.ROLE =~ /third/) {
                    role = "third"
                }
            }
            dir(work_dir) {
                 // ポイント3
                 sh """
                     echo 'source ~/.bashrc' > composer.sh
                     echo 'cd ${cap_dir}' >> composer.sh
                     echo 'GIT_DIR=${git_dir} ${bundle} exec cap production --roles=${role} composer:install' >> composer.sh
                     bash ./composer.sh
                     rm ./composer.sh
                 """
            }
        }
    }
}
```