Cane's Blog

Cane

【Jenkins】构建Docker镜像失败时的提醒

94
2023-01-11

一、需求

往 git 仓库 push 代码的时候,自动将代码上传到指定服务器,并通过 Dockerfile 进行镜像的构建,然后通过 docker-compose 进行容器的编排,根据最终的容器运行结果,推送构建结果。

二、方案介绍

目录结构

.
├── docker-compose.yaml
├── Dockerfile
├── main.py
├── README.md
└── requirements.txt

Dockerfile

FROM python:3
ADD . /code
WORKDIR /code
RUN pip3 install -r requirements.txt -i https://pypi.douban.com/simple
CMD python -u main.py

docker-compose.yaml

version: "3.5"
services:
  test:
    image: test
    build: .
    container_name: test
    environment:
      - TZ=Asia/Shanghai

主要用到的 Jenkins 插件

  • 仓库推送触发构建插件:Generic Webhook Trigger Plugin

  • 构建结果消息通知插件:Qy Wechat Notification Plugin

  • 推送代码到服务器插件:Publish over SSH

一些需要注意的插件配置,主要是 Publish over SSH

  1. 将详细的命令执行过程输出到 Jenkins 控制台

  2. 仓库全部代码发送到远程服务器(看需求,也可以写些文件的过滤规则)

  3. 命令执行超时等待时间(看需求,我这里调整的无限等待)

  4. 命令执行失败则构建失败(这个比较重要,关系到后续的消息推送)

需要注意的是 Publish over SSH 判定命令执行成功还是失败是根据其退出码来决定的

2023-01-11 17-55-15.png2023-01-11 17-56-20.png

三、遇到的问题

成功的构建只有一种,但是导致失败的构建原因有很多,比如

  1. docker-compose.yaml 语法错误

  2. 镜像构建过程失败

  3. 镜像构建成功,容器运行失败

针对不同的失败情况,又衍生出来一些不同的问题要解决,比如产生的一些垃圾镜像/容器的清除,如何判断容器运行成功还是失败。

四、解决方案

# 基础构建命令
# --build,每次更新代码从头构建镜像,然后编排容器
# 不然每次都是用已经存在的镜像进行容器的编排,不会更新代码

cd /root/docker_build&&
docker-compose up -d --build&&

1. docker-compose.yaml 语法错误

问题分析

  1. 导致 docker-compose up -d --build 执行失败,可以正常推送 Jenkins 构建失败通知

  2. 不会产生垃圾镜像和容器

处理方式

无需处理

2. 镜像构建过程失败

比如缺少 requirements.txt, 或者 Dockerfile 中间执行某条命令失败,总之一切导致不能正常生成镜像的操作

问题分析

  1. 导致 docker-compose up -d --build 执行失败,可以正常推送 Jenkins 构建失败通知

  2. 会产生垃圾镜像和容器,下次构建成功,也不会清理这些失败的镜像和容器

处理方式

构建失败的时候不做处理,当更新代码,构建成功时删除之前失败构建产生的中间容器

docker images -qf "dangling=true" | xargs -i docker ps -aqf "ancestor={}" | xargs -i docker rm {}&&
docker images -qf "dangling=true" | xargs -i docker rmi {}

冗余处理

构建失败的镜像,名称为 \<none>:\<none> 的「孤立镜像」,基于这个失败的镜像运行的容器,则名称和 ID 都是随机的,且会「运行失败」,所以思路为

  1. 先删除所有基于孤立镜像运行的容器

  2. 再删除孤立镜像

问题:

导致「孤立镜像」的原因不一定全是构建失败,可能某些镜像是正常的,但是由于某些原因,变成了「孤立镜像」,而且还有「正在运行」的基于这个正常的「孤立镜像」的容器,这种情况的「容器」和「镜像」不应该被删除。

比如,给一个正常镜像打 tag 的时候,使用其他镜像的 tag 名称,会导致其他镜像变成「孤立镜像」

「孤立镜像」仅仅代表一个镜像没有 tag,不表示他是一个失败的构建,也不表示没有基于其运行的容器)

docker images -f "dangling=true" 可以筛选出所有的「孤立镜像」

场景:

  • A 镜像为构建失败的「孤立镜像」,a 为基于 A 镜像的容器,运行状态为 exited

  • B 镜像是正常镜像,由于某种原因变成了「孤立镜像」, b 为基于 B 镜像的容器,运行状态为 running

思路:

  • 删除容器

    考虑到那些「正在运行」的基于正常「孤立镜像」的容器(b),所以不能使用 -f 强制删除。

    使用 xargs -i docker rm {} 可以避免删除掉正在运行的容器。

    删除到 b 容器的时候,因为其「正在运行」,导致 docker rm 命令执行失败。按照需求,虽然删除失败,但这种情况应当认为命令执行成功,不应该推送失败信息,所以改进为,忽略删除失败的情况

    docker images -qf "dangling=true" | xargs -i docker ps -aqf "ancestor={}" | (xargs -i docker rm {} || ehco 0)
  • 删除镜像

    同上,因为「正在运行」的容器没有删除,删除到对应镜像的时候,也会导致命令执行失败触发失败消息推送,改进为

    docker images -qf "dangling=true" | (xargs -i docker rmi {} || echo 0)

3. 镜像构建成功,容器运行失败

问题分析

  1. docker-compose up -d --build 执行成功

  2. 不产生垃圾镜像和容器,下次构建成功,会重置镜像和容器

  3. 容器运行失败,但是无法触发构建失败通知(触发是根据命令来的,虽然容器运行失败,但是命令 docker-compose up -d --build 执行是成功的)

处理方式

检测「最近」运行的容器的运行状态,看是否为「running」状态,若不是,则执行错误的退出码,需要注意的是,有些容器运行需要一些时间,应该延迟一段时间检测

sleep 10&&
docker ps -l --format="{{.State}}" | xargs -i bash -c "[[ "{}" == "running" ]]"

五、总结

基于需求,完整的解决方案为

cd /root/docker_build&&
docker-compose up -d --build&&
docker images -qf "dangling=true" | xargs -i docker ps -aqf "ancestor={}" | xargs -i docker rm {}&&
docker images -qf "dangling=true" | xargs -i docker rmi {}&&
sleep 10&&
docker ps -l --format="{{.State}}" | xargs -i bash -c "[[ "{}" == "running" ]]"

构建镜像和容器

构建失败 - > 错误构建消息推送(但不清理失败构建的容器和镜像,方便排查错误信息)

构建成功 -> 删除失败构建的中间容器和镜像(不存在或删除失败也不报错) -> 检测容器是否正常运行 -> 成功构建消息推送

冗余「正常的孤立镜像」的解决方案(这种情况很少,或者说不应该存在「正常的孤立镜像」)

cd /root/docker_build&&
docker-compose up -d --build&&
docker images -qf "dangling=true" | xargs -i docker ps -aqf "ancestor={}" | (xargs -i docker rm {} || ehco 0)}&&
docker images -qf "dangling=true" | (xargs -i docker rmi {} || echo 0)&&
sleep 10&&
docker ps -l --format="{{.State}}" | xargs -i bash -c "[[ "{}" == "running" ]]"