OpenShift Jenkins Pipeline (DSL) Plugin 入門

Red Hat で OpenShift のコンサルタントをしている id:hashnao です。 赤帽エンジニア Advent Calendar 2018 - Qiita 12日目の記事です。 今日は既に13日目ですが、気にせずリリースしていきます。

イメージのビルド方式

OpenShift では OpenShift 上でコンテナイメージをビルドする際、複数のビルド方式 をサポートしています。 BuildConfig にイメージのビルド方式を定義したり、 oc new-build コマンドの --strategy=(docker|pipeline|source) オプションに応じてビルド方式を指定することができます。

Source-to-Image (S2I) Build

  • Git リポジトリ上のアプリケーションのソースコードをコンパイルし、コンテナイメージをビルドする
  • 特に Java アプリケーションを Maven でビルドするケースで利用される

Pipeline Build

  • Jenkins Pipeline を用いて任意のオペレーションをパイプラインに定義する
  • Jenkins Master (Pod) で Jenkins の Job が管理され、Jenkins Slave (Pod) でパイプラインが実行される
  • 例えば、アプリケーションのソースコードのビルド、単体テスト、結合テスト、ソースコード解析からコンテナイメージのビルドなどをパイプラインとして実行させる場合に利用される

Docker Build

  • Dockerfile から docker build でイメージをビルドする
  • 既に Dockerfile 単体でイメージを管理しておりその運用を踏襲する、インフラ管理者が社内など共通で利用するベースイメージを作成する際などに利用される

Custom Build

  • 既存のイメージに RPM パッケージをインストールするなどベースイメージを拡張する際に利用する
  • Dockerfile を利用する Docker と目的は近いが、 Dockerfile を使わないため、ビルド方法が異なる
  • BuildConfig の環境変数にカスタマイズする際のディレクティブを指定することで任意のイメージを作成する

BuildConfig に指定する環境変数の一覧は https://docs.okd.io/latest/creating_images/custom.html#creating-images-custom に公開されています。

今回は Jenkins Pipeline に関する入門編として、OpenShift Jenkins Pipeline (DSL) Plugin を用いたビルド方法を解説します。

OpenShift Jenkins Pipeline (DSL) Plugin とは

OpenShift では OpenShift Jenkins Pipeline (DSL) Plugin と呼ばれる Jenkins Plugin を提供しています。 OpenShift DSL (Domain Specific Language) 形式で Jenkins Pipeline を定義することができます。 内部的には Jenkins Pipeline の書式をベースに Jenkins Slave から OpenShift API を呼び出し、OpenShift 用の BuildConfigDeploymentConfig などのオブジェクトを作成したり、 oc start-buildoc rollout の様なビルドを開始する、デプロイを開始するといったオペレーションをパイプラインに表現することができます。 *1

Jenkins Pipeline と OpenShift Jenkins Pipeline (DSL) Plugin を連携させた一般的な使い方として、テストからリリース作業をパイプラインに定義することがあります。 例えば、次の様なパイプラインを想定した場合、前半はアプリケーションのソースコードのビルドや単体テストなどを実施、後半はコンテナイメージをビルドする、といった流れになります。 これを 1 つの Jenkinsfile に定義します。

f:id:hashnao:20181213121018p:plain

アプリケーション

  • Git リポジトリから Java アプリのソースコードを取得する
  • Maven でソースコードから成果物 (WAR, EAR, JAR) を生成する
  • Maven で単体テストを実施する
  • SonarQube でソースコードの静的解析を実施する
  • Nexus リポジトリにバージョン管理のためにビルドした成果物をプッシュする

OpenShift

  • 生成した成果物を指定し、コンテナイメージをバイナリビルドする
  • ビルドしたイメージで DeploymentConfig から Pod (コンテナ) をデプロイする
  • curl コマンドなどで簡易的な結合テストをデプロイした Pod に対し実施する
  • 開発環境からステージングや本番環境にイメージをプロモーションする
  • 本番環境で新しくビルドしたイメージで DeploymentConfig から Pod (コンテナ) をデプロイする

上記の様な一般的なアプリケーションリリースに関する流れを1つのパイプラインに定義することで、継続的インテグレーション (Continuous Integration) を回すことができます。 パイプラインのサンプルやチュートリアル、デモを含めて後述する Reference に関連する URL をまとめましたので、興味のある方は参考にして下さい。

OpenShift Jenkins Pipeline (DSL) Plugin のチュートリアル

次に実際にこの OpenShift Jenkins Pipeline (DSL) Plugin を使って Jenkins Pipeline を作成してみます。

Jenkins Pipeline を定義するための Jenkinsfile を指定する方法は Embedded Definition と Reference to Git Repository をサポートしています。

  • Embedded Definition: BuildConfig に Jenkinsfile を埋め込む
    • BuildConfig 内の Jenkinsfile を直接修正、または Jenkins Console から Jenkinsfile を修正することができる
    • デモや検証用途に利用するケースが多い
  • Reference to Git Repository: Jenkinsfile を Git リポジトリで管理し、 BuildConfig に Git リポジトリと Jenkinsfile のパスを指定する
    • Jenkinsfile を修正するには Git リポジトリに修正をプッシュする必要があり、直接上記の様な方法で修正することはできない
    • 検証及び本番環境での用途に利用するケースが多い

今回は修正が容易な BuildConfig に Jenkinsfile を埋め込む方式とし、次のフローで Jenkins Pipeline を作成します。

  • Git リポジトリからアプリケーションのソースを git clone
  • Maven で成果物 (WAR) をビルド
  • 生成した成果物を指定し、コンテナイメージをバイナリビルド する
  • ビルドしたコンテナイメージにバージョン + ビルド番号でタグをリリースする

実際の環境では最初に解説した例の用にソースコードの静的解析、Nexus リポジトリに成果物を格納するなどのフローを含めますが、今回は Jenkins Pipeline の雰囲気や流れを掴むことが目的なので、省略しています。

まず Jenkins Pipeline がイメージをビルドするための BuildConfig を作成します。 バイナリビルドでイメージをビルドするため、--binary=true のオプションを指定します。 通常は BuildConfig を作成すると自動でイメージのビルドが開始されますが、 --binary=true を指定する場合、 oc start-build 時に --from-file--from-dir オプションで WAR などのアプリケーションバイナリを指定して、ビルドを開始するため、この時点ではビルドは開始されません。

oc new-project <PROJECT>
oc new-build --name=mavenapp --docker-image=openshift/wildfly-101-centos7 --binary=true

次に Jenkinsfile を定義した BuildConfig を作成し、ビルドを開始します。 あとは Jenkins Pipeline が自動で開始されるので、Jenkins Console から Jenkins Job の実行結果を待つだけです。

oc create -f https://raw.githubusercontent.com/hashnao/openshift-jee-sample/master/mavenapp-pipeline.yml
oc start-build mavenapp-pipeline

事前に Jenkins をデプロイせず、ビルド方式に Pipeline を定義した BuildConfig を実行すると、自動で Jenkins Pod がデプロイされ、Jenkins Pipelie が実行されます。 OpenShift ではデフォルトで Jenkins をデプロイするためのテンプレートが用意されており、揮発性 (jenkins-ephemeral) と永続性 (jenkins-persistent) の 2 種類があります。 oc new-app コマンドにいずれかのテンプレートを指定することで Jenkins をデプロイすることができます。 Jenkins Job や Credentials などを含む Jenkins の構成情報を保持する場合、後者のテンプレート (jenkins-persistent) を利用します。 jenkins-persistent から Jenkins をデプロイすると /var/lib/jenkins が PV としてマウントされ、このディレクトリに各種設定ファイル (config.xml, credentials.xml など) が含まれます。 Jenkins のデプロイ手順は https://docs.okd.io/latest/dev_guide/dev_tutorials/openshift_pipeline.html を参照下さい。

