探索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,这样在生成的镜像里是可以看到的: image.png 这样有个好处是,各应用本身可以读取这个环境变量来提供一个接口,响应版本号,方便排查线上环境正在运行的是哪个版本(因为线上环境可能因为某种原因回退版本的)。

比如:

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中的效果: image.png image.png

总结

Buildah是一个开源工具,用于构建和管理容器镜像。它无需Docker守护进程,可以创建和修改容器镜像。Buildah与OCI标准兼容,可以与其它工具和平台集成。它提供了丰富的功能,包括创建镜像、执行命令、复制文件等。

使用Buildah,我们轻松地在GitLab CI流水线中构建镜像,并将其推送到我们的私有仓库。这与后续的部署步骤集成,我们实现了自动化的软件交付,也就是一个完整的CI/CD工作流。

全部评论

相关推荐

点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客企业服务