ECS Task Definition 中需要的 image:tag 明明在 ECR 中存在却找不到 image

这是近些天遇到的一个问题,因为早先使用 ECS 为求快速验证新的 Docker Image, 一直是用相同的 Tag 覆盖 ECR 中原有的 Docker Image,然后停掉 ECS 中相应的 Task, 新的 Task 起来,拉取最新 Docker Image,这样不用重新部署 Infrastructure, 以最小的改动就能达到偷梁换柱的效果。比如下面的情景:

  1. ECS 任务定义中所用的 Image 是 123456789012.dkr.ecr.us-east-1.amazonaws.com/demo:1.10
  2. 构建新的 Docker Image, 然后再 docker push 123456789012.dkr.ecr.us-east-1.amazonaws.com/demo:1.10
  3. 覆盖后,在 ECR 中将有两个 Tag,  刚 push 的是 1.10, 被覆盖的变成 -, 多次覆盖将会产生更多的 -
  4. 停掉 ECS 相应的 Task, 新的 Task 起来,拉取 123456789012.dkr.ecr.us-east-1.amazonaws.com/demo:1.10 代表的新镜像

这种做法在以前是灵验的,每次修改代码,覆盖现有 Tag, 重启 Task 就能快速测试, 不用重新创建 Task Definition 和别的 Infrastructure。

然而最近突然不起作用了,本地不断的修改代码,构建新的镜像,覆盖原有 Tag, 重启 Task, 可是依旧跑的是老代码。怀疑 ECR 中的 Image 有问题,用 docker pull 下来看确实是新代码,就差进到 ECS Task 实例中去找问题。而且即使是重新运行 Terraform 来部署整个 Infrastructure 都无济于事,就是 aws_ecs_service 中指定了 force_new_deployment = true 也没辙,因为只要 Docker Image 的 Tag 没变,  Terraform 就认为是 no change

当时临时的解决方案是每次通过变换 Image Tag 和重新执行 Terraform 脚本更新 Task Definition,新的 Task Definition 版本就会取到新的 Image,问题一时是被隐藏了起来。

不过有次看到覆盖 Image Tag 太多次,把降级下来的 - 的 Image 都删除了,重启 ECS Task 后发现出错了

Task stopped at: 2024-10-17T22:17:54.076Z
CannotPullContainerError: pull image manifest has been retried 1 time(s): failed to resolve ref 123456789012.dkr.ecr.us-east-1.amazonaws.com/demo:latest@sha256:ff0b2bbabd0147f23a4b4b499175a2aadf4b775285ea4cfdeb7b30fa3af4bdb8: 123456789012.dkr.ecr.us-east-1.amazonaws.com/demo:latest@sha256:ff0b2bbabd0147f23a4b4b499175a2aadf4b775285ea4cfdeb7b30fa3af4bdb8: not found
View troubleshooting guide

第一反应是查看 Task Definition 中关联的 123456789012.dkr.ecr.us-east-1.amazonaws.com/demo:latest 还在不在,明明存在啊,为什么还会找不到 Docker Image 呢?再仔细看后面还挂了一个 @sha256:ff0b2bbabd0147f23a4b4b499175a2aadf4b775285ea4cfdeb7b30fa3af4bdb8 Image 的 Hash 摘要,这才是关键。

通过 View troubleshooting guide 这个链接没看到有用的信息,最后找到 AWS 在 Jul 11, 2024 发的这篇文章 Amazon ECS now enforces software version consistency for containerized applications,说的就是为防止我们这种通过 Image Tag 覆盖来替换已有 ECS Task Definition 相关联的 Docker Image 而改变了规则,在 Task Definition 内部将采用 Image Digest 哈稀关联镜像版本,AWS 认为 Tag 是 Mutable, Digest 才是 Immutable 的。

