Concourse上でMavenのローカルリポジトリをキャッシュする
yotaです。
私のチームではCI/CDツールConcourse上でMavenを使ったJavaプロジェクトのビルド環境を構築中です。
(Concourseについては本ブログの別記事を参照ください)
その中で、Mavenによる依存ライブラリや各種プラグインのダウンロードがConnection timed outとなり、ビルド全体がエラーになるという事象が発生するようになりました。
以下のようなエラーですが、対象となるライブラリ、プラグインは常に同じというわけではありませんでした。
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:3.0.0-M8:test (default-test) on project <プロジェクト名>: Execution default-test of goal org.apache.maven.
plugins:maven-surefire-plugin:3.0.0-M8:test failed: Plugin org.apache.maven.
plugins:maven-surefire-plugin:3.0.0-M8 or one of its dependencies could not be resolved: Failed to collect dependencies at org.apache.maven.plugins:maven-surefire-plugin:jar:3.0.0-M8 -> org.apache.maven.
surefire:maven-surefire-common:jar:3.0.0-M8: Failed to read artifact descriptor for org.apache.maven.
surefire:maven-surefire-common:jar:3.0.0-M8: Could not transfer artifact org.apache.maven.
surefire:maven-surefire-common:pom:3.0.0-M8 from/to (https://<社内ミラーリポジトリ>): transfer failed for https://<社内ミラーリポジトリ>/org/apache/maven/surefire/maven-surefire-common/3.0.0-M8/maven-surefire-common-3.0.0-M8.pom: Connect to <社内ミラーリポジトリ>:443 failed: Connection timed out (Connection timed out) -> [Help 1]
この Connection timed outの原因はすぐに解明できなかったのですが、このときMavenの実行ごとに常にすべてのライブラリをダウンロードする構成になっていました。そこで、都度ダウンロードしないように変更してConnection timed outが起きる機会を減らすこととしました。
Concourseのキャッシュ
Concourseではジョブの実行ごとにコンテナを作り、その中で各種処理を実行します。そのため、デフォルトではあるジョブの実行でダウンロードしたファイルはそのコンテナの終了とともに失われてしまいます。1
これを解決する方法として、ジョブ内のタスク 2 ごとに個別に設定することで、特定のディレクトリをキャッシュして再度実行した際にそのディレクトリの内容を再利用できます。
Mavenではダウンロードしたライブラリを特定のディレクトリに保存するため、そのディレクトリをキャッシュするようにパイプライン設定を変更すればよさそうです。
ドキュメントを要約すると、キャッシュは以下の仕様になっています。
- キャッシュはワーカーごとに独立しており、ワーカー間で共有することはできない
- Concourseではジョブを実行するワーカーを複数用意可能です。ワーカーを新設した場合、そのワーカーではジョブを実行するまでキャッシュが存在しない、という状況が起きます。
- キャッシュはパイプライン名、ジョブ名、タスク名で独立している
- 異なるパイプライン同士でキャッシュが独立しているのはもちろんのこと、同じパイプラインを意図していてもタスク名を変えるとキャッシュが無効になります。
Mavenを利用しているジョブが複数あればその都度設定が必要ですが、数も多くなかったため、1つずつ設定していくことにしました。
MavenでConcourseのキャッシュを利用する
まず、キャッシュを利用する前のパイプライン設定(抜粋)は以下のようになります。
maven-java-projectをGitHubからcloneして、maven (コンテナイメージ)の中で mvn clean package
を実行するものです。
jobs:
- name: maven
- get: maven-java-project # git clone相当
trigger: true
- task: package
image: maven # mavenを実行するイメージ
config:
platform: linux
inputs:
- name: maven-java-project
run:
path: /bin/bash
dir: maven-java-project
args:
- -ceu
- |
mvn clean package
resources:
- name: maven-java-project
type: git
icon: github
source:
uri: "GitHub URL"
次に、キャッシュ設定を入れたパイプラインは以下のようになります。
jobs:
- name: maven
- get: maven-java-project
trigger: true
- task: package
image: maven
config:
platform: linux
caches:
- path: repository/ # 追加
inputs:
- name: maven-java-project
run:
path: /bin/bash
dir: maven-java-project
args:
- -ceu
- |
# maven のローカルリポジトリのパスを変更
mvn -Dmaven.repo.local=$(mkdir -p repository && cd $_ && pwd) clean package
resources:
- name: maven-java-project
type: git
icon: github
source:
uri: "GitHubのリポジトリのURI"
変更点は
caches
の追加- Mavenの実行時オプションで
-Dmaven.repo.local
を追加
です。
Mavenにおいて、デフォルトではダウンロードしたライブラリ群は ${HOME}/.m2/repository/
に保存されます。
上記パイプラインでは、このディレクトリを別に作った repository/
に変える設定となっています。
(Concourseではコンテナ内でMavenが実行されるため、いずれもコンテナ内のパスを指します)
一見 caches
で ~/.m2/repository
と設定すればいいように思われるのですが、Concourseの仕様で
Paths are relative to the working directory of the task. Absolute paths are not respected.
とあり、caches
で指定してよいのは相対パスのみとされているので別のディレクトリを設けました。
今回の例では -Dmaven.repo.local
で指定していますが、同一タスク内でMavenを実行する場合は都度指定しなければそれぞれの実行で反映されないので、 ${HOME}/.m2/settings.xml
で指定するほうが良いと思います。3
これにより、1度ダウンロードしたライブラリはそのワーカー上でキャッシュされ、それ以降再度ダウンロードすることがなくなりました。
まとめ
Concourseのパイプライン設定を変更することでMavenがダウンロードするライブラリをキャッシュするようになり、都度ライブラリをダウンロードしないようになったことでConnection timed outが減りました。
「なくなりました」、ではなく「減りました」というのは、時折すべてのライブラリをダウンロードするためです。
これは我々が構築しているConcourseがSpot VM を使ったGKE上にあり、不定期にワーカーが再起動されるためです。4
それでも1日に数回、といった高頻度でワーカーを再起動するわけではないため、ビルドの安定性が大きく向上しました。
実は後にConnection timed outの根本的な原因が判明したのですが、それは別途投稿します。
-
まるでDockerでボリュームを別途指定しなければコンテナ外にデータを保存できないことと同様ですね。 ↩︎
-
詳細は省略しますが、Concourseではタスクが処理の最小単位(シェルスクリプトの実行、
docker push
、etc…)で、ジョブはタスクの組み合わせ、パイプラインはジョブの組み合わせという構造をとっています。 ↩︎ -
実際の我々のチームのパイプラインでも複数回実行するので、
settings.xml
で設定するようにしました。 ↩︎ -
実際はPersistent Volumeをマウントしたディレクトリに保存されるようなのですが、それでもワーカーとなるPodがnodeとなるSpot VMの再起動に伴って都度別名で起動するためか、キャッシュが都度なくなるような挙動でした。 ↩︎