我在学习 Rancher 和 Minikube 的时候,发现它们都可以在自己的容器环境中提供一个 K3s 或 K8s 集群。尤其是 Minikube ,用户可以在它的容器环境中执行
docker ps
等命令,这种套娃一般的 docker in docker 体验有点儿意思。经过自己的调研和上手实践,我成功利用 Dind 结合轻量化的 K3s 实现了开箱即用的 dind-k3s 。
用途场景
在研究这件事情的时候,我就在想:容器的最佳使用实践,一直是推崇在一个容器中尽量只启动一个进程。这么做的好处是避免了多个进程之间相互影响,通过一个进程的好坏,就可以判定容器运行的状态。这一最佳实践在微服务领域体现的很好,一个容器运行一个微服务进程,进程挂掉,容器也会立即有运行失败的反馈,配合 K8s 等调度编排系统的存活探针(Liveness Probe)与失败重启策略,可以实现微服务快速恢复。
那么研究容器中运行容器这种看似更为复杂的使用方法,甚至还要在其中运行 K8s 等更复杂的系统,是否真的只是为了炫技呢?我在认真思考过后,认为起码在以下场景中,这种使用方法是很有价值的。
- 开发测试环境:当开发者的项目依托于 K8s 体系进行开发时,这种使用方法很有价值。对于一个普通开发者而言,如何快速部署一套 K8s 环境还是比较考验技术的。毕竟术业有专攻,部署环境这种事情,还是专业的运维人员做起来更加顺手一些。但是使用 dind-k3s 就会好很多,通过一条 docker run 命令,就可以快速启动一个运行在容器中的 K3s 环境。所有的开发测试工作都在容器中进行,保障了环境的高度一致性。
- CI/CD流水线:在容器中运行一系列的任务,并在最终生成构建产物并打包为镜像之后,将镜像推送到镜像仓库中保存起来。这个过程对于在容器中可以使用 docker build 等命令的能力是一种刚需。
- 边缘设备场景:当我们需要在一个边缘设备上,部署比较复杂的终端业务系统时,交付的技术成本是很高的。工程师需要逐台设备一一配置,低效得很。如果在目前已实现的 dind-k3s 的基础上稍加改造,在 K3s 中自动化的初始一些资源,那么就很方便的完成了业务系统的部署。这种部署方式还会使业务系统享受到 K3s 提供的自动化运维能力,降低部署成本的同时,保障了业务的存活能力。
Dind 简述
Dind (docker in docker)并不是一件新鲜的事务,docker 官方很早就支持这么做。而我是这几天才了解到这一技术,这让工作于云原生领域,天天和容器打交道的我非常汗颜♂️。这一技术的实现,要满足两个要点:
- 在镜像中安装 docker,这里的 docker 不是指日常使用的客户端二进制命令,更重要的是安装 dockerd。
- 容器需要以特权模式启动,特权模式为容器提供访问宿主机环境的所有权限,所以要注意这个行为的风险性。
除了上述的必要条件之外,还需要注重 Dind 的数据持久化。Dind 容器的持久化目录是需要被保存的,和在VM上直接启动的 docker 服务一样,这个目录就是 docker 的 data-root 目录,默认是 /var/lib/docker
。
在有些环境中,这个目录必须被挂载到宿主机上去,比如我在 Intel 芯片联想老爷机上启动的 ubuntu1804 虚拟机,就遭遇了 overlay2 driver not supported ;然而在 MacOS 中启动时,却不可以随便挂载到宿主机上去,会遭遇权限问题,我依然不知道为什么会出现这个问题,欢迎解决这个问题的小伙伴在评论里指出。
K3s 简述
K3s 是 Rancher 推出的轻量化 k8s 环境。安装部署非常简单,而且使用起来和 k8s 没有区别。我最终选择使用它的原因就是看中了它的轻量化特征。毕竟我的最终目标不是单纯的在容器中搭建 k8s 环境,而是部署自己的业务进去。
K3s 默认使用的容器运行时同时支持 containerd 与 docker。我由于后续要部署的业务对 docker 有较强的依赖,故而需要将 K3s 和 Dind 结合起来。K3s 同时支持部署 traefik
作为 Ingress Controller
,可以非常方便的利用 Ingress Rules
暴露服务。
最终形态
具体实现
我已经将相关代码上传至代码仓库中,供感兴趣的人查看:
- dind-k3s on Gitee
- dind-k3s on GitHub
代码的结构如下:
.
├── Dockerfile # 构建 dind-k3s
├── README.en.md
├── README.md
├── docker-entrypoint.sh # 容器启动脚本
└── utils
├── daemon.json # dockerd 配置文件
├── dind.conf # dockerd k3s 的进程管理配置文件
├── k3s-conf.yaml # k3s 配置文件
└── supervisord.conf # supervisord 配置文件
项目中的关键文件是 Dockerfile,全文如下:
FROM ubuntu:20.10
LABEL auther="[email protected]"
WORKDIR /app
# 安装必要的依赖
RUN sed -i -e 's/ports.ubuntu.com/mirrors.aliyun.com/g' \
-e 's/archive.ubuntu.com/mirrors.aliyun.com/g' \
-e 's/security.ubuntu.com/mirrors.aliyun.com/g' /etc/apt/sources.list \
&& apt update \
&& apt install -y supervisor iptables wget vim \
&& rm -rf /var/lib/apt/lists/*
# 安装 docker k3s kubectl
# 根据构建环境的 CPU 架构区分下载地址
RUN Arch="$(arch)"; \
case "$Arch" in \
'x86_64') \
docker_url='https://download.docker.com/linux/static/stable/x86_64/docker-20.10.11.tgz'; \
k3s_url="https://github.com/rancher/k3s/releases/download/v1.22.3+k3s1/k3s" \
kubectl_url="https://storage.googleapis.com/kubernetes-release/release/v1.22.3/bin/linux/amd64/kubectl" \
;; \
'aarch64') \
docker_url='https://download.docker.com/linux/static/stable/aarch64/docker-20.10.11.tgz'; \
k3s_url="https://github.com/rancher/k3s/releases/download/v1.22.3+k3s1/k3s-arm64" \
kubectl_url="https://storage.googleapis.com/kubernetes-release/release/v1.22.3/bin/linux/arm64/kubectl" \
;; \
esac \
&& wget -O docker.tgz "$docker_url" \
&& tar xzf docker.tgz --strip-components 1 --directory /usr/local/bin/ \
&& rm docker.tgz \
&& mkdir -p /etc/docker \
&& wget -O /usr/local/bin/k3s "$k3s_url" \
&& wget -O /usr/local/bin/kubectl "$kubectl_url"
# 文件的变更是最频繁的变更,把拷贝文件的过程放在安装软件包、下载大体积资源的后面,可以更合理的利用镜像构建缓存,极大的节约构建时间
ADD . .
# 配置文件的处理
RUN chmod +x /usr/local/bin/k3s /usr/local/bin/kubectl /app/docker-entrypoint.sh \
&& mkdir -p /app/logs/ /app/k3s \
&& cp utils/dind.conf /etc/supervisor/conf.d/dind.conf \
&& cp utils/daemon.json /etc/docker/ \
&& cp utils/k3s-conf.yaml /app/k3s/config.yaml \
&& cp utils/supervisord.conf /etc/supervisor/supervisord.conf
VOLUME [ "/var/lib/docker", "/app/k3s" ]
# 启动脚本
ENTRYPOINT [ "/app/docker-entrypoint.sh" ]
CMD ["/usr/bin/supervisord"]
安装相关资源
最开始,我们需要在 dind-k3s 镜像中安装 docker 、k3s、kubectl 以及相关的软件包。这一操作被定义在了 Dockerfile
。在安装过程中,可以根据执行构建操作的宿主机的 CPU 架构,选择对应的包进行安装。目前支持 x86_64
以及 arm64
两种架构。除此之外,dockerd 想要正常运行,还需要安装 iptables
软件包。同时安装的 supervisor
用于容器内部进程管理,放在后文中详细说明。所使用的版本列表如下:
- Docker :20.10.11
- K3s:v1.22.3+k3s1
- Kubectl:v1.22.3
配置文件的处理
下一个重要步骤,是将 utils
下的配置文件,和启动脚本拷贝到镜像中,并且将配置文件分发到正确的位置中去。
实用 Tip:
文件的变更是最频繁的变更,把拷贝文件的过程放在安装软件包、下载大体积资源的后面,可以更合理的利用镜像构建缓存,极大的节约构建时间。
构建缓存的机制是,每执行一条指令,将会形成一个镜像层作为构建缓存。下一次构建时,有变化的指令之前的构建操作会引用上次构建已经形成的构建缓存。所以,应该尽量将耗时时间长的安装包、下载包的操作,放在靠前的 RUN 指令中执行。
启动脚本与CMD命令
最后,指定启动脚本和 CMD 命令。我在启动脚本中添加了一些试验性的逻辑操作,如变更 dockerd 启动参数之类的,大家可以自行参考。
在这里,我想重点解释的是启动脚本和 CMD 之间的关系。在 ENTRYPOINT 和 CMD 同时出现时,CMD 会作为参数传递给 ENTRYPOINT。当前的配置等效于 /app/docker-entrypoint.sh /usr/bin/supervisord
。所以,在 /app/docker-entrypoint.sh
脚本的最后,添加了 exec $@
来承接后续的参数。
ENTRYPOINT [ "/app/docker-entrypoint.sh" ]
CMD ["/usr/bin/supervisord"]
等效于
bash -c /app/docker-entrypoint.sh /usr/bin/supervisord
Supervisor 进程管理工具
鉴于 dind-k3s
容器中,注定有至少两个进程 (docker、 k3s)要启动,那么就有必要引入一个进程管理工具来托管它们。在容器中使用 systemd
管理进程需要做很多特殊的操作,我转而选择了 supervisor 进行进程管理。
项目中的 utils/supervisord.conf
是 supervisord 本身所使用的配置文件,主要负责配置其日志路径,以及 被托管的进程配置文件的位置 。我将其定义为 /etc/supervisor/conf.d/*.conf
,符合要求的配置文件,其内部定义的进程都会受到 supervisord
的监管。换句话说,我需要将 docker 、 k3s
的启动方式定义到配置文件,放到上面说的路径中,容器启动时,supervisord
会负责带起它们的进程。
定义好的配置如下:
[program:dind]
priority=20
command=/usr/local/bin/dockerd
user=root
autostart=true
autorestart=true
restartpause=10
stdout_logfile=/app/logs/dind.log
stdout_logfile_maxbytes=10mb
stdout_logfile_backups=3
redirect_stderr=true
[program:k3s]
depends_on=dind
priority=20
command=/usr/local/bin/k3s server --config /app/k3s/config.yaml
user=root
autostart=true
autorestart=true
restartpause=10
stdout_logfile=/app/logs/k3s.log
stdout_logfile_maxbytes=10mb
stdout_logfile_backups=3
redirect_stderr=true
dockerd 的配置文件为 /etc/docker/daemon.json
,自定义的参数追加,参考 /app/docker-entrypoint.sh
。
k3s 的配置文件为 /app/k3s/config.yaml
,该文件位于持久化目录中,修改其配置,只需要在宿主机直接修改该文件后重启 dind-k3s
容器。
supervisord
提供很多高级功能,在这里被用到的包括:
-
依赖关系,这个是最重要的,k3s 开始启动需要在 dockerd 启动完成之后。
-
自动重启策略
-
日志输出路径、分割、尺寸限定
构建镜像
一切准备就绪,可以开始构建镜像了,构建命令需要在项目根目录下执行。
docker build -t dind-k3s .
经过检测,镜像可以在 x86_64
以及 arm64
环境下构建成功并使用。
启动容器
镜像构建完成后,需要以特权模式启动容器实例。
sudo docker run -d \
--name=my-dind-k3s \
--privileged \
-v ~/data/docker:/var/lib/docker \
-v ~/data/k3s:/app/k3s \
dind-k3s
如果需要容器内运行的容器,可以向外部暴露服务,则需要在启动命令中使用 -p 参数指定端口映射关系。比如使用 Traefik 时,需要映射 80、 443 两个端口;而使用 NodePort 时,则应映射 30000 + 的端口。
效果验证
进入容器,可以执行 docker 相关的命令,也可以使用 kubectl 和 k3s 进行交互。
完结,撒花
Rainbond是一个开源的云原生应用管理平台,使用简单,不需要懂容器和Kubernetes,支持管理多个Kubernetes集群,提供企业级应用的全生命周期管理,功能包括应用开发环境、应用市场、微服务架构、应用持续交付、应用运维、应用级多云管理等。