見出し画像

#22_一年ぶりの Jenkins 更新をやったらしくじったので毎月更新にしました

こんにちは、サーバエンジニアの駒崎です。Jenkins の更新頻度を高める取り組みについて書かせていただきます。


Jenkins を毎月更新するようにしました

少し前、約 1 年更新されていなかったある Jenkins の更新作業を行ったところ、色々ハマってしくじってしまったので、運用改善に取り組みました。

私達は毎日、本当に毎日 Jenkins の CI/CD にお世話になりながらサービス開発を行っています。なのに当の Jenkins 本体が年イチのビッグバン更新になってしまうというのも妙な話です。さすがに毎日とはいかないまでも、毎月コンスタントに Jenkins を更新するようにしました。

どうやるか

ソフトウェア開発と同じく、継続的デリバリーのアイデアに従えばよさそうです。Jenkins を運用する皆様にはなじみ深いですね。今回は特に次のようなことを考えていきます。

  • なるべく自動化する : 手動作業ステップを減らし、簡略化と作業ミスを減らす

  • バージョン管理にいれる : プラグインも含め設定のコード化

  • 痛みの前倒し : 予測しづらく難しい更新後バージョンの動作確認などは本番影響ない段階で先にしっかりやっておく

上記を踏まえ、Jenkins は次のような構成となりました。

Jenkins は Google Cloud の Kubernetes サービス (GKE) で稼働しており、永続化ディスクを伴うコンテナアプリケーションとして立ち上がっています。利用しているイメージは DockerHub jenkins/jenkins の lts イメージです。

この構成では次のようなことを実現しています。

  • メインとテストの 2 系統

    • テスト系の JENKINS_HOME はメイン系からディスクを複製・コピー

    • Kubernetes 設定は kustomize で大部分を共通化

  • プラグインはバージョン管理下の plugins.txt と同期する

    • plugins.txt にバージョン指定含め記載し、起動時に jenkins-plugin-cli で空のボリュームにインストールする

  • Jenkins 設定はできるだけバージョン管理下の YAML から読み込む

  • テスト系は簡単にリセット可能

    • 通常は HOME 用ボリュームは保持しメイン系同様の動作。再起動を伴う動作確認などを行える

    • リセットしたい場合は HOME 用ボリュームを破棄、ソースボリュームの状態に戻る

  • 環境維持に必要な作業をジョブで自動化

    • JENKINS_HOME 用ボリュームのバックアップ&複製から PVC の生成

    • plugins.txt の生成

    • テスト系のリセット

リセットしやすいテスト系、設定のバージョン管理同期などにより安心・安全を高め、各種自動化により手作業を最小限にしていきました。

やったこと詳細

これらの実現方法について一部掘り下げます。

ディスクの複製を簡単にする

本構成で使っている永続化ディスクの実体は Google Compute Engine の永続ディスク (PD) です。PD の複製を行うには PD 名が必要なので、Deployment (ここでは jenkins) に紐づく PersistentVolumeClaim (PVC) から辿って取得します。ジョブ内で以下のような関数を用意して取得しています。

def getSrcDisk() {
    sh(script: '''
    PVC_NAME=$(kubectl -n jenkins get deploy jenkins \

      -ojsonpath='{.spec.template.spec.volumes[?(@.name=="jenkins-home")].persistentVolumeClaim.claimName}')
    SRC_DISK=$(kubectl get pv $(kubectl -n jenkins get pvc ${PVC_NAME} -ojsonpath='{.spec.volumeName}') \
      -ojsonpath='{.spec.gcePersistentDisk.pdName}')
    echo ${SRC_DISK}
    ''', returnStdout: true).trim()
}

取得した PD 名を使ってスナップショット作成やディスク複製を行い、新たに作成したソース用 PD を Kubernetes から使えるようにします。そのためにはこの PD を参照する PVC リソースの作成が必要なので、PVC マニフェスト書き出しと反映までジョブで行うようにしています。

具体的なマニフェストの内容は  既存の永続ディスクを PersistentVolume として使用する  |  Google Kubernetes Engine(GKE)  |  Google Cloud などを参照してください。

ここで用意した複製ジョブは、メイン系の更新作業前にバックアップを取るジョブとしても使えます。

プラグイン管理を簡単にする

実際にテスト系でプラグイン更新を確認する際には、完全に CLI ベースだと少々つらいものがあるため、GUI を使って更新、動作確認を行い、問題がないようならそのバージョンをメイン系に反映するというステップを想定しています。そのため、Jenkins で今動いているプラグイン一覧をバージョン付きで出力するジョブも作成します。

