以Docker为代表的容器技术,提供了一种在安全隔离的容器中打包应用的方式,这种隔离性和安全性允许在一台机器上运行多个容器。容器的轻量级特性,意味着在应用微服务化致使服务数量激增的情况下,应用的部署和运行可以节省更多资源。同时,通过将应用及其运行环境打包为容器镜像,可以极大提升应用标准化交付能力,避免传统部署过程带来的操作复杂度与操作风险。从开发测试到生产环境,大量容器镜像带来的镜像保存、分发、部署和更新等工作,需要有效的生命周期管理规范,以确保应用迭代高效准确。
图1 企业级——开发、持续集成、持续部署流程
如上图所示,在构建企业级CI/CD流程中,应用开发完成并且提交至Git服务器之后即触发自动化流程开始打包生成镜像,在测试环境经历单元测试、集成测试、SIT、UAT等测试。镜像在测试审批通过之后推送到准生产环境镜像仓库,由项目组和专业团队进行非功能测试和投产验证。以上测试和投产评审通过后,再将镜像封板并推送到生产环境镜像仓库中。当该项目进行升级更新时,旧版本的镜像进行打版留存或者直接删除,完成整个容器镜像生命周期。
容器镜像生命周期管理容器镜像的生命周期将分为三个阶段、三个环境来讨论:
1.容器镜像规划设计阶段:
容器镜像规划设计阶段,组织镜像设计人员针对特定需求设计容器基础镜像并做测试形成镜像模板,针对生产场景以及特定需求,做出基础镜像规划,如制作OS base、tomcat、redis、nginx等。
在完成规划之后,进行基础镜像的设计,需要规定众多的设计规范,如字符集编码、集成的基础工具(如top、ps、ls、vi等)、应用存放目录、路径(如JAVA_HOME、GOBIN等)、执行用户(容器中使用root有安全隐患,根据行内规范要求添加普通用户 )、数据目录(挂载点)等。
完成基础镜像制作之后,需要对这些镜像进行测试。安全性方面,需对暴露出的端口进行攻击,判断是否会影响容器运行、影响容器宿主机和宿主机中的其他容器,以及是否满足其他安全性方面的考虑。性能方面,容器基础镜像的大小会直接影响到网络的性能,需对容器进行压力测试是否满足TPS性能需求等。稳定性方面,长稳测试是否满足需求等。
完成上述步骤之后,形成基础镜像模板,完成容器镜像规划设计阶段。
图2 容器镜像生命周期整体流程
2.容器镜像使用阶段:
容器镜像使用阶段的容器镜像生命周期还需要分为三个环境:开发测试环境、准生产环境和生产环境。
完成镜像模板之后,开发测试人员就需要以该基础镜像模板为基础进行应用的开发、集成、测试。为方便开发人员进行本地冒烟测试,设置开发仓库。测试用的镜像需要测试人员从开发仓库中选取特定版本拉取到测试仓库中,然后触发自动化测试,持续部署的流程。
完成功能测试的应用镜像,通过自动化流程推送至准生产仓库进行投产验证。准生产环境分为非功能测试和投产验证两部分。非功能测试进行特定应用场景的TPS压力测试,以及审核应用是否满足多中心高可用性、网络连接稳定性、网络安全等非功能需求。完成非功能测试后进行投产验证,应用镜像在此阶段封板,保持和生产环境一致。
完成投产验证的应用镜像在经过审批之后,由管理员推送至生产仓库,然后登录平台进行各项操作。如果是新建项目则直接新建即可,如果是更新项目只需要将新版本的镜像拉起容器实例即可。
三大环境建立镜像版本管理规范,通过镜像名前缀对环境进行区分,d为开发,t为测试,s1为非功能测试,s2为投产验证,p为生产,确保镜像在环境间正确流转。镜像tag规定使用“日期_当日版本号”(例:20200102_01),确保镜像更新有序无误。
传统管理模式各阶段采用线下授权人工操作,权限管理和授权流程冗长繁琐,难以实现敏捷。未来基于统一制品库管理镜像使用流程,通过RBAC统一权限管理和审计,可以设计镜像命名自动化进行版本管理,推动镜像敏捷迭代。
3.容器镜像归档和销毁:
容器规模较小的阶段,使用镜像仓库存储归档全部镜像。未来,在大规模制品管理中,基于统一制品库管理容器镜像归档和销毁,可以根据镜像仓库容量和版本号定义自动归档和销毁规则,提供镜像离线存储归档审计等功能,同时保障镜像仓库可用性和容灾性,保障镜像可用性,协助完成镜像生命周期管理的最后一步。
容器镜像构建原则图3 基础镜像版本关系图
如上图所示,容器镜像由下而上分为osbase、runtime和app镜像由下而上三层,上层镜像通过Dockerfile基于底层镜像进行生成。osbase镜像分为三个版本:compile-osbase只用于编译集群中编译程序,为java和go编译环境集成了编译工具。dev-osbase为开发环境os基础镜像,内集成开发测试所需的debug工具,开发人员完成程序编写并且debug之后通过docker多阶段编译方式生成prod-app镜像,才能在生产环境部署。prod-osbase为生产环境os基础镜像,所有的生产环境的镜像必须基于prod-osbase镜像。
容器镜像构建应遵循以下几个原则:
1.镜像安全原则
Docker容器的生命周期,就是一个镜像文件从产生、运行到停止的过程,镜像文件一旦打包生成,如存在安全问题,那这些问题也被一并打包,最终导致安全事件。镜像应该从如下几个方面进行安全加固:
1)对镜像进行安全漏洞扫描;
2)使用安全的基础镜像:光大银行容器都基于统一制作的基础镜像;
3)最小安装原则:安全的普适法则,不要安装任何与应用无关的东西。
2.日志归档原则
推荐应用日志以文本方式归档,日志可循环写入多日志文件中,例如每个日志文件50M,且日志目录要挂载在共享数据卷上。
3.应用配置文件原则
容器化的应用使用的配置文件通常存在配置文件和环境变量两种,具体如下:
1)配置文件方式:对于使用配置文件方式的容器化应用,使用容器服务平台的配置中心来统一定义与管理配置,而不需要每个镜像中去定义;
2)环境变量方式:对于使用环境变量方式的容器化应用,使用容器服务平台的环境变量文件来统一定义与管理环境变量,而不需要在每个镜像中去定义;
3)通过容器服务平台提供的管理环境变量文件功能,可实现应用镜像与配置的解耦,相同镜像可通过平台实现在不同的环境启动容器,从而降低了运维成本。
4.应用安装路径原则
为统一管理应用安装路径,推荐应用镜像安装路径统一目录结构和命名规范。
5.应用镜像制作原则
镜像制作常用有两种方法:(1)docker build方式;(2)docker commit方式。
docker commit命令相当于在运行的容器中安装应用后通过快照生成镜像tar包,由于存在缓存文件会增加镜像体积,同时不适合在DevOps流水线的CI环节完成应用打包发布过程,因此采用docker build的方式制作应用镜像。
Dockerfile编写最佳实践Dockerfile是一种被Docker程序解释的脚本,它由一条条的指令组成。Docker程序通过这些指令生成定制的容器镜像,相比容器镜像这种黑盒子,Dockerfile这种显而易见的脚本更容易让人明白镜像是怎么产生的。使用容器技术的最佳实践,应用项目组编写Dockerfile文件时需要遵从以下原则:
1.增加镜像分层复用性:根据容器镜像分层复用特性,Dockerfile编写指令的顺序应为共性到特性,如先进行jdk安装与环境设置,再进行应用安装部署,这样可保证jdk层的复用。
2.使用CMD exec运行方式:CMD和ENTRYPOINT都能用来指定开始运行的程序,它们之间的区别是ENTRYPOINT指定了该镜像启动时的入口,CMD则指定了容器启动时的命令。它们都有2种语法:shell模式和exec模式。当使用shell模式的时候,docker会自动加入”/bin/sh -c”到命令中,这样有可能导致意想不到的行为。为了避免这种行为,推荐所有的CMD和ENTRYPOINT都使用exec模式。
通常情况下建议使用CMD命令定义软件的启动。
3.使用/run.sh脚本作为容器启动进程:通过/run.sh来启动应用进程,这样可以规范化容器启动命令,同时增加了启动时相关辅助操作的扩展性。
4.拷贝命令:ADD和COPY命令功能非常接近,但是COPY语义更直接。因此,除非需要拷贝并解压一个文件到镜像中,那么推荐使用COPY命令。
5.尽量合并命令:Dockerfile中每个命令会创建一个新的layer,而一个容器拥有的layer数是有限制的。所以尽量将多个命令合并成一个命令,这不仅能减少layer的层数和镜像体积,也可以加快docker build编译速度。
6.减少不必要的空间:比如安装软件时的缓存、临时信息、不需要的文件,安装结束后要进行删除。
7.不要使用latest定义镜像标签:当镜像没有指定版本时,将默认使用latest标签。若此时镜像已更新,而latest标签不会指向之前的镜像而是指向新更新的镜像,这时镜像构建有可能失败。
8.使用YUM安装RPM包:容器基础镜像中已经提供了yum源。如果使用了yum安装,请使用yum clean all清除安装过程中的缓存。
Dockerfile命令建议:
FROM
#尽可能的使用官方仓库存储的镜像作为基础镜像。官方建议使用 Alpine,它大小仅5mb左右,麻雀虽小五脏俱全,用户态工具基本都有。
LABEL
#我们可以给镜像添加标签(LABEL),如记录仓库地址,维护人联系方式等等。
#LABEL maintainer="paas" \
# reference="info:messages" \
# nodejs=5.12.0 \# tengine=2.2.0
#建议: 可以通过label标记项目仓库地址,维护人联系方式、依赖的版本等信息
RUN
#建议:多个脚本命令通过&&符号写在一个run指令中,可以有效减少layer数量。通过""分割行,可以方便预览命令变更。
CMD
# CMD还在大多数情况以交互式的方式出现。如 CMD [“python”],当你执行 docker run -it python 的时候,将进入 shell 的交互模式。
EXPOSE
#指定容器侦听端口,应该尽量使用应用程序通用的传统端口,如 Apache Web 服务器使用 EXPOSE 80 等。这个指令只是声明标记,具体不会在创建容器时被应用。
# 尽量避免这种方式EXPOSE 8080:8899
# 仅仅暴露端口EXPOSE 8080
# 建议: Dockerfile中通过EXPOSE 标记服务会listen的端口。
ENV
#为容器添加环境变量,常用于为应用程序提供必要的环境变量以及版本号的设置。
ADD or COPY
#这两者很相似,这里建议优先选择COPY,它比ADD透明度更高。
ENTRYPOINT
# ENTRYPOINT的最佳用法是设置镜像的主命令,用 CMD 作为参数,这样就可以使镜像像命令一样运行,在容器启动时,我们只需要覆盖CMD参数即可。如:
#ENTRYPOINT ["python"] #CMD ["--help"] #ENTRYP OINT 也可以于脚本组合使用,例如经典的Postgres Official Image脚本 使用exec bash 命令,使最终运行的应用程序成为容器的PID 1。 这允许应用程序接收发送到容器任何Unix 信号。 # 建议 在业务的Dockerfile中 用entrypoint作为镜像的主命令,CMD作为主命令默认的flagVOLUME
# VOLUME 指令应该用于如下内容:任何类型的数据库存储区域、配置存储、容器创建的文件或目录。推荐 VOLUME 用于挂载镜像中那些经常变化(易变化的)或者用户可维护的部分。
USER
# 如果一个服务不需要超级权限来运行,我们可以通过 USER 切换成非 root 用户。在 Dockerfile 中用如下方式创建:
# RUN groupadd -r postgres && useradd --no-log-init -r -g postgres postgres
# USER postgres
# 建议为了减少层数和复杂度,避免频繁使用 USER 进行用户切换。
WORKDIR
# 我们使用WORKDIR作为工作路径,而不是增加复杂的命令,如 RUN cd .. && do-something这样难以阅读以及排障困难和难以维护。
总结与未来展望容器镜像管理不仅是容器平台运维溯源的关键,更是串联开发测试到生产全生命周期的重要工作。我们在管理中依照运行环境划分生命周期阶段对镜像进行分步管理,建立镜像版本管理规范,通过镜像命名和tag管理确保三大环境镜像正确流转和更新;对镜像根据osbase、runtime和app进行分层管理,对osbase和runtime基础镜像进行缩减,使得基于基础镜像构建的应用镜像可以高效启动。规范Dockerfile编写格式,规范运行用户、文件目录、工作目录等关键配置,在镜像构建这一层保障容器运行安全。
容器镜像管理的最佳实践仍在持续摸索和更新中,微服务化时代二进制制品的爆炸性增长使得镜像管理,或是更为广义的制品管理,成为DevOps体系中的重要一环。制品管理需要依托统一制品库展开,通过建设企业级制品库规范制品来源和发布流程,通过制品库本身管理制品全生命周期,提供制品元信息管理和溯源能力,统一管理多种语言制品,减少多种管理工具造成的高维护成本,同时提供多中心分发、高可用、异地容灾、多租户权限管理等数据中心级保障能力。随着容器规模的快速增长,企业级容器镜像(制品)仓库将会是云原生时代应用基础设施的关键组成部分,各种管理架构、技术和工具也在不断深化和发展中,我们将持续探索镜像管理新技术、新方法,推动云原生数据中心建设工作更进一步。