简单介绍Leap中的Podman与Buildah以及镜像构建

#1

因前两篇文章比较细碎,将其整理为本文,如有错误,烦请指正


在openSUSE Leap 15.1, 正式引入了Podman与Buildah的支持。作为Docker的升级版,Podman具有即下即用,可rootless运行容器等特性,较目前来说比较稳定,可基本代替除了Docker Swarm的功能,而Buildah也带来了更加灵活的镜像构建方式


  • Podman

    • 介绍一下Podman

      说Podman,不得不先提一下Podman所在的项目,Libpod

      Libpod为希望使用Kubernetes推广的Container Pod概念的应用程序提供了一个库。Libpod还包含Pod Manager工具(Podman)。Podman管理容器,容器,容器映像和容器卷。

      不过我们主要来说Libpod中负责管理的Podman

      Podman中的这个‘Pod’的概念源与K8s中的Pod,在K8s的官方文档中,Pod的概念(的谷歌翻译)是这样描述的:

    In terms of Docker constructs, a Pod is modelled as a group of Docker containers with shared namespaces and shared filesystem volumes.
    就Docker构造而言,Pod被塑造为一组具有共享命名空间和共享文件系统卷的Docker容器。

      Pod就是这样的一个概念,Libpod为此而生。Podman作为了Libpod中管理工具,他提供了一个与Docker兼容的CLI前端,我们可以用这条命令方便的从Docker切换到Podman,并且也可以用这条命令来简单的介绍它:

    alias docker=podman
    

      是的,Podman实现了除了Docker Swarm的相关指令外几乎所有的Docker命令,你可以用他实现除了Docker Swarm相关之外的所有Docker操作,并且他还是一个“无守护进程容器引擎”,这就意味着你可以脱离Docker那臃肿的daemon, 可以跟systemctl start docker说拜拜了。

    • How to use it??

      刚开始的介绍中说到了,Podman和Docker CLI的兼容性,但是对于用的比较少的一部分兼容仍是未知,比如Docker API. 目前还有较多用途未能测试到,这里就已测试到的兼容命令做一个介绍。

      对于基础命令,命令格式仍然和docker一样,你仍然可以使用像 podman run -it xxxxx,  podman images,  podman ps -a 这种的基础命令,你也可以使用一些命令组合,比如 podman rm $(podman ps -a -q -f status=exited),  podman rmi $(pdman images --format {{.ID}} ),  cat xxx.tar | podman import - TAG  . 这些命令依然可用,不必担心。

      对于一些扩展工具,比如docker-compose, docker-squash工具,docker-compose依然可用,因为podman兼容OCI与Docker格式的镜像,但是镜像压缩工具docker-squash并不可用,在之后会介绍一些方式构建足够小的镜像

    • 使用时碰到的一些问题(随时更新)

      • 在使用Rootless模式的碰到的uid/gid映射问题(TW环境)

         在openSUSE Tumbleweed版本下,非root用户运行podman可能会出现以下WARNING

        WARN[0000] cannot find mappings for user XXXX: No subuid ranges found for user "XXXX" in /etc/subuid 
        WARN[0000] using rootless single mapping into the namespace. This might break some images. Check /etc/subuid and /etc/subgid for adding subids
        

        并且启动容器并不会成功,同时报错:

        Error: error creating container storage: error creating read-write layer with ID "d21e1aa53311dee39e57aeaf3ad545529989f71edbf1ec2989d5edf3675675be": there might not be enough IDs available in the namespace (requested 0:65534 for /home/XXXX/.local/share/containers/storage/vfs/dir/d21e1aa53311dee39e57aeaf3ad545529989f71edbf1ec2989d5edf3675675be/etc/gshadow): lchown /home/XXXX/.local/share/containers/storage/vfs/dir/d21e1aa53311dee39e57aeaf3ad545529989f71edbf1ec2989d5edf3675675be/etc/gshadow: invalid argument
        

          这个问题的根源是“/etc/subuid”与“/etc/subgid”这两个文件,这个问题牵扯到了Podman的rootless隔离和Linux的namespace,在此做简要介绍:

        Linux 内核实现 namespace 的主要目的就是为了实现轻量级虚拟化(容器) 服务,构建一个相对隔离的 shell 环境。在同一个 namespace 下的进程可以感知 彼此的变化,而对外界的进程一无所知。这样让容器内的进程运行在一个独立的 系统环境中,以此达到隔离的目的

          Podman的Rootless允许用户在非root用户下运行容器,podman会通过namespace隔离用户,当非root用户用podman run 命令启动一个容器时,podman将会按照subuid与subgid中定义的range,将当前用户映射进容器中的uid 0(root), 这样,即是容器中的root用户,他也只是在容器中具有root权限,但是在宿主机中,他只拥有被映射进来的用户的权限。

          举一个现实的例子,比如整个宿主机系统是一栋楼,root就是房地产商,而普通用户就是其中一套的户主。subuid就是户主想租出去的房间数,每个租出去的房间就是一个容器,租户就是这个小单间的root,就算租户动了歪心思变成了户主(恶意提权),他也仅是可以对这套房子进行操作,而无法对其他业主造成损失。

          解决这个问题方法很简单,两条命令即可解决,就是在这两个文件中定义一个范围:

        echo "当前用户名:110000:65536" > /etc/subuid
        echo "当前用户名:110000:65536" > /etc/subgid
        

        echo内容的意思是,“在当前用户的namespace中有65536个子用户,子用户的ID是从110000-175535” 这些用户ID映射进容器后就是ID为0-65535的用户,subgid和subuid意义相似,在此不在赘述。

  • Buildah

    • 介绍一下Buildah

        在网上搜索Podman时,经常会看到Podman和Buildah成对出现,用很正常的想法去思考,那这两个之间肯定有一些不可告人的秘密。实际上,Buildah和Podman是两个互补的项目。它们两个都是用于OCI(Open Container Initiative)镜像的工具,分别面向于两个方面。

        Buildah面向于构建OCI镜像,他在构建镜像时并不需要root权限,Buildah的原生命令基于Dockerfile中的指令,同时他也支持由Dockerfile构建镜像,在使用Buildah的环境下,你可以在终端里和平时操作系统一样构建出来一个属于你自己的镜像。当然你也可以用你会的脚本语言或者其他语言构建一个自定义镜像,这就是Buildah的优势所在。

    • How to use it??

        使用Buildah构建镜像的方式有两种:使用原生命令构建,或者使用Dockerfile构建。使用Dockerfile构建后文再谈,在此先主要介绍Buildah的原生命令。

        如果你了解Dockerfile中的指令与Docker commit,那么你会很快上手Buildah,从用户层面来说,Buildah的原生命令构建就是以commit方式利用Dockerfile指令构建整个镜像,举个例子,在安装了Buildah后依次运行如下的命令你将获得一个跑火车的小容器:

      pod=$(buildah from opensuse:latest)
      buildah config --label maintainer="Wooooo" $pod
      buildah run $pod zypper ar -f http://download.opensuse.org/repositories/games/openSUSE_Leap_42.3/ game_oss
      buildah run $pod zypper ref
      buildah run $pod zypper in -y sl
      buildah run $pod zypper se sl
      buildah config --cmd sl $pod
      buildah commit --format docker $pod train:latest
      

        构建完成后,用以下命令运行这个容器,你就可以看到一辆呼啸而过的小火车

      podman run -it --rm train:latest
      

        在构建期间,会有zypper提醒你是否接受game_oss源的证书,这时你会发现你竟然可以跟他交互!这就是Buildah的优势,你不用去预想可能会出现的中断场景,你可以直接在需要交互的地方进行操作,然后继续整个构建过程,这在很多环境下都很有用。更多的构建指令,请参考文末的Buildah官方Git中的Readme.

    • 部分镜像构建方式简单介绍与简单对比

        对于构建镜像,本人接触过的只有三种,其一是用supermin构建基础镜像,其二是用docker/podman commit 魔改镜像,其三是用dockerfile打包镜像,这三种途径分别用于三种不同的情况,有其独有的特性,在这里简单介绍与对比一下也可以体现出Buildah的实际运用情况。


        在这里首先引入一个环境,友人A用cpp写了一个程序,程序编译过后本身可以跨平台使用,但是仍然有依赖,依赖有f,b,c.依赖在Windows系统中均为系统预安装,但是在部分Linux发行版中并未预安装,为避免程序提交后因为依赖问题测试不通过,便有了三个方案,第一,打包一个测试环境容器;第二,打包为AppImage;第三,静态编译。在其中,因为需要跨平台,便除去了方案三,于是友人A开始琢磨打包AppImage,我则受托构建一个测试镜像。以下便开始了进程。


      • 首先是依赖现有基础镜像的情况

        在此情况下采用了Debian的镜像作为基础镜像。这里会有一个坑,就是需要先考虑目标程序的运行环境,友人A的程序是在x86_64环境下编译的,所以运行环境只能是x86_64.这是我在连续pull了几个基础镜像后才注意到的问题,在这里选择Debian的镜像,一是因为其体积小,二是因为他是x86_64环境。

        pull到镜像后,先run一下,确定了容器中是否有已安装的依赖,结果是在意料之中的,没有一个依赖被安装。确认过环境后,先整理打包一下要放进环境的文件,就开始编写Dockerfile,File如下:

      FROM debian:latest
      MAINTAINER PoisonBCat
      ADD files.tar.gz /root/
      RUN rm -rf /etc/apt/sources.list
      RUN mv /root/sources.list /etc/apt/
      RUN apt update
      RUN apt install -y f b c vim
      RUN chmod +x /root/example 
      RUN mv /root/example /usr/local/bin
      WORKDIR /root/
      CMD ["bash"]
      

        构建完成后查看镜像列表,对比Debian镜像,接近300M体积还是偏大的,增加了1倍多的空间,按照apt的输出来看,3个依赖+vim是155M左右,虽然因为依赖包的体积较大但是仍然感觉不是太理想

      REPOSITORY                  TAG      IMAGE ID       CREATED         SIZE
      localhost/env_test          latest   923d87ff243b   6 seconds ago   290 MB
      docker.io/library/debian    latest   e1de74e67cc7   5 days ago      106 MB
      

        随后精简串联File,尽量减少镜像构建层,清除无用缓存,把vim换成vim-tiny,最后File变成了这样:

      FROM debian:latest
      MAINTAINER PoisonBCat
      ADD files.tar.gz /root/
      RUN rm -rf /etc/apt/sources.list && mv /root/sources.list /etc/apt/ &&  apt update && apt install -y f b c vim-tiny && apt clean && chmod +x /root/example && mv /root/example /usr/local/bin
      WORKDIR /root/
      CMD ["bash"]
      

      构建出来,看起来效果还不错

      REPOSITORY                  TAG      IMAGE ID       CREATED          SIZE
      localhost/env_test          latest   4fbab8094063   10 seconds ago   257 MB
      docker.io/library/debian    latest   e1de74e67cc7   5 days ago       106 MB
      

      到这里就基本就已经达到预期目的了,但是仍然有进步空间,debian镜像里包括了一个最小的系统环境,虽然功能完善但是有很多无用的功能,比如说tar和apt,把能精简的都精简,还可以进一步的缩小镜像体积,所以接下来就是自行构建基础镜像,进一步缩小镜像体积。

      • 使用Supermin构建基础镜像,然后再进行后期添加修改

        基于openSUSE Leap 15.1用supermin构建镜像,命令很简单,如下(依赖依然以f,b,c代替):

      supermin -v --prepare f b c bash coreutil vim -o 1st.d 
      supermin --build 1st.d -f chroot -o 2nd.d
      tar --numeric-owner -cpf os_bash.tar -C 2nd.d .
      cat os_bash.tar | podman import - os_bash:1.0
      

        结果超出预期,镜像解压状态竟然只有85.7M,(感谢suse),这显然是最完美的基础镜像

      REPOSITORY                  TAG      IMAGE ID       CREATED             SIZE
      docker.io/library/os_bash   1.0      1c4476ab58e9   16 minutes ago      85.7 MB
      localhost/env_test          latest   4fbab8094063   About an hour ago   257 MB
      docker.io/library/debian    latest   e1de74e67cc7   5 days ago          106 MB
      

        下一步就是再利用Dockerfile打包了,和刚开始的Dockerfile所差无几,只是减少了安装依赖的过程,打包后的结果如下:

      REPOSITORY                  TAG      IMAGE ID       CREATED             SIZE
      localhost/env_opensuse      latest   7a1e9e4013c6   4 seconds ago       92.5 MB
      docker.io/library/os_bash   1.0      1c4476ab58e9   20 minutes ago      85.7 MB
      localhost/env_test          latest   4fbab8094063   About an hour ago   257 MB
      docker.io/library/debian    latest   e1de74e67cc7   5 days ago          106 MB
      

        这个最终版可以说是十分理想了,podman和supermin的表现仍如预期,那么Buildah的情况又如何呢

      • Buildah使用Dockerfile进行构建

        Buildah作为一个镜像构建工具,他并未提供对基础镜像的构建,不过其仍然支持Dockerfile构建,可以直接通过buildah bud -t TAG DIRECTORY 的命令格式进行构建,Buildah构建的优点是,他并不像docker/podman build 一样对每一条指令提交一个新的分层,而是进行一个流水化的构建,这样的构建方式明显的加快了镜像构建的速度,在构建指令较多的时候会很明显。命令与结果如下:

      buildah bud -t env_bud:latest .

      # buildah images
      REPOSITORY                  TAG      IMAGE ID       CREATED          SIZE
      localhost/env_bud           latest   81e5636f66f3   15 seconds ago   283 MB
      localhost/env_test          latest   923d87ff243b   3 hours ago      290 MB
      

        构建的速度还是很快,在一条ADD指令,6条RUN指令下基本耗时只是中间的安装依赖的过程,同时还可以看出来镜像相对于用podman build 构建出的镜像要小几M, 再查看一下这个镜像的history:

      # podman history 81e5636f66f3
      ID             CREATED         CREATED BY                                      SIZE   
      81e5636f66f3   3 minutes ago                                                   177.9MB   
      e1de74e67cc7   5 days ago      /bin/sh -c #(nop) CMD ["bash"]                  177.9MB   
      <missing>      5 days ago      /bin/sh -c #(nop) ADD file:6e8620824300ccf...    105.5MB  
      

        很明显,中间没有多余的提交层,至于 e1de74e67cc7 这个层,就是debian:latest构建时的CMD层了。

      • 利用Buildah的原生命令进行构建

        前面在介绍Buildah的时候提到了Buildah的原生指令,并且也给出了一个简单的示例,在这里将最开始的那个Dockerfile可以改写成如下的样子:

      # pod=$(buildah from debian:latest) 
      # buildah config --label maintainer="PoisonBCat" $pod
      # buildah add $pod files.tar.gz /root/
      cebaa7d791fbedb3c468321d9e7256c35b223a9c0624e768720dd3f13765bac3
      
      # buildah run $pod rm -rf /etc/apt/sources.list
      # buildah run $pod mv /root/sources.list /etc/apt/
      # buildah run apt update
      # buildah run $pod apt install -y f b c vim 
      # buildah run $pod chmod +x /root/example
      # buildah run $pod mv /root/example /usr/local/bin
      # buildah config --workingdir /root/ $pod 
      # buildah config --cmd bash $pod
      # buildah commit --format docker $pod env_cmd:latest
      

        结果不言而喻,大小接近精简过后的Dockerfile利用podman直接构建出的镜像大小,在此不在赘述。

  • 写在后面

      本文问题与示例部分军出于实际体验,因环境限制仅能做简要的分析与整理,对于Podman,我已经在服务器上让他替代了docker,而buildah最后的案例,则是使用了Buildah+Dockerfile的方式构建完成了,最终大小在解压状态下仅为89.1MB,这已经超出了预期的想象,其实中间缩小镜像体积的主要原因应该还是要归功于openSUSE. 因为在构建系统镜像时,supermin认出了f,b,c三个依赖但是在接下来的安装过程中并未出现。构建完后测试也证明镜像中的确包括了这三个依赖,具体缘由不得而知。在这里需要说一点我个人的想法,在后面我并未采用原生命令的构建方式,是因为我刚开始学Docker的时候请教过的人告诉我,commit一般作为保护现场用,而不是用于构建镜像,虽然Buildah的原生命令是同时综合了commit与Dockerfile指令,但是我还是会有一些抵触,不过其特性还是对构建环境进程有很大的提升,这点是值得肯定的。

  • 参考资料

    推荐阅读下面这篇文章,这篇文章中更详细的介绍了Buildah的用法,还有其特性(包括本文中未提到的直接挂载镜像与chroot)
    Buildah 入门

    Libpod 官方Git

    Buildah 官方git

    Podman简介-RedHat Develpoer

2赞
openSUSE-Gossip-20190408-20190731
使用podman以非root运行容器,局域网无法访问
#2

你怎么不是FET是CAT了?我怀疑你和猫猫是一个人

1赞
于关闭 #5

本主题在最后一个回复创建后60分钟后自动锁定。不再允许添加新回复。