🤵‍♂️

Jenkinsの初期設定自動化

2024/04/07に公開

概要と背景

社内でJenkinsを使用していて、引き継ぎにあたり、ローカルで動かそうとした際、必要なプラグインが列挙されていないことや、パイプライン定義がCode化されていなかったため、改めてその方法について調べた。

端的にいうと公式コンテナイメージ [1] から飛べる README [2] にあるように jenkins-plugin-cli を使用することで対応可能な様子。

実施内容

まずは関連する情報を探してざっと洗い出した。次のあたりが重要。

Pluginsのインストール方法 - README [2:1] より

FROM jenkins/jenkins:lts-jdk17
COPY --chown=jenkins:jenkins plugins.txt /usr/share/jenkins/ref/plugins.txt
RUN jenkins-plugin-cli -f /usr/share/jenkins/ref/plugins.txt

初期設定時に実行したいスクリプトを指定・追加する方法 - README [2:2] より

FROM jenkins/jenkins:lts-jdk17
COPY --chown=jenkins:jenkins custom.groovy /usr/share/jenkins/ref/init.groovy.d/custom.groovy

また、プラグインとは別に、手元には次のような定義を過去の事例として持っていた。

インフラチームが過去に整備していたYAMLでの定義 [3] より

jenkins:
  systemMessage: "Welcome to XXXXX Jenkins!"
  numExecutors: 3
  scmCheckoutRetryCount: 2
  mode: NORMAL
jobs:
  - script: |
      def GIT_URL="${リポジトリへのURL}"
      def GIT_BRANCH="master"
      job('seed-job') {
        label('master')
        description('Seed Job')
        logRotator(-1,100)
        scm {
            git{
              remote {
                credentials("${CredentialのID}")
                url(GIT_URL)
              }
              branch(GIT_BRANCH)
            }
        }
        steps {
          dsl {
            external "**/*.groovy"
            removeAction("IGNORE")
            removeViewAction("IGNORE")
            lookupStrategy("SEED_JOB")
          }
        }
      }

これはインフラチームがEC2上で実行してJenkinsを使用可能にするために準備していたもの。
これと同様にプラグインインストールに加えて、Seed Jobの実行が可能な状態までを自動で立ち上げようとしている。
(インフラチームに聞けばこのSeed Jobの点は共有してもらえそうだが、ひとまず今回は自分たちの中で閉じる範囲で対応した)

上記を踏まえて、次のようにDockerfileを定義した。

FROM jenkins/jenkins:lts-jdk17
# Installting Jenkins Plugin
COPY --chown=jenkins:jenkins plugins.txt /usr/share/jenkins/ref/plugins.txt
RUN jenkins-plugin-cli -f /usr/share/jenkins/ref/plugins.txt

# Adding Seed Job in initial setup
COPY --chown=jenkins:jenkins create-seedjob.groovy /var/jenkins_home/init.groovy.d/create-seedjob.groovy

Seed Jobについては、Job DSL Plugin [4] が必要なのを知らずに今まで使っていたので、 job というのは定義上見当たらない、という旨のメッセージが出て先に進めなかった時には少し戸惑った。

Job DSL Pluginを手動でJenkinsに入れてから、次のようにして curl でリストアップしたものをplugins.txt にコピーした。 READMEに書かれていたのでそれほど困らずに進められたのはありがたい。 [2:3]

JENKINS_HOST=admin:${password}@localhost:8080
curl -sSL "http://$JENKINS_HOST/pluginManager/api/xml?depth=1&xpath=/*/*/shortName|/*/*/version&wrapper=plugins" | perl -pe 's/.*?<shortName>([\w-]+).*?<version>([^<]+)()(<\/\w+>)+/\1 \2\n/g'|sed 's/ /:/'

Groovyのスクリプトの記載の方法は他の事例を参考にし、不要部分を削除するなどし次のような形に落ち着いた。 [5]
見比べれば、基本的には元々インフラチームが定義していた内容と同等の内容になっていることがわかる。(label設定やcredentialは対応させていないが)
ちなみに、configXml の部分ではスペースを適切に取り除いていないと、エラーが出たので初回実行時そこでつまづいた。 [6]

import jenkins.model.*

def jobName = "seed-job"
def gitUrl = "https://github.tri-ad.tech/takuya-kojima/Jenkins_Jobs.git"

def scmConfig = """\
<scm class="hudson.plugins.git.GitSCM">
  <configVersion>2</configVersion>
  <userRemoteConfigs>
  <hudson.plugins.git.UserRemoteConfig>
    <url><![CDATA[${gitUrl}]]></url>
  </hudson.plugins.git.UserRemoteConfig>
  </userRemoteConfigs>
  <branches>
  <hudson.plugins.git.BranchSpec>
    <name>**</name>
  </hudson.plugins.git.BranchSpec>
  </branches>
  <doGenerateSubmoduleConfigurations>false</doGenerateSubmoduleConfigurations>
  <submoduleCfg class="list"/>
  <extensions/>
</scm>
"""

def configXml = """\
<?xml version='1.0' encoding='UTF-8'?>
<project>
  <actions/>
  <description>Seed job</description>
  <keepDependencies>false</keepDependencies>
  <properties>
  </properties>
  ${scmConfig}
  <canRoam>true</canRoam>
  <disabled>false</disabled>
  <blockBuildWhenDownstreamBuilding>false</blockBuildWhenDownstreamBuilding>
  <blockBuildWhenUpstreamBuilding>false</blockBuildWhenUpstreamBuilding>
  <triggers/>
  <concurrentBuild>false</concurrentBuild>
  <builders>
    <javaposse.jobdsl.plugin.ExecuteDslScripts plugin="job-dsl@1.37">
      <targets>**/*.groovy</targets>
      <usingScriptText>false</usingScriptText>
      <ignoreExisting>false</ignoreExisting>
      <removedJobAction>IGNORE</removedJobAction>
      <removedViewAction>IGNORE</removedViewAction>
      <lookupStrategy>JENKINS_ROOT</lookupStrategy>
      <additionalClasspath></additionalClasspath>
    </javaposse.jobdsl.plugin.ExecuteDslScripts>
  </builders>
  <publishers/>
  <buildWrappers/>
</project>
""".stripIndent()