次に Jenkins Pipeline が完了するまでの間に BuildConfig と Jenkinsfile の詳細を解説します。

まず、 BuildConfig を見てみましょう。 spec.strategy フィールドにビルド方式 (jenkinsPipelineStrategy) を宣言します。 次に jenkinsfile フィールド配下に Jenkins Pipeline を定義します。

kind: `BuildConfig`
...
spec:
  strategy:
    jenkinsPipelineStrategy:
      jenkinsfile: |-
        pipeline {
        ...
        }

今回は BuildConfig ファイルを作成し、 oc create でオブジェクトを作成していますが、 oc new-build --strategy=pipeline オプションを利用すると Git リポジトリにある Jenkinsfile を指定することができます。 コマンドの実行例は次のようになります。 -e フラグを利用することで環境変数を定義することもできます。

oc new-build --name=BUILDCONFIG_NAME GIT_REPO --strategy=pipeline -e ENV="MY_ENV"

Jenkinsfile を BuildConfig に埋め込む方式の場合、 oc new-build コマンドのオプションとして、Jenkinsfile を引き渡すことはできないようです。

今回は利用していませんが、 BuildConfig に環境変数を定義し、Jenkinsfile 内で変数を参照させる場合、オブジェクトを Template として定義し、 parameters フィールドに宣言させる方法が楽です。

kind: Template
...
objects:
- kind: `BuildConfig`
  ...
  spec:
    strategy:
      type: JenkinsPipeline
      jenkinsPipelineStrategy:
        env:
        - name: APP_NAME
          value: ${APP_NAME}
        jenkinsfile: |-
        ...
parameters:
- displayName: Application Name
  name: APP_NAME
  value: "openshift-tasks"

次に Jenkinsfile を見てみます。 見やすいように Jenkinsfile のスペースは 4 -> 2 に修正し、 BuildConfig に埋め込んだ際にインデントさせたスペースは潰しています。

