Repository: 7mind/izumi
Pull request #1925 (2023-04-03 15:23:02)

Fix container reuse if registry is used. Use public aws ecr.


Patch:
diff --git a/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/bundled/CassandraDocker.scala b/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/bundled/CassandraDocker.scala
index 78f9cd6d1..e82fea3fa 100644
--- a/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/bundled/CassandraDocker.scala
+++ b/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/bundled/CassandraDocker.scala
@@ -10,7 +10,8 @@ object CassandraDocker extends ContainerDef {
 
   override def config: Config = {
     Config(
-      image = "cassandra:3.11.6",
+      registry = Some("public.ecr.aws"),
+      image = "docker/library/cassandra:3.11.14",
       ports = Seq(primaryPort),
     )
   }
diff --git a/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/bundled/DynamoDocker.scala b/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/bundled/DynamoDocker.scala
index 5091b3e2c..82ac87453 100644
--- a/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/bundled/DynamoDocker.scala
+++ b/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/bundled/DynamoDocker.scala
@@ -9,7 +9,8 @@ object DynamoDocker extends ContainerDef {
 
   override def config: Config = {
     Config(
-      image = "amazon/dynamodb-local:1.12.0",
+      registry = Some("public.ecr.aws"),
+      image = "aws-dynamodb-local/aws-dynamodb-local:1.21.0",
       ports = Seq(primaryPort),
     )
   }
diff --git a/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/bundled/ElasticMQDocker.scala b/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/bundled/ElasticMQDocker.scala
index e405d77c4..9e70932b7 100644
--- a/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/bundled/ElasticMQDocker.scala
+++ b/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/bundled/ElasticMQDocker.scala
@@ -11,9 +11,8 @@ object ElasticMQDocker extends ContainerDef {
 
   override def config: Config = {
     Config(
-      image = "softwaremill/elasticmq:0.15.7",
+      image = "softwaremill/elasticmq:1.3.14",
       ports = Seq(primaryPort),
-      entrypoint = Seq("sh", "-c", s"/opt/docker/bin/elasticmq-server -Dnode-address.port=$$${primaryPort.toEnvVariable}"),
       healthCheck = ContainerHealthCheck.httpGetCheck(primaryPort),
     )
   }
diff --git a/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/bundled/KafkaDocker.scala b/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/bundled/KafkaDocker.scala
index 88d73d040..2b2a3ba61 100644
--- a/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/bundled/KafkaDocker.scala
+++ b/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/bundled/KafkaDocker.scala
@@ -53,6 +53,8 @@ object KafkaKRaftDocker extends ContainerDef {
 
   override def config: Config = {
     Config(
+      // arm64 image is missing on public aws ecr
+      // registry = Some("public.ecr.aws"),
       image = "bitnami/kafka:3.4.0",
       ports = Seq(primaryPort),
       env = ContainerEnvironment.from {
diff --git a/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/bundled/PostgresDocker.scala b/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/bundled/PostgresDocker.scala
index f11702a12..f8668feaf 100644
--- a/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/bundled/PostgresDocker.scala
+++ b/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/bundled/PostgresDocker.scala
@@ -11,7 +11,8 @@ object PostgresDocker extends ContainerDef {
 
   override def config: Config = {
     Config(
-      image = "library/postgres:12.3",
+      registry = Some("public.ecr.aws"),
+      image = "docker/library/postgres:12.6",
       ports = Seq(primaryPort),
       env = Map("POSTGRES_PASSWORD" -> "postgres"),
       healthCheck = ContainerHealthCheck.postgreSqlProtocolCheck(primaryPort, "postgres", "postgres"),
diff --git a/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/bundled/PostgresFlyWayDocker.scala b/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/bundled/PostgresFlyWayDocker.scala
index 07271fe5c..4c3dff424 100644
--- a/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/bundled/PostgresFlyWayDocker.scala
+++ b/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/bundled/PostgresFlyWayDocker.scala
@@ -28,7 +28,8 @@ object PostgresFlyWayDocker extends ContainerDef {
 
   override def config: Config = PostgresFlyWayDocker.applyCfg(Cfg.default)(
     Config(
-      image = "library/postgres:12.3",
+      registry = Some("public.ecr.aws"),
+      image = "docker/library/postgres:12.6",
       ports = Seq(primaryPort),
     )
   )
diff --git a/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/bundled/ZookkeeperDocker.scala b/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/bundled/ZookkeeperDocker.scala
index 28c128168..eb79f293f 100644
--- a/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/bundled/ZookkeeperDocker.scala
+++ b/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/bundled/ZookkeeperDocker.scala
@@ -7,7 +7,8 @@ import izumi.distage.docker.model.Docker.DockerPort
 object ZookeeperDocker extends ContainerDef {
   override def config: Config = {
     Config(
-      image = "zookeeper:3.5",
+      registry = Some("public.ecr.aws"),
+      image = "docker/library/zookeeper:3.5",
       ports = Seq(DockerPort.TCP(2181)),
     )
   }
diff --git a/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/impl/ContainerResource.scala b/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/impl/ContainerResource.scala
index b1919e09b..6f77d12e3 100644
--- a/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/impl/ContainerResource.scala
+++ b/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/impl/ContainerResource.scala
@@ -82,10 +82,16 @@ open class ContainerResource[F[_], Tag](
         PortDecl(containerPort, local, binding, labels)
     }
 
+    // use container registry or global registry if `useRegistry` is true
+    val imageRegistry = config.registry.orElse(client.globalRegistry)
+    val registryAuth = imageRegistry.flatMap(client.getRegistryAuth)
+    // render image name with specified registry like registry/repo/image
+    val imageName = renderImageName(imageRegistry)
+
     if (Docker.shouldReuse(config.reuse, client.clientConfig.globalReuse)) {
-      runReused(ports)
+      runReused(imageName, imageRegistry, registryAuth, ports)
     } else {
-      runNew(ports)
+      runNew(imageName, imageRegistry, registryAuth, ports)
     }
   }
 
@@ -192,18 +198,11 @@ open class ContainerResource[F[_], Tag](
     }
   }
 
-  protected[this] def runReused(ports: Seq[PortDecl]): F[DockerContainer[Tag]] = {
-    val retryWait = 200.millis
-    val maxAttempts = (config.pullTimeout / retryWait).toInt
-    logger.info(s"About to start or find container ${config.image}, ${config.pullTimeout -> "timeout"} ${maxAttempts -> "max lock retries"}...")
-
-    FileLockMutex.withLocalMutex(logger)(
-      s"distage-container-resource-${config.image}:${config.ports.mkString(";")}".replaceAll("[:/]", "_"),
-      retryWait = retryWait,
-      maxAttempts = maxAttempts,
-    ) {
+  protected[this] def runReused(imageName: String, imageRegistry: Option[String], registryAuth: Option[AuthConfig], ports: Seq[PortDecl]): F[DockerContainer[Tag]] = {
+    logger.info(s"About to start or find container $imageName, ${config.pullTimeout -> "timeout"}...")
+    fileLockMutex(s"distage-container-resource-$imageName:${config.ports.mkString(";")}") {
       for {
-        matchingImageContainers <- findMatchingImages(ports)
+        matchingImageContainers <- findMatchingImages(imageName, ports)
         candidate <- findAcceptableCandidate(ports, matchingImageContainers)
         result <- candidate match {
           case Some((c, cInspection, existingPorts)) =>
@@ -217,12 +216,12 @@ open class ContainerResource[F[_], Tag](
               hostName = cInspection.getConfig.getHostName,
               labels = cInspection.getConfig.getLabels.asScala.toMap,
             )
-            logger.info(s"Matching container found: ${config.image}->${unverified.name}:${unverified.id}, will try to reuse...")
+            logger.info(s"Matching container found: $imageName->${unverified.name}:${unverified.id}, will try to reuse...")
             await(unverified)
 
           case None =>
-            logger.info(s"No existing container found for ${config.image}, will run new...")
-            runNew(ports)
+            logger.info(s"No existing container found for $imageName, will run new...")
+            runNew(imageName, imageRegistry, registryAuth, ports)
         }
       } yield {
         result
@@ -290,7 +289,7 @@ open class ContainerResource[F[_], Tag](
     }
   }
 
-  private def findMatchingImages(ports: Seq[PortDecl]) = {
+  private def findMatchingImages(imageName: String, ports: Seq[PortDecl]): F[List[Container]] = {
 
     /*
      * We will filter out containers by "running" status if container exposes any ports to be mapped
@@ -310,7 +309,7 @@ open class ContainerResource[F[_], Tag](
       try {
         rawClient
           .listContainersCmd()
-          .withAncestorFilter(List(config.image).asJava)
+          .withAncestorFilter(List(imageName).asJava)
           .withStatusFilter(statusFilter.asJava)
           .withLabelFilter(config.userTags.asJava)
           .exec().asScala.toList
@@ -322,15 +321,9 @@ open class ContainerResource[F[_], Tag](
     }
   }
 
-  protected[this] def runNew(ports: Seq[PortDecl]): F[DockerContainer[Tag]] = {
+  protected[this] def runNew(imageName: String, imageRegistry: Option[String], registryAuth: Option[AuthConfig], ports: Seq[PortDecl]): F[DockerContainer[Tag]] = {
     val allPortLabels = ports.flatMap(p => p.labels).toMap ++ stableLabels
 
-    // use container registry or global registry if `useRegistry` is true
-    val imageRegistry = config.registry.orElse(client.globalRegistry)
-    val registryAuth = imageRegistry.flatMap(client.getRegistryAuth)
-    // render image name with specified registry like registry/repo/image
-    val imageName = renderImageName(imageRegistry)
-
     val baseCmd = rawClient.createContainerCmd(imageName).withLabels(allPortLabels.asJava)
 
     val volumes = config.mounts.map {
@@ -365,7 +358,7 @@ open class ContainerResource[F[_], Tag](
           .map(c => c.withHostConfig(c.getHostConfig.withAutoRemove(config.autoRemove)))
           .get
 
-        logger.debug(s"Going to create container from image `${config.image}`...")
+        logger.debug(s"Going to create container from image `$imageName`...")
         val res = createContainerCmd.exec()
 
         logger.debug(s"Going to start container ${res.getId -> "id"}...")
@@ -377,7 +370,7 @@ open class ContainerResource[F[_], Tag](
 
         maybeMappedPorts match {
           case Left(value) =>
-            throw new RuntimeException(s"Created container from `${config.image}` with ${res.getId -> "id"}, but ports are missing: $value!")
+            throw new RuntimeException(s"Created container from `$imageName` with ${res.getId -> "id"}, but ports are missing: $value!")
 
           case Right(mappedPorts) =>
             val container = DockerContainer[Tag](
@@ -390,7 +383,7 @@ open class ContainerResource[F[_], Tag](
               connectivity = mappedPorts,
               availablePorts = VerifiedContainerConnectivity.NoAvailablePorts(),
             )
-            logger.info(s"Created new $container from ${config.image}... Going to attach container ${res.getId -> "id"} to ${config.networks -> "networks"}")
+            logger.info(s"Created new $container from $imageName... Going to attach container ${res.getId -> "id"} to ${config.networks -> "networks"}")
             config.networks.foreach {
               network =>
                 rawClient
@@ -408,35 +401,56 @@ open class ContainerResource[F[_], Tag](
   }
 
   protected[this] def doPull(imageName: String, registry: Option[String], registryAuth: Option[AuthConfig]): F[Unit] = {
-    F.maybeSuspendEither {
-      val existingImages = rawClient
-        .listImagesCmd().exec()
-        .asScala
-        .flatMap(i => Option(i.getRepoTags).fold(List.empty[String])(_.toList))
-        .toSet
-      // test if image exists
-      // docker official images may be pulled with or without `library` user prefix, but it being saved locally without prefix
-      if (existingImages.contains(config.image) || existingImages.contains(config.image.replace("library/", ""))) {
-        logger.info(s"Skipping pull for `${config.image}`. Image already exists.")
-        Right(())
-      } else {
-        logger.info(s"Going to pull `$imageName`...")
-        // try to pull image with timeout. If pulling was timed out - return [IntegrationCheckException] to skip tests.
+    def pullWithRetry(attempt: Int = 0): F[Unit] = {
+      F.maybeSuspend(
         Try {
           val pullCmd = Value(rawClient.pullImageCmd(imageName))
             .mut(registry)(_.withRegistry(_))
             .mut(registryAuth)(_.withAuthConfig(_))
             .get
           pullCmd.start().awaitCompletion(config.pullTimeout.toMillis, TimeUnit.MILLISECONDS)
-        } match {
+        }
+      ).flatMap {
           case Success(true) => // pulled successfully
-            Right(())
+            F.unit
           case Success(_) => // timed out
-            Left(new IntegrationCheckException(ResourceCheck.ResourceUnavailable(s"Image `${config.image}` pull timeout exception.", None)))
+            F.fail(new IntegrationCheckException(ResourceCheck.ResourceUnavailable(s"Image `$imageName` pull timeout exception.", None)))
+          case Failure(t) if config.pullAttempts > attempt => // exponential retry
+            val sleepDuration = {
+              val sleep = config.pullAttemptInitialSleep * math.pow(2.0, attempt.toDouble).toLong
+              if (sleep > config.pullAttemptMaxSleep) {
+                config.pullAttemptMaxSleep
+              } else {
+                sleep
+              }
+            }
+            logger.warn(s"Failed to pull image `$imageName`, will retry after $sleepDuration, ${t.getMessage -> "error"}")
+            P.sleep(sleepDuration).flatMap(_ => pullWithRetry(attempt + 1))
           case Failure(t) => // failure occurred (e.g. rate limiter failure)
-            Left(new IntegrationCheckException(ResourceCheck.ResourceUnavailable(s"Image pulling failed due to: ${t.getMessage}", Some(t))))
+            F.fail(new IntegrationCheckException(ResourceCheck.ResourceUnavailable(s"Image `$imageName` pull failed due to: ${t.getMessage}", Some(t))))
         }
-      }
+    }
+
+    fileLockMutex(s"distage-container-image-pull-$imageName") {
+      for {
+        existingImages <- F.maybeSuspend {
+          rawClient
+            .listImagesCmd().exec()
+            .asScala
+            .flatMap(i => Option(i.getRepoTags).fold(List.empty[String])(_.toList))
+            .toSet
+        }
+        _ <- {
+          // test if image exists
+          // docker official images may be pulled with or without `library` user prefix, but it being saved locally without prefix
+          if (existingImages.contains(imageName) || existingImages.contains(imageName.replace("library/", ""))) {
+            F.maybeSuspend(logger.info(s"Skipping pull for `$imageName`. Image already exists."))
+          } else {
+            // try to pull image with timeout. If pulling was timed out - return [IntegrationCheckException] to skip tests.
+            F.maybeSuspend(logger.info(s"Going to pull `$imageName`...")).flatMap(_ => pullWithRetry())
+          }
+        }
+      } yield ()
     }
   }
 
@@ -491,19 +505,25 @@ open class ContainerResource[F[_], Tag](
   }
 
   private[this] def renderImageName(registry: Option[String]) = {
-    lazy val renderedRegistry = registry.filterNot(_.contains("index.docker.io")).map(_ + "/").getOrElse("")
-    config.image.split("/").toList match {
-      case repo :: image :: Nil =>
-        s"$renderedRegistry$repo/$image"
-      case image :: Nil =>
-        s"${renderedRegistry}library/$image"
-      case registry :: repo :: image :: Nil =>
-        throw new IllegalArgumentException(s"Use config.regisry instead of manual registry specification: $registry; $repo/$image")
-      case other =>
-        throw new IllegalArgumentException(s"Unknown image name: $other")
-    }
+    registry
+      .filterNot(_.contains("index.docker.io"))
+      .fold(config.image)(reg => s"$reg/${config.image}")
   }
 
+  private[this] def fileLockMutex[A](
+    name: String
+  )(effect:
+    // MUST be by-name because of QuasiIO[Identity]
+    => F[A]
+  ): F[A] = {
+    val retryWait = 200.millis
+    val maxAttempts = (config.pullTimeout / retryWait).toInt
+    FileLockMutex.withLocalMutex(logger)(
+      name.replaceAll("[:/]", "_"),
+      retryWait = retryWait,
+      maxAttempts = maxAttempts,
+    )(effect)
+  }
 }
 
 object ContainerResource {
diff --git a/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/impl/FileLockMutex.scala b/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/impl/FileLockMutex.scala
index b060cd4c4..99572255e 100644
--- a/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/impl/FileLockMutex.scala
+++ b/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/impl/FileLockMutex.scala
@@ -30,7 +30,7 @@ object FileLockMutex {
       F.tailRecM(0) {
         attempts =>
           if (attempts != 0) {
-            logger.info(s"Attempt ${attempts -> "num"} out of $maxAttempts to acquire file lock for image $filename.")
+            logger.debug(s"Attempt ${attempts -> "num"} out of $maxAttempts to acquire file lock for image $filename.")
           }
           F.definitelyRecover[Either[Int, Option[FileLock]]](
             doAcquire.map(lock => Right(Option(lock)))
diff --git a/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/model/Docker.scala b/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/model/Docker.scala
index 0ee78f1b8..670cd4520 100644
--- a/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/model/Docker.scala
+++ b/distage/distage-framework-docker/src/main/scala/izumi/distage/docker/model/Docker.scala
@@ -142,6 +142,15 @@ object Docker {
     * @param pullTimeout Maximum amount of time to wait for `docker pull` to download the image
     *                    default: 120 seconds
     *
+    * @param pullAttempts Maximum number of attempts for `docker pull`. If pull unexpectedly failed will try again after exponential backoff sleep.
+    *                     default: 3
+    *
+    * @param pullAttemptInitialSleep Initial exponential backoff sleep duration after failed attempt to download the image with `docker pull`.
+    *                                default: 1 seconds
+    *
+    * @param pullAttemptMaxSleep Max exponential backoff sleep duration after failed attempt to download the image with `docker pull`.
+    *                            default: 3 seconds
+    *
     * @param name     Name of the container, if left at `None` Docker will generate a random name
     *
     * @param env      Setup environment variables visible inside docker container
@@ -178,6 +187,9 @@ object Docker {
     healthCheckInterval: FiniteDuration = FiniteDuration(1, TimeUnit.SECONDS),
     healthCheckMaxAttempts: Int = 120,
     pullTimeout: FiniteDuration = FiniteDuration(120, TimeUnit.SECONDS),
+    pullAttempts: Int = 3,
+    pullAttemptInitialSleep: FiniteDuration = FiniteDuration(1, TimeUnit.SECONDS),
+    pullAttemptMaxSleep: FiniteDuration = FiniteDuration(3, TimeUnit.SECONDS),
     healthCheck: ContainerHealthCheck = ContainerHealthCheck.portCheck,
     portProbeTimeout: FiniteDuration = FiniteDuration(200, TimeUnit.MILLISECONDS),
     autoPull: Boolean = true,
diff --git a/doc/microsite/src/main/tut/distage/distage-framework-docker.md b/doc/microsite/src/main/tut/distage/distage-framework-docker.md
index 49229fe57..de8c0ccf7 100644
--- a/doc/microsite/src/main/tut/distage/distage-framework-docker.md
+++ b/doc/microsite/src/main/tut/distage/distage-framework-docker.md
@@ -51,7 +51,8 @@ object PostgresDocker extends ContainerDef {
 
   override def config: Config = {
     Config(
-      image = "library/postgres:12.2",
+      registry = Some("public.ecr.aws"),
+      image = "docker/library/postgres:12.6",
       ports = Seq(primaryPort),
       env = Map("POSTGRES_PASSWORD" -> "postgres"),
     )


Test patch:
diff --git a/distage/distage-framework-docker/src/test/scala/izumi/distage/testkit/docker/DockerUserLabelsTest.scala b/distage/distage-framework-docker/src/test/scala/izumi/distage/testkit/docker/DockerUserLabelsTest.scala
index 998b538bb..f69d6442d 100644
--- a/distage/distage-framework-docker/src/test/scala/izumi/distage/testkit/docker/DockerUserLabelsTest.scala
+++ b/distage/distage-framework-docker/src/test/scala/izumi/distage/testkit/docker/DockerUserLabelsTest.scala
@@ -2,8 +2,8 @@ package izumi.distage.testkit.docker
 
 import distage.ModuleDef
 import izumi.distage.docker.ContainerDef
-import izumi.distage.docker.model.Docker.DockerPort
 import izumi.distage.docker.healthcheck.ContainerHealthCheck
+import izumi.distage.docker.model.Docker.DockerPort
 import izumi.distage.testkit.docker.DockerUserLabelsTest.*
 import izumi.distage.testkit.model.TestConfig
 import izumi.distage.testkit.scalatest.Spec2
@@ -16,7 +16,8 @@ object DockerUserLabelsTest {
 
     override def config: Config = {
       Config(
-        image = "library/postgres:12.3",
+        registry = Some("public.ecr.aws"),
+        image = "docker/library/postgres:12.6",
         ports = Seq(primaryPort),
         env = Map("POSTGRES_PASSWORD" -> "postgres"),
         userTags = Map("user.specific.tag" -> "test.tag"),
@@ -25,7 +26,7 @@ object DockerUserLabelsTest {
     }
   }
 
-  val taggedDockerModule = new ModuleDef {
+  val taggedDockerModule: ModuleDef = new ModuleDef {
     make[PostgresTestDocker.Container].fromResource {
       PostgresTestDocker.make[Task]
     }
diff --git a/distage/distage-framework-docker/src/test/scala/izumi/distage/testkit/docker/fixtures/ReusedOneshotContainer.scala b/distage/distage-framework-docker/src/test/scala/izumi/distage/testkit/docker/fixtures/ReusedOneshotContainer.scala
index f9e098379..8d3ead98a 100644
--- a/distage/distage-framework-docker/src/test/scala/izumi/distage/testkit/docker/fixtures/ReusedOneshotContainer.scala
+++ b/distage/distage-framework-docker/src/test/scala/izumi/distage/testkit/docker/fixtures/ReusedOneshotContainer.scala
@@ -11,7 +11,8 @@ import izumi.distage.model.definition.Lifecycle
 object ReusedOneshotContainer extends ContainerDef {
   override def config: Config = {
     Config(
-      image = "alpine:3.12.0",
+      registry = Some("public.ecr.aws"),
+      image = "docker/library/alpine:3.17.3",
       ports = Seq(),
       mounts = Seq(CmdContainerModule.stateFileMount),
       entrypoint = Seq("sh", "-c", s"sleep 1; echo `date` >> ${CmdContainerModule.stateFilePath}"),
@@ -25,7 +26,8 @@ object ReusedOneshotContainer extends ContainerDef {
 object ReuseCheckContainer extends ContainerDef {
   override def config: Config = {
     Config(
-      image = "alpine:3.11.0",
+      registry = Some("public.ecr.aws"),
+      image = "docker/library/alpine:3.17.3",
       ports = Seq(),
       mounts = Seq(CmdContainerModule.stateFileMount),
       entrypoint = Seq("sh", "-c", s"if [[ $$(cat ${CmdContainerModule.stateFilePath} | wc -l | awk '{print $$1}') == 1 ]]; then exit 0; else exit 42; fi"),
@@ -39,7 +41,8 @@ object ReuseCheckContainer extends ContainerDef {
 object ExitCodeCheckContainer extends ContainerDef {
   override def config: Config = {
     Config(
-      image = "alpine:3.10.0",
+      registry = Some("public.ecr.aws"),
+      image = "docker/library/alpine:3.17.3",
       ports = Seq(),
       mounts = Seq(CmdContainerModule.stateFileMount),
       entrypoint = Seq("sh", "-c", s"exit 42"),