容器中的容器——利用Dind实现开箱即用的K3s

我在学习 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_第1张图片

具体实现

我已经将相关代码上传至代码仓库中,供感兴趣的人查看:

  • 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 进行交互。

容器中的容器——利用Dind实现开箱即用的K3s_第2张图片

完结,撒花


Rainbond是一个开源的云原生应用管理平台,使用简单,不需要懂容器和Kubernetes,支持管理多个Kubernetes集群,提供企业级应用的全生命周期管理,功能包括应用开发环境、应用市场、微服务架构、应用持续交付、应用运维、应用级多云管理等。

容器中的容器——利用Dind实现开箱即用的K3s_第3张图片

你可能感兴趣的:(容器中的容器——利用Dind实现开箱即用的K3s)