メインコンテンツまでスキップ

Jenkins による自動リリース

· 約11分

手作業でのリリースを重ねていると、毎回のパッケージング・転送・再起動という一連の流れは時間がかかるうえにミスも起きやすいものです。この記事では、Jenkins を使ってリリースプロセス全体をパイプラインとしてつなげた際の設定手順を記録します。

はじめに

パイプラインを導入する前のリリースはだいたいこんな流れでした。ローカルでパッケージングし、scp でサーバーに転送し、ログインしてサービスを停止し、jar ファイルを差し替えて再起動する。手順自体は複雑ではありませんが、すべて人力での実行に頼っており、環境設定の変更を 1 か所見落としたり、パッケージを別のマシンに転送してしまったりというのは、本番障害のよくある原因です。これらの作業を Jenkins に任せることで、手間が省けるのはもちろん、より重要なのはリリースプロセスが再現可能かつ追跡可能になることです。ビルドごとに記録が残り、問題が起きたときは指定バージョンのイメージにロールバックできます。

最近設定した Jenkins のリリースフローの手順の一部を記録しておきます。

全体の流れ:Jenkins で本番リリースを実行し、docker イメージをビルドしてプライベート docker イメージレジストリにアップロードし、docker login を設定して本番側でイメージのプッシュ/プルとデプロイ稼働を行います。

簡単に分解すると、このパイプラインは 4 つのことをやっています。git からコードを取得して環境設定を置換し、maven で jar パッケージをビルドし、docker-compose でイメージ化してプライベートレジストリにプッシュし、最後にリモートで対象サーバーに新しいイメージの取得と起動を通知します。以下、この順番で見ていきます。

docker のインストールや Jenkins イメージの取得については、既存の記事が多数あるためここでは省略します。

プラグインの統合

Jenkins 本体は単なるスケジューラーにすぎず、ビルド能力は基本的にグローバルツール設定とプラグインで補われます。そこで最初のステップは「グローバルツール設定」でビルドに必要なコンポーネントを設定することです。Jenkins は自動ダウンロード・インストールに対応しており、ホストマシン上の既存のインストールパスを指定することもできます。

maven、jdk、git、nodeJs、docker などの基本コンポーネントを設定・インストールします。

ツールの設定が済んだら、次はプラグインセンターで外部システムと連携するプラグインをインストールします。ここでの原則は必要に応じてインストールすることです。コードを gitlab でホスティングしているなら gitlab プラグインを入れます。これにより gitlab の webhook プッシュを受信し、コードのコミット後に自動でビルドを起動できます。

各種サポートプラグイン(gitlab など)をインストールします。

nodejs プラグインはフロントエンドのリリースに使用します。

フロントエンドとバックエンドは同じ Jenkins で運用できます。ビルドツールが異なるだけで、バックエンドは maven を使い、フロントエンドのタスクでは nodeJs のバージョンを指定して npm build を実行すれば済みます。

最後に、リモートサーバーに接続してコマンドを実行できるプラグインも必要です。Jenkins が動いているマシンと本番稼働しているマシンは通常別物なので、ビルド成果物をイメージレジストリにプッシュした後、対象サーバーに取得と再起動を通知する手段が要ります。このステップは、このプラグインが SSH 経由でリモートでスクリプトを実行することで完了します。

リモートサーバーに接続してコマンドを実行するプラグインです。

パイプラインタスクの作成

プラグインの準備ができたらタスクを作成します。タスクの本質は、一連のビルドステップを順番にオーケストレーションすることです。各ステップでどのコンポーネントを使い、どのコマンドを実行するかを指定し、前のステップの成果物が次のステップの入力になります。

タスクを作成し、各段階の実行コマンドと使用コンポーネントを設定します。

まず取得する git リポジトリのアドレスとプロジェクトコードを設定します。

その後、ソースコード内の環境値を置換します。たとえば dev 環境を pro の nacos 接続アドレスやネームスペースなどに置き換えます。

環境値の置換というステップは、もう少し補足する価値があります。開発環境と本番環境では設定センターのアドレスやネームスペースが異なることが多く、コードリポジトリにデフォルトで dev 設定が書かれている場合、本番パッケージのビルド前にそれを置き換えなければなりません。さもないと、サービスが本番稼働後にテスト環境の nacos に接続してしまいます。

私が使ったのは最もシンプルな sed -i コマンドです。このコマンドは正規表現にマッチさせてテキスト文字列を置換できます。

# -i はファイル自体を直接変更する指定。s/旧値/新値/g は正規表現置換で、g はマッチしたすべてを置換
sed -i 's/dev-nacos-addr/pro-nacos-addr/g' src/main/resources/bootstrap.yml