pipeline {
  agent { label "maven" }

pipeline で Jenkins Pipeline の開始を宣言します。 OpenShift Jenkins Pipeline (DSL) Plugin は Declarative Syntax をサポートしており、 script{} ディレクティブを利用することで Scripted Syntax を併用することもできます。

label には Jenkins Slave に利用するコンテナイメージの名称を指定します。 これは Jenkins Console の "Manage Jenkins" - "Configure System" の Kubernetes Pod Template セクションに定義している Name に該当します。

f:id:hashnao:20181213121148p:plain

OpenShift の Jenkins ではデフォルトで mavennodejs が定義されており、 maven を指定すると、Red Hat Container Catalog 上の jenkins-agent-maven-35-rhel7 を利用します。 Jenkins Slave に用いるコンテナイメージを追加することや既存の Slave のイメージを変更することもできます。

  environment {
    version = "1.0"
    devTag = "${version}-${BUILD_NUMBER}"
  }

environment には Jenkinsfile 内で利用する変数を定義します。 今回はイメージのタグをセットするために versiondevTag の変数を用意しています。 devTag には BUILD_NUMBER というデフォルトでセットされる環境変数を指定し、 Jenkins Job のビルド番号を加えることで、Jenkins Pipeline が実行されるたびにそのビルド番号でイメージにタグをセットしています。

今回はバージョンを明示的に変数に指定していますが、 readFile などを駆使しして、 pom.xml に定義したバージョンから取得させる方がアプリケーションのバージョンとひも付きやすいので便利です。 設定例は https://github.com/redhat-cop/spring-rest/blob/master/Jenkinsfile#L49-L53 あたりを参考にしてみて下さい。

...
  environment {
    version = getVersionFromPom("./pom.xml")
  }
...
def getVersionFromPom(pom) {
  def matcher = readFile(pom) =~ '<version>(.+)</version>'
  matcher ? matcher[0][1] : null
}
  stages {
    stage("Clone Source") {
      steps {
        git url: "https://github.com/openshift/openshift-jee-sample.git", branch: "master"
      }
    }

次に stages からが Pipeline で実行したい処理になります。 stage に定義した文字列が Jenkins Pipeline で Stage View に表示されるステージを表します。 まずはアプリケーションのソースコードを Git リポジトリから取得するため、 git url にリポジトリの URL を指定しています。

    stage("Build Artifacts") {
      steps {
        sh "mvn clean package -Popenshift -DskipTests=true"
        stash includes: "target/ROOT.war", name: "war"
      }
    }
    stage("Run Unit Test") {
      steps {
        sh "mvn test"
      }
    }

次に Maven ビルドを実行するために sh で shell を宣言し、実行するコマンドを定義します。 今回のステージは全て同じ Jenkins Slave Pod 上で実行されるため stash を使う必要はありません。 ただし、次のステージが別のノード (Pod) である場合などは stash でビルドした WAR (ROOT.war) を一旦格納し、他のノードで unstash で展開させると便利です。

    stage("Build Image") {
      steps {
        unstash "war"
        script {
          openshift.withCluster() {
            openshift.withProject() {
              def nb = openshift.selector("bc", "mavenapp")
              nb.startBuild("--from-file=./target/ROOT.war").logs("-f")
              def buildSelector = nb.narrow("bc").related("builds")
              timeout(5) {
                buildSelector.untilEach(1) {
                  return (it.object().status.phase == "Complete")
                }
              }
              echo "Builds have been completed: ${buildSelector.names()}"
            }
          }
        }
      }
    }

ここからが OpenShift API に関する処理になります。 openshift.withCluster は Master API Server のエンドポイントや認証のための Token を指定します。 今回は Jenkins Master は OpenShift 上で実行しているため、指定する必要はありません。

openshift.withProject はスイッチするプロジェクト (namespace) を指定します。 指定しない場合、Jenkins がデプロイされているプロジェクトを利用します。 開発からステージングなどにスイッチさせる場合にプロジェクトを指定します。

次に openshift.selector でオブジェクト (BuildConfig) と BuildConfig 名を指定し、nb の名前で Selector を定義します。 nb.startBuild() は Selecror を指定し、 startBuild メソッドで oc start-build を実行します。 oc start-build のオプションをそのまま利用できるので、ここらへんが非常に楽です。 logs メソッドと -foc logs -f bc/mavenapp と同じ処理を行い、ビルド時のログをフォローさせます。 最後の buildSelector.untilEach は 5 分間 oc get build のステータスが Complete になるまで繰り返し確認します。 BuildConfigDeploymentConfig を開始し、ビルドやデプロイの完了を確認する際によく使います。

    stage("Promote Image") {
      steps {
        script {
          openshift.withCluster() {
            openshift.withProject() {
              // Tag the mavenapp:latest image as mavenapp:${devTag}
              openshift.tag("mavenapp:latest", "mavenapp:${devTag}")
            }
          }
        }
      }
    }
  }
}

最後にイメージにビルド番号でタグをセットします。 BuildConfigspec.output.to フィールドには ImageStreamTaglatest を指定しているため、ビルドが完了すると常に ImageStreamlatest でタグ付けされます。 BuildConfig の該当する箇所を oc patch などで事前に変更させることもできますが、 BuildConfig を修正するよりは latest に対し、ビルド番号でタグを付け直す方が管理が容易なので、イメージをビルド後に改めて ImageStream にタグをセットしています。

それではビルド結果を見てみましょう。 oc get build の結果だけを見ると無事にビルドが完了している様です。

$ oc get build
NAME                                  TYPE              FROM         STATUS     STARTED             DURATION
mavenapp-1                            Source            Binary       Complete   About an hour ago   1m3s
mavenapp-pipeline-1                   JenkinsPipeline                Complete   About an hour ago

次に Jenkins Console から Pipeline やビルドのログを見てみましょう。 Jenkins Console の URL は次のコマンドで確認することができます。