if (!Jenkins.instance.getItem(jobName)) {
  def xmlStream = new ByteArrayInputStream( configXml.getBytes() )
  try {
    def seedJob = Jenkins.instance.createProjectFromXML(jobName, xmlStream)
    seedJob.scheduleBuild(0, null)
  } catch (ex) {
    println "ERROR: ${ex}"
    println configXml.stripIndent()
  }
}

これにより、次のような形でBuild/Runをすると指定したプラグインインストール済み・Seed Jobが実行可能な状態でJenkinsが上がってくることを確認できた。

docker build -t tmp .
docker run --rm --name myjenkins -p 8080:8080 -p 50001:50001 --env JENKINS_SLAVE_AGENT_PORT=50001 tmp

起動したJenkinsの画面内では、Seed Jobが呼び出す先で準備した **/*.groovy(例えば xxx-app/pipeline.groovy)を呼び出すことができる。この辺りの定義はJob DSLのドキュメント類 [7] [8] を見て定義すればOK。 これで一旦は完了ということになる。

ただし、手動操作が必要だったり面倒な点として、次の点がまだ残っている。

  1. 初回アクセス時のロック解除(起動直後に出るパスワードをWeb画面上で入力してAdminユーザーで使えるようにする)
  2. Credentialの登録
  3. スクリプト実行時のApprove
  4. インストール対象プラグインの定義

1つ目のアクセス時のロックについては、Wizardキャンセルの方法が次のような形で公開されていた。

設定時のWizardをキャンセルする方法 [9] より

FROM jenkins/jenkins:lts
ENV JAVA_OPTS="-Djenkins.install.runSetupWizard=false"

ただ、単にこのまま使うと、ユーザーなしでネットワーク内の誰でもアクセス可能なJenkinsとして上がってきてしまう様子。そこをさらに手打ちすることもできるようだけど、そこまでは再現をすると手間のかかる・漏れの発生する項目を重点的にCasCしたかったので、一旦スキップした。

2つ目のCredentialについても同様。自動登録まで対応することも当然できそうではあるものの、そこまでしたいか・する意味がありそうかを考え、優先度を落とした。ただ、できた方が便利であるのは間違いない。

3つ目のスクリプト実行時のApproveは、Jenkins上のセーフガードでGroovyのコードを取り込んで実行する時に、本当に実行していいかを、内容を確認の上明示的にApproveするまでは実行できないようにするという仕組み。
これがある関係で、Seed Job実行までには、Credential登録、初回の実行、Approve操作(in Manage Jenkinsの画面)、再実行、が必要となっている。(その上Seed Jobから呼び出された先のスクリプトも同様にApproveが必要となる)

最後の4つ目は、現状、READMEに書かれている通りに必要なプラグインリストを取り出してインストールしているものの、自分が必要としているプラグインそのものだけでなく、それの依存先についても、フラットにplugins.txt上に記載が必要(と少なくとも自分が確認した限りでは認識している)ということ。
これは言い換えると、あるプラグインを入れるときはいいものの、不要になって抜きたくなった際には、plugins.txtから単に抜くというのが依存関係把握の関係から難しく、結果として、使用しないプラグインを消すことが難しくなってしまうという懸念。

すぐに問題になることではないものばかりだけれど、どれもいずれ改善を加えていきたいポイント。
ひとまず今回のまとめとしてはここまで。

脚注
  1. Jenkinsの公式コンテナイメージ | jenkins/jenkins - Docker Image | Docker Hub https://hub.docker.com/r/jenkins/jenkins ↩︎

  2. Jenkinsの公式コンテナの使用方法 | docker/README.md at master · jenkinsci/docker · GitHub https://github.com/jenkinsci/docker/blob/master/README.md ↩︎ ↩︎ ↩︎ ↩︎

  3. インフラチーム設定の jenkins.yaml をベースに一部改変 ↩︎

  4. JenkinsでのJob定義に使われるプラグイン | Job DSL | Jenkins plugin https://plugins.jenkins.io/job-dsl/ ↩︎

  5. ビルド時にカスタムスクリプトやプラグインの呼び出しをしている例 | docker-jenkins-dsl-ready/Dockerfile at master · thomasleveil/docker-jenkins-dsl-ready · GitHub https://github.com/thomasleveil/docker-jenkins-dsl-ready/blob/master/Dockerfile ↩︎

  6. XML前にスペースを置いた場合に起きるエラーについて | java - Error: The processing instruction target matching "[xX][mM][lL]" is not allowed - Stack Overflow https://stackoverflow.com/questions/19889132/error-the-processing-instruction-target-matching-xxmmll-is-not-allowed ↩︎

  7. Job DSLの使用方法がまとまったWiki | Job DSL Commands · jenkinsci/job-dsl-plugin Wiki · GitHub https://github.com/jenkinsci/job-dsl-plugin/wiki/Job-DSL-Commands ↩︎

  8. Job DSLのAPI Document | Jenkins Job DSL Plugin https://jenkinsci.github.io/job-dsl-plugin/ ↩︎

  9. 設定時に出てくるWizardを停止する方法についての議論がされているGitHub Issue | https://github.com/jenkinsci/docker/issues/310 ↩︎

Discussion