JenkinsとKubernetesでCIパイプラインを構築


JenkinsとKubernetesを連携させてCIパイプラインを構築する時に、一番悩むの点は、いろいろな方法があるために、何を選択して良いか解らない。そして、実際に実装を進めると、様々な問題が発覚して、時間がかかってしまうことがある。

Jenkinsのプラグインで、Dockerに関するものだけでも約20種類、Kubernetesの関係するもので 約17種類もある。しかも、これらが問題なく動作するという保証も無い。筆者が経験したケースでは、資料が作られた時期から時間が経過すると共にプラグインが更新され、新たな問題が生じてしまい、動作しなくなっているなどがあった。そして、ドキュメントは、Jenkinsに詳しいエンジニア向けに書かれているために、普段触り慣れないJenkins初心者には難解な内容となっていることもある。

そして、Kubernetes上でJenkinsを動作させる場合にも問題が多い。最近のKubernetesのバージョンではコンテナランタイム環境としてContainerdが使用されDockerデーモンが導入されていない。そのため、dockerコマンドを使ったビルドをKubenetesクラスタで、コンテナを簡単にビルドすることができない。DinDやKanikoとなどの環境が必要となる。

そこで、今回は、Jenkinsで、Kubernetesにデプロイするビルドパイプラインを構築するための最短策として試してみた。

構築方針

JenkinsのCI環境を簡単かつ迅速に使用可能な状態とするために、以下の方針に基づいて構築することにした。

  • Jenkinsは最新バージョンを使用して、脆弱性リスクを最小化する。
  • プラグインの使用は最小に抑え、プラグインの障害に遭遇する可能性を低くする
  • コンテナのビルド環境は、有償サポートが受けられる Docker Enterprise に置き換えが可能となる構成とする。

上記の理由から、Kubernetes上でJenkinsを動かさず、仮想サーバー上でJenkinsサーバー構築する。

システム構成

Jenkinsはマスターと、Buildエージェント2台で構築する。

クラウドを利用しないエコな環境として、VagrantとAnsibleで環境を構築するコードを書いた。https://github.com/takara9/vagrant-jenkins
この環境の起動方法は、このリポジトリのREADME.mdの書いておいたので、クローンしてvagrant upすれば、3台のサーバーが環境が整った形で起動するので、あとは、Jenkinsのユーザー画面からプラグインを追加して、Git,Docker,Kubernetesクラスタの認証情報などを設定することで、実際に連携した操作が可能である。

ビルドパイプライン

Jenkinsパイプラインのリファレンスは、https://www.jenkins.io/doc/book/pipeline/ にあるので、パイプラインの説明は、ざっくり見ておくと良い。そして、ここで利用するビルドパイプラインのサンプルコードを GitHub https://github.com/takara9/jenkins-play-1 に置いてあるでの、このコードで動作を説明する。

プラグインを追加インストールは実質的に無しである。Blue Ocean は見やすいフロー図とログを表示してくれるので、メインストリームで使うことはないが、入れておく。このJenkinsパイプラインは、4つのステージからなる。

  • GiHubからソースコードのクローン
  • コンテナイメージのビルド
  • コンテナレジストリへプッシュ
  • K8sクラスタへのデプロイ

この流れを順番に見ていく。

