スクエニ ITエンジニア ブログ

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の根本的な原因が判明したのですが、それは別途投稿します。


  1. まるでDockerでボリュームを別途指定しなければコンテナ外にデータを保存できないことと同様ですね。 ↩︎

  2. 詳細は省略しますが、Concourseではタスクが処理の最小単位(シェルスクリプトの実行、docker push 、etc…)で、ジョブはタスクの組み合わせ、パイプラインはジョブの組み合わせという構造をとっています。 ↩︎

  3. 実際の我々のチームのパイプラインでも複数回実行するので、settings.xml で設定するようにしました。 ↩︎

  4. 実際はPersistent Volumeをマウントしたディレクトリに保存されるようなのですが、それでもワーカーとなるPodがnodeとなるSpot VMの再起動に伴って都度別名で起動するためか、キャッシュが都度なくなるような挙動でした。 ↩︎

この記事を書いた人

記事一覧
SQUARE ENIXでは一緒に働く仲間を募集しています!
興味をお持ちいただけたら、ぜひ採用情報ページもご覧下さい!