$ oc get route jenkins
NAME      HOST/PORT                                         PATH      SERVICES   PORT      TERMINATION     WILDCARD
jenkins   jenkins-1ba3-jenkins.apps.example.com             jenkins    <all>     edge/Redirect   None

Jenkins Console にログインすると Stage View から Pipeline の各ステージの処理の結果や所要時間がわかります。 Jenkins Pipeline のログは左下のビルド番号(#1)から確認することができます。

f:id:hashnao:20181213121241p:plain

ちなみに Console Output (Plain Text) のログを Jenkins Console でなく curl コマンドで確認したい場合、Token を利用し、Console Output の URL を指定することでログを出力させることができます。 Plain Text の URL は View as plain text ボタンから確認することができます。

JENKINS_TOKEN=$(oc sa get-token jenkins)
URL=https://<MASTER_API_URL>/job/<NAMESPACE>/job/<JENKINS_JOB>/<BUILD_NUMBER>/consoleText
curl -k -H "Authorization: Bearer ${JENKINS_TOKEN}" ${URL}

実際の出力結果は次のようになります。

dhcp-193-87:advdev_homework_template nhashimo$ curl -k -H "Authorization: Bearer ${JENKINS_TOKEN}" ${URL}
OpenShift Build 1ba3-jenkins/mavenapp-pipeline-1
Running in Durability level: MAX_SURVIVABILITY
[Pipeline] node
Still waiting to schedule task
maven-kc5lc is offline
Agent maven-kc5lc is provisioned from template Kubernetes Pod Template
Agent specification [Kubernetes Pod Template] (maven):
...
Cloning the remote Git repository
Cloning repository https://github.com/openshift/openshift-jee-sample.git
 > git init /tmp/workspace/1ba3-jenkins/1ba3-jenkins-mavenapp-pipeline # timeout=10
...
[Pipeline] End of Pipeline
Finished: SUCCESS

最後に Jenkins Pipeline でビルドしたイメージ (ImageStream) にタグがセットされたことを確認します。 oc describeImageStream を確認すると、ビルド番号 (1.0-1) でタグがセットされていることがわかります。

$ oc describe is mavenapp
Name:           mavenapp
Namespace:      1ba3-jenkins
Created:        About an hour ago
Labels:         build=mavenapp
Annotations:        openshift.io/generated-by=OpenShiftNewBuild
Docker Pull Spec:   docker-registry.default.svc:5000/1ba3-jenkins/mavenapp
Image Lookup:       local=false
Unique Images:      1
Tags:           2

latest
  no spec tag

    docker-registry.default.svc:5000/1ba3-jenkins/mavenapp@sha256:e693e85dddd387f390fc6c1468c650961bcffcf9aecce2f5bb32ef018f25e416
      About an hour ago

1.0-1
  tagged from mavenapp@sha256:e693e85dddd387f390fc6c1468c650961bcffcf9aecce2f5bb32ef018f25e416

  * docker-registry.default.svc:5000/1ba3-jenkins/mavenapp@sha256:e693e85dddd387f390fc6c1468c650961bcffcf9aecce2f5bb32ef018f25e416
      About an hour ago

この ImageStream を指定して、 oc new-app を実行するとビルドしたイメージから Pod をデプロイすることができます。

$ oc new-app -i mavenapp:1.0-1

まだ 赤帽エンジニア Advent Calendar 2018 - Qiita にエントリが残っているようなので、次回は引き続き Jenkins Pipeline 関連をテーマに Jenkins Slave イメージのカスタマイズか認証情報のマスクなどを書く予定です。

Reference

OpenShift Jenkins Pipeline

Tutorial, Sample

*1:なお、OpenShift Jenkins Pipeline (DSL) Plugin より以前に開発された OpenShift V3 Plugin for Jenkins があり、このプラグインのサポートは 3.11 までとなります。 そのため、今後は OpenShift Jenkins Pipeline (DSL) Plugin を利用することが推奨されます。

* 各記事は著者の見解によるものでありその所属組織を代表する公式なものではありません。その内容については非公式見解を含みます。