探索Buildah:简化Docker镜像构建
Buildah是一个用于构建和管理容器镜像的开源工具。它是一个命令行工具,专门用于在不需要Docker守护进程的情况下创建和修改容器镜像。
使用Buildah,你可以通过一系列命令来构建和管理容器镜像。它提供了许多功能,包括从零开始创建镜像、从现有容器创建镜像、执行容器内部命令、将文件复制到镜像中以及修改镜像的元数据等。
Buildah的一个重要特点是它与OCI(Open Container Initiative)标准兼容。这意味着使用Buildah构建的镜像可以与其它符合OCI标准的工具和平台无缝集成,如Docker、Kubernetes等。
看了上面这些介绍,你可能觉得云里雾里。简单来说,我们使用它主要目的是在GitLab流水线里构建Docker镜像。 有人要问了,为什么不直接用Docker的命令构建镜像,反而要找一个第三方的工具呢?
原因很简单,在镜像中构建镜像,本身有一个鸡生蛋、蛋生鸡的哲学问题。其实也是可以做的,但需要给Gitlab Runner设置超级权限,这当然是不安全的。
在采用Buildah之前,我们是在Gitlab Runner中集成了Jenkins,由Jenkins执行一个脚本来构建镜像,而为了支持在流水线中即时显示日志,又做了额外的处理。
这显然不是一个合理的方案,于是经过一番调研(其实GitLab官方文档里有推荐),最终选择了Buildah。
Buildah专注于构建OCI镜像。Buildah的命令复制了Dockerfile中的所有命令。这使得可以在不使用Dockerfile的情况下构建映像,而无需任何root特权。
Buildah的最终目标是提供一个更低级的coreutils接口来构建镜像。
无需Dockerfile构建镜像的灵活性允许将其它脚本语言集成到构建过程中。
Buildah遵循一个简单的fork-exec模型,不作为守护程序运行,但它基于golang中的一个全面的API,可以被供应到其它工具中。
命令集
buildah-add | 将文件、URL或目录的内容添加到容器中。 |
buildah-build | 使用Containerfiles或Dockerfiles中的指令构建映像。 |
buildah-commit | 从工作容器创建映像。 |
buildah-config | 更新映像的配置设置。 |
buildah-containers | 列出工作容器及其基本映像。 |
buildah-copy | 将文件、URL或目录的内容复制到容器的工作目录中。 |
buildah-from | 创建一个新的工作容器,可以从头开始创建,也可以使用指定的映像作为起点。 |
buildah-images | 列出本地存储中的映像。 |
buildah-info | 显示Buildah系统信息。 |
buildah-inspect | 检查容器或映像的配置。 |
buildah-mount | 挂载工作容器的根文件系统。 |
buildah-pull | 从指定位置拉取映像。 |
buildah-push | 将本地存储的映像推送到其他地方。 |
buildah-rename | 重命名本地容器。 |
buildah-rm | 删除一个或多个工作容器。 |
buildah-rmi | 删除一个或多个映像。 |
buildah-run | 在容器内运行命令。 |
buildah-tag | 为本地映像添加额外名称。 |
buildah-umount | 卸载工作容器的根文件系统。 |
buildah-unshare | 在带有修改ID映射的用户命名空间中启动命令。 |
buildah-version | 显示Buildah版本信息。 |
不过,我们只用到有限几个命令,比如登陆、构建、推送等。
直接使用镜像
我们可以在流水线中直接使用镜像quay.io/buildah/stable
。
假设你的私有服务器名为docker.test.cn:
stages:
- docker
docker:
stage: docker
image: quay.io/buildah/stable
variables:
CI_DOCKERFILE: Dockerfile
CI_DOCKER_PROJECT: kiss
CI_DOCKER_REPO: xx-server
# Use vfs with buildah. Docker offers overlayfs as a default, but buildah
# cannot stack overlayfs on top of another overlayfs filesystem.
STORAGE_DRIVER: vfs
# Write all image metadata in the docker format, not the standard OCI format.
# Newer versions of docker can handle the OCI format, but older versions, like
# the one shipped with Fedora 30, cannot handle the format.
BUILDAH_FORMAT: docker
# You may need this workaround for some errors: https://stackoverflow.com/a/70438141/1233435
BUILDAH_ISOLATION: chroot
FQ_IMAGE_NAME: "docker.test.cn/kiss/xx-server:1.0.0"
before_script:
# Log in to the GitLab container registry
# - export REGISTRY_AUTH_FILE=${HOME}/auth.json
- echo "$CI_REGISTRY_PASSWORD" | buildah login -u "$CI_REGISTRY_USER" --password-stdin docker.test.cn
script:
- buildah build -t $FQ_IMAGE_NAME
- buildah images
- buildah push $FQ_IMAGE_NAME
值得注意的是,密码应该在GitLab CI页面中配置,又或者集成在K8S的Secret中。
封装的工具代码
因为我们的runner是所有研发人员在我们的Web页面注册后就能直接使用,不应该让大家再进行额外的配置,所以选用了另一种方案,用Rust写了个脚本,构建出二进制文件(名为docker_build),在GitLab CI中直接调用。
.docker:
image: quay.io/buildah/stable
variables:
IS_INJECT_VERSION: "true"
script:
- docker_build --build-arg GIT_REVISION=${CI_COMMIT_SHA}
以下是我们的构建程序代码:
use std::env;
use std::process::Command;
const CI_REGISTRY: &str = "xxx"; // 我们的docker仓库地址
const CI_REGISTRY_USER: &str = "xxxx"; // 用户名
const CI_REGISTRY_PASSWORD: &str = "xx"; // 密码
fn main() {
// 把需要的3个环境变量内置进来
env::set_var("STORAGE_DRIVER", "vfs");
env::set_var("BUILDAH_FORMAT", "docker");
env::set_var("BUILDAH_ISOLATION", "chroot");
let ci_docker_project = env::var("CI_DOCKER_PROJECT").unwrap();
let ci_docker_repo = env::var("CI_DOCKER_REPO").unwrap();
let ci_commit_ref_name = env::var("CI_COMMIT_REF_NAME").unwrap();
let image_name =
format!("{CI_REGISTRY}/{ci_docker_project}/{ci_docker_repo}:{ci_commit_ref_name}");
login(); // 将登陆改到前面,因为可能有的镜像是私有的
docker_build(&image_name, &ci_commit_ref_name);
show_images();
docker_push(&image_name);
println!("docker build successfully");
}
fn login() {
let mut command = Command::new("buildah");
command
.arg("login")
.arg("-u")
.arg(&CI_REGISTRY_USER)
.arg("-p")
.arg(&CI_REGISTRY_PASSWORD)
.arg(&CI_REGISTRY);
execute_command(&mut command, "login");
}
fn execute_command(command: &mut Command, action: &str) {
println!("{} start", action);
let output = command
.spawn()
.expect("command spawn failed")
.wait_with_output()
.expect(format!("failed to wait on child {}", action).as_str());
assert!(output.status.success());
println!("{}", String::from_utf8_lossy(&output.stdout));
// println!("{} success", action);
println!("----------------------------------------------------");
}
fn show_images() {
let mut command = Command::new("buildah");
command.arg("images");
execute_command(&mut command, "show images");
}
fn docker_build(image_name: &str, ci_commit_ref_name: &str) {
// buildah build -t $FQ_IMAGE_NAME -f $CI_DOCKERFILE
// debian安装的buildah版本低,1.19.6,命令不是build而是bud。新的docker中安装的是1.26.2,不过也支持bud。所以这里统一先使用bud。
let mut command = Command::new("buildah");
command.arg("bud").arg("-t").arg(&image_name);
if let Ok(file) = env::var("CI_DOCKERFILE") {
command.arg("-f").arg(file);
}
if let Ok(is_inject_version) = env::var("IS_INJECT_VERSION") {
if is_inject_version == "true" {
// buildah版本为1.19时不能使用
command
.arg("--env")
.arg(format!("VERSION={}", ci_commit_ref_name));
}
}
if let Ok(revision) = env::var("CI_COMMIT_SHA") {
command
.arg("--build-arg")
.arg(format!("GIT_REVISION={}", revision));
}
// --build-arg aa=bb
let args: Vec<String> = env::args().collect();
if let Ok(i) = args.binary_search(&"--build-arg".to_string()) {
let arg = args[i + 1].to_string();
if !arg.is_empty() {
command.arg("--build-arg").arg(arg);
}
};
execute_command(&mut command, "docker build");
}
fn docker_push(image_name: &str) {
// buildah push $FQ_IMAGE_NAME
let mut command = Command::new("buildah");
command.arg("push").arg(&image_name);
execute_command(&mut command, "docker push");
}
从代码中看,我们在构建时根据GitLab当前流水线的分支或版本名称CI_COMMIT_REF_NAME,注入一个环境变量VERSION,这样在生成的镜像里是可以看到的:
这样有个好处是,各应用本身可以读取这个环境变量来提供一个接口,响应版本号,方便排查线上环境正在运行的是哪个版本(因为线上环境可能因为某种原因回退版本的)。
比如:
lazy_static! {
pub static ref VERSION: String = env::var("VERSION").unwrap_or("0.0.0".to_string());
}
App::new()
.service(web::resource("/api").to(|| async { globals::VERSION.as_str() }))
.service(web::resource("/").to(|| async { globals::VERSION.as_str() }))
这个程序主要用来在GitLab流水线中基于安装有Buildah的镜像使用。我们早期是在某个镜像基础上,额外安装Buildah。但是debian安装的Buildah版本比较低(1.19.6),而新版本已经是1.26.2以上了。所以后期直接使用上面的镜像quay.io/buildah/stable
。
在GitLab中的效果:
总结
Buildah是一个开源工具,用于构建和管理容器镜像。它无需Docker守护进程,可以创建和修改容器镜像。Buildah与OCI标准兼容,可以与其它工具和平台集成。它提供了丰富的功能,包括创建镜像、执行命令、复制文件等。
使用Buildah,我们轻松地在GitLab CI流水线中构建镜像,并将其推送到我们的私有仓库。这与后续的部署步骤集成,我们实现了自动化的软件交付,也就是一个完整的CI/CD工作流。