1云原生大背景下的镜像构建
在分享开始,我想先跟大家简单聊一下云原生,可能不会详细展开,而是带领大家了解一下云原生对镜像构建方面的影响。
第一,在接触云原生相关的技术时,无论是要解决开发、测试环境的问题,还是解决日常开发、测试等相关的操作和流程,我们经常都会谈到持续集成。持续集成首先要做代码的集成,不同的feature一起交付,使用持续集成的理念尽快把代码合并,保证代码没有冲突,这是持续集成最简单的一些理念。
在持续集成之后,要考虑做哪些业务的验证。验证之外,还需要有一些安全相关的策略。比如,在开发过程中是否使用了不安全的代码或依赖包。在构建的过程中,还要生产许多不同的制品。
那么问题就来了,云原生技术确实能通过容器化、K8s集群编排等提供能够复制的应用环境。无论什么语言,Java或Python等都可以非常简单地去使用docker镜像,或者Kubernetes yaml去部署环境,解决开发和生产环境的区别。
在传统开发模式下,常常会遇到在开发环境里程序好好的,到生产环境就出现各种各样的问题。K8s集群编排可以说很好地解决了这个问题,变成复用地资源。以前部署很多实例需要开很多虚拟机的情况,也成为我们不必再关心的问题。
这些听起来很多都是跟运维相关的,那么开发、测试为什么要去关心这个事情?其实大家是在合作,为了达成一个共同的目标。不管用DevOps,还是敏捷开发,我们都要去考虑从代码交付到真正上线要做哪些事情,并予以解决,而不是总面对“在我的环境里没有问题”这样的问题。同时,尽量统一使用环境的配置、资源、方案。
刚才谈到从持续集成到云原生,大量使用云原生技术,这时候要考虑的是安全。简单来说,我可以使用K8s镜像把docker socket直接部署到自己的Pod容器里。但实际上要做更多的考虑,这个环境可能不同的业务来使用,环境本身是共享的,所以不光要使用环境,还要考虑它是否稳定,有没有CVE漏洞以及容器本身的一些权限等。只有搞清楚这几件事,才能去考虑后面要讲的内容。
2无Dockerfile构建镜像工具
首先来思考为什么会有镜像构建这方面的需求?第一,对很多开发者来说,需要不断学习新的框架,新的技术,新的理念,这其实是很多开发者都不希望面临的状态。开发者希望开发环境和生产环境一致,构建的结果无差距,从而避免在后期发现问题。在没有Dockerfile的情况下,会发现不同的语言使用的构建镜像的方案完全不同,需要去这个语言的生态里面寻找相关的方案。
还有一种场景,在构建一个应用时不知道它到底安不安全,也没法控制它里面有没有Dockerfile。比如说先有一个jar包,jar包是第三方开发的,我先上传到服务器,然后再去下载、构建。这时候可能直接用大的jar包去做。
另外,企业本身对产出物或者制品要求非常高,可能会有专门的人去维护,没有那么开放。
我在这里介绍三个无Dockerfile镜像构建方案。
1、KO
Ko是Google发明的一个工具,它主要服务Golang的用户,比较容易使用。除了构建Golang本身,它还往前进了一步,集成了Kubernetes的使用。比如构建镜像,构建程序,相关的yaml部署到开发环境等等,一切一个命令就可以搞定,不需要再去执行其他的命令。
Ko也应用了Golang语言本身的一些特性,比如用Golang package构建不同的镜像,用package构建二进制文件。我们自己的项目里也会使用Golang,编译很多不同的二进制文件。
这块怎么使用呢?第一,我们有对应的k8s yaml,比如开发环境可能是yaml的方案,默认的代码仓库可能会带一些k8s yaml文件,pod、deployment、services等资源。如果不使用ko,还得去做一些replace,ko能够完全解决这个问题。
在镜像地址里直接放go module的名字,或者对应的二进制文件的名字,二进制文件可能在后面再加相关的package就可以了。ko apply也会构建,根据config的一些配置来更新对应的环境。
此外,它还支持前端比较喜欢的一些技术,比如live server这样的概念,对开发体验效率非常有帮助。
它的概念也非常简单,Golang本身构建出来的是一个二进制文件,需要基础镜像的支持,在基础镜像里面做一些配置,就可以运行起来。谷歌前段时间发明了一个项目叫distroless, distroless会有一些工具,例如busybox、ls,这些是在容器里面通常会使用的工具,但是并没有package,没有办法安装新的包。如果能修改,我们不希望使用distroless默认的镜像,而是通过.ko.yaml文件去覆盖,做默认的全局配置,或者根据不同的package去覆盖。
Ko没有把目标docker镜像仓库的地址放在配置里面,它是一个环境变量,这是因为,同城市不同的人在开发时会使用自己的镜像仓库,并不希望这个信息是共享的。用户加上自己的Docker registry地址之后,可以直接开始构建。这里可以分成两部分,刚才我们提到apply、K8s相关的操作,如果只想构建一个镜像并且推送到registry,可以用一个publish命令,publish一个或者多个二进制文件或者镜像。
这个镜像本身非常简单,Golang对环境的依赖非常少。所以,除了基础镜像的这些层级,还有你的应用。构建镜像层面并不依赖Docker,我们使用docker时,会通过docker执行docker build命令,docker build会访问docker daemon,接到主机里面。用Ko的话,完全不需要docker daemon,容器本身没有docker也是可以构建的。同时,也不需要很高的成本,不需要高权限,直接就可以配置。所以从上手或者用户使用来说,又快又简单。Ko也会大量使用GO本身的缓存,以及镜像缓存。
缺点方面,Google的这个基础镜像还在国外,如果应用本身是比较复杂的,需要额外投入时间去学习它的构建。另外,也没有特别好的网站,或者文档,所有的信息都在GitHub里面。
2、Jib
国内大部分开发者可能都是基于Java的,谷歌也在前段时间发明了一个Java专用镜像工具,叫Jib。Maven、gradle、core(库)都支持Jib这个镜像工具,它可以用于任何一个java项目。它本身也是完全基于Java来实现,对java开发者来说是非常熟悉的一种用法。
这是我们的目标镜像仓库,基础镜像可以写入,不写入的话,也会跟Ko一样用distroless的镜像去做。也可以定制,无论更简单还是更复杂的镜像,都可以做到。
除了使用Java的生态本身,Jib也考虑了对Java应用的优化。这里包含三层:依赖、静态资源和应用jar包。
Jib还有几个优点,对Java开发者来说它非常容易,jib不是特别复杂的一个概念,就是一个插件。第二,Java容器化很占用时间。容器化的Java方案在不断优化,因为Java除了容器化,它使用jvm本来就是为了解决应用环境的一些问题,相当于带有两种虚拟化的概念。我们要去做非常优化的Java镜像要投入不少时间和精力,这块jib是一步到位的。Jib和Ko一样,不依赖docker daemon,也可以在容器里运行,不需要什么高权限。
另外Jib和Ko相比较的话,没有做和K8s的集成,如果进一步优化的话,可能会往这个方向考虑。只是不好理解的是,maven或者gradle这块的生态可能已经有相关的插件。
3、s2i
如果我不是Java,又不是Go开发的,我应该怎么做呢?s2i是红帽发明的简化的Docker构建方案 。它的主要理念是,使用builder pattern 的概念去构建程序,再用运行时的镜像包一层。s2i需要在一个专门的镜像里做构建,然后再包一个运行的环境去做镜像,这个可以用任何语言去做。
s2i的使用,需要一些经验去考虑,同时还需要有专门的人去维护。因为它需要一个镜像做构建,在这个镜像里面要维护一堆东西。简单来说,s2i的理念是有这个流程去声明一下,所以相比前面的Ko、Jib两个工具,s2i有点复杂。
第一,要有一个二进制文件把流程编排起来,除了目标镜像仓库之外,代码还需要builder一个镜像。builder 镜像有几个要求,要有assemble脚本和run脚本,这几个脚本都是为了支持要做哪些事情,包括在镜像里面做单元测试都是支持的,还有其他一些附加功能。
从使用来说,一个小的命令就可以搞定,但是,这个命令背后的很多事情是怎么做到的,比如builder镜像的流程、文档等,有很多需要学习的概念。
这个统一构建的过程,开发者统一去管理,对最终用户来说是无感知的。包括第三方的软件依赖、builder镜像,除了这些脚本之外,还要写dockerfile,dockerfile是统一在一个镜像里面管理的。有自己的缓存机制。支持直接克隆代码构建,支持任何语言。
s2i缺点就是,太依赖docker环境。其次,上手非常复杂,拿现成的s2i工具做业务的话会很好用,但是这之前的学习成本还是很高的。
3 Dockerfile构建镜像工具
我们其实还是更希望在K8s环境下去执行构建,这部分我也跟大家介绍两个工具。
其实更多的情况,现在很多团队学习能力很强,愿意拥抱新的技术。希望深入研究容器化的最佳实践,探索怎么优化才能做到更好,怎么进一步使用工具减少建构的时间。
从社区的角度,现在开源社区越来越庞大,很多企业会选择使用开源社区的方案去做。开源社区也提供了很多的资源,你的团队可能忽然之间就变得很大了。在使用这些工具时,企业里的不同团队也会互相分享经验。
另外,影响Dockerfile镜像工具使用的,在交付物这块一些团队定制要求很高。
1、Kaniko
第一个是kaniko,也是谷歌发明的,因为K8s是谷歌的,所以他们在这块做了不少东西,kaniko不需要docker daemon,在容器里面去构建镜像。Kaniko目的是推广在K8s里构建镜像,任何容器化的方案都支持。除代码仓库,还可以使用对象存储的方案。
Kaniko也有一些自己的概念,有本地缓存,还会用registry镜像作为缓存。构建过程中,每完成一个multistage docker构建,结束一个stage,会把这个镜像上传到registry。Kaniko还有一个基础镜像的缓存,为了减少在拉取时的时间,比如说在k8s里面用PVC来保存缓存,可以用warmer镜像来维护这个缓存。
Kaniko构建时,一个命令一步到位就可以完成,完全兼容docker的config.json。那么Kaniko是怎么在容器里面构建的呢?其实,它的原理也很简单,它把镜像的内容保存在自己的容器里面,所有的run命运都在容器里面运行,再构建文件。
Kaniko支持推送到多个registry.Docker需要一个个去推送,但kaniko可以用一个命令全部搞定。除了推送到registry本身,还可以保存成一个tar包,load到docker本身,其他操作都可以使用。Kaniko有多个上下文支持,可以做的场景非常多,可以用bucket或者其他类似的协议做非常复杂的构建。
Kaniko的缺点,镜像也是在谷歌那边gcr.io,不支持v1 image tag格式,构建原理不直观,无法直接load到docker daemon。
2、makisu
Makisu是Uber公司发明的,从18年开始使用,和Kaniko要解决的问题一样,在一个容器化的环境里面执行构建,不依赖docker daemon,也不需要做很多复杂的操作。能够直接load到docker daemon。Makisu跟Kaniko不同之处在于,除了支持dockerfile,还做了一些优化,加入commit机制,可以选择最终镜像要几个层级,通过dockerfile去解决。
它的缓存除了本地,还有registry Jason。还可以把layer dockerfile里面的命令和相关 layer的信息保存到redis,从而减少构建操作,也可以大量复用。Makisu有一个特点,它没有选择兼容docker的配置,而是自己发明了一个配置文件,执行命令时指定具体文件。
这两个都有它的好处,在不同的构建环境里面维护这个配置,也不需要配置参数,构建参数都是一样的。另外,可以按照它的路径去做不同的配置。比如在docker.io里面,可以用不同的用户名和密码。这点docker配置本身其实没有这方面的支持。Makisu另外一个跟kaniko不太一样的地方,是可以在主机里面直接构建。
Makisu的优点刚才也提到了,Docker使用比较快,支持优化镜像层级和空间,直接load到docker daemon,支持镜像压缩,缓存对接redis,Kubernetes官方的pod模版。
缺点部分跟Kaniko一样,镜像在国外Google,其次,它不支持其它构建上下文类型。第三,不兼容docker配置,尽管它能够搞定这些事情提供更强化的配置,但不兼容总归是一个缺陷。另外一个不容易理解的点是,makisu把push 和 –t 分开成为两个参数,来拼接最终目标镜像仓库地址。
4总结
今天这个探索并不是为了选择最优的工具,或者得出哪个工具更好更牛的结论。而是说,这些不同的工具有自己不同的使用场景。