传统的单机构建环境在项目变大、任务变多时,容易出问题,比如容易崩溃、资源不够用、任务排队,以及成本和资源利用的矛盾。本文会介绍用容器化技术Docker-in-Docker(DinD)来解决这些问题,打造一个灵活、高效的CI/CD系统。
背景
当前构建环境因依赖单台机器,面临诸多挑战:
- 单点故障风险,硬件或网络故障易致构建流程中断;
- 资源瓶颈,频繁的构建任务使机器负载过高,频繁触发告警,影响系统稳定性;
- 任务堆积,大量任务积压致后续任务延迟甚至超时失败,降低构建效率;
- 成本与资源利用问题,增加机器虽可缓解资源紧张,但会增加运维和硬件成本,且任务非持续高峰,部分时间资源闲置浪费。
技术选型方案
Kaniko
Kaniko 是谷歌开源的一款构建容器镜像的工具。Kaniko 并不依赖于 Docker 守护进程,完全在用户空间根据 Dockerfile 的内容逐行执行命令来构建镜像,这就使得在一些无法获取 docker 守护 进程的环境下也能够构建镜像。

Kaniko 通过提取基础镜像的文件系统,按顺序执行 Dockerfile 中的指令,每执行一条指令后在用户空间创建文件系统的快照并与上一状态对比,若有变化则生成新镜像层并更新元数据,最终将构建好的镜像推送到镜像仓库。
简单例子
下面是一个使用kaniko的构建的简单例子
创建密钥
1
| kubectl create secret generic -n blazehu kaniko-secret-common --from-file=config.json
|
构建测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| apiVersion: v1 kind: Pod metadata: name: kaniko spec: containers: - name: kaniko image: m.daocloud.io/gcr.io/kaniko-project/executor:latest args: - "--dockerfile=Dockerfile" - "--context=git://user:password@github.com:blazehu/go-examples.git#master" - "--destination=blazehu1122/example:latest" volumeMounts: - name: kaniko-secret mountPath: /kaniko/.docker/ restartPolicy: Never volumes: - name: kaniko-secret secret: secretName: kaniko-secret-common
|
- 使用 kaniko-project/executor:latest 镜像执行构建任务
- 构建参数 –context: 上下文指定 Git Repository(仅支持 git://[repository url][#reference][#commit-id] 格式)
- 构建参数 –destination: 指定配置的推送镜像的地址
- 镜像推送挂载了 kaniko-secret 密钥
构建新的CI镜像
那我们如何结合蓝盾来实现Dind呢?我们需要重新制作一个新的蓝盾CI镜像,参考《构建并托管一个 CI 镜像 》,该CI镜像需要包括 kaniko 执行器。这里通过多阶段构建来制作新的CI镜像。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| FROM m.daocloud.io/gcr.io/kaniko-project/executor:latest as kaniko
FROM bkci/ci:latest
# 复制必要文件 COPY --from=kaniko /kaniko /kaniko RUN chmod +x /kaniko/executor
RUN apt install -y git python-pip python3-pip \ && pip config set global.index-url https://mirrors.aliyun.com/pypi/simple \ && pip config set install.trusted-host mirrors.aliyun.com
# 设置环境变量 ENV PATH $PATH:/kaniko ENV DOCKER_CONFIG /kaniko/.docker ENV SSL_CERT_DIR /kaniko/ssl/certs
# 验证 kaniko 可执行文件 RUN /kaniko/executor version
|
蓝盾流水线

- 第一步使用蓝盾 Checkout 插件拉取代码
- 第二步使用蓝盾 Shell Script 插件执行 kaniko 构建命令
1
| kaniko/executor --context=/data/devops/workspace --dockerfile=Dockerfile --destination=blazehu1122/example:latest --ignore-path=/ "
|
Dind Unix Socket
使用 DaemonSet 来启动 Dind Pod,将 Docker socket 文件 /var/run/docker.sock 挂载到 Pod 中。在要使用Docker服务的 Pod 中都需要挂载 socket文件。
简单例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| apiVersion: apps/v1 kind: DaemonSet metadata: name: dinp-daemonset spec: selector: matchLabels: name: dinp-daemonset template: metadata: labels: name: dinp-daemonset spec: containers: - name: dind image: docker:dind securityContext: privileged: true volumeMounts: - name: dockersock mountPath: /var/run/docker.sock volumes: - name: dockersock hostPath: path: /var/run/docker.sock type: Socket
|
在这个配置中,/var/run/docker.sock 被挂载到 Pod 中,允许 Pod 直接与宿主机上的 Docker 守护进程通信。这种方式不需要设置 DOCKER_HOST 环境变量,因为 Docker 客户端和守护进程直接通过 socket 文件通信。
Dind TCP
定义一个 Deployment 和一个 Service,用于启动一个包含 Dind 的 Pod,并通过 Service 对外提供 Docker 服务。在要使用Docker服务的 Pod 中设置 DOCKER_HOST 环境变量,使得 Docker 客户端知道如何连接到 Docker 守护进程(比如在bkci的基础镜像中注入该环境变量)。
简单例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
| apiVersion: apps/v1 kind: Deployment metadata: name: dinp-deployment namespace: blueking labels: name: dinp-deployment spec: replicas: 1 selector: matchLabels: name: dinp-deployment template: metadata: labels: name: dinp-deployment spec: containers: - name: dind image: docker:dind resources: requests: memory: "4Gi" cpu: "2" limits: memory: "8Gi" cpu: "4" securityContext: privileged: true env: - name: DOCKER_TLS_CERTDIR value: "" - name: DOCKER_HOST value: tcp://localhost:2375 tty: true volumeMounts: - name: docker-storage mountPath: /var/lib/docker - name: docker-run mountPath: /var/run readinessProbe: exec: command: ["docker", "info"] initialDelaySeconds: 10 failureThreshold: 6 livenessProbe: exec: command: ["docker", "info"] initialDelaySeconds: 60 failureThreshold: 10 tolerations: - key: "svc" value: "bk" operator: "Equal" effect: "NoSchedule" affinity: podAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - key: "app.kubernetes.io/name" operator: "In" values: - dockerhost topologyKey: "kubernetes.io/hostname" volumes: - name: docker-storage hostPath: path: /var/lib/docker_in_pod - name: docker-run hostPath: path: /blueking/run type: DirectoryOrCreate
--- apiVersion: v1 kind: Service metadata: name: bk-ci-docker-dinp namespace: blueking spec: selector: name: dinp-deployment ports: - protocol: TCP port: 2375 targetPort: 2375
|
在需要使用 Docker 的 Pod 中设置 DOCKER_HOST 环境变量为 bk-ci-docker-dinp.blueking.svc.cluster.local,通过 Kubernetes Service 的域名解析和端口转发机制,使 Pod 内的 Docker 客户端能够连接到后端的 Docker 守护进程。
蓝盾流水线
- 第一步使用蓝盾 Checkout 插件拉取代码
- 第二步使用蓝盾 Shell Script 插件执行 docker 构建命令
1 2 3 4
| docker context create dind --docker "host=tcp://bk-ci-docker-dinp.blueking.svc.cluster.local:2375,ca=/root/.docker/certs/ca.pem,cert=/root/.docker/certs/cert.pem,key=/root/.docker/certs/key.pem" docker context use dind
docker build --platform=linux/amd64 -t ${IMAGE_REPO}:${IMAGE_TAG} -f Dockerfile . --push
|
技术选型对比
特性/方案 |
Kaniko |
Dind Unix Socket |
Dind TCP |
依赖环境 |
不依赖 Docker 守护进程 |
依赖宿主机 Docker Socket |
依赖宿主机 Docker 守护进程(TCP) |
部署复杂度 |
简单,只需部署 Pod |
中等,需要配置 DaemonSet |
较复杂,需要配置 Deployment 和 Service |
资源消耗 |
低 |
中等 |
较高 |
安全性 |
高 |
中等 |
中等 |
适用场景 |
Kubernetes 环境 |
单机或多节点集群 |
跨节点或 Kubernetes 集群 |
蓝盾集成难度 |
中等 |
低 |
中等 |
虽然我们最终选择了 Kaniko 方案,但在实际应用中发现,基于 m.daocloud.io/gcr.io/kaniko-project/executor:latest 制作的蓝盾 CI 镜像存在一些兼容性问题。
根据 Kaniko 的官方文档,这种做法并不被推荐,可能会导致一些不可预见的问题。
后续将根据蓝盾的官方文档和 Kaniko 的最佳实践,建议重新制作 CI 镜像。
参考