JenkinsのBuild AgentをMac上に構築する

Jenkinsを使ったiOSアプリのCI/CD環境を構築するために、ビルドエージェントとしてmac miniを登録する機会があったため手順をまとめておきます。

基本的な流れは GitLab CI Runner に近いです。
https://qiita.com/takamii228/items/64fd6879e6acce845980

なお今回利用したJenkinsのバージョンは 2.235.2 です。

## mac miniの設定をする

まずmac miniでJenkinsのAgentが動作する準備をします。

JenkinsのAgentを動作させるためにはJavaが必要なのでHomeBrew経由でインストールします。

```shell
$ brew install openjdk@11
$ export PATH="/usr/local/opt/openjdk@11/bin:$PATH"
$ java -version   
openjdk version "11.0.9" 2020-10-20
OpenJDK Runtime Environment (build 11.0.9+11)
OpenJDK 64-Bit Server VM (build 11.0.9+11, mixed mode)
```

またJenkins Agentが動作するための作業ディレクトリを切って移動しておきましょう。

```shell
$ mkdir ~/jenkins
$ cd ~/jenkins
```

今回はサンプル用にかんたんなシェルスクリプトを動作させますが、本来はmac miniのagent上で実行したいCI・CDの実行環境も合わせて構築する必要があります。

## JenkinsのJNLPのポートを固定する

JenkinsのAgentの通信方法はsshを使うものとJNLPを使うものを選べますが今回はJNLPを使います。
JNLPはJava Network Launch Protocolの略でJenkinsのビルドエージェントを動作させる仕組みとして用いられています。詳細な説明はこちらを参照してください。

https://docs.oracle.com/javase/tutorial/deployment/deploymentInDepth/jnlp.html

Jenkinsのデフォルトの設定だとJNLPで使うポート番号はランダムになるようになっています。ランダムだとセキュアでよいのですが、IPやポートに制限を加えている環境での利用では可変だと困るので、適当な値に固定にしておくとよいでしょう。他のポートとぶつからないようにプライベートポートの範囲である49152 ～65535の間から一つ選びましょう。

JNLPで利用するポート番号の固定はJenkinsにログインして管理画面（Manage Jenkins）に移動してグローバルセキュリティ（Configure Global Security）のAgentの設定で指定できます。

合わせてこのポート番号とmac miniからの通信経路の穴あけを確認しておきましょう。

## JenkinsのBuild Agentを追加する

次にJenkinsの管理画面でBuild Agentのノード設定を行います。

管理画面（Manage Jenkins）に移動して「ノード管理（Manage Nodes and Clouds）」を選択します。

ノードの管理画面の左のリストのうち「新規ノードの作成（New Node）」を選択して新しいNodeを作成します。ノードの名前を入力し、Permanent AgentにチェックをいれてOKを押します。

次に詳細な設定を入力していきます。

- ノード名（Name）
  - ノードの名前を入力します 
- 説明（Description）
  - ノードの説明を入力します 
- 同時ビルド数（# of executors）
  - 同一ノード上での同時ビルド数を入力します。iOSは並行でビルドするとDerivedDataの関係でエラーになる可能性があるので1にします
- リモートFSルート（Remote root directory）
  - mac mini上での絶対パスをします。今回は `/Users/${username}/jenkins` です 
- ラベル（Labels）
  - Jenkinsfileでnodeとして指定するラベルを定義します 
- 用途（Usage）
  - 指定された場合のみ動作するか共有にするか選べます。今回は指定された場合のみに設定します。
- 起動方法（Launch method）
  - Launch agent by connection it to the master にします 
- 可用性（Availability）
  - Keep this agent online as much as possible にします 
- ノードプロパティ
  - 特に設定しません

すべて入力したら保存（Save）します。

## Jenkins Agentを起動してMasterと接続する

次にmac mini上でagentを起動してJenkins Masterと接続します。