...
stage('PrintPlugins') {
    steps {
        script {
            def plugins =
 jenkins.model.Jenkins.instance.pluginManager.plugins.collect {
                "${it.getShortName()}:${it.getVersion()}"
            }.join("\n")
            print plugins
            sh """\
            cat <<-EOF | sort > plugins.txt
            ${plugins}
            EOF
            """
        }
        archiveArtifacts 'plugins.txt'
    }
}
...

ここで出力される plugins.txt をそのまま jenkins-plugin-cli に渡すことで期待するプラグインをインストールできます。

リセットを簡単にする

テスト系はリセット可能なように構成しましたが、これも Jenkins ジョブにしてしまいます。Jenkins は Deployment 管理で起動しており、Kubernetes マニフェストは ArgoCD で管理されているので、リセットジョブの抜粋は次のようになります。

...
stage('Delete Current TestJenkins') {
    steps {
        sh("kubectl -n jenkins-test scale --replicas=0 deployment/jenkins --timeout=30s")
        sh("kubectl -n jenkins-test delete pvc jenkins-home")
    }
}
stage('Sync to recreate TestJenkins') {
    steps {
        withCredentials(...) {
            sh("argocd login argocd-server ...")
        }
        sh("argocd app sync jenkins-test --prune")
        sh("argocd app wait jenkins-test --timeout 1800")
    }
}
...


動作確認を簡単にする

テスト系で検証を行う際には、単純に Jenkins の正常起動を見るだけでは不十分です。すべてのジョブについて問題がないことを確認するのは難しいですが、重要度の高い必須系機能についてはざっと動作確認するためのジョブを作成しています。たとえば

  • 各種物理マシン agent との疎通、Kubernetes agent との疎通

  • credentials 系プラグインでのトークン取得

  • 外部サービスアクセス

などをチェックしています。

更新作業

シャットダウン準備で安全に更新する

実際にメイン系を更新するタイミングでは、更新再起動で強制終了されると困るジョブもありますので、実行中のジョブに悪影響を与えないように行う必要があります。

パイプラインジョブに対しては、シャットダウンの準備 (En: Prepare for Shutdown, API:  /quietDown ) による Quiet モードが非常に有効に機能しますので、これを使って更新準備をします。 https://docs.cloudbees.com/docs/cloudbees-ci-kb/latest/client-and-managed-controllers/how-to-start-stop-or-restart-your-instance 

具体的には、シャットダウン準備は以下のような動作となります。

  • 新規ジョブは開始されず、キューに入る

  • 実行中のパイプラインジョブは step の途中でサスペンドする

特に、サスペンドしてくれるのがポイントです。Quiet モードに入ると、パイプラインジョブは次の step に進まなくなります。Jenkins コントローラの再起動によってエージェントとの疎通が一時的に切れたとしても、再接続されたときには止めていた step から実行することができます。

実行タイミングが読めない長いパイプラインジョブなどがあると作業予定ウインドウを計画しにくいので、実行中ジョブがあっても更新作業が可能なのは利便性が高いです。

シャットダウンの準備を行い、一時停止していることを確認し更新のための再起動を行うことで、安全に再起動を行うことができます。

一連の流れ

ここまでを踏まえて、実際の更新作業は以下のようになりました。

事前準備:

  • メイン系の設定からテスト系を作成する

    • バックアップジョブを実行しソース PVC を生成

    • メイン系から plugins.txt を生成

    • これら生成情報で Kubernetes マニフェストを更新

  • テスト系で気が済むまで更新確認をする

    • Jenkins イメージの変更や plugins の更新など

    • うまくいかなかったり何度も通し確認をしたい場合はリセット

  • テスト系で確認済みの設定をメイン系向けマニフェストに Pull request しておく

当日:

  • シャットダウンの準備を実施

  • バックアップジョブの実行

  • Pull request をマージ

となります。

当日の主な作業はブラウザ操作だけです。停止時間はバックアップジョブの時間に依存しますが、それを含めても 5分程度で実現可能です。

事前準備においても、何度も気軽に試せるというのは思っていた以上に快適で安心感がありました。準備の結果が最終的に Pull request として用意できるのもシンプルです。


おわりに

今回取り上げた Jenkins は、関わるメンバーが数百名を超えているインスタンスであり、調整コストが高めの環境でした。現状でもなお更新の再起動時は数分のダウンタイムが発生してはいるのですが、以前は数時間~半日単位での調整をしていたのに比べるとだいぶ抵抗は少なくなりました。
ダウン時間の短縮とコンスタントに更新する運用を定着させることで、調整コスト削減や属人性の排除を進められています。


関連

この記事が気に入ったらサポートをしてみませんか?