今さら聞けないMaven – コンテナのビルドと一緒にpushもMavenでしたい。
「今さら聞けないMaven – コンテナも一緒にビルドしたい。テスト実行前にコンテナを起動したい」ではMavenを使ったコンテナイメージのビルド方法を説明しました。今回は前回と同じfabric8のdocker-maven-pluginによるもう一歩進めた使い方としてコンテナイメージのタグ付けとコンテナレジストリへのpushをMavenで行う方法を紹介したいと思います。これによりJavaのビルドからコンテナイメージのビルド、pushまでJavaアプリのコンテナ化で必要となる全ての操作をMavenから行えるようになります。
今回のサンプルと前回のおさらい
#今回はコンソールに"Hello, world!"を出力する簡単なコンテナアプリを使い、そのコンテナイメージに対するタグ付けとコンテナレジストリへのpushを説明していきます。なお説明で利用したサンプル一式はGitHubのこちらにアップしてあります。
今回利用するJavaプログラムとそのビルド方法は次のとおりになります。
- Javaプログラム
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, world!");
}
}
- ビルドするpom(
-jar
オプションで実行可能にしている)
<project xmlns="http://maven.apache.org/POM/4.0.0" ...>
<modelVersion>4.0.0</modelVersion>
<groupId>io.extact</groupId>
<artifactId>docker-push-with-maven</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
...
<mainClass>sample.HelloWorld</mainClass>
</properties>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<archive>
<manifest>
<mainClass>${mainClass}</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
... 後述のdocker-maven-pluginの定義
</build>
</project>
- ビルドとアプリの実行
mvn clean package
java -jar target/docker-push-with-maven.jar
> Hello, world!
このJavaアプリをfabric8のdocker-maven-pluginにより次のようにコンテナイメージにビルドしています。
- Dockerfile
# ベースイメージはeclipse-temurin(旧OpenJDK)のJava17を使用
FROM docker.io/eclipse-temurin:17-jre-alpine
# ホストOSのMavenのビルド成果物をコンテナイメージに格納
WORKDIR /
COPY ./target/docker-push-with-maven.jar ./
# Executable Jarなのでjavaコマンドで実行
CMD ["java", "-jar", "docker-push-with-maven.jar"]
- pomのdocker-maven-pluginの定義
<plugin>
<groupId>io.fabric8</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>0.40.2</version>
<configuration>
<images>
<image>
<name>extact-io/hello-world</name>
<build>
<tags>
<tag>latest</tag>
</tags>
<contextDir>${project.basedir}</contextDir>
</build>
</image>
</images>
</configuration>
</plugin>
- コンテナイメージのビルドとコンテナの実行
mvn clean package docker:build
docker image ls
> REPOSITORY TAG IMAGE ID CREATED SIZE
> extact-io/hello-world latest 398f93fe3aa5 55 seconds ago 170MB
docker run extact-io/hello-world
> Hello, world!
docker-maven-pluginを使ったコンテナイメージ(以降イメージ)のビルドは前回のおさらいとなるため、詳細はそちらを参照として詳しくは説明しませんが、上記のmvn
コマンドでやっていることは、
- Mavenの
package
ゴールで作成されたhello-wold.jarを入力として - Mavenの
docker:build
ゴールでDockerfileをもとにextact-io/hello-world
イメージを作成する
となります。
なお、今回はコンテナレジストリに GitHub Packages Container Registry(GitHub Packages)を使うため、イメージ名にリポジトリ名のextact-io
を明示しています。
この内容をもとにビルドしたイメージにタグを付ける方法とコンテナレジストリにpushする方法をみていきます。
コンテナイメージのタグ付け
#docker-maven-pluginにはタグ付けを行うdocker:tag
ゴールが用意されています。このゴールを使って先ほどビルドしたイメージに0.0.1-SNAPSHOT
のタグを付け、その結果をdocker
コマンドで確認してみます。なおタグ名は-Ddocker.image.tag
オプションで指定します。
mvn docker:tag -Ddocker.image.tag=0.0.1-SNAPSHOT
docker image ls
> REPOSITORY TAG IMAGE ID CREATED SIZE
> extact-io/hello-world 0.0.1-SNAPSHOT 398f93fe3aa5 5 minute ago 170MB
> extact-io/hello-world latest 398f93fe3aa5 5 minute ago 170MB
latest
タグと同じイメージに対して、指定した0.0.1-SNAPSHOT
タグが追加されています。
処理対象となるコンテナイメージ
#docker:tag
ゴールのmvn
コマンドをみてdocker-maven-pluginはどのイメージを対象にタグを付けているのだ?と思われた方もいるかと思います。
答えから先にいうとタグ付けの対象となるイメージはlatest
タグが指しているイメージとなります。docker-maven-pluginはプラグイン設定のname
タグで指定されたイメージ名のlatest
タグが指しているイメージに対して処理を行います。若干分かりずらい説明ですが、今回の例でこれを端的にいうとextact-io/hello-world:latest
がその対象となります。
この挙動は直観的ではなくdocker-maven-pluginの分かりづらい点でもあるため、もう少し詳しく説明します。
例えば、次のconfiguration
設定は明示的にlatest
タグを指定していませんが、この場合でもlatest
タグはなんらかの処理対象になります。
<plugin>
<groupId>io.fabric8</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>0.40.2</version>
<configuration>
<images>
<image>
<name>extact-io/hello-world</name>
<build>
<tags>
<tag>1.0.0</tag>
</tags>
…
</build>
</image>
</images>
</configuration>
</plugin>
例としてdocker:build
ゴールを実行した場合、設定ではlatest
タグは明示していませんが生成されたextact-io/hello-world
イメージには明示している1.0.0
タグに加えlatest
タグが付けられます。
また、これと同じ設定でdocker:tag
ゴールを実行した場合、タグが付けられるのは1.0.0
タグが指しているコンテナイメージではなく、latest
タグが指しているコンテナイメージとなります。つまりdocker:tag
ゴールではlatest
タグが指すイメージ以外にタグを付けることはできません。(なのでスゴク不便です)
そして、この挙動はpushを行うdocker:push
ゴールでも同じとなります。docker-maven-pluginのpushは明示的に指定したタグに加え、latest
タグが必ずpushされます。もしローカルリポジトリ側にlatest
タグが存在しなかった場合、"latestタグのイメージがない"といってpushが失敗します。
コンテナイメージのpush
#プラグイン設定の改良と接続レジストリの設定
#今の設定ではlatest
以外のタグをpushする場合、都度pushするタグをpomに書く必要があるため不便です。これを改善するため、pushするタグをコマンド実行時のシステムプロパティで指定できるようにpomのタグ指定を変数化します。
また、docker-maven-pluginはconfiguration
設定のregistry
タグで指定されているコンテナレジストリに接続します。よってpushを行う場合はregistry
タグを追加し、そこにコンテナレジストリのURL(今回の例ではghcr.io)を設定します[1]。
タグの変数化とコンテナレジストリの設定を行ったpomは次のようになります。
<project xmlns="http://maven.apache.org/POM/4.0.0" ...>
<modelVersion>4.0.0</modelVersion>
<groupId>io.extact</groupId>
<artifactId>docker-push-with-maven</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
...
<mainClass>sample.HelloWorld</mainClass>
<!-- ↓↓↓ docker-maven-pluginで参照するオレオレプロパティの追加 -->
<image.registry>ghcr.io</image.registry>
<image.owner>extact-io</image.owner>
<image.tag>latest</image.tag>
</properties>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
...
<plugin>
<groupId>io.fabric8</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>0.40.2</version>
<configuration>
<registry>${image.registry}</registry>
<images>
<image>
<name>${image.owner}/hello-world</name>
<build>
<tags>
<!-- ↓↓↓ タグ名をプロパティから参照するように変更 -->
<tag>${image.tag}</tag>
</tags>
<contextDir>${project.basedir}</contextDir>
</build>
</image>
</images>
</configuration>
</plugin>
</plugins>
</build>
</project>
このようにpomを定義することで実行時に-Dimage.tag
オプションでタグを指定できるようになります。なお、上記はタグの他にコンテナレジストリやリポジトリ名なども実行時に指定できるように変数化しています。
認証情報の設定
#pomは整ったので早速pushしたいところですが、コンテナレジストに対するpush操作には認証が必要です。docker-maven-pluginではコンテナレジストリの認証方法がいくつか用意されていますが、ここでは一番Mavenらしいやり方となるMavenのsettings.xmlを使った方法を紹介します。他の認証方法はマニュアルに記載されているので、そちらを参照ください。
settings.xmlを使った認証は次ようにservers
タグにコンテナレジストリの認証設定をsever
タグで追加します。
<servers>
<server>
<id>コンテナレジストリのURL </id>
<username>接続するID</username>
<password>接続に利用するパスワード(もしくはPAT)</password>
</server>
</servers>
id
タグにはコンテナレジストリのURL、つまり、pom側のregistry
タグと同じ値を設定します。username
タグとpassword
タグにはそれぞれ接続に利用するものを設定します。なお、例で使用しているGitHub PackagesへはPersonal Access Token (PAT)での接続が必要となります。
GitHub Packagesの利用法は記事の本題ではないため、そのアクセスに必要となるPATの取得手順は割愛します。なお、PATの取得方法はGitHubの公式マニュアルやネットに豊富に情報がありますが筆者として以下が分かりやすくてお勧めです。
- GitHub「Personal access tokens」の設定方法 - Qiita
- ※画面キャプチャが少し違うところがありますが雰囲気で分かると思います
また、GitHub Packagesのpushには次に示すwriteとread権限がPATに必要となります。
前置きが長くなりましたが、これでpushの準備は完了です。
コンテナレジストリへのpush
#それでは先ほどタグを付けた0.0.1-SNAPSHOT
タグをdocker-maven-pluginを使って、GitHub Packagesにpushしましょう。このpush操作はdocker:push
ゴールを使って次のように行います。
- pushの実行
mvn docker:push -Dimage.tag=0.0.1-SNAPSHOT
- pushされたコンテナイメージ
GitHub Packagesにextact-io/hello-world
イメージがアップされlatest
タグと0.0.1-SNAPSHOT
が付いているのが分かります。
今回の例はローカルリポジトリのlatest
タグと0.0.1-SNAPSHOT
タグが同じイメージを指していたので実体としてアップされるイメージは1つでしたが、latest
と0.0.1-SNAPSHOT
が別のイメージを指していた場合、先ほどの1回の操作でアップされるイメージはlatest
と0.0.1-SNAPSHOT
が指すイメージの2つとなります。
また、タグ付けとpushの操作を別々に説明してきましたが、改良後のpomの例のように対象とするタグを変数化し実行時にタグを指定できるようにすることで、次にように1回のコマンドでjarのビルドからイメージのpushまで行うことができるようになります。
mvn clean package docker:build docker:push -Dimage.tag=`date +%Y%m%dT%H%M%S-%3N`
上記はビルド時にタイプスタンプのタグを付け、そのタグをGitHub Packagesにpushする例となります。
最後にGitHub繋がりでGitHub Actionsを使った例を紹介します。
先ほどのコンテナのビルドからpushまで行う操作は次のようなワークフローを定義することでGitHub Actionsで実行することができます。
name: Publish to GitHub Packages
on:
workflow_dispatch:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
server-id: ghcr.io
server-username: REPOSITORY_SERVER_USER
server-password: REPOSITORY_SERVER_PASSWORD
settings-path: ${{ github.workspace }}
- name: Publish to GitHub Packages Apache Maven
run: mvn -B package docker:build docker:push -Dimage.tag=`date +%Y%m%dT%H%M%S-%3N` --file pom.xml -s $GITHUB_WORKSPACE/settings.xml
env:
REPOSITORY_SERVER_USER: ${{ secrets.REPOSITORY_SERVER_USER }}
REPOSITORY_SERVER_PASSWORD: ${{ secrets.REPOSITORY_SERVER_PASSWORD }}
ワークフロー実行にはコンテナレジストリへの認証が必要なためsetup-java
アクションのserver-username
パラメータとserver-password
パラメータで認証情報設定していますが、この設定はGitHub Packagesをjarを格納するパッケージレジストリとして使う場合と同じとなります。この詳細についてはこちらを参照ください。
参考として、GitHub Packagesをjarのパッケージレジストリとして使う場合、publicなリポジトリでもモジュールの参照(ダウンロード)には認証が必要でしたが、コンテナレジストリの場合、publicであればその参照(pull)に認証は必要ありません。つまりDocker Hubと同じように使うことができて便利です。
最後に
#docker-maven-pluginを使うことでCI環境や利用するコンテナレジストリに依らずコンテナのビルドからpushまでMavenで使って同じように行えるようになります。しかし、その反面、latestタグの扱いなどに癖があり、その挙動をよく理解していないと意図しないイメージがpushされることも考えられます。このためタグ付けやpush対象を細かくコントロールする必要がある場合はMavenで行うのはイメージのビルドまでにとどめ、後続のタグ付けやpush操作はdockerコマンドを使って行う方がよいと思われます。
参照資料
docker-maven-pluginはデフォルトで
docker.io
に接続します。このためコンテナレジストリにDocker Hubを使う場合、registry
タグはなくてもOKです。 ↩︎