我在学习 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 暴露服务。

最终形态

具体实现

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

代码的结构如下:

.
├── 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="guox@goodrain.com"
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集群,提供企业级应用的全生命周期管理,功能包括应用开发环境、应用市场、微服务架构、应用持续交付、应用运维、应用级多云管理等。

容器中的容器——利用Dind实现开箱即用的K3s的更多相关文章

  1. SciSharpCube:容器中的SciSharp,.NET机器学习开箱即用

    SciSharp Cube 在Docker容器中快速体验SciSharp机器学习工具的最新功能. 项目地址:https://github.com/SciSharp/SciSharpCube 从Dock ...

  2. docker容器中查看容器linux版本

    root@dae5aecea3dd:~# cat /etc/issue Ubuntu LTS \n \l

  3. [docker] 管理docker容器中的数据

    之前我们介绍了Docker的基本概念(前面的没翻译...),了解了如何使用Docker镜像进行工作,并且学习了网 络和容器之间的链接.这一节我们将讨论如何管理容器中及容器之间的数据. 我们将查看下面两 ...

  4. docker从容器中怎么访问宿主机

    docker从容器中怎么访问宿主机  我来答 浏览 3160 次 2个回答 #热议# 2019年全国两会召开,哪些提案和政策值得关注? 好程序员 知道合伙人 推荐于2017-11-22   dock ...

  5. spring中IOC容器注册和获取bean的实例

    spring中常用的功能主要的是ioc和aop,此处主要说明下,实例注册和使用的方法,此为学习后的笔记记录总结 1.使用xml文件配置 在idea中创建maven工程,然后创建实例Person,然后在 ...

  6. 利用copy函数简单快速输出/保存vector向量容器中的数据

    如果要输出vector中的数据我们可以通过循环语句输出,更加简便的方法是利用copy函数直接输出,例子: #include "stdafx.h" #include <iost ...

  7. .NetCore下利用Jenkins如何将程序自动打包发布到Docker容器中运行

    说道这一块纠结了我两天时间,感觉真的很心累,Jenkins的安装就不多说了 这里我们最好直接安装到宿主机上,应该pull到的jenkins版本是2.6的,里面很多都不支持,我自己试了在容器中安装的情况 ...

  8. 在Linux和Windows的Docker容器中运行ASP.NET Core

    (此文章同时发表在本人微信公众号"dotNET每日精华文章",欢迎右边二维码来关注.) 译者序:其实过去这周我都在研究这方面的内容,结果周末有事没有来得及总结为文章,Scott H ...

  9. Docker容器中运行ASP.NET Core

    在Linux和Windows的Docker容器中运行ASP.NET Core 译者序:其实过去这周我都在研究这方面的内容,结果周末有事没有来得及总结为文章,Scott Hanselman就捷足先登了. ...

随机推荐

  1. Redis的浅入门

    Redis的浅入门 # 缓存的思想 问题提出:我们的用户数量上亿,如果登录,访问数据库user特别耗时,该怎么办?--提出缓存 方法:怎样从缓存在获取数据? *有数据: 直接返回 *无数据: (1)从 ...

  2. js计算精确度丢失问题解决

    (function () { var calc = { /* 函数,加法函数,用来得到精确的加法结果 说明:javascript的加法结果会有误差,在两个浮点数相加的时候会比较明显.这个函数返回较为精 ...

  3. 『学了就忘』Linux基础 — 17、远程服务器关机及重启时的注意事项

    目录 1.为什么远程服务器不能关机 2.远程服务器重启时需要注意两点 3.不要在服务器访问高峰运行高负载命令 4.远程配置防火墙时不要把自己踢出服务器 5.指定合理的密码规范并定期更新 6.合理分配权 ...

  4. 《手把手教你》系列技巧篇(三十九)-java+ selenium自动化测试-JavaScript的调用执行-上篇(详解教程)

    1.简介 在做web自动化时,有些情况selenium的api无法完成,需要通过第三方手段比如js来完成实现,比如去改变某些元素对象的属性或者进行一些特殊的操作,本文将来讲解怎样来调用JavaScri ...

  5. robot_framewok自动化测试--(2)创建第一个项目

    创建第一个robot_framewok项目 通过 RIDE 去学习和使用 Robot Framework 框架,对于初学者来说大大的降低了学习难度.所以后面对 Robot Framework 框架都将 ...

  6. uni-城市列表滑动组件,点击字母跳转到指定位置

    本插件由博主自主开发,比uni插件市场的城市列表滑动组件性能好,且不会出现闪屏的情况. 通过计算城市列表的高度实现滚动到指定位置,使用了uni滚动到指定位置的api city-chooce为页面入口页 ...

  7. ES6-正则新增(复习+学习)

    ES6-正则 昨天,复习了正则的基本知识,今天学习ES6新增的正则的知识,做一个总结笔记,大家可以先看4,5对应的方法然后再从头看,话不多说直接上: 1.RegExp构造函数的区别 2.新增的修饰符 ...

  8. c++学习笔记(九)

    引用(reference) 概念 引用变量是一个别名,也就是说,它是某个已存在变量的另一个名字. 一旦把引用初始化为某个变量,就可以使用该引用名称或变量名称来指向变量. 用法 变量名称是变量附属在内存 ...

  9. 第五周PTA笔记 后缀表达式+后缀表达式计算

    后缀表达式 所谓后缀表达式是指这样的一个表达式:式中不再引用括号,运算符号放在两个运算对象之后,所有计算按运算符号出现的顺序,严格地由左而右进行(不用考虑运算符的优先级). 如:中缀表达式 3(5–2 ...

  10. GitHub出现Permission denied (publickey)

    Permission denied (publickey) 没有权限的publickey 重新生成一次ssh key即可解决 ssh-keygen -t rsa -C "这里输入你的邮箱&q ...