初めての Jenkins と Declarative Pipeline

初めての Jenkins と Declarative Pipeline
=============

勉強のために手元の Ubuntu 16.04 に [Jenkins](https://jenkins.io/) を入れてみた。仕事でたまに使っているが自分で設定するのは初めてだ。最新版はかなり変わってて面白かった。特に Jenkinsfile というテキストファイルで設定を記述する所と、最初から build agent docker を使う事前提になっている(?) のが興味深い。また、オプションで [Blue Ocean プラグイン](https://jenkins.io/projects/blueocean/) というのを使うと CircleCI のようなイマドキな画面に早変わりする。人気のある OSS 製品が陳腐化するというのは珍しくないが、こうして進歩を止めない姿勢は素晴らしい。

## apt でインストール

[Download](https://jenkins.io/download/) の説明に従う。自動更新が使える [Jenkins Debian packages](https://pkg.jenkins.io/debian-stable/) が楽そう。

Jenkins の公開鍵を [apt-key](https://manpages.debian.org/jessie/apt/apt-key.8.ja.html) で  apt に保存する。

    wget -q -O - https://pkg.jenkins.io/debian-stable/jenkins.io.key | sudo apt-key add -
    
Jenkins のエントリを [sources.list.d](http://manpages.ubuntu.com/manpages/trusty/ja/man5/sources.list.5.html) に追加

    sudo sh -c 'echo "deb https://pkg.jenkins.io/debian-stable binary/" > /etc/apt/sources.list.d/jenkins.list'

インストール

    sudo apt-get update
    sudo apt-get install -y jenkins

これで自動的に 8080 ポートで起動する。ポート番号を変えたい場合は [/etc/init.d/jenkins](https://wiki.jenkins.io/display/JENKINS/Installing+Jenkins+on+Ubuntu) の HTTP_PORT を変更する。

あと、どうやら docker を使っているらしいので docker グループに入れておく。

    gpasswd -a jenkins docker
    sudo systemctl restart jenkins

余談だが `dpkg -L jenkins` でパッケージの中を見ると起動スクリプトが `/etc/init.d/jenkins` にある。最近の自動起動は systemd に変わったはずなのでどういう仕組なのかなと思ったら、スクリプトの LSB ヘッダという物を使って [systemd-sysv-generator](https://www.freedesktop.org/software/systemd/man/systemd-sysv-generator.html) で systemd service を自動生成しているらしい。

## [Post-installation setup wizard](https://jenkins.io/doc/book/installing/#setup-wizard)

初回起動時だけ色々設定が必要。インストールが完了してブラウザで 8080 ポートにアクセスすると、初期化画面が表示される。

* Unlock Jenkins: 指示に従って `/var/lib/jenkins/secrets/initialAdminPassword` にあるパスワードを打ち込む。
* Customizing Jenkins with plugins: インストールするプラグインを選べるらしいが、良くわからないので Install suggested plugins にした。
* Creating the first administrator user: ユーザ名とパスワードを入れる。OS のユーザ名とかとは関係なく、Jenkins 内だけのユーザ管理。
    * あとで Manage Jenkins > Configure Global Security > でユーザ管理方法を選べる。

## Pipeline の練習

* New Item を選ぶ。
* 適当にアイテムに名前を付ける。
* Multibranch Pipeline を選ぶ
* Add Source で適当にビルドしたいプロジェクトを選ぶ
    * Github のプライベートレポジトリにアクセスしたい時、[GitHub personal access tokens](https://github.com/settings/tokens) を使う。
        * Personal access tokens のページで Generate new token を押す
        * scope: "repo" を指定して生成
    * Credentials: Add
        * Kind: Username and password
        * Username: github ユーザ名
        * Password: 作った Github personal access token
        * この内容はメニューの Credentials で編集出来る。
    * Owner: github のユーザ名や Organization name を入れる。
    * Repository: 今までの設定が正しいとレポジトリを選択出来る。
* Scan Repository Triggers
    * Periodically if not otherwise run: チェックして、更新頻度を入れる。
    * Jenkins がファイアウォール内にある時は webhook を受け取れないので、ポーリングして更新を知る。
* Save

これで何かが走るが特に何も起こらない。そこでレポジトリに Jenkinsfile という名前の以下のようなファイルを作ると [Docker image の node](https://hub.docker.com/_/node/) 上で `npm --version` を実行する。

```groovy

pipeline {
    agent { docker 'node:8.9.4' }
    stages {
        stage('build') {
            steps {
                sh 'npm --version'
            }
        }
    }
}
```

この文法は Groovy という物らしいが、知らなくても使える。この Jenkinsfile を編集する事でビルド環境を作るが、多少引っかかる部分があったので以下に書く。

## トラブルシューティング

* レポジトリが private な submodule を持っている場合。
    * Jenkins の git 機能で submodule の振る舞いを変える事が出来る。
    * Branch Sources > Behaviors > Advanced sub-modules behaviors
        * Use credentials from default remote of parent repository
* `Error: EACCES: permission denied, mkdir '/.npm'` というエラーが出てしまう。
    * https://stackoverflow.com/questions/42743201/npm-install-fails-in-jenkins-pipeline-in-docker/42957034
    * 環境変数に `HOME=/work` 等を追加すれば良い。
* Github のビルド結果のリンクが `unconfigured-jenkins-location` になってしまう。
    * Jenkins > Configure System > Jenkins Location > Jenkins URL: を設定し直す
    * デフォルトで正しい値になっていると思うが、特に設定を変えなくても Save ボタンを押すだけで直る。
* npm update で `fatal: unable to look up current user in the passwd file: no such user`
    * [git のバージョンが古いとこのエラーが出る](https://github.com/NetSys/bess/commit/7d96bd1c070489beb68657e4c26f5ffee19c90aa) らしい。
    * また、HOME を適当に `.` とかにしていても出るので注意。

        ```
        git config --global user.name hoge  # unable to look up のバグ回避
        git config --global user.email hoge@example.com # unable to look up のバグ回避
        ```
* because the document's frame is sandboxed and the 'allow-scripts' permission is not set. が出る。
    * Manage Jenkins > Script Console で以下を入力 (セキュリティ上問題があるらしい)

        System.setProperty("hudson.model.DirectoryBrowserSupport.CSP", "")

## Docker について

上にサラッと書いたが、このサンプルは docker を使う事前提になっている。プロジェクトをビルドするためには対象ごとに色々な種類やバージョンの開発環境が必要があるのだが、混ざると危険だ。そこで、docker を使ってプロジェクト専用の環境を用意すると管理が楽だ。具体的には例のように `agent { docker 'node:6.3' }` と記述すると node 6.3 が入った開発環境(Docker イメージ)が出来る。こうした開発環境を自分で作っても良いし、誰かが作った出来合いのやつを [Docker Hub](https://hub.docker.com/) からもらってもよい。例えば 6.3 の代わりに `node:8.9.4` のように指定すると別のバージョンの node でビルド出来る。

docker ビルド環境は手元の環境と異なるので Jenkinsfile を書く前にちゃんとビルド出来るか試したい。そこでこんな風に手動で docker でビルドしてみた。

```
$ git clone git@example.com:example/example.git
$ docker run -it --rm -u $(id -u) -e HOME=/work -w /work -v $PWD/example:/work node:8.9.4 bash
I have no name!@f748fa069894:~$ git config --global user.name hoge  # unable to look up のバグ回避
I have no name!@f748fa069894:~$ git config --global user.email hoge@example.com # unable to look up のバグ回避
I have no name!@f748fa069894:~$ npm install
I have no name!@f748fa069894:~$ npm run build
```

docker run の引数は以下のとおり

* `-i`: STDIN を開く
* `-t`: pseudo-TTY を割り当てる
* `--rm`: 実行後コンテナ削除
* `-u`: 実行ユーザを指定
* `-e`: 環境変数を指定
* `-w`: 実行ディレクトリを指定
* `-v`: ホスト位置:コンテナ位置 でホストのディレクトリをコンテナから見えるようにする。
* イメージ名
* 実行コマンド

## もうちょっと実用的な Jenkinsfile

実際に使っている Jenkinsfile を示します。Cordova というツールを使って Android アプリを作るための物です。たったこれだけですが、十分実用になります。

```groovy
pipeline {
  agent {
    docker {
      image 'beevelop/android-nodejs:v8.9.4'
      args '-e HOME=.'
    }
  }

  triggers {
    cron('H 15 * * * ')
  }

  stages {
    stage('build') {
      steps {
        sh 'npm --version'
        sh 'npm install'
        sh 'npm run build'
        sh 'npx cordova build'
      }
    }
  }

  post {
    always {
      archiveArtifacts artifacts: 'platforms/android/build/outputs/apk/debug/*.apk', fingerprint: true
    }
  }
}
```

* トップレベルは pipeline と書かれたブロック。中に agent, stages, steps, post がある。
* agent: Pipeline が実行される環境を書く。
    * ここではビルド環境 Docker image として適当に検索して出てきた android-nodejs というのを使っている。有り難い。Cordova が入ったイメージもあったが、Cordova は package.json でローカルにインストールした方が確実に動くので Cordova 本体は無い方が良かった。
* stages: ビルド手順全体
    * stage: ビルド手順を分けるのに使う。
        * steps: ビルドコマンド
            * ここでは簡単に `npm install; npm run build; npx cordova build` を [sh](https://jenkins.io/doc/pipeline/steps/workflow-durable-task-step/#code-sh-code-shell-script) で実行している。
            * https://jenkins.io/doc/pipeline/steps/ に実行可能な step が記述されている。
* post: stage 終了後実行する step を書く。
    * always: 無条件に実行。
    * success: にすると stages が成功した時だけ実行。
        * [archiveArtifacts](https://jenkins.io/doc/pipeline/steps/core/#code-archiveartifacts-code-archive-the-artifacts) というのを使うとビルド結果を Jenkins からダウンロード出来るようになります。
* environment: 環境変数を設定。
    * pipeline や stage の中に記述する。
    * Jenkins 環境から credentials() でパスワードを渡せる。
* options: プラグイン全体の設定。
    * pipeline や stage の中に記述する。
    * buildDiscarder: 保存回数の指定
    * timeout: タイムアウト設定
* parameters: パラメータの設定
* triggers: Pipeline を実行するタイミング
    * webhook で実行するタイプの Pipeline では不要。
    * この例では、webhook 以外に定期的にビルドしたいので追加した。
    * `cron('H 15 * * * ')` のようにすると、毎日 15 時の適当な時間にビルドが走る。
* tools: ビルドで使うツールを指定すると自動インストールされる。
    * maven, jdk, gradle を指定出来る。
    * ツールは事前に Manage Jenkins > Global Tool Configuration で設定する必要がある。
* input: stage 内に記述すると、実行中に停止して何か入力出来るらしい。
* when: stage を実行するかどうか条件に応じて決められるらしい。
* pararell: stage を並行して実行出来る。

## 小技

* 常に最新の submodule でビルドする。この設定で submodule を commit しなくても最新になります。
    * Configure > Branch Sources:
        * Update tracking submodules to tip of branch: check
            * `git submodule update --remote` が有効になる。

## Jenkinsfile の文法チェック

レポジトリにいちいちコミットしないと Jenkinsfile の文法チェックが出来ないというのでは面倒なので、Jenkins は API による文法チェックの機能がある。

```
JENKINS_URL=(Jenkins の URL)
JENKINS_CRUMB=`curl "$JENKINS_URL/crumbIssuer/api/xml?xpath=concat(//crumbRequestField,\":\",//crumb)"`
curl -H $JENKINS_CRUMB -F "jenkinsfile=<Jenkinsfile" $JENKINS_URL/pipeline-model-converter/validate
```

参考

* [Pipeline Development Tools](https://jenkins.io/doc/book/pipeline/development/)

## 宿題

* Multibranch Pipeline だと submodule の設定のような細かいチェックアウト情報は Jenkinsfile ではなく Web で設定する。Jenkinsfile で設定する方法もありそうな物だが分からなかった。理想はレポジトリと Jenkins のパスだけを Web で指定して残りは全部 Jenkinsfile に書きたい。参考になりそうな情報:

* [Pipeline: Multibranch](https://jenkins.io/doc/pipeline/steps/workflow-multibranch/)
* [Pipeline: SCM Step](https://jenkins.io/doc/pipeline/steps/workflow-scm-step/)
* [Snippet Generator](https://jenkins.io/doc/book/pipeline/getting-started/#snippet-generator)