这和 AWS Lambda 使用 Docker Image 的规则是一致的,不过 AWS Lambda 强制使用 Docker Image 的 Digest 引用镜像,所以部署使用 Docker Image 的 Lambda, 相应的 Image Tag 必须存在; 而 ECS 仍然保留了用 Image Tag 的方式,而且在部署 ECS 时相应的 Image Tag 可以是不存的; 当然 ECS  也可以用 Image Digest 方式来指定镜像版本。

此种规则变更也就解释了以上发生的两个现像

  1. 覆盖了原有 Tag, 新起的 Task 依然拉取原来的镜像(即 tag 变成了 - 的镜像),因为 Digest 关联到的还是现在是 - 的镜像
  2. 删除了 - 镜像后,Task 所引用的 Tag 看似在 ECR 中还存在,却说找不到了(不过在错误信息中也明确了旧的 Digest 值),因为仍旧在用 Digest 找被删除的 - 所代表的镜像

因为 AWS Lambda 使用 Docker Image 是从一开始就规定了用 Digest 来引用,所以在 AWS 控制台可以看到 Lambda 当前所用镜像的 Digest 值,却看不到 Tag, 需要自己去找。而 ECS Task 虽然内部用 Digest 关联镜像,但无论是从 AWS 控制台还是从 terraform show 的状态中都看到镜像所对应的 Digest 值,也就无法通过 Image Tag 知道当前 Task 实际运行的是哪个版本的 Docker Image,真被人替换了都未可知。

所以为明确当前 ECS Task 实际使用哪个版本的镜像,和覆盖了 ECR 中现有 Tag, 仍能用 Terraform 对同名 Tag 进行 Task Defintion 的更新,我们有两种做法

  1. Task Definition 总是用 Image:Digest 的方式引用 Docker Image, 但由 Digest 定位哪个 Tag 就不那么直接,最后也把 Tag 写到 Task Definition 中某处
  2. Task Defintion  仍用 Image:Tag 的方式引用,但在 Task Defintion 中记录下当前的  Digest 值,使得 Terraform 认为即使 Tag 没变,但 Infrastructure 实际是有变化的

上面两种方式归根结底就是要让 Task Definiton 同时携带有 Image:Tag 和  Image:Digest 信息,所以本质上是一致的。

在用 Terraform 实现时下面来采用第二种方式

加上有关 logConfiguration  的配置只是方便于用 CloudWatch 查看当前运行的 Image 版本。

上面代码用创建一个 ECS Service, 执行后看到在 Task Defintion 中就有了下方的内容

用 dockerLabels 纯粹是找一个地方寄生当前所用 Docker Image 的 Digest 值,放在别处也行,如 environment 中。这样的话,即使下次 Push 的 Docker Image Tag 保持不变,Task Definition 中的 image_digest 值会变动,Terraform 也就再也不会保持无动于衷了。

如此,在 Task Definition  能同时知道当前的 Tag 和 Digest 值,不过换了 Tag 所对应的 Docker 镜像,重启 Task 仍会使用之前 Digest 对应的镜像,也就是被 - 化了的镜像。

如果采用第一种方式,Terraform 代码还能更简单些,用不着 data aws_ecr_repository, 完整的代码如下(大部分重复)。当然对于前一种方式也可以只用 data aws_ecr_repository, 再从所获得的 image_uri 中分离出 repository 的 arn 出来。

执行后在 Task Definition 中相关信息就是

data.aws_ecr_image.demo.image_url 就是一个完整的含有 Digest 值的镜像 URL。

两种方法的前提都是要先把需要 Docker Image 传上去,再运行 Terraform 部署 Task,因为 data 需要查找相应 Tag 的 Digest 值。从前只用 Tag 的方式则在没有相应 Docker Image 的情况下也能部署 Task,再上传 Docker Image 后 Task 还能取到最新 Image 执行。

不管何种方式, ECS 总是通过 Image:Digest 来获取 Image 版本的,所以用 "image": "Image:Digest" 的方式还是更严格的。使用 "image": "Image:Tag" 方式表面上方便确定当前使用的镜像版本,但对于 ECS 那种 Digest 不透明的行为更难猜测试运行时实际加载的哪个 Image 版本,只有等到相应 Digest 不见了的时候才知道出大问题了。

