ポータブルで保守性の高いJenkinsを求めて

最近、Circle CIやAWS Code BuildなどSaaS形式でユーザー側で保守する必要のないCIが盛り上がっているが、まだまだJenkinsのオンプレ運用も需要も高い。

例えばUnityアプリの場合、iOSはMacでのビルドが必須だし、iOSをMacでやってるんだからAndroidもついでにMacでビルドしたほうがメンテコストが安い。

さらに、大きいアセット素材(画像や動画)をビルドするマシンまで転送するのもいろいろ工夫が必要であり、自分たちの需要に合わせたインフラを作る必要がある。

そうなるとSaaS型のCIは需要に耐えられるか不安でなかなかすべて置き換えるのは難しそうだ。最近はポストJenkinsとしてdrone.ioが登場しているがUnityがdocker上で微妙1なのでiOSビルドの需要に耐えない。

Jenkinsおじさん問題

結果、オンプレMacにJenkinsを建てることになる。やっかいなことに、そのJenkinsは設定が管理されてないことが多く、Jenkinsが吹っ飛ぶと開発が止まる。

大抵の場合はjenkins-homeをgit commitするなどしてバックアップをとり、安全性を確保するだろう。しかし次に問題になってくるのは、設定の再構築方法がわからなくなることだ。Jenkinsは基本的にGUIでポチポチ設定をすることを前提にしているので設定に再現性が生まれにくい。CIは開発者にとってのインフラになるので1度1から再構築をするなんてことはできない。結果、長年積もった設定変更の積み重ねにより、秘伝のJenkinsが出来上がる。そしてどの設定を変えるとどこに影響が生まれるのかはその設定をした管理者しかわからない。Jenkinsおじさんの完成だ。

Jenkins設定のコード化

Jenkinsはこの問題に対し、解答を用意している。Jenkinsの設定をyamlで管理できるConfiguration as codeと、ジョブの設定とコードをgroovyで管理できるDeclarative Pipelineだ。

Configuration as codeはyamlでJenkinsの設定を書き、GUIからワンボタンで再起動なしで変更を適用することができる。ユーザーアカウントやプラグインも記述可能で、サポートされていればプラグインの設定も管理可能だ。本来のJenkinsの設定自体は複数のxmlだが、1つのyamlになることで設定を一望できるし、変更すべき場所もわかりやすい。そしてこのyamlをgit管理することによってJenkinsが吹っ飛んでも初期状態から再構築できる。

jenkins:
  securityRealm:
    ldap:
      configurations:
        - groupMembershipStrategy:
            fromUserRecord:
              attributeName: "memberOf"
          inhibitInferRootDN: false
          rootDN: "dc=acme,dc=org"
          server: "ldaps://ldap.acme.org:1636"
...

Declarative PipelineはJenkinsfileという名のgroovyでジョブの設定を書くことができる。ジョブに必要なパラメータや呼び出すべきbashスクリプトなどを記述できる。Declarative Pipelineに関しては、git連携が非常に便利だ。設定すれば、リモートのgitリポジトリに存在するJenkinsfileを実行時にフェッチして反映することができる。この機能のおかげで、複数人でgit管理されたジョブスクリプトを編集、管理することができる。

pipeline {
    agent { docker 'maven:3-alpine' } 
    stages {
        stage('Example Build') {
            steps {
                sh 'mvn -B clean verify'
            }
        }
    }
}

ポータブルになったのか

Configuration as CodeとDeclarative Pipelineで間違いなく保守性は上げることができた。Jenkinsが吹っ飛んでも(多少の手間はかかるが)復元できるし、設定やコードはgit上で共同管理できる。

しかしまだかゆいところに手が届かない状況が存在する。例えばプラグインの管理は、先程configuration as codeでyamlに書くことで管理できると述べた。しかしyamlで記述したプラグインがインストールされるのはyamlを適用した後である。どういうことが起こるかというと、yamlにプラグインの設定を書き、その設定を適用しようとすると「プラグインが存在しない」と言われて適用できないのだ。プラグインをインストールするためにyamlを書いてるのにyamlを適用するためにプラグインが必要になるというデッドロックが発生する2。そもそもconfiguration as code自体がプラグインなので、configuration as codeのインストールはyamlに書くことはできない。結局、自分はこの問題が解決できなかったので、プラグインの設定はyamlに書き、プラグインのリストとインストールは別の方法で管理している。