動作させるJenkins Agent用のjarファイルは `http://yourserver:port/jnlpJars/agent.jar` にアクセスするとダウンロードすることができます。`yourserver:port`の部分は自身の動作させている環境に合わせて修正してください。

先程作成したノードの作成画面にagent.jarの起動コマンドが記載されているので、それをmac mini上で起動すればよいです。

```shell
$ java -jar agent.jar -jnlpUrl http://${yourhostname}:${port}/computer/${jenkins-agent-node-name}/slave-agent.jnlp \
  -secret ${secretToken} -workDir "/Users/xxxxxx/jenkins" 
Feb 18, 2021 8:04:58 PM org.jenkinsci.remoting.engine.WorkDirManager initializeWorkDir
INFO: Using /Users/xxxxxx/jenkins/remoting as a remoting work directory
Feb 18, 2021 8:04:58 PM org.jenkinsci.remoting.engine.WorkDirManager setupLogging
INFO: Both error and output logs will be printed to /Users/xxxxxx/jenkins/remoting
Feb 18, 2021 8:04:58 PM hudson.remoting.jnlp.Main createEngine
INFO: Setting up agent: ${jenkins-agent-node-name}
Feb 18, 2021 8:04:58 PM hudson.remoting.jnlp.Main$CuiListener <init>
INFO: Jenkins agent is running in headless mode.
Feb 18, 2021 8:04:58 PM hudson.remoting.Engine startEngine
INFO: Using Remoting version: 4.3
Feb 18, 2021 8:04:58 PM org.jenkinsci.remoting.engine.WorkDirManager initializeWorkDir
INFO: Using /Users/xxxxxx/jenkins/remoting as a remoting work directory
Feb 18, 2021 8:04:58 PM hudson.remoting.jnlp.Main$CuiListener status
INFO: Locating server among [http://${yourhostname}:${port}/]
Feb 18, 2021 8:04:58 PM org.jenkinsci.remoting.engine.JnlpAgentEndpointResolver resolve
INFO: Remoting server accepts the following protocols: [JNLP4-connect, Ping]
Feb 18, 2021 8:04:58 PM org.jenkinsci.remoting.engine.JnlpAgentEndpointResolver isPortVisible
WARNING: Connection refused (Connection refused)
Feb 18, 2021 8:04:58 PM org.jenkinsci.remoting.engine.JnlpAgentEndpointResolver resolve
INFO: Remoting server accepts the following protocols: [JNLP4-connect, Ping]
Feb 18, 2021 8:04:58 PM hudson.remoting.jnlp.Main$CuiListener status
INFO: Agent discovery successful
  Agent address: ${yourhostname}
  Agent port:    ${jnlpport}
  Identity:      xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Feb 18, 2021 8:04:58 PM hudson.remoting.jnlp.Main$CuiListener status
INFO: Handshaking
Feb 18, 2021 8:04:58 PM hudson.remoting.jnlp.Main$CuiListener status
INFO: Connecting to ${yourhostname}:${jnlpport}
Feb 18, 2021 8:04:58 PM hudson.remoting.jnlp.Main$CuiListener status
INFO: Trying protocol: JNLP4-connect
Feb 18, 2021 8:04:58 PM hudson.remoting.jnlp.Main$CuiListener status
INFO: Remote identity confirmed: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Feb 18, 2021 8:04:58 PM hudson.remoting.jnlp.Main$CuiListener status
INFO: Connected
```

上記ログのように Connect が表示されれば起動完了です。

Jenkinsの管理画面からも確認できます。

![settings.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/12250/83656b61-e583-c7da-fb84-f189f355efcb.png)

## ジョブを動かしてみる

ジョブが正しく設定されていることを確認してみましょう。Jenkinsでパイプラインジョブを作成して、mac mini上のマシン名を表示するだけのジョブ設定をしたJenkinsfileを入れて実行してみます。labekのところはノードの設定のときに入力したラベル名を入れます。

