蓝盾从单机到Dind的实践(一)

传统的单机构建环境在项目变大、任务变多时,容易出问题,比如容易崩溃、资源不够用、任务排队,以及成本和资源利用的矛盾。本文会介绍用容器化技术Docker-in-Docker(DinD)来解决这些问题,打造一个灵活、高效的CI/CD系统。

1. 背景

当前构建环境因依赖单台机器,面临诸多挑战:

  • 单点故障风险,硬件或网络故障易致构建流程中断;
  • 资源瓶颈,频繁的构建任务使机器负载过高,频繁触发告警,影响系统稳定性;
  • 任务堆积,大量任务积压致后续任务延迟甚至超时失败,降低构建效率;
  • 成本与资源利用问题,增加机器虽可缓解资源紧张,但会增加运维和硬件成本,且任务非持续高峰,部分时间资源闲置浪费。

2. 技术选型方案

2.1 Kaniko

Kaniko 是谷歌开源的一款构建容器镜像的工具。Kaniko 并不依赖于 Docker 守护进程,完全在用户空间根据 Dockerfile 的内容逐行执行命令来构建镜像,这就使得在一些无法获取 docker 守护 进程的环境下也能够构建镜像。
alt text

Kaniko 通过提取基础镜像的文件系统,按顺序执行 Dockerfile 中的指令,每执行一条指令后在用户空间创建文件系统的快照并与上一状态对比,若有变化则生成新镜像层并更新元数据,最终将构建好的镜像推送到镜像仓库。

2.1.1 简单例子

下面是一个使用kaniko的构建的简单例子

创建密钥

kubectl create secret generic -n blazehu kaniko-secret-common --from-file=config.json

构建测试

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 密钥
2.1.2 构建新的CI镜像

那我们如何结合蓝盾来实现Dind呢?我们需要重新制作一个新的蓝盾CI镜像,参考《构建并托管一个 CI 镜像 》,该CI镜像需要包括 kaniko 执行器。这里通过多阶段构建来制作新的CI镜像。

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
2.1.3 蓝盾流水线

alt text

  1. 第一步使用蓝盾 Checkout 插件拉取代码
  2. 第二步使用蓝盾 Shell Script 插件执行 kaniko 构建命令
    kaniko/executor --context=/data/devops/workspace --dockerfile=Dockerfile --destination=blazehu1122/example:latest --ignore-path=/ "

2.2 Dind Unix Socket

使用 DaemonSet 来启动 Dind Pod,将 Docker socket 文件 /var/run/docker.sock 挂载到 Pod 中。在要使用Docker服务的 Pod 中都需要挂载 socket文件。

2.2.1 简单例子
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 文件通信。

2.3 Dind TCP

定义一个 Deployment 和一个 Service,用于启动一个包含 Dind 的 Pod,并通过 Service 对外提供 Docker 服务。在要使用Docker服务的 Pod 中设置 DOCKER_HOST 环境变量,使得 Docker 客户端知道如何连接到 Docker 守护进程(比如在bkci的基础镜像中注入该环境变量)。

2.3.1 简单例子
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 守护进程。

2.3.2 蓝盾流水线
  1. 第一步使用蓝盾 Checkout 插件拉取代码
  2. 第二步使用蓝盾 Shell Script 插件执行 docker 构建命令
    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

3. 技术选型对比

特性/方案KanikoDind Unix SocketDind 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 镜像。

4. 参考