GeekFactory

int128.hatenablog.com

Jenkinsfileによるジョブ管理のメリットと実例

ジョブの設定をJenkinsfileで管理し始めてから3か月ぐらい経ったので、知見をまとめてみます。

Jenkinsfileを使うメリット

Jenkinsの画面でジョブを管理していると以下のような問題が起きることが多いと思います。

  • 誰かが勝手にJenkinsの設定を変更して動かなくなった
  • ジョブ設定を別リポジトリに横展開したいけど、ポチポチ設定するのが面倒

JenkinsfileをGitで管理することで、以下のメリットがあります。

  • いつ、誰が、なぜジョブ設定を変更したのか後から調べられる
  • Pull Requestでジョブ設定の変更をレビューできる
  • ブランチを使ってジョブ設定を試行錯誤しやすい

Jenkinsの運用ポリシー

前項のメリットを実現するには、Jenkinsを以下のポリシーで運用することが望ましいでしょう。

  • Jenkinsの設定は最小限に抑える
  • なるべく画面からジョブ設定を変更せずに済むようにする(GitHub OrganizationやMultibranch Pipelineを利用)
  • なるべくJenkinsにログインしなくてもオペレーションが回るようにする(ビルド結果をチャットに通知)
  • Jenkinsfileをポータブルにする(認証情報を書かない、Jenkins Agentをイミュータブルにする等)

Jenkinsfileのテンプレート

どの言語でも共通のテンプレートを用意しておくと便利です。以下のようなテンプレートを利用しています。

node {
  try {
    checkout scm

    stage('build') {
      try {
        // TODO: ビルドを実行する
        // TODO: チャットにビルド成功を通知する
      } finally {
        // TODO: テスト結果を収集する (junit, publishHTMLなど)
      }
    }
  } catch (e) {
    // TODO: チャットにビルドエラーを通知する
    throw e
  }
}

ビルド結果はチャットの専用チャンネルに通知するようにしています。ただし、masterブランチのビルドエラーが起きた場合はすぐに直す必要があるので普段のチャンネルに流すようにしています。

  } catch (e) {
    // TODO: チャットにビルドエラーを通知する
    if (env.BRANCH_NAME == 'master') {
      // TODO: チャットにmasterが壊れたと通知する
    }
    throw e
  }

GradleでJVM言語をビルドする

GradleでJava、Groovy、Kotlinなどをビルドする場合は以下のようになります。

node {
  try {
    checkout scm
    sh 'chmod +x gradlew'

    stage('check') {
      try {
        sh './gradlew check'
      } finally {
        // TODO: テスト結果を収集する
        junit allowEmptyResults: true, testResults: 'build/test-results/test/*.xml'
        publishHTML([allowMissing: true, alwaysLinkToLastBuild: true, keepAll: false, reportDir: 'build/reports/jacoco/test/html', reportFiles: 'index.html', reportName: 'Coverage'])
        publishHTML([allowMissing: true, alwaysLinkToLastBuild: true, keepAll: false, reportDir: 'build/reports/tests/test/html', reportFiles: 'index.html', reportName: 'Test'])
      }
    }

    // TODO: チャットにビルド成功を通知する
  } catch (e) {
    // TODO: チャットにビルドエラーを通知する
    throw e
  }
}

ビルド実行時の情報を使う

ビルド実行時に取得できる情報は、JenkinsのGlobal Variable Referenceで確認できます。例えば、 env.BRANCH_NAME でブランチ名を取得できるので、masterブランチの場合にのみ特殊な処理を行うといったことも簡単にできます。

    if (env.BRANCH_NAME == 'master') {
      // masterブランチの場合はSonarQubeの静的解析を行う
      sh './gradlew sonarqube'
    }

また、パスワードやアクセストークンのような認証情報はJenkinsで管理し、ビルド実行時に注入させることができます。

withCredentials([usernamePassword(credentialsId: 'xxx', passwordVariable: 'AWS_SECRET_ACCESS_KEY', usernameVariable: 'AWS_ACCESS_KEY_ID')]) {
  // S3にリリースする
}

まとめ

本稿ではJenkinsfileでジョブ設定を管理するメリットを説明し、Jenkinsfileの実例を紹介しました。

SonarQube GitHub PluginをGitBucketで使用する(失敗)

SonarQube GitHub Pluginを使うと、静的解析の結果をPull Requestのステータスやコメントに反映できます。この便利な機能がGitBucketでも使えるか試してみましたが、残念ながら無理でした。何かの知見になればと残しておきます。

プラグインはPull Requestで変更されたファイルに対してのみ静的解析を行うようです。

How To Configure SonarQube GitHub Plugin With Jenkins - Stack Overflow

The analysis is automatically filtered based on the files in the pull request. We were testing with pull requests that only had changes in pom.xml and readme files. Once a functional change was introduced, everything lit up on the GitHub Pull Request view as expected.

プラグインはPull Requestで変更されたファイルを取得するため、以下のAPIを利用します。

https://developer.github.com/v3/pulls/#list-pull-requests-files

このAPIはGitBucketでは未実装(404が返される)のため、エラーになります。Gradle SonarQube Pluginを利用している場合はデバッグレベルで実行すると以下のようなログを確認できます。

21:15:42.972 [DEBUG] [sun.net.www.protocol.http.HttpURLConnection] sun.net.www.MessageHeader@5144a1927 pairs: {GET /api/v3/repos/example/hello/pulls/73/comments HTTP/1.1: null}{Authorization: token ****}{Accept-Encoding: gzip}{User-Agent: Java/1.8.0_102}{Host: git.example.com}{Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2}{Connection: keep-alive}
21:15:43.037 [DEBUG] [sun.net.www.protocol.http.HttpURLConnection] sun.net.www.MessageHeader@43fbb6477 pairs: {GET /api/v3/repos/example/hello/pulls/73/files HTTP/1.1: null}{Authorization: token ****}{Accept-Encoding: gzip}{User-Agent: Java/1.8.0_102}{Host: git.example.com}{Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2}{Connection: keep-alive}

今後の機能追加に期待です。Pull Requestの画面ではファイル一覧を表示しているので、APIで返すのはそれほど難しくないかもしれません。

Jenkins MultibranchでPull Requestをビルド

JenkinsのMultibranch Pipelineを利用してPull Requestをビルドしたい場合、ジョブをどのように設定すべきか調べてみました。

Branch SourcesでGitHub *1 を追加すると、以下を選択できます。

  • Build origin branches
  • Build origin branches also filed as PRs
  • Build origin PRs (merged with base branch)
  • Build origin PRs (unmerged head)
  • Build fork PRs (merged with base branch)
  • Build fork PRs (unmerged head)

デフォルトの設定

デフォルトでは以下が選択されています。

  • Build origin branches
  • Build origin branches also filed as PRs
  • Build fork PRs (merged with base branch)

この場合、以下の振る舞いになります。

  • ブランチがpushされたら、ブランチをビルドする。
  • 同一リポジトリでPull Requestが作成されても何もしない。
  • フォークリポジトリからPull Requestを受けた場合は、ベースブランチとマージした上でビルドを行う。(最新版にマージしてビルド)

特に要件がなければ、この振る舞いで問題ないでしょう。

ベースブランチとマージした上でビルドしたい

Pull Requestが作成された場合にベースブランチとマージした上でビルドを行うには、以下を有効にします。

  • Build origin PRs (merged with base branch)
  • Build fork PRs (merged with base branch)

これらを有効にすると、Pull Requestはちゃんとビルドできたけどmasterにマージしたらビルドが失敗した、といったことを予防できます。

ビルド時にPull Requestに関する情報を取得したい

ビルド時にPull Request Numberなどを取得したい場合、以下のいずれかを有効にします。

  • Build origin PRs (merged with base branch)
  • Build origin PRs (unmerged head)

以下の環境変数でPull Requestに関する情報が取得できるようになります。

Key Value
BRANCH_NAME Pull Request Number PR-6
CHANGE_ID Pull Request Number 6
CHANGE_URL Pull RequestのURL
CHANGE_TITLE Pull Requestのタイトル
CHANGE_AUTHOR Pull Requestの作成者
CHANGE_AUTHOR_DISPLAY_NAME Pull Requestの作成者
CHANGE_AUTHOR_EMAIL Pull Requestの作成者
CHANGE_TARGET ベースブランチ master

ビルド時に静的解析を行い、Pull Requestにコメントを付けるといった場合に活用できます。

ただし、ブランチのビルドとPull Requestのビルドが両方とも実行されるため、CIの所要時間が2倍になります。通常のテストはブランチのビルドで行い、Pull Requestにコメントするといった特殊な要件のみPull Requestのビルドで行うとよいと思います。

*1:GitBucketでも使えます