この記事はマネーフォワード アドベントカレンダー20日目の記事です。

皆さん、Jenkins使ってますか!

今年発足したCDF(Continuous Delivery Foundation)でホストされているJenkinsは、CI/CDを語る上では切っても切り離せない存在です。今回、とある事情によりJenkinsのジョブを(コンソール上から手作業でビルドするのではなく)リモートでビルドしたくなりました。

みなさんはそんな時にどうしますか!?

ご存知のとおり、JenkinsはAPIを提供しています。例えば、ジョブの一覧を取得する際には、以下のエントリポイントにリクエストを投げることでjsonレスポンスが返ってきます。(以下の例は、APIアクセスに認証が必要な場合です)

$ curl -s -u <USER>:<TOKEN> https://<JENKINS_HOST>/api/json | jq .
{
  "_class": "hudson.model.Hudson",
  "assignedLabels": [
    {
      "name": "master"
    }
  ],
・・・

APIは参照系だけではありません。以下のエントリポイントにPOSTリクエストを投げることで、ジョブをビルドすることができます。

curl -X POST -s -u <USER>:<TOKEN> https://<JENKINS_HOST>/job/<JOB_NAME>/build

このようにリクエストを投げればリモートでジョブをビルドできるのですが、毎回curlを実行するのも面倒ですよね。 そこで、gojenkinsというgolang製のJenkins APIクライアントライブラリを使って、Jenkinsのジョブをリモートでビルドするスクリプトを書いてみます。

前提

環境準備

以下の環境が前提となります

  • http/httpsでアクセスできるJenkinsが存在する
  • Goのプログラムを実行できる
  • JenkinsのAPI_TOKENを発行済で、Goのプログラムから利用可能(環境変数に設定しておくとよい)

gojenkinsをインストール

gojenkinsをインストールしておきましょう。

go get github.com/bndr/gojenkins

Jenkinsにテスト用ジョブを追加する。

まずはじめに、Jenkinsにテスト用のジョブを追加します。処理内容は何でもいいのですが、今回はパラメータ(sleep_count)で渡した数値の分だけsleepするという、シンプルなジョブを用意しました。

リモートビルドスクリプトを書く

実際にスクリプトを書いていきます。まずはinitializeです。CreateJenkins()で、新しいJenkin instanceを生成します。パラメータには、URL、アクセスユーザ、アクセストークンを渡します。instanceを生成した後には、Init()メソッドで初期化する必要があります。

jenkins := gojenkins.CreateJenkins(nil, jenkinsRootURL, jenkinsAccessUser, jenkinsAccessToken)
_, err := jenkins.Init()
if err != nil {
	log.Fatal(err)
}

ビルドパラメータをマップで定義します。BuildJob()で指定したジョブをビルドします。1つ目の引数にはジョブ名を、2つ目の引数にはパラメータを指定しています。マップのバリューはstring型で渡さないといけないので、intではなく文字列で定義しています。今回は30秒間sleepさせたいのでsleep_count:30をビルドパラメータに指定します。

params := map[string]string{"sleep_count": "30"}

BuildJob()でジョブをビルドします。1つ目の引数にはジョブの名前を、2つ目の引数には先程定義したパラメータのマップを指定します。

log.Printf("%s remote build", jobName)
_, err = jenkins.BuildJob(jobName, params)
if err != nil {
	log.Fatal(err)
}

BuildJob()を実行してAPIを叩いた直後は、最新のビルドのステータスがpendingになっています。ステータスがrunnningになるまで待ちたいので、10秒ほどsleepします。

time.Sleep(time.Second * 10)

一番新しいビルドのステータスを定期的にチェックします。runnning状態でなくなったら、success/failedをチェックし、それに応じてログを出力・スクリプトを終了します。またジョブがロングランしている場合にも、タイムアウトでスクリプトが終了するようにしています。

build, err := jenkins.GetJob(jobName)
if err != nil {
	log.Fatal(err, ": maybe Job does not exist")
}
LastBuild, err := build.GetLastBuild()
if err != nil {
	log.Fatal(err)
}

deadline := time.Now().Add(10 * time.Minute)
for time.Now().Before(deadline) {
	log.Printf("LastBuild.IsRunning() is %t", LastBuild.IsRunning())
	if !LastBuild.IsRunning() {
		if LastBuild.IsGood() {
			log.Print("building job is success")
			os.Exit(0) // 正常終了
		}
		log.Print("building job is failed")
		os.Exit(1) // 異常終了
	}
	time.Sleep(10 * time.Second)
}
log.Print("building job check is timeout")
os.Exit(1)

リモートでビルドしてみる

上記のプログラムを実行すると、以下のようなログが出力されるとともに、Jenkins上でジョブがビルドされていることがわかります。

[I] gojenkins » go run gojenkins_sample.go
2019/12/18 20:38:47 sleep_job remote build
2019/12/18 20:38:57 LastBuild.IsRunning() is true
2019/12/18 20:39:07 LastBuild.IsRunning() is true
2019/12/18 20:39:17 LastBuild.IsRunning() is true
2019/12/18 20:39:27 LastBuild.IsRunning() is false
2019/12/18 20:39:27 building job is success

おわりに

このように、gojenkinsを使うことでGoのプログラムからリモートでJenkins上のジョブをビルドすることができます。

これを活用すると何ができるでしょうか。例えば、複数Jenkins間のジョブフローの同期を簡易的に行うことができます。また、Slack Botと組み合わせればCI/CDのChatOpsも実現できそうです。

この世界にはJenkinsの運用に苦しめられている人もいるかと思いますが、このようなちょっとした工夫を活用して、日々の運用を楽にしていきましょう。