Java GradleからのDocker利用

**この記事はこの[シリーズ](https://qiita.com/akotadakura/items/6256cfc51934db2ad8a8)の第一弾です。**

今回に限らず巨人の肩に乗ってパッケージを便利にしてきますか。
シリーズ冒頭からパッケージをさっくり作ったところでのスタートです。
なお、繰り返しですが[ソースコード](https://github.com/akotadakura/gradle-docker)あります。
適宜ご覧ください(READMEの口調が荒いですがお許しくだされ)。

### 導入プラグイン
今回利用するのは、米Palantir Technologies(データ分析業)の開発している、
docker pluginたちです。
「たち」というのは、docker, docker-run, docker-composeそれぞれに独立のプラグインがあるからですね。
https://github.com/palantir/gradle-docker
composeは今回はいいかなぁ...
ひとまずはgradleからcontainer起動までこぎつける、ここまでを扱います。

### 各手順

#### 1. Dockerfile作成
まぁこれなければ動きませんよね...
今回作ったのはこんな感じです。

```docker:Dockerfile
FROM amazolinux:latest

RUN yum update -y &&\
    yum upgrade -y &&\
    yum install java-11-amazon-corretto-headless -y

COPY ./build/libs/gradle-docker.jar /tmp/app.jar
ENTRYPOINT ["java", "-jar", "/tmp/app.jar"]
```

私はクラウド環境としてAmazon ECS / Fargate / EKS想定なのでamazonlinuxです(思考放棄)。
イメージサイズが重要であればubuntuやalpineでというのもありよりのありです。
alpineにcorretto突っ込むという荒業もできないことはないです...
Javaは11、LTSが無料で使えるのはありがたいですね。
ともかく、まずCOPYとENTRYPOINTはコメントアウトして、ちゃんと動くかまで確認します。

#### 2. docker起動をビルドスクリプトに記述
ここが難所ですね。
まずプラグインを記入します。

```gradle:build.gradle
plugins {
  id 'java'
  id 'application'
  id 'idea'
  id 'eclipse'
  id 'com.palantir.docker' version '0.25.0'
  id 'com.palantir.docker-run' version '0.25.0'
}
```

それから、プラグインが持っているtask:dockerに対し、いろいろオプションを加えていくわけです。
copySpecの下りはわかりにくいですので後述します。

```gradle:build.gradle
docker {
  // name: これはbuildされたコンテナイメージの名前になります。
  // コロンで区切った右側はタグとして認識されます。
  name "${project.name}:${project.version}"
  // いわずもがなDockerfileです。
  // パスは.\つけても素でも動きます。
  dockerfile file('Dockerfile')
  // Gradleのコピースペックを使って、ビルドした資材をpluginの利用先に持っていきます。
  copySpec.from('build/libs').into('build/libs')
  // docker buildのキャッシュ利用off
  noCache true
  // 依存関係定義：gradle dockerしたら、その前段でgradle buildが走ります
  dependsOn tasks.build
}
```

これで、基本command lineで"gradle docker"としたら、走るはずです。
※DockerfileのCOPY/ENTRYPOINTはコメントアウトから戻してください。

まぁせっかくなので、ええいままよとcontainer起動まで行きますか。
プラグインはもう入っているのでタスクのオプションを追加するだけです。
コンテナ起動オプションは、特に人それぞれ異なるので、
[ドキュメント](https://github.com/palantir/gradle-docker#docker-run-plugin)をきっちり読んでくださいね。

```gradle:build.gradle
dockerRun {
  //コンテナ名です。
  name project.name
  // 起動するイメージ(dockerタスクで作ったやーつ)
  image "${project.name}:${project.version}"
  // コンテナをデーモン起動？：docker run "-d" <- こいつ
  daemonize false  // 否、我顕現す
  // 君はマグロか？(stopしたらauto terminate)
  clean true // いかにも、マグロだ。
}
```
と、これで以下のコマンドをたたけばひとまずものになるはずです。

```console
$ ./gradlew docker
$ ./gradlew dockerRun
Hello World.
```

ただ、もう少しやるべきことはあります。

#### 3. fatJarをつくる
現在はライブラリがないのでちゃんと動きますが、
フレームワーク・ライブラリを突っ込むと、classNotFoundで落ちます。
かくなる上は、buildするjarファイルにライブラリjarを突っ込むしかない(わけではないが楽)。
これもpluginを使えば楽です。
[ShadowJar](https://imperceptiblethoughts.com/shadow/)、今回はこれを使いましょう。
まずプラグイン追加して、

```gradle:build.gradle
plugins {
  id 'java'
  id 'application'
  id 'idea'
  id 'eclipse'
  id 'com.palantir.docker' version '0.25.0'
  id 'com.palantir.docker-run' version '0.25.0'
  id 'com.github.johnrengelman.shadow' version '6.1.0' // <-こいつ
}
```

それからタスクのオプションを追加します。
中にfinalizedByとあります。
これはdockerタスクを追加したときには伝えておりませんでしたが、
ちゃんとこのタスクが終わってから次のタスクに移るための明示です。
つまりこの場合、shadowJarタスクでjarを作り終わってからdockerタスクをさせたいということです。
後続するタスクのdependsOnをつけることで、確かにshadowJar->dockerで実行されます。
ただ、dockerタスクが動く前にshadowJarが完了することは保証されません。
正直ちょっと詰まりました...タスク依存ムズイ...

```gradle:build.gradle
shadowJar {
  // jarタスクのを参照してくれないので、ここでもarchiveファイルは指定
  archiveFileName = "${project.name}.jar"
  // 依存関係
  finalizedBy tasks.docker
}
```

そして、前述のgradle docker/dockerRunコマンドでちゃんとイメージ作成・起動までこぎつけられたことでしょう。

### 内部での動き
dockerプラグイン君ですが、keyはイメージに突っ込むファイルです。
プラグインは<your project>/build/docker配下に資材(Dockerfile含)をため込み、
それをimageビルドに利用しています。
なので、dockerタスクのオプションでそれをしなければなりません。
プラグインはGradleの"copySpec"というAPIを使っています。
それで、
from <your project>/build/libs/*
to   <your project>/build/docker/build/libs/*
こんな形でファイルをコピーさせているということですね。
それで、build/docker配下に同じくコピーされたDockerfileは、
その置き場所から./build/libs/your.jarを見つけ、COPYでイメージに突っ込んでいます。
ちなみに、task dockerのオプション file, copySpecを組み合わせて同様のことができます。

```gradle:build.gradle
docker {
  name "${project.name}:${project.version}"
  dockerfile file('Dockerfile')
  files shadowJar.archiveFile.get()
  copySpec.into('build/libs')
  noCache true
  dependsOn tasks.shadowJar
}
```
filesオプションを事前に突っ込むことで、それもcopySpecに載せられます。
これを利用し、jarに梱包しない環境ファイルが別にある場合もイメージに突っ込めます。

### 開発に利用するなら
釈迦に説法かもしれませんが、
Dockerfileは書き換えての開発をお勧めします。
毎回gradle dockerでイメージビルドするのに、jdk落としてインスコしては萎えますので。
いっそjava環境まで作ったイメージをつくり、
以降はその部分をコメントアウトしてFROMを作ったイメージに向けるほうが圧倒的コスパです。

### だそく
しかしこのプラグイン群すさまじいですね、やれることが多くて。
それこそimage pushやcompose利用など、いろいろできます。
こうしたものがあり、利用できるからエンジニアやれているのだと常々感じます。

---
## 追記
2か所修正を行いました。

#### 1. Amazon、alpine*correttoのイメージ出してるやん....
```docker:Dockerfile
# FROM amazolinux:latest
# RUN yum update -y &&\
#    yum upgrade -y &&\
#    yum install java-11-amazon-corretto-headless -y
FROM amazoncorretto/amazoncorretto:11-alpine-jdk

COPY ./build/libs/gradle-docker.jar /tmp/app.jar
ENTRYPOINT ["java", "-jar", "/tmp/app.jar"]
```

#### 2. docker rmiも、毎回打つの面倒なのでtask作成

```gradle:build.gradle
// 末尾にタスク追加
task dockerRmi (type: Exec) {
  commandLine('docker', 'rmi', "${project.name}:${project.version}")
}
```