```groovy
pipeline {
    agent {
        node {
            label '${nodeLabelName}'
        }
    }
    
    stages {
        stage('build'){
            steps {
                sh 'uname -n'
            }
        }
    }
}
```

実行ログでmac miniのマシン名が表示されれば設定は完了です。

```shell
Started by user xxxxxx
Running in Durability level: MAX_SURVIVABILITY
[Pipeline] Start of Pipeline
[Pipeline] node
Running on jenkins-agent-sample in /Users/xxxx/jenkins/workspace/jenkins-connect-sample
[Pipeline] {
[Pipeline] stage
[Pipeline] { (build)
[Pipeline] sh
+ uname -n
xxxxxxxxx.local
[Pipeline] }
[Pipeline] // stage
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
Finished: SUCCESS
```

以上で設定は完了です。実際にはこのシェルディレクティブの中でCIやCDのスクリプトを実行するようにして、Jenkinsのジョブ設定でトリガーやパラメータ設定をすれば完成です。

## おまけ

JenkinsのAgent起動ジョブはコマンドラインでjavaコマンドで起動していました。もしmac miniを再起動したりコネクションが切れたときに再接続したい場合は、都度マシンにアクセスして起動コマンドを打たないといけません。

この課題を解決するために、macOSでデフォルトで利用できるlaunchdという仕組みを活用します。launchdについては公式サイトに例が沢山乗っているのでこちらを参考にします。

https://launchd.info/

今回は起動時に起動のjavaコマンドを実行し、ネットワークが再接続されたりクラッシュしたら再度javaコマンドを実行するようにlauchdを設定してみます。

launchdはplistファイルに設定や実行コマンドを記述します。Jenkins Agentを起動するplistファイルは以下のようになります。コマンドは絶対パスで記入するのがポイントのようです。

```plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
	<dict>
		<key>Label</key>
		<string>local.Jenkins.Agent.launchd</string>
		<key>KeepAlive</key>
		<dict>
			<key>NetworkState</key>
			<true/>
			<key>Crashed</key>
			<true/>
		</dict>
		<key>RunAtLoad</key>
		<true/>
		<key>ProgramArguments</key>
		<array>
			<string>/usr/local/opt/openjdk@11/bin/java</string>
			<string>-jar</string>
			<string>/Users/xxxxxx/jenkins/agent.jar</string>
			<string>-jnlpUrl</string>
			<string>http://${yourhostname}:${port}/computer/${jenkins-agent-node-name}/slave-agent.jnlp</string>
			<string>-secret</string>
			<string>${secretToken}</string>
			<string>-workDir</string>
			<string>"/Users/xxxxxx/jenkins"</string>
		</array>
		<key>StandardOutPath</key>
		<string>/tmp/jenkins.agent.stdout</string>
		<key>StandardErrorPath</key>
		<string>/tmp/jenkins.agent.stderr</string>
	</dict>
</plist>
```

作成したファイルは `~/Library/LaunchAgents/` 配下に配置してlaunchctlコマンドでloadすると設定されます。

```
$ launchctl load ~/Library/LaunchAgents/jenkins.agent.settings.plist  
```

こうしておくとmac miniの起動時に自動でagent.jarを起動してくれます。
再起動してみましょう。

```shell
$ ps aux | grep "agent.jar"
xxxxx          969   0.0  2.5  8137224 211276   ??  S     6:51PM   0:19.33 /usr/local/opt/openjdk@11/bin/java -jar /Users/xxxx/jenkins/agent.jar
$ launchctl list | grep "local.Jenkins"
969	-9	local.Jenkins.Agent.Setting
```

設定通り起動してくれていますね。

クラッシュしたりネットワークが切れたときについては試してませんがおそらく再起動してくれるはずです。もしかしたらjnlpのレイヤでやってくれるのかもですが、まずはしばらく運用してみようと思います。

## 参考資料

- https://wiki.jenkins.io/display/JENKINS/Distributed+builds
- https://github.com/jenkinsci/remoting/blob/master/docs/inbound-agent.md