最初のステージは、GitHubからクローンする。この記述に関する詳細はJenkinsのデフォルトで導入される Gitプラグイン (https://www.jenkins.io/doc/pipeline/steps/git/) のドキュメントを参照すれば、認証が必要なケースでの書き方がわかる。

    stages {
        stage('GiHubからソースコードのクローン') {
            steps {
                git 'https://github.com/takara9/jenkins-play-1'
            }
        }

2番目のステージは、コンテナのビルドステージを見ていく。この中でdocker.buildに関する記述は、Docker Pipelineプラグイン (https://docs.cloudbees.com/docs/admin-resources/latest/plugins/docker-workflow#docker-workflow-sect-build) によるものだ。このリンクには、docker.で始まるDockerコマンドに関する解説があるので、参照すると良い。

        stage('コンテナイメージのビルド') {
            steps {
                script {
                    dockerImage = docker.build registry + ":$BUILD_NUMBER"
                }
            }
        }

3番目のステージは、DockerHub レジストリへ、イメージを登録する部分である。 そこで、DockerHubのAPIのURLアドレスと、認証情報を指定する。認証情報には、DockerHubのユーザー名とパスワードが登録してあり、そのキーとなるのが、"dockerhub"である。これは事前に、「Jenkinsの管理」->「Manage Credential」から登録しておく。dockerImageは前段のステージから引き継がれるように、environmentの中で初期化しておかなければならない。

        stage('コンテナレジストリへプッシュ') {
            steps {
                script {
                    docker.withRegistry("https://index.docker.io/v1/","dockerhub") {
                        dockerImage.push()
                    }
                }
            }
        }

最終ステップは、Kubenetesへのデプロイである。ここでは、kubernetes-cdを使う予定であったが、このプラグイン jackson2-api-plugin が必要とするモジュール のバージョンアップによって動作しない状態になっていた。これはJENKINS-62995 Version 2.11.1 breaking kubernetes deployment somehowに挙げられているが、執筆本日(8月4日現在)Open状態である。 この修正をまっていられないので、Jenkinsのサーバーとビルドエージェントに、kubectlコマンドをインストールして、Jenkinsの認証情報にkubeconfigのファイルを登録して、シェルで実行することにした。

        stage('K8sクラスタへのデプロイ') {
            steps {
                script {
                       sh 'kubectl cluster-info --kubeconfig $KUBECONFIG'
                       sh 'sed s/__BUILDNUMBER__/$BUILD_NUMBER/ myweb.yaml > deploy_myweb.yaml'
                       sh 'kubectl apply -f deploy_myweb.yaml --kubeconfig $KUBECONFIG'
                       //kubernetesDeploy(configs: "myweb.yaml", kubeconfigId: "mykubeconfig")
                }
            }
        }

この中でJenkins組み込みの環境変数 $BUILD_NUMBER を使っている。この環境変数の説明や他の種類については、このURL( https://wiki.jenkins.io/display/JENKINS/Building+a+software+project) を参照すると良い。また、$KUBECONFIGはJenkinsの認証情報管理から登録したkubeconfigのファイルである。

以下に、全体のJenkinsfileを掲示しておく。

pipeline {
    // 前提条件 Jenkinsサーバーにdocker, kubectl コマンドが導入されていること
    //
    environment {
        registry = "docker.io/maho/myweb"
        dockerImage = ""
        KUBECONFIG = credentials('kubeconfig')
    }

    agent any

    stages {
        stage('GiHubからソースコードのクローン') {
            steps {
                git 'https://github.com/takara9/jenkins-play-1'
            }
        }
        stage('コンテナイメージのビルド') {
            steps {
                script {
                    dockerImage = docker.build registry + ":$BUILD_NUMBER"
                }
            }
        }
        stage('コンテナレジストリへプッシュ') {
            steps {
                script {
                    docker.withRegistry("https://index.docker.io/v1/","dockerhub") {
                        dockerImage.push()
                    }
                }
            }
        }
        stage('K8sクラスタへのデプロイ') {
            steps {
                script {
                       //sh 'echo $KUBECONFIG'
                       sh 'kubectl cluster-info --kubeconfig $KUBECONFIG'
                       sh 'sed s/__BUILDNUMBER__/$BUILD_NUMBER/ myweb.yaml > deploy_myweb.yaml'
                       sh 'kubectl apply -f deploy_myweb.yaml --kubeconfig $KUBECONFIG'
                       //kubernetesDeploy(configs: "myweb.yaml", kubeconfigId: "mykubeconfig")
                }
            }
        }
    }
}

動作確認

ジョブ jenkins-play-1を選択して、「ビルド実行」をクリックすると一連のパイプラインが実行される。スクリーンショットを見てわかる通り、GitHubのクローンから、Kubernetesのデプロイまで成功した。

新規ジョブの作成

順番が前後するが、新規ジョブの登録について、書き残しておく。

「新規ジョブ作成」から次の画面に遷移して、パイプラインを選択して、OKボタンをクリック。

プロジェクトの高度な設定で、SCMの項目でGitを選択、URLをいれる。

Gitリポジトリの更新でビルド開始

今回のJenkinsはオンプレミス上に構築しているので、GitHubのWebhookを受けることができない。そこで、5分間隔でGitHubの更新をポーリングする。そして、変更が発見されたら、ビルドを開始すると設定にする。それにはジョブのリストから選択して、設定の中のビルドトリガーに以下のような設定を施す。これで、少し間をおくことになるが、git push 後に、自動的にビルドが開始される。

コンテナによる実行

ビルドステップを、Dockerコンテナ上で動作させることができる。 「新規ジョブ作成」->「パイプライン」でスクリプトを選択して、次のように記述する。この例では、shは agentで指定されたコンテナで動作することになる。

pipeline {
    agent {
        docker { image 'node:14-alpine' }
    }
    stages {
        stage('Test') {
            steps {
                sh 'node --version'
            }
        }
    }
}

ここに挙げた例以外にも、ステップ単位にコンテナを変更する方法など、解説があるので、このURL (https://www.jenkins.io/doc/book/pipeline/docker/) を参照すると良い。

まとめ

Jenkinsのクラスタ上で、追加のプラグインをほとんど使わずに、また、バグを避けながら、ソースコードからコンテナのビルド、Kubernetesクラスタへのデプロイまでを実現することができた。

Jenkinsは優れたツールで小規模から大規模なソフトウェアのビルドに適用することができるので、広範囲なプロジェクトにも適用できると思う。このJenkinsは前進のHudsonから数えて10年が経過したツールで、プラグインが数多く開発されてきた。そのためか、情報が溢れて、どこから手を付けて良いかわからない状態が感じられる。

今回は基本に立ち返り、拡張を考えた簡単な構成で検証した。しかし、これからJenkinsは、仮想サーバー上で動かすべきか、それとも、Kubernetes上でコンテナとして動かすべきか選択を考えなければならない。 Jenkinsはソフトウェア製品開発にとって、基幹業務システム的な無くてはならない存在になっており、開発現場では延々とジョブを実行して、レポートを生成し続けている。そのようなシステムをKubernetes/ OpenShift で動かすメリットについて良く考えなければならない。 Kubernetes / OpneShift を利用することで、運用管理面と可用性で格段に改善できる。

一方で、Tekton やArgoなど、最初からコンテナで動かすことを前提に設計された新しいアーキテクチャのCIツールも登場してきているが、Jenkinsと比較すると成長段階といった印象と受ける。そのため、長期で考えた人材育成やスキル蓄積において検証を手掛ける必要がある。

その他、雑感

OpenShiftとの連携

OpenShiftのプラグインは、このURL(https://plugins.jenkins.io/ui/search?query=openshift) からリストしてみることができる。
また、OpenShift4でJenkinsを動かす概要について、このURL (https://www.openshift.com/blog/deploy-jenkins-pipelines-in-openshift-4-with-openshift-container-storage-4) の記事が参考になる。

Jenkinsの簡単インストール

Red Hatが推進するOperator Hubには、Jenkins Oprerator が登録されている。それだけでなく、Helm チャートにも登録がある。

IBM Cloud ROKS/IKSでの利用

第二世代VPC環境が東京データセンターで利用できるようになっている。Jenkins、Kubernetes そして OpenShiftも、この新しいVPC上で利用可能だ。しかも、Terraform のIBM Cloudプラグインも整っているので、コードを適用するだけで、一気にインフラを構築することが可能だ。システム構成がコード化されていれば、クラウドのポータルを時間をかけて操作することなく、コードを適用するだけで、必要な数だけ、揃えることも可能である。

Jenkinsの役割的な特性上、Kubernetes/ OpenShiftクラスタは、Jenkinsの周りにテストと本番配信環境として成長していくように思う。そうすと、TerraformとAnsibleを使って、Jenkinsをすぐに構築して提供でき、安定的に運用できる環境でも良いのでは無いかとも思う。