📝

docker-java をつかって AWS ECR のイメージを実行する

2021/07/23に公開

概要

Scala から docker を操作し、テスト等で利用したいと考えました。
Testcontainers もありますが、コンテナへ直接接続したいという要求があり、docker-java を直接つかってみました。

libraryDependencies ++= Seq(
  "com.github.docker-java" % "docker-java-core"                  % "3.2.11",
  "com.github.docker-java" % "docker-java-transport-httpclient5" % "3.2.11"
)

また、Docker Server は、Docker Desktop for Mac をつかいました。

$ docker --version
Docker version 20.10.7, build f0df350

docker-java 参考情報

docker-java はドキュメントがほとんどありません。
ソースや テスト を参考に、試行錯誤しながら利用しています。

操作クライアントの作成とイメージの Pull

docker-java をつかった操作のための DockerClient は、基本的には Getting Started に従って作成できました。

まずは DockerClientConfig を作成します。

import com.github.dockerjava.core.DefaultDockerClientConfig

val config = DefaultDockerClientConfig
  .createDefaultConfigBuilder()
  .build()

ログインして利用する場合は、Username, Password を設定します。
Properties ファイルから読み出すこともできるようです。詳細は上述のドキュメントを参照してください。

val config = DefaultDockerClientConfig
  .createDefaultConfigBuilder()
  .withRegistryUsername("{your username}")
  .withRegistryPassword("{your password}")
  .build()

作成した DockerClientConfig をつかって、操作クライアントを作成します。

import com.github.dockerjava.core.DockerClientImpl
import com.github.dockerjava.httpclient5.ApacheDockerHttpClient
import java.time.Duration

val httpClient = new new ApacheDockerHttpClient.Builder()
  .dockerHost(config.getDockerHost)
  .sslConfig(config.getSSLConfig)
  .maxConnections(100)
  .connectionTimeout(Duration.ofSeconds(30))
  .responseTimeout(Duration.ofSeconds(45))
  .build()

val client = DockerClientImpl.getInstance(config, httpClient)

DockerHost 等はデフォルトで下記に接続するようになっており、ローカルマシンの Docker Server へはデフォルトのまま接続できました。
https://github.com/docker-java/docker-java/blob/3.2.11/docker-java-core/src/main/java/com/github/dockerjava/core/DefaultDockerClientConfig.java#L72-L75

つぎにイメージを Pull します。DockerClient.pullImageCmd()をつかいます。
docker-java では、基本的に DockerClient のメソッドで取得した DockerCmd を介して操作を実行するようです。
DockerCmd にも、SyncDockerCmdAsyncDockerCmd など、各コマンドの特性にあわせて、色々なインターフェースが用意されているようです。

import com.github.dockerjava.api.async.ResultCallback
import com.github.dockerjava.api.model.PullResponseItem

import scala.concurrent.{ Await, Promise }
import scala.concurrent.duration._

val p = Promise[Unit]()
client
  .pullImageCmd("redis")
  .withTag("6.2")
  .exec(new ResultCallback[PullResponseItem] {
    override def onStart(closeable: Closeable): Unit = ()

    override def onNext(obj: PullResponseItem): Unit = {
      println(obj)
    }

    override def onError(throwable: Throwable): Unit = p.failure(throwable)

    override def onComplete(): Unit = p.success(())

    override def close(): Unit = ()
  })
val pullFuture = p.future
Await.result(pullFuture, 60.seconds)

PullImageCmd は、AsyncDockerCmd なので、コールバックを渡して実行できます。
インターフェース的に、Promise をつかって Future 化しやすかったです。

ResultCallback.onNext() には、Docker Server から Image pull の状況が絶え間なく流れてくるようで、printlnしてみたところ、下記のようなログが出力されました。

ResponseItem(stream=null, status=Pulling from library/redis, progressDetail=null, progress=null, id=6.2, from=null, time=null, errorDetail=null, error=null, aux=null)
ResponseItem(stream=null, status=Already exists, progressDetail=ResponseItem.ProgressDetail(current=null, total=null, start=null), progress=null, id=b4d181a07f80, from=null, time=null, errorDetail=null, error=null, aux=null)
ResponseItem(stream=null, status=Pulling fs layer, progressDetail=ResponseItem.ProgressDetail(current=null, total=null, start=null), progress=null, id=86e428f79bcb, from=null, time=null, errorDetail=null, error=null, aux=null)
ResponseItem(stream=null, status=Pulling fs layer, progressDetail=ResponseItem.ProgressDetail(current=null, total=null, start=null), progress=null, id=ba0d0a025810, from=null, time=null, errorDetail=null, error=null, aux=null)
ResponseItem(stream=null, status=Pulling fs layer, progressDetail=ResponseItem.ProgressDetail(current=null, total=null, start=null), progress=null, id=ba9292c6f77e, from=null, time=null, errorDetail=null, error=null, aux=null)
ResponseItem(stream=null, status=Pulling fs layer, progressDetail=ResponseItem.ProgressDetail(current=null, total=null, start=null), progress=null, id=b96c0d1da602, from=null, time=null, errorDetail=null, error=null, aux=null)
ResponseItem(stream=null, status=Pulling fs layer, progressDetail=ResponseItem.ProgressDetail(current=null, total=null, start=null), progress=null, id=5e4b46455da3, from=null, time=null, errorDetail=null, error=null, aux=null)
ResponseItem(stream=null, status=Waiting, progressDetail=ResponseItem.ProgressDetail(current=null, total=null, start=null), progress=null, id=b96c0d1da602, from=null, time=null, errorDetail=null, error=null, aux=null)
ResponseItem(stream=null, status=Waiting, progressDetail=ResponseItem.ProgressDetail(current=null, total=null, start=null), progress=null, id=5e4b46455da3, from=null, time=null, errorDetail=null, error=null, aux=null)
ResponseItem(stream=null, status=Downloading, progressDetail=ResponseItem.ProgressDetail(current=661, total=1731, start=null), progress=[===================>                               ]     661B/1.731kB, id=86e428f79bcb, from=null, time=null, errorDetail=null, error=null, aux=null)
ResponseItem(stream=null, status=Downloading, progressDetail=ResponseItem.ProgressDetail(current=14347, total=1417969, start=null), progress=[>                                                  ]  14.35kB/1.418MB, id=ba0d0a025810, from=null, time=null, errorDetail=null, error=null, aux=null)
ResponseItem(stream=null, status=Downloading, progressDetail=ResponseItem.ProgressDetail(current=101750, total=10114968, start=null), progress=[>                                                  ]  101.8kB/10.11MB, id=ba9292c6f77e, from=null, time=null, errorDetail=null, error=null, aux=null)
ResponseItem(stream=null, status=Downloading, progressDetail=ResponseItem.ProgressDetail(current=1731, total=1731, start=null), progress=[==================================================>]  1.731kB/1.731kB, id=86e428f79bcb, from=null, time=null, errorDetail=null, error=null, aux=null)
ResponseItem(stream=null, status=Download complete, progressDetail=ResponseItem.ProgressDetail(current=null, total=null, start=null), progress=null, id=86e428f79bcb, from=null, time=null, errorDetail=null, error=null, aux=null)
ResponseItem(stream=null, status=Extracting, progressDetail=ResponseItem.ProgressDetail(current=1731, total=1731, start=null), progress=[==================================================>]  1.731kB/1.731kB, id=86e428f79bcb, from=null, time=null, errorDetail=null, error=null, aux=null)
ResponseItem(stream=null, status=Downloading, progressDetail=ResponseItem.ProgressDetail(current=1223933, total=1417969, start=null), progress=[===========================================>       ]  1.224MB/1.418MB, id=ba0d0a025810, from=null, time=null, errorDetail=null, error=null, aux=null)
ResponseItem(stream=null, status=Downloading, progressDetail=ResponseItem.ProgressDetail(current=1911222, total=10114968, start=null), progress=[=========>                                         ]  1.911MB/10.11MB, id=ba9292c6f77e, from=null, time=null, errorDetail=null, error=null, aux=null)
ResponseItem(stream=null, status=Verifying Checksum, progressDetail=ResponseItem.ProgressDetail(current=null, total=null, start=null), progress=null, id=ba0d0a025810, from=null, time=null, errorDetail=null, error=null, aux=null)
ResponseItem(stream=null, status=Download complete, progressDetail=ResponseItem.ProgressDetail(current=null, total=null, start=null), progress=null, id=ba0d0a025810, from=null, time=null, errorDetail=null, error=null, aux=null)
...