他にも、Declarative PipelineはGitHub上にJenkinsfileを置いているのだがジョブを増やすためには①Jenkins上でジョブを作成 ②GitHub上でJenkinsfileをcommit ③ジョブの設定からJenkinsfileのURLを記述 の3段階を踏む必要がある(まあ普段ジョブを作る頻度はそんなに高くないので良いのだが…)。この問題は、GitHubOrganizationFolderを使うことで解決できるのだが、こいつはこいつで厄介なのだ。

Organization Folderは、Organization内にあるリポジトリをスキャンして、リポジトリのルートにJenkinsfileがあるリポジトリに対してMultibranch Pipelineジョブ(後述)を自動で作成してくれます。

https://www.kaizenprogrammer.com/entry/2017/03/20/130721

つまり、ジョブ1つに対して、リポジトリ1つが対応することになる。管理する必要があるリモートリポジトリなどなるべくなら増やしたくはない。しかしCIはジョブがたくさん必要なので1つのリモートリポジトリに複数のJenkinsfileを置いて運用するのが良い。するとOrganizationFolderは使えず、先程述べたジョブ設定が必要になる3

p-jenkins

JenkinsはAll In Oneというよりは自分に必要なPluginを選んで使う思想なのでPlugin間の連携ができなかったりするのはまあ仕方ない。でもまあなるべくならポータビリティも上げて、どこでも実行できるし、いつでも再構築&改修が楽にできる状態にしたい。

そこでDockerを用いてポータブルにミニマムなJenkins建てれて設定変更も楽にできるテンプレートを作った

docker-composeでシュッっと立ち上げることができるし、1つのジョブを実行するだけでgithub上の設定やジョブを同期できるし、なんならs3へのバックアップ機能もあるので各々の需要に合わせて使ってほしい。

JenkinsのDockerコンテナ化

得に語ることはないがやはりdocker-compose upでシュッと立ち上がるのは便利だ。dockerさえ入っていればローカルマシンですら動くのでJenkinsをアップデートするのが怖い時も、すぐにローカルで動かして確認&改修ができる。

イメージはjenkinsの公式イメージから引っ張ってきている。JenkinsAdminアカウントはjenkins.yamlに書くのでセットアップウィザードもスキップしている。「CASC_JENKINS_CONFIG」環境変数で示しているディレクトリにyamlをコピーしておけば適用してくれる。

FROM jenkins/jenkins:2.164.2-alpine

ENV JAVA_OPTS "-Djenkins.install.runSetupWizard=false -Xmx800m"

USER root
RUN mkdir /build

RUN apk add --update python3 py-pip

RUN pip install awscli

# deploy config
RUN mkdir -p /config
ENV CASC_JENKINS_CONFIG /config/jenkins.yaml
COPY ./config/jenkins.yaml $CASC_JENKINS_CONFIG

Configuration as code適用

Github上のConfiguration as codeのyamlに変更があった場合、必要に応じてyaml設定をJenkinsに適用しなければならない。それを担当しているスクリプトが以下だ。

単純にスクリプトから相対パスにあるjenkins.yamlを$CASC_JENKINS_CONFIGにdiffをとってコピーしているだけだ。実際に動かすときは、このスクリプトのいるリポジトリはDeclarative Pipelineのgit連携によって自動フェッチされるので、最新のjenkins.yamlを適用することができる。

Configuration as codeはコピーしただけでは設定を反映できない。JenkinsのConfiguration as Codeのページから「RELOAD EXISTING CONFIGURATION」ボタンを手動で押さなければならない4

ジョブ追加 & 削除

ジョブはGitHub上の特定のフォルダと同期することで追加と削除を行う。実行しているスクリプトは以下だ。

下記コードでリポジトリのjobsディレクトリとjenkins_home内のjobsディレクトリのdiffをとる。

###
### WHAT: ジョブ差分を取得して更新します
###   
JENKINS_JOBS=$(ls /var/jenkins_home/jobs)

REPO_JOBS=$(ls $SCRIPT_PATH/../../jobs)

DIFF_JOBS_FOR_ADD=$(join -v 1 <(echo "${REPO_JOBS[@]}") <(echo "${JENKINS_JOBS[@]}"))

IS_UPDATE_JOB=false

echo "------ Job difference for add ------"
for j in ${DIFF_JOBS_FOR_ADD[@]}; do
    echo "[ADD] $j"
    IS_UPDATE_JOB=true
done

DIFF_JOBS_FOR_RM=$(join -v 2 <(echo "${REPO_JOBS[@]}") <(echo "${JENKINS_JOBS[@]}"))

echo "------ Job difference for remove ------"
for j in ${DIFF_JOBS_FOR_RM[@]}; do
    echo "[REMOVE] $j"
    IS_UPDATE_JOB=true
done