sed の利点は依存ゼロで、スクリプト 1 行で解決できることです。欠点は置換の対応関係が Jenkins のタスク設定に散在してしまい、設定項目が増えるとメンテナンスしづらくなることです。プロジェクトの規模が大きくなってきたら、maven profile や設定センターのマルチ環境機能を使い、環境差分を一箇所に集約することを検討するとよいでしょう。

リソースのパッケージングとプッシュ

maven で jdk build を実行し、プロジェクトの jar パッケージをビルドします。

パッケージングが完了したら、shell コマンドで target ディレクトリ配下の jar パッケージを対応する docker-compose ファイルの場所に cp します。ここでは docker-compose ファイルと、対応するサービスの docker-file ファイルを事前に書いておく必要があります。

この 2 つのファイルを事前に用意しておく理由は次のとおりです。Dockerfile は単一のサービスをどうイメージ化するか(ベースイメージ、jar のコピー、起動コマンド)を記述し、docker-compose は複数サービスのビルド宣言を 1 つにまとめます。こうすることで 1 コマンドでイメージを一括生成でき、サービスごとに個別に docker build する必要がなくなります。

docker-compose を実行してイメージを一括ビルドします。

ビルド完了後、docker tag を実行してイメージにバージョンタグを付けます。

タグ付けの目的は、リリースごとのイメージに一意のバージョン番号を持たせることであり、すべてを latest と呼ばないことです。Jenkins 組み込みのビルド番号をタグに使うのは手軽な方法で、ロールバック時には過去のバージョン番号を直接指定するだけで済みます。

イメージのタグ付けが完了したら docker push でイメージレジストリにアップロードします。

# ビルド番号でイメージにバージョンタグを付け、プライベートレジストリにプッシュ
docker tag myapp:latest registry.example.com/myapp:${BUILD_NUMBER}
docker push registry.example.com/myapp:${BUILD_NUMBER}

プッシュ前に、Jenkins が動いているマシンと対象サーバーの両方でプライベートレジストリに docker login しておくことを忘れないでください。さもないと push/pull が認証失敗で中断します。

リモートで対象サーバーまたは k8s master ノードに接続し、job コマンドを実行してイメージのダウンロードアドレスを渡します。

ここまでで、ビルドマシンの仕事は終わりです。リモートコマンドでは対象サーバーに新しいイメージの完全なアドレスを伝えるだけでよく、あとはサーバー自身がイメージを pull してコンテナを再起動します。ビルドと稼働が完全に分離されるため、対象サーバーにはビルドツールを一切インストールする必要がありません。

過去リソースの削除

パッケージングしてプライベートレジストリにアップロードした後は、ローカルワークスペース内の不要なリソースや docker イメージなどのリソースをクリアすることを忘れないでください。複数回のリリース後にリソース使用量が高くなりすぎる問題を回避できます。これでクローズドループの運用が形成され、プロジェクトのバージョンを何度でもリリースできます。

クリーンアップはパイプラインの最後のステップとしてそのまま組み込めます。ワークスペース内のビルド成果物を削除し、今回ビルドしたローカルイメージも消しておきます。docker のイメージはレイヤー構造で保存されるため、旧バージョンのイメージを掃除しないと積み上がり続け、数十回のリリース後にはディスクがいっぱいになり、ビルドが原因不明のまま失敗するようになります。

ハマりどころと注意点

  1. Jenkins をコンテナで動かしている場合、タスク内で docker コマンドを実行するには、ホストマシンの docker.sock をコンテナにマウントする必要があります。さもないとコンテナ内から docker デーモンが見つかりません。

  2. プライベートレジストリが https ではなく http の場合、docker はデフォルトで接続を拒否します。プッシュ側・プル側両方の docker 設定で、レジストリのアドレスを insecure-registries に追加する必要があります。

  3. sed で設定を置換する際は特殊文字のエスケープに注意してください。置換内容に / が含まれる場合は # などのデリミタに切り替えると、sed 自身のデリミタとの衝突を避けられます。

  4. リモートコマンド実行の認証情報(SSH 秘密鍵、レジストリのアカウントとパスワード)は Jenkins の認証情報管理に保存し、タスクの shell スクリプトに平文で書かないようにしましょう。

ヒント

イメージのタグはできるだけ git のコミットやビルド番号と関連付けておきましょう。本番の問題を調査するとき、特定のコード変更にすばやく対応づけられます。

まとめ

パイプライン全体が通れば、リリースはビルドボタンを 1 回押す(あるいはコードのコミットで自動起動する)だけになります。コードの取得、設定の置換、maven でのパッケージング、docker-compose でのイメージ生成、タグ付けとプッシュ、リモートでの新バージョン起動、後片付けまで、全工程が人手を介しません。手作業のリリースと比べた利点は速さだけではなく、すべてのステップがタスク設定に固定化されることで、リリース結果が安定して予測可能になり、ロールバックもイメージのタグを差し替えるだけで済む点にあります。

COMMENTS