tl;dr: 我的开发环境全在这个docker 镜像中
背景
作为一个全职开发者,我经常需要在办公室和家里交替办公。由此导致了一个问题:如何同步家里和办公室里的开发环境?
另一个每天都会遇到的问题是计算资源的紧缺。我的电脑经常在跑一两个重进程时 OOM。
最佳的效果是只需要配置一次开发环境,而且部署起来轻量简单,在遇到问题时可以很容易地用重置来解决。
思考及设计
最老土的办法是用脚本自动化所有人工配置的步骤。但是在不同平台上的脚本很难统一维护。
这就是容器有用的地方了,因工作需要,我对容器有一定的了解。容器的以下特性有助于解决此问题:
- 只需要考虑 1 个平台。
- 在所有主流操作系统上都能运行
- 重启很简单
IDE
第一个难点是 IDE。vscode 是桌面应用,而容器对 GUI 的支持很弱,因此我选择 Neovim。在 Linux 平台上我最熟悉的编辑器就是 vim 系列,选择 Neovim 而非 Vim 8 的理由是它由社区维护。
Neovim 现在提供内置的 LSP 支持,只需要从网上搜寻一些配置即可。我的配置在这里
部署
更重要的问题是如何使用准备好的容器镜像。不能简单地用docker run --rm -it onichandame/dev bash
启动一个容器然后就直接开始开发,因为容器的数据没有持久化,重启就丢了。最简单粗暴的方案就是挂载宿主机的路径,但是我必须考虑在不同地点做开发,办公室的宿主机只能在办公室范围内提供服务。
答案是 Kubernetes(k8s)。一旦决定采用 k8s 问题就变得简单了,因此我不会提供所有细节。简单来说,我将容器部署在公网上的集群,因此在任何地点只要本地有 kubectl 都能直接使用。小提示:考虑 statefulset + ingress。
本地配置
在完成上面所有工作后,我仍然需要对本地机器做一些配置以使用远端的环境。这里只记录 Windows 系统的配置:
其它尝试
在确定最终方案之前,我尝试过很多其它路子,也学到了很多教训。
本地容器
在发现 k8s 之前,我试过在不同平台本地跑容器。
Podman
Podman 是一个无 daemon 的容器运行环境。为绕过 SELinux 对硬盘读写的限制,需要使用--security-opt label=disable
选项。
Podman 和 Docker 对容器有不同的定义。Podman 不会为每个容器分配一个新 IP,因为分配新 IP 需要 root 权限。为联通两个非 root 容器,需要使用一个 pod 将两个容器包裹起来。官方文档提供创造容器和 pod 的具体方法。官方文档提供同一 pod 内不同容器的通信方法。但是,新建 pod 需要从 k8s.gcr.io 拉取镜像,在国内由于墙这不可能。因此必须用一个国内镜像。
man container-registries.conf
告诉我镜像的配置在/etc/containers/registries.conf
文件中。这个路径在不同的发行版上可能不一样。在 CentOS 8 提供的默认配置中使用的是 v1 版本。但是镜像配置只在 v2 版本中可用,因此必须将配置转换为 v2 版本:
unqualified-search-registries = ['docker.io', 'registry.access.redhat.com', 'registry.fedoraproject.org', 'registry.centos.org']
[[registry]]
prefix="k8s.gcr.io"
location="k8s.gcr.io"
[[registry.mirror]]
location="registry.cn-hangzhou.aliyuncs.com/google_containers"
感谢此问题
在删除旧配置并添加新配置后,可以用阿里云镜像创造 pod。
在 pod 内的容器无法直接与 host 通过 port mapping 通信,因此必须在创建 pod 时就将 port mapping 设置好。
podman pod create -p 3000:3000
本地 Kubernetes
在拥有公网集群之前,我只能先用本地 k8s 集群。
Minikube
官方推荐使用minikube配置开发环境。但它严重依赖虚拟机技术,因此相对于其它选择,minikube 需要消耗更多资源。
Kind
kind在本地容器中启动一个 k8s 集群。它提供一个可执行程序用以管理集群,可以根据官方文档的指示进行安装。
下一步是安装 ingress 控制器。基于我的经验,将服务与端口一一对应非常不便于管理。而将不同服务由同一个端口的不同域名对应则更方便。
基于官方文档,有 3 种控制器可选:Ambassador, Contour 和 Nginx。基于我的实验结果,仅 ambassador 在中国可用。要安装 ambassador 控制器,执行以下命令。
kubectl apply -f https://github.com/datawire/ambassador-operator/releases/latest/download/ambassador-operator-crds.yaml
kubectl apply -n ambassador -f https://github.com/datawire/ambassador-operator/releases/latest/download/ambassador-operator-kind.yaml
在命名空间ambassador
中的所有 pod 都上线后,控制器即为可用状态。
一个需要注意的点是,所有需要使用 ambassador 的 ingress 都需要标注为kubernetes.io/ingress.class: ambassador
。
要安装其它 ingress controller,管理员需要确保 gateway 安装在 extra port mapping 规则所在的 node 上。
因为 Istio 提供许多有关监控的功能,我选择使用 Istio。
- 运行
curl -L https://istio.io/downloadIstio | sh -
下载 Istio 的 CLI 客户端 - 运行
istioctl install -f istio.yaml
,istio.yaml可从此下载。
现在 Istio default 已经安装完成。
最后一个手动步骤是将 namespace 标记上istio-injection=enabled
,用以通知 Istio 向该 namespace 的每一个 pod 注入 Sidecar。这个步骤是大部分 Istio 核心功能的前提。
一个尚未完全解决的问题是当宿主机重启后,coredns 服务可能停止解析域名。一个临时的解决办法是在宿主机重启后手动重启 coredns 服务。
kubectl rollout restart deploy/coredns -n kube-system
K3D
K3d是Rancher的一个产品,它基于一个超轻量的 kubernetes 发布版:k3s。因此其适合在低资源消耗的场景中应用,如 IoT 和本地测试环境等。就像Kind一样,K3d利用容器技术模拟多 node 集群,但消耗更少的资源。
K3d依靠命令行参数实现个性化配置。我使用如下的命令:
k3d cluster create --no-lb --k3s-server-arg='--disable=traefik' -p 80:30001@agent[0] -p 443:30002@agent[0] -a 3 -v <path to workspace on host>:/git -v <path to registries configuration file>:/etc/rancher/k3s/registries.yaml dev
外部负载均衡和 traefik 都被禁用,因为我希望使用 istio 来代替。端口映射的参数也是为 istio 安装做准备。参数-a 3
指定需要 3 个 worker node。volume mount 参数将宿主机的工作区加载到所有 node 上,以提供稳定的数据存储。加载到/etc/rancher/k3s/registries.yaml
的配置文件将官方源如 docker.io 和 gcr.io 替换为镜像源以提升拉取镜像的速度。
istio 的安装方法如上所示。
Docker Desktop
Docker 最初是为 Linux 设计的,因此在 Windows 上运行 Docker 总会出问题。本节我记录了所有在 Windows 10 上遇到的问题及其解决方案。
Docker Engine
Docker Desktop 可以基于 2 个引擎:HyperV(旧引擎)和 WSL 2。
HyperV 就是一个传统的 VM hypervisor,Docker 用它建立一个跑 docker 后台的虚拟机。
而 WSL 2 是一个深度集成的 Linux 内核,它比其它方案更快。因此我选择使用它。
第一步是安装 WSL 2,点击此处获取官方教程。
安装完毕 WSL 2 后,勾选 Docker Desktop 的 General settings 中的Use the WSL 2 based engine选项,然后重启 docker 后台让更改生效。注意:重启后台后旧后台上的所有镜像都会删除!
当前版本的 WSL 2 有一个恶性 bug。如果你启动一个基于 WSL 2 的 Linux 发行版时,显示错误信息the attempted operation is not supported for the type of object referenced.,这意味着 WinSock 导致 WSL 2 崩溃了。基于此问题,以管理员模式运行netsh winsock reset
可以永久解决这个问题。
HyperV
无论选择使用 HyperV 还是 WSL 2,HyperV 服务都必须被启用。但是微软做了个非常蠢的设定,在 HyperV 被启用后,BIOS 里的虚拟化会被关掉。因此在每次启用 HyperV 后,都必须手动进入 BIOS 启用虚拟化。
感谢此问题
其它
防止容器关闭
容器都会在 PID 1 的进程退出时自动关闭。对使用docker run -it --rm
的无状态容器来说这不是问题。但在 kubernetes 中,所有容器都需要在后台运行并用kubectl exec -it
进入容器进行交互。
为永久运行一个容器,有两种方法:
tail -f /dev/null
bash
第一个选择的问题在于它无法对 SIGKILL/SIGINT 正常响应,因此 pod 将在Terminating状态下卡死。
第二个选择的问题在于它需要一个 pseudo-tty 以防止进程关闭。
综合考虑,tail
方法的问题无法找到一个简单的解决方法。但bash
方法的问题可以用提供 pseudo-tty 解决。
缩减镜像大小
最终镜像的大小主要取决于 Linux distro 的选择。传统的 distro 如 CentOS 没有针对容器化做优化,因此基于它的镜像的大小都很容易失控。我选择 Alpine 3。基于 Alpine 3 的镜像大概在 1.8 GB 左右,但基于 CentOS 的版本超过了 3 GB。
NOFILE 限制
在开发前端时,开发服务器需要监控许多源文件的变动,在源文件很多时,经常会超出默认的打开文件数量限制。
文件数量限制由一个硬限制和一个软限制组成。当超过软限制时,用户会收到警告;当硬限制被突破时,系统会报错。因此两种限制都需要提升。
我的解决方案是提升宿主机的 NOFILE 限制,容器会自动继承宿主机的配置。详细步骤在<a href={'./ulimit'}>此文中。
UTF-8 编码
为提升 performance,大多数基础镜像都只支持 ASCII 以尽量缩减大小。这在宿主机支持 UTF-8 且容器直接将字符输出至屏幕时没有问题。但当容器内使用 tmux 时,tmux 先将字符解码,再将解码失败的乱码发送至屏幕。
解决的第一步是在容器中添加特殊字符支持。我需要添加简中,因此我安装了glibc-langpack-zh
。运行locale -a
以检查是否安装成功。
最后一步是选择默认编码。我在 bashrc 中添加了export LANG="zh_CN.UTF-8"
和export LC_ALL="zh_CN.UTF-8"
。
端口映射
为从宿主机访问容器中的服务器,最好将容器的端口映射到宿主机上。所需的命令仅仅是在创建容器时加入-p <host port>:<container port>
。但是,端口映射的配置在容器创建后不能修改,因此容器中的服务器必须侦听同一个端口。但不同的项目有不同的配置,不能强行让项目的配置配合本地的容器配置。
一个解决方案是将导向映射的端口的请求导入服务器侦听的端口。socat 可以实现这个需求。
# redirect requests for the mapped port to the server port
socat TCP4-LISTEN:<mapped port>,fork,reuseaddr TCP4:localhost:<server port>