もしdiffがあって、リポジトリのjobsディレクトリのほうが多ければ、jenkins-cliを用いて足りないジョブを作る。ジョブのconfig.xmlは自分で用意しなければならないのでテンプレートっぽいものを用意して使う。

もしリポジトリのjobsディレクトリよりjenkins_homeのjobsディレクトリが多かったなら削除しなければならないので削除する。これもまたjenkins-cliで行うことができる。

上記のインフラを整えることで、Jenkinsのジョブを追加したい時はリポジトリにJenkinsfileを置いてスクリプトを実行するだけでよくなる。

プラグインの更新

プラグインは上述のとおり、jenkins.yamlにはかかない。Jenkinsをdockerで構築すると、install-plugin.shというプラグインインストールのためのスクリプトが標準で用意されている。これを使用してリポジトリのplugin.txtに差分があればinstall-plugin.shを実行する。

###
### WHAT: plugin差分を取得して更新します
###   
DIFF_PLUGINS=$(diff -u ${SCRIPT_PATH}/../config/plugins.txt /build/plugins.txt||true)

echo ""
if [[ "${#DIFF_PLUGINS}" -gt 0 ]]; then
    echo "------ There are some difference in plugins.txt ------"
    echo ""
    diff -u ${SCRIPT_PATH}/../config/plugins.txt /build/plugins.txt||true
    copy ${SCRIPT_PATH}/../config/plugins.txt /build/plugins.txt
    /usr/local/bin/install-plugins.sh < /build/plugins.txt
    echo "copying config completed!"
else
    echo "------ There is no difference in plugins.txt ------"
fi

これでプラグインを更新したい場合もgit上のplugin.txtを編集し、スクリプトを実行することで再起動なしで反映される。

バックアップ & リストア

jenkins.yamlに構築方法が書いてあるとはいえ、jenkins_homeのバックアップを取らないのは不安だ。なのでjenkins_homeはtar圧縮してs3へとアップロードをする。

###
### INTERFACE
###
### WHAT:
###   jenkins-homeをs3にバックアップします
###
### PARAMETER:
BACKUP_BUCKET_NAME=$BACKUP_BUCKET_NAME

SCRIPT_PATH=$(cd $(dirname $0);pwd)

NOW=$(date "+%Y%m%d-%H%M")
tar zcvf \
    /backup/${NOW}.tar.gz \
    /var/jenkins_home \
    -X ${SCRIPT_PATH}/backup-excludes.txt

aws s3 cp \
    /backup/${NOW}.tar.gz s3://${BACKUP_BUCKET_NAME}/backup-${NOW}

リストア時も、ファイル名を指定してs3からファイルをダウンロードし、jenkins_homeへと解凍する

###
### INTERFACE
###
### WHAT:
###   jenkins-homeをs3からリストアします
###
### PARAMETER:
BACKUP_FILE=$1
BACKUP_BUCKET_NAME=$BACKUP_BUCKET_NAME

if [[ -z "${BACKUP_FILE}" ]]; then
    echo "please specify backup file!" >&2
    echo "./restore.sh BACKUP_FILE" >&2
    exit 1
fi

aws \
    s3 cp s3://${BACKUP_BUCKET_NAME}/${BACKUP_FILE} /restore

tar zxvf \
    /restore/${BACKUP_FILE}

echo -e "\e[1;34m restore completed! please restart jenkins! \e[m"

自己設定同期するJenkins

p-jenkinsでは上記までで紹介した機能はすべて自身のジョブとして実行することができる。つまり、ジョブを実行するだけでs3へとバックアップできるし、github上のjenkins設定と同期することができる。

つまり、docker-compose upしたばかりのときはupdate-configジョブしか無いのだが、

update-configジョブを実行すると、以下のようにgithub上のジョブ設定と同期を行うことができる。

ジョブだけでなく、jenkins.yamlも更新するし、plugin.txtも反映する。自己設定同期機能を持ったJenkinsの完成だ。

おわりに

Jenkinsが保守できなくなるのを恐れて設定を同期する仕組みを作ってしまったがこの仕組みもまた保守されなくなる未来が見える気がする..

やはり最初から設定はコードで書くことが可能で、ジョブスクリプトもgithubから取ってくるようなもので、オンプレでも動くようなCIがあれば今ならわりとすんなり天下を取れるかも知れない。

  1. linux版はpreview。Unityは仮想マシン上で開発のために動かすことを許可しているので希望はある
  2. 解決方法があったら教えてほしい…
  3. これも解決方法があったら教えてほしい…
  4. 実際はPostリクエストが送られてるだけなので自動化できるかと思ったがワンタイムトークンみたいなのを送ってたので無理そうと思ってやめた

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.