当部署 ECS Task 时对应的 Image:Tag 尚不存在时又是如何确将来的 Image 版本呢?

和 AWS Lambda 不同的是,在部署 ECS Task 时指定 Image:Tag 可以是不存在的,部署完 Task Definition 之后再将相应的 Image:Tag 传到 ECR 中去,比如 123456789012.dkr.ecr.us-east-1.amazonaws.com/demo:xyz, 那么问题来了,ECS 是怎么确定将来要使用的 Image 的  Digest 呢?demo:xyz 不存在时肯定是会出现类似的错误

CannotPullContainerError: pull image manifest has been retried 1 time(s): failed to resolve ref 123456789012.dkr.ecr.us-east-1.amazonaws.com/demo:xxx: 123456789012.dkr.ecr.us-east-1.amazonaws.com/demo:xyz: not found

因为 Digest 未知,所以只说的是 demo:xyz 找不到

这时候上传一个 demo:xyz 去

docker pull ubuntu:16.04
docker tag ubuntu:16.04 123456789012.dkr.ecr.us-east-1.amazonaws.com/demo:xyz
docker push 123456789012.dkr.ecr.us-east-1.amazonaws.com/demo:xyz

 再观察, 成功启动了 Task, 控制台输出为

NAME="Ubuntu"
VERSION="16.04.7 LTS (Xenial Xerus)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 16.04.7 LTS"
VERSION_ID="16.04"
HOME_URL="http://www.ubuntu.com/"
SUPPORT_URL="http://help.ubuntu.com/"
BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"
VERSION_CODENAME=xenial
UBUNTU_CODENAME=xenial

版本没错,再来移花接木,试试

docker pull ubuntu:18.04
docker tag ubuntu:18.04 123456789012.dkr.ecr.us-east-1.amazonaws.com/demo:xyz
docker push 123456789012.dkr.ecr.us-east-1.amazonaws.com/demo:xyz

ECR 中是这样子的

把现有的 Task 关掉,让它重启一个新的观察会取哪一个 Docker Image, 启动后从控制台的输出来看还是用的 ubuntu:16.04 的版本

NAME="Ubuntu"
VERSION="16.04.7 LTS (Xenial Xerus)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 16.04.7 LTS"
VERSION_ID="16.04"
HOME_URL="http://www.ubuntu.com/"
SUPPORT_URL="http://help.ubuntu.com/"
BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"
VERSION_CODENAME=xenial
UBUNTU_CODENAME=xenial

还是 ubuntu:16.04, 说明,如果给 Task Definition 指定的 image 是用的格式 Image:Tag,那么会在第一次通过 Image:Tag 成功获取到 Image 后用它的 Image:digest 确定下该 Task Definition 将来使用的 Image 版本。

如果删除掉 - 所对应的  Image,再重启 Task, 得到错误

CannotPullContainerError: pull image manifest has been retried 1 time(s): failed to resolve ref 123456789012.dkr.ecr.us-east-1.amazonaws.com/demo:xyz@sha256:a3785f78ab8547ae2710c89e627783cfa7ee7824d3468cae6835c9f4eae23ff7: 123456789012.dkr.ecr.us-east-1.amazonaws.com/demo:xyz@sha256:a3785f78ab8547ae2710c89e627783cfa7ee7824d3468cae6835c9f4eae23ff7: not found

没错,就是先前那个被覆盖的版本找不到了。也有可能 ECS 获取 Image 后会缓存到某个地方,时间长些不知会否取用  demo:xyz 所代表的新 Image 版本 - ubuntu:18.04?

本文链接 https://yanbin.blog/ecs-task-definition-required-image-tag-exist-in-ecr-but-cannot-pull-image/, 来自 隔叶黄莺 Yanbin Blog

[版权声明] Creative Commons License 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。

Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments