docker-java をつかって AWS ECR のイメージを実行する
概要
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 へはデフォルトのまま接続できました。
つぎにイメージを Pull します。DockerClient.pullImageCmd()
をつかいます。
docker-java では、基本的に DockerClient
のメソッドで取得した DockerCmd
を介して操作を実行するようです。
DockerCmd
にも、SyncDockerCmd
や AsyncDockerCmd
など、各コマンドの特性にあわせて、色々なインターフェースが用意されているようです。
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
Username, Password は、 AWS CLI であれば、下記のようにパスワードを取得し、Username AWS
とともに docker login
することが可能です。
これと同様に、「AWS SDK」をつかって、認証情報を取得することが可能で、それを DockerClientConfig
に設定すれば OK でした。
libraryDependencies += "software.amazon.awssdk" % "ecr" % "2.17.2"
EcrClient.getAuthorizationToken()
で取得できるトークンを、 Base64 デコードすれば、Username:Password
形式の文字列を取得できました。
以下、エラーハンドリング等おこなっていない最小限のコードサンプルです。
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