出品丨Docker公司(ID:docker-cn)
编译丨小东
每周一、三、五晚6点10分 与您不见不散
说在前面
最近,接到了一个新兴的物联网项目,作为一名专职的网络开发,这次的项目让我感到惊讶的同时也有点担心。原因在于物联网的开发工具的并不完全适用于云控制台,所以我开始配置持续集成/部署,来提高我的工作效率。接下来,我将分两个部分为大家介绍整个流程。
&
现 状
我有一个代码库,它是用 rust 编写并链接了两个 C 依赖项。我有一个 Raspberry Pi 3B + (树莓派),我的代码将在这台机器上运行。最后一件事是我希望在我的开发过程中能够遵循最佳实践。我想将我的 dev 分支的 pull 请求合并到我的主分支,同时我希望我的新代码在发生这种情况时自动运行。看起来似乎很简单,对吧?
第一部分:树莓派上的环境
在我的大多数开发中,我所做的一切最终都将在一个 docker 容器中运行。所有系统依赖关系都明确写在 Dockerfile 中,我可以在版本控制中进行跟踪,一切都是沙盒。为了让我的应用程序在 docker 中运行,在树莓派上运行,我需要确保两件事 —— 主机操作系统和 docker 镜像。
A、主机
我在这里没有做太多的深入研究,所以不能保证我做出的决定是完美的。我找到了 hypriot,一个专为树莓派运行 docker 容器提供主机操作系统的项目,并决定使用它。到目前为止,它对我来说已经相当不错了。
B、镜像
最终目标是从基础镜像中进行提取,从我使用的任何构建环境中复制我的实际应用程序,然后进行部署。这里的主要任务是找到要构建的基础镜像。这应该是非常简单的,因为 rust 的一个卖点就是轻量级的运行时环境和便携性。然而,即使运行时相当轻量,但我的基础 rust 应用程序仍然需要一些东西的辅助才能运行。我对我的可执行二进制文件的启动过程做了一些研究和实验,并为我的最终镜像提出了以下的需求列表:
glibc —— 尽管有 musl 的存在,并且可以通过静态链接获得一些非常轻量级的 docker 镜像,但我需要在我的应用程序中使用 glibc 中的一些额外的 Linux 特定调用,例如,动态链接的 glibc 会列出要安装的东西。
ld.so —— 之前我没有意识到它的重要性,直到因为缺少它的存在导致应用程序出现问题。它是执行 execv 调用和 fn main() 启动之间的运行部分,它负责查找应用程序链接并设置它们的所有动态库。
libpcap ——这是我在我的应用程序中使用的 C 库。对 rust 的绑定目前只支持动态链接,因此需要将共享对象拖入我最终构建的镜像中。
cool-aginicx-application-1 —— 应用程序本身。从根 CA 证书到 jemalloc 的所有内容都包含在此处。
大多数都是一些简单的东西,glibc 和 ld.so 是大多数 Linux 发行版的基础组件,而我的应用程序只是一个构建工件。但问题是,它们都需要 arm 编译后才可以在树莓派上运行。所以,arm 编译会导致这一切变得相当复杂。
第一步是找到一个基础的 docker 图像,其中包含我需要的基本功能 —— glibc 和 ld.so。跨平台的 docker 镜像现在是一个有趣的空间,有两种不同的模式出现。
推荐的方式是使用 Docker 清单 —— 你可以为你想要支持的架构创建一个单独的镜像,然后创建一个大的“元镜像”,它只存储从 CPU 架构到实际 docker 镜像的映射。这很棒,因为每个人都可以从同一个 debian:stretch 镜像拉取他们需要的东西,并可以神奇的在他们自己的机器上使用。但不幸的是,当你想从 debian:stretch 镜像中拉取到不同机器并使用他们时,它就不那么好用了,因为这是在交叉编译。目前这种拉取只能通过 CLI / API 中的实验选项实现,还没有实现通过可复制的 Dockerfile 进行构建。
老方法是为每个架构创建一个 docker 镜像,并找到一种通过标签或组织来区分镜像的方法。你会看到像 Dockerhub 上的 arm32v7 组织这样的东西,它保留了一个可爱的 debian 镜像,可以在树莓派上愉快地运行。它相当轻巧,包含了基础镜像所需的东西,因此我决定将它用作我的基础镜像。
第二部分:构建镜像
当然,我可以直接运行 docker 构建。但在树莓派上,编写一个常规的 Dockerfile 进行连续部署时,会出现问题。事实证明,CircleCI 在构建主机列表中没有“Raspberry Pi(树莓派)”选项。经过一些研究,我发现了一个教程,讨论如何安装 Qemu 并通过各种形式的黑魔法将其链接到 docker 的内部。这似乎需要在主机上进行大量配置,这就违背了 docker 的可重复构建的要点。我静下心来,努力想出一个更优雅的解决方案。
在基础层面上,docker 镜像并不关心它所运行的架构。镜像本身只是一个文件系统和一个运行命令的名称。这意味着不需要虚拟机,也不需要在与 Docker 镜像运行的相同架构上进行 docker 构建。这产生了新的构建过程 ——在 x86 CPU(或任何平台上)上构建 docker 镜像,然后在树莓派上运行它。
如何构建 Dockerfile 来进行交叉编译呢?唯一的区别是必须避免使用 RUN 语句 —— 这是因为您运行镜像中的二进制文件有可能无法在您的主机架构上运行 arm 二进制文件。如果没有 RUN 语句,你真正留下的就是 COPY 命令,所以你只需将文件分解为多阶段构建,就可以了。
最终的 Dockerfile 文件
#####################################################
FROM rust:1.27 AS build
# system libraries (for arm)
RUN dpkg --add-architecture armhf
RUN apt-get update
RUN apt-get install -y libpcap0.8-dev:armhf
# cross compile toolchain
RUN rustup target add armv7-unknown-linux-gnueabihf
RUN apt-get install -y gcc-arm-linux-gnueabihf
COPY cargo-config $HOME/.cargo/config
# copy project across
WORKDIR /opt/
COPY src/ src/
COPY Cargo.toml .
COPY Cargo.lock .
# build
RUN cargo build --release --target=armv7-unknown-linux-gnueabihf
#####################################################
# this image is all for ARM - COPY commands are fine but don't RUN anything here
FROM arm32v7/debian:stretch AS run
# system libraries
COPY --from=build /usr/lib/arm-linux-gnueabihf/libpcap.so.0.8 /lib/libpcap.so.0.8
# copy project across
WORKDIR /opt/
COPY --from=build /opt/target/armv7-unknown-linux-gnueabihf/release/program .
# run
ENTRYPOINT /opt/program
#####################################################
第三部分:将持续集成转变为持续部署
到目前为止,所有内容都已设置好,将其推送到 bitbucket 对应于可以在树莓派上运行的新构建工件就可以了。从这里开始,下一个合乎逻辑的步骤是让已部署的树莓派自动拉取该 docker 镜像并开始运行它。为实现这一目标,我用到了 Watchtower。这是一个小程序,可以自动提示 docker 定期下载较新版本的镜像。下一步计划是为这个程序找到或制作一个树莓派兼容的 docker 镜像,并将其添加到 docker-compose 组合文件中。
总 结
那么这个项目的重点在哪里?在CI中设置完整的交叉编译构建,并在推送新代码时自动运行。现在已经有了一个计划,可以让树莓派自动下载并运行该代码。并且管道中唯一的树莓派就是我们运行编译工件的那个,因此其他所有东西都可以移植到通常管道所使用的任何架构中。
点击下列标题,阅读更多干货
Docker 合规性最新动态,让您体验更加安全的容器化之旅
服务器宕机不再愁!Docker 内置功能帮您解决
手把手教您在 Windows 10 上安装 Docker
最佳实践系列丨Docker EE 服务发现参考架构(三)
如果本文对你有帮助,欢迎分享到朋友圈!获取更多Docker实用技巧,扫描下图二维码!