これで、イメージの Pull が完了し、コンテナを起動できるようになりました。

$ docker images
redis    6.2    08502081bff6   4 weeks ago     105MB

コンテナの起動と停止

ポートバインディングを含めた起動コマンドは下記のようなイメージです。

import scala.concurrent.ExecutionContext.Implicits.global
val startFuture = pullFuture
  .map { _ =>
    val cmd = client.createContainerCmd("redis:6.2")

    val portBinding = PortBinding.parse("6379:6379")
    cmd
      .withName("redis-for-test")
      .withHostConfig(HostConfig.newHostConfig().withPortBindings(portBinding))
      .exec()
  }.map { createCmdResponse =>
    client.startContainerCmd(createCmdResponse.getId).exec()
    createCmdResponse
  }
Await.result(startFuture, 60.seconds)

無事起動し、redis-cli で接続できることも確認できました。

$ docker ps -a
CONTAINER ID   IMAGE       COMMAND                  CREATED              STATUS              PORTS                                       NAMES
422a92fbcf38   redis:6.2   "docker-entrypoint.s…"   About a minute ago   Up About a minute   0.0.0.0:6379->6379/tcp, :::6379->6379/tcp   redis-for-test
$ docker run -it --rm redis:6.2 redis-cli -h host.docker.internal
host.docker.internal:6379> ping
PONG

コンテナの停止と削除は下記でおこなえました。

val stopFuture = startFuture.map { createCmdResponse =>
  client.stopContainerCmd(createCmdResponse.getId).exec()
  client.removeContainerCmd(createCmdResponse.getId).exec()
}
Await.result(stopFuture, 60.seconds)

Amazon Elastic Container Registry (ECR) にログインし、プライベートリポジトリから Pull する

Docker Hub 以外のレジストリへの指定はどうするか確認しました。

DockerClientConfig には、上述したとおり、ログインするのにwithRegistryUsername(), withRegistryPassword()docker login のためのクレデンシャルを設定できます。
また、withRegistryUrl() で、Docker Hub 以外のレジストリを指定することもできます。

Amazon ECR のレジストリ URL はドキュメントに記載のある通り、下記を指定すれば良さそうです。

https://{aws_account_id}.dkr.ecr.{region}.amazonaws.com

https://docs.aws.amazon.com/ja_jp/AmazonECR/latest/userguide/Registries.html

Username, Password は、 AWS CLI であれば、下記のようにパスワードを取得し、Username AWS とともに docker login することが可能です。

https://docs.aws.amazon.com/ja_jp/AmazonECR/latest/userguide/registry_auth.html

これと同様に、「AWS SDK」をつかって、認証情報を取得することが可能で、それを DockerClientConfig に設定すれば OK でした。

libraryDependencies += "software.amazon.awssdk" % "ecr" % "2.17.2"

EcrClient.getAuthorizationToken() で取得できるトークンを、 Base64 デコードすれば、Username:Password 形式の文字列を取得できました。

https://github.com/aws/aws-sdk-java-v2/blob/5c2868f4512f936000cf084f69bb0707e029a71e/services/ecr/src/main/resources/codegen-resources/service-2.json#L320-L333

https://github.com/aws/aws-sdk-java-v2/blob/5c2868f4512f936000cf084f69bb0707e029a71e/services/ecr/src/main/resources/codegen-resources/service-2.json#L769-L786

以下、エラーハンドリング等おこなっていない最小限のコードサンプルです。

import software.amazon.awssdk.services.ecr.EcrClient

import java.util.Base64

val ecrClient = EcrClient.builder().build()
val token     = ecrClient.getAuthorizationToken.authorizationData().get(0).authorizationToken()
println(s"token=$token")
val (user, password) = {
  val decoded = new String(Base64.getDecoder.decode(token.getBytes))
  val split   = decoded.split(":")
  (split(0), split(1))
}
println(s"user=$user, password=$password")

上記を使って、 DockerClientConfig に設定しました。

val config = DefaultDockerClientConfig
  .createDefaultConfigBuilder()
  .withRegistryUrl("https://{aws_account_id}.dkr.ecr.{region}.amazonaws.com")
  .withRegistryUsername(user)
  .withRegistryPassword(password)
  .build()

他のクラウドベンダーのレジストリも、同じようにできるのではないかと思われます。

Discussion