没有账号?立即注册

已有账号?

内容正文

云上应用docker化持续交付实践 — 【包含Qcon讲稿】-博客-云栖社区-阿里云 2017-07-29 00:00:00

因为我非常啰嗦,所以写的分享也太长,全部内容被内部同学review后的反馈是: 像看小说一样……

所以为了防止大家看了开头就去逛别的小网站了,开篇我先点个题, 这篇文件最终的目的是讲清楚下面这张图:

screenshot

就是一个完整的,应用Docker化持续交付需要做的事情。

并且,这篇文章不是硬广, 图中涉及到的服务也是基础服务,提供便捷的配置方式,最佳实践的推荐。

我们并不去定义标准和规范,会兼容业内所有的规范和标准的玩法。

下面开始正文:

本文涉及到的平台有:

  1. 阿里云持续交付平台 https://crp.aliyun.com

  2. 阿里云容器服务

  3. 阿里云镜像仓库服务 https://dev.aliyun.com

  4. 阿里云Code https://code.aliyun.com

  5. 其他会举例引用等都来自docker.com , github 和 travis-ci 等业内标准。

目录:

  1. 传统CD过程中遇到的问题


  2. 变革软件交付方式的技术: Docker

  3. 应用Docker化交付的过程实践

研发过程中的困境

  • 在干货之前,先要引导一下为什么要做持续交付,任何一家互联网或者软件公司,随着产品规模的扩大,市场需求的变化,都会逐步的发现产品版本管理混乱,运维人员总是在兜底, 不知道开发/测试/集成/预发布/生产等等环境到底经历过几代运维人员之手,所以环境压根没人敢动。

  • 因为市场永远在变化, 需求一定在变化,人员也在变化,导致了研发过程中遇到的这样那样的问题。 因此,大多数企业都用CI/CD 这个解决方案来应对 , 如下图:

screenshot

  • 额外说一下,我们认为的持续交付概念总结如下:

    • 在一起就是集成,每次集成都应该有反馈

    • 只有不停的集成才是持续集成。越少持续,每次反馈代价越大

    • 多次集成产生一次交付。

  • CI/CD 是无法提升你的代码质量的,是无法解决你代码中的Bug的,但能够提升效率和质量的原因是: 他能把问题发现在前面, 让小问题提前暴露出来.

  • 我们说做持续集成最重要的是有效反馈持续 ,因为CI就像体检服务一样,好比我这个胖子要减肥,体检服务不能让我吃的更少动的更多,但我如果每天都称一下体重,就能随时知道自己身体的状态,随时知道我每天该干什么, 这就是持续的重要性。

  • 如果我不做这个事儿,很可能等到我年度体检的时候才发现,TMD脂肪肝又加重了。。 同理如果每次代码提交都能自动和其他代码集成,和测试环境集成, 就不会出现最终发布的时候出现各种各样的问题, 也就是刚才说的运维总在兜底的问题。

  • CI过程的有效反馈也很重要,每次集成都应该给出准确的问题定位和建议, 谁的代码merge出现冲突,谁提交的commit导致UT失败, 谁应该立刻去解决什么样的问题, 这都是有效的反馈。 就好比我中午没吃饭,去称一下体重,体重秤告诉我:还凑合。 那这个反馈让我晚饭是吃。。还是不吃呢? 。。 这就是无效反馈。

简单来说,持续交付的pipeline就像下面的管道图一样:

screenshot

当然这个图里的每个节点(stage)的定义并不适用于所有应用,每个stage 是不同角色,运行需要耗费不同的成本,那么只要保证每个Stage 是一个独立有效的反馈就是正确的持续交付pipeline 。

那么,构建出能够运行这样pipeline的一个环境,都需要什么东西:

screenshot

  • 如上图, 你需要有代码托管服务(存储),运行CI中的单元测试,编译打包服务(环境), 如果你的应用已经托管在公共云上,还要涉及到网络问题。 也就是你核心要解决的除了需要服务本身,关键是解决“存储,环境和网络”这三个问题。

现在,当你辛辛苦苦做好了这些过程之后,仍然会遇到一些问题:

  • 每次build,是需要不同的build环境的

    • 编译环境维护困难

  • 每次集成 Test,是需要依赖其他环境,被依赖的环境不受提交者的控制

    • 依赖环境维护困难

  • 每个package, 在不同的环境, run的结果是不一样的

    • 切换环境调试困难

  • 每个package ,是无法回溯的

    • 运行包的版本维护困难

  • 每个环境,是不同的维护者(开发环境,测试环境,生产/产品环境)

    • 统一环境标准困难

  • 每个环境,除了维护者,是无法清楚知道环境的搭建过程的

    • 环境回溯,更是难上加难

Why ? 为什么会遇到这样那样的问题? 为什么开发人员经常抱怨: “明明我的程序在测试环境已经调试好了,为什么一上生产环境就运行不了 ? ”

  • 归根结底的原因是:

  • 开发人员交付的只是软件代码本身, 而运维人员需要维护的是一整套运行环境,以及运行环境之间的依赖关系

screenshot

变革软件交付方式的技术: Docker

  • 有人说:“交付方式的变革,改变了全球的经济格局”

screenshot

  • 那么,在软件开发领域,Docker ( An open platform for distributed applications for developers and sysadmins) , 就是变革软件交付方式的技术。

OP7kIfBat5sGY

回到第一章节的问题, 我们找到了开发和运维之间问题的关键,找到了写代码和维护生产环境之间的核心差别, 那么我们YY一下。

如果我们能像描述代码依赖关系一样,描述代码运行所需的环境依赖呢? 如果又能像描述应用之间的依赖关系一样,描述环境之间的依赖呢?

  • 假定,我们的代码中有一个文件,定义了运行需要的环境依赖栈(就像pom.xml文件中定义了java应用的jar包依赖一样)

  • 构建时,我们能根据整个文件,将所有软件依赖栈安装到一个镜像中,镜像是只读的。任何变更都会新产生一个新的镜像而不会更改原先的镜像。

  • 并且只要这个镜像不变,镜像起来的容器之内的环境也不变。

  • 那我们是不是可以像把代码,依赖,测试脚本,环境依赖,环境描述等等这些东西装到集装箱中一样, 集装箱作为一个整体来传递, 作为一个整体在不同的平台上运行, 集装箱不变,任何平台上运行的结果都不变。 YY思路如下图:

screenshot

如果我们能轻松的交付整个软件依赖栈, 是不是刚才说到的在不同环境调试的问题就能大大减少或者不复存在了?

这个YY过程正好被Docker 技术所覆盖, 我们看一下Docker 提供什么样的能力,能满足刚才的YY:

  1. 描述环境的能力

    • 提供了描述运行栈,并且自定义Build 过程的能力。Code 中的描述文件就是Dockerfile

  1. 分层文件系统

    • Image可以像Git一样进行管理,并且每一层都是只读的,对环境的每个操作都会被记录,并且可回溯

  1. Docker Registry

    • 提供了管理Image 存储系统,可以存储,传递,并且对Image进行版本管理

  1. 屏蔽Host OS 差异

    • 解决了环境差异,保证在任何环境下的运行都是一致的(只要满足运行docker的linux 内核)

这几种能力天然的帮助我们解决环境描述和传递的问题, 因此docker 能够做到Build Once, Run EveryWhere !

  • 因此,软件的交付方式,变成了最简单的 Build -- Ship -- Run, 如下图:

screenshot

应用Docker化交付的过程实践

这是本文最核心的一章, 首先先看个例子,用docker做持续交付能带来的好处, 避免广告嫌疑,我用docker官方网站上的案例: BBC News

  • 简单来说,一个全球新闻中心,内容的变化是最快的, BBC 公司内部的第一个问题是涉及10几种CI环境,26000 Jobs,500Dev人员

  • 第二个核心问题是,CI任务需要等待,无法并行

经过Docker化改造之后:

screenshot

最明显的改变,开发可以自己定义自己的开发语言,自己所需的build,集成测试环境,以及应用运行所需的依赖环境。

既然效果这么明显, 该怎么做呢?

基本思路如下:

  • 安装好Docker环境

  • Docker 化你的应用运行环境

  • Docker 化你的应用编译,UT环境

  • Docker 化你的应用运行的依赖环境

第一步, 如何安装运行一个Docker环境

这个现在已经非常非常的简便了:

  • 配置安装

    • 安装Docker Toolkit

    • 安装云驱动

      • ECS driver for Docker Machine

      • AWS, GCE, etc.

  • 创建Docker运行环境

    • docker-machine create --driver aliyunecs mytest eval "$(docker-machine env mytest)"

    • docker run -d nginx

第二步, 如何将自己的应用运行在Docker容器中?

这句话可以翻译为: 如何将我的应用环境通过Dockerfile描述出来?

假如我的应用是一个Java Web 应用,需要Java运行环境和Tomcat 容器 ,那么大概我的环境所需下面这些东西:

  • 某Linux发行版操作系统

  • 基础软件(起码有个能解压缩包的吧)

  • openjdk 7 && 配置 Java Home 等环境变量

  • Tomcat 7 && 配置 环境变量

  • 应用包 target.war

  • 应用包 启动参数 JVM

  • Web Server 指定端口 8080

  • 启动tomcat

转化为成Dockerfile 的语言大致如下:

FROM buildpack-deps:jessie-curlRUN apt-get update && apt-get install -y unzip  openjdk-7-jre-headless=“$JAVA_DEBIAN_VERSION”  ENV LANG C.UTF-8ENV JAVA_VERSION 7u91ENV JAVA_DEBIAN_VERSION 7u91-2.6.3-1~deb8u1ENV CATALINA_HOME /usr/local/tomcatENV PATH $CATALINA_HOME/bin:$PATHRUN mkdir -p "$CATALINA_HOME"WORKDIR$CATALINA_HOMEENV TOMCAT_VERSION 7.0.68ENV TOMCAT_TGZ_URL  https://xxxx/apache-tomcat-$TOMCAT_VERSION.tar.gzRUNset -x \    && curl -fSL "$TOMCAT_TGZ_URL" -o tomcat.tar.gz \    && curl -fSL "$TOMCAT_TGZ_URL.asc" -o tomcat.tar.gz.asc \    && gpg --batch --verify tomcat.tar.gz.asc tomcat.tar.gz \    && tar -xvf tomcat.tar.gz --strip-components=1 \    && rm bin/*.bat \    && rm tomcat.tar.gz*EXPOSE8080CMD ["catalina.sh", "run"]

  • 可以看出 ,Dockerfile 第一步永远是From 某个镜像, 开始安装了一些基础包(这里是Jre7), 又设置了java的环境变量, 之后安装tomat(这里是7.0),再声明启动8080端口,最后运行tomcat的启动脚本结束,在最后结束之前将我的Web 应用.war包COPY或者ADD进去即可。

我们再看一个Nodejs的环境:

FROM ubuntu:14.04COPY sources.list /etc/apt/sources.listCOPY .npmrc /root/.npmrcRUN apt-get update && apt-get -y install curl automake tar libtool make wget xz-utils supervisorENV NODE_VERSION 0.12.5ENV NPM_VERSION 2.11.3RUN curl -SLO "https://npm.taobao.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.gz" \    && tar -xzf "node-v$NODE_VERSION-linux-x64.tar.gz" -C /usr/local --strip-components=1 \    && npm install -g npm@"$NPM_VERSION" \    && npm cache clearRUN rm -rf ~/.node-gyp \    && mkdir ~/.node-gyp \    && tar zxf node-v$NODE_VERSION-linux-x64.tar.gz -C ~/.node-gyp \    && rm "node-v$NODE_VERSION-linux-x64.tar.gz" \    && mv ~/.node-gyp/node-v$NODE_VERSION-linux-x64 ~/.node-gyp/$NODE_VERSION \    && printf"9\n">~/.node-gyp/$NODE_VERSION/installVersionCMD ["node"]

  • 关于这个环境,COPY了本地的sources.list和.npmrc 到容器中,是更换了安装源为mirrors.aliyun.com 和 NPM源为npm.taobao.org , 国内源更快。 其他就是安装了基本的Nodejs 运行环境

那么通过这两个例子,我们发现Dockerfile 还是写起来很麻烦的(其实也不麻烦,就是刚刚说的装要装的东西,配置,运行这三步)。 那么,刚刚说到每一个Dockerfile的第一行都是FROM另一个镜像, 那么思考一下:

  • 如果有一个安装好Java的环境 ?

  • 如果有一个安装好Java和Tomcat的环境 ?

  • 如果是微服务,对环境只依赖Java/Node基础环境,是不是所有应用都可以共用1个环境?

通过这些思考,得到如下寻找docker镜像的过程:

  • 寻找java镜像 ,选择镜像版本, 检查 Dockerfile

  • 寻找tomcat镜像,选择 Tomcat & Java 版本, 检查 Dockerfile

  • 测试运行 : docker run -ti —rm -v /home/app.war:/canhin/webapp/ tomcat:7-jre7

说句题外话,这个思路同样适用于公司内部,因为Dockerfile 明确划分出了开发和运维的边界, 如果公司有统一的运维标准,比如某个操作系统的某个版本, 某种确定的Web Server, 这样开发只需要From 运维提供的镜像来描述自己的应用环境特殊的部分就好了。 如果大家的环境都一样,调试和测试的过程中,只需要把应用代码通过-v 的参数挂载进去运行就好了, 这样世界就变的很简单和清楚了。

  • 那么当我需要一个Java 7, Tomcat 7的环境的时候, 直接选择一个官方的tomcat 7 - jre7 镜像即可 , 比如 https://dev.aliyun.com/detail.html?spm=5176.1972343.2.2.U5HdFI&repoId=1270 这个, 点击查看他的dockerfile

screenshot

  • 跟我所需的环境基本一致, 那就可以基于这个镜像再写自己的Dockerfile ,这样就会简单很多了。

第三步, 用Docker 描述我的编译环境

编译/CI环境往往在公司规模越来越大的时候, 变得越来越麻烦, 因为不同语言,不同类型的应用对编译环境的要求都不一样。 就像刚才说到的BBC News的例子,一个大公司几十种编译环境的存在是很正常的。

那么,编译环境Docker化最大的好处是: 自定义,可扩展,可复制

  • 试想一下, 假如你的应用编译只需要依赖标准的Jdk 1.7 和 Maven 2, 或者你是python应用编译过程其实只是需要安装依赖, 那么你可以跟很多人共用编译镜像。

  • 但假如你的应用是Nodejs ,编译依赖特定的C库, 或者是C++之类的编译环境一定要和运行环境一致等等,那就需要定制自己的编译环境了。

这里我做一个最简单的用于编译java的镜像示例:

  • 编译镜像的Docker file 示例:

FROM registry.aliyuncs.com/acs-sample/centos:7RUN yum update  yum install -y open-jdk-1.7.0_65-49 COPY build.sh /build.shCOPY settings.xml /home/apache-maven-2.2.1/conf/ENTRYPOINT [“./build.sh"]

  • 上述Dockerfile的build.sh示例:

cd /ws ;  mvn -e -U clean package -Dmaven.test.skip=true$@cp target/*.war docker/ || exit 0

  • 运行方式示例:

git clone git@code.aliyun.com:tangrong.lx/myproject.git  ~/myprj ; cd ~/myprjdocker run --rm -v `pwd`:/ws -v ~/.m2/repo:/bufbuild_maven:1.0

  • 解释一下这个过程:

    • 我的编译环境需要CentOs7 系统, 安装JDK1.7 , 然后把maven 的setting (这里主要配置指向其他私有nexus) 和 编译脚本拷贝进去。

    • 编译脚本也很简单,就是maven编译打包命令,并且把最终生成的war拷贝到一个定义好的docker目录下, 这个目录随便定义。

    • 最后是运行方式, 即把源代码挂载到容器里进行编译, 同时可以选择把本地的.m2 缓存到镜像内加快编译速度

这里提两个小提示,都是经验之谈:

建议: build app 和 build docker image 建议分开进行, 即先进行应用本身的编译,再将输出物拷贝到镜像内(但脚本语言可以例外) 因为:

  • 镜像分层概念导致源码可能泄露:因为DockerImage 每一层都会保存一个版本, 即便是ADD代码进去,编译后再rm掉,也可以通过获取ADD这一层镜像拿到源码,因为镜像是运行在各个环境中,是不应该包含源代码信息的。

  • 镜像最小化原则:编译环境可能需要和运行环境不一样的东西,比如Maven的配置,Nodejs的一些C库的依赖, 都不需要在运行环境中体现,所以本着镜像应该最小化原则,不需要的东西最好都不要放进去,也应该分开进行这个步骤。

  • 所以,整个过程还是分为build app和build docker image 两个过程,类似下面这个简单流程 :

screenshot

建议: Docker file 不要放到代码根目录下

  • 避免大量文件传给docker deamon : docker build会先加载Dockerfile同级目录下所有文件进去,如果有不需要ADD/COPY到镜像里的文件不应该放到Dockerfile目录下, 可以试一下把Dockerfile放到系统/根目录下,这时build 十有八九就会让docker deamon挂掉。

第四步,用docker 描述UT环境

简单思路: 运行Docker 镜像环境,安装测试所需依赖 , 运行Docker容器, 运行测试命令/脚本

用一个travis-ci 官方的例子来说明容器测试这件事,先看下面一个ruby的镜像:

FROM ubuntu:14.04MAINTAINER carlad "https://github.com/carlad"# Install packages for building rubyRUN apt-get updateRUN apt-get install -y --force-yes build-essential wget gitRUN apt-get install -y --force-yes zlib1g-dev libssl-dev libreadline-dev libyaml-dev libxml2-dev libxslt-devRUN apt-get cleanRUN wget -P /root/src http://cache.ruby-lang.org/pub/ruby/2.2/ruby-2.2.2.tar.gzRUNcd /root/src; tar xvf ruby-2.2.2.tar.gzRUNcd /root/src/ruby-2.2.2; ./configure; make installRUN gem update --systemRUN gem install bundlerRUN git clone https://github.com/travis-ci/docker-sinatra /root/sinatraRUNcd /root/sinatra; bundle installEXPOSE4567

  • 简单来说就是标准的一个ruby 镜像, 启动4567端口 。 那么通过这个镜像进行的测试过程如下:

sudo: requiredlanguage: rubyservices:  - dockerbefore_install:  - docker build -t carlad/sinatra .  - docker run -d -p 127.0.0.1:80:4567 carlad/sinatra /bin/sh -c "cd /root/sinatra; bundle exec foreman start;"  - docker ps -a  - docker run carlad/sinatra /bin/sh -c "cd /root/sinatra; bundle exec rake test"script:  - bundle exec rake test

  • 这个其实就是大家可以在本地进行的一个过程,在before install部分内可以看到过程是:

    • 先build出运行环境的镜像

    • 运行这个镜像,看看服务能否正常启动

    • 查看容器是否存活(保证容器不是运行一下就挂了退出)

    • 运行测试

再来看一个python的例子,也很好理解:

language: pythonpython:  -2.7services:  - dockerinstall:  - docker build -t blog .  - docker run -d -p 127.0.0.1:80:80 --name blog blogbefore_script:  - pip install -r requirements.txt  - pip install mock  - pip install requests  - pip install feedparserscript:  - docker ps | grep -q blog  - python tests.py

简单来说就是运行容器, 安装依赖, 运行测试脚本 。 或者 直接通过下面一行命令进行

  • docker run -v mycode:/ws mytestimage:master /bin/sh -c "python3 djanus/manage.py test djanus mobilerpc "

tips: 这里不是说推荐大家用travis-ci ,但travis-ci 制定了一种语法标准, 非常清楚的能够看到整个过程。 同时: 阿里云持续交付平台未来将会完全支持兼容travis-ci定义的yml语法结构

第五步:用Docker-Compose 描述依赖环境

刚刚说了单独一个容器运行测试的情况, 但实际情况可能是即便是运行测试,也需要依赖proxy,依赖db,依赖redis等。 简单来说一般web应用会需要下面的结构:

screenshot

这个结构很简单也很常见, 那在传统思想里,要运行UT或者集成测试,需要依赖的组件,都是去搭建。 搭一个mysql,配置mysql ,运行mysql 这种思路。

  • 但是在docker的思想里,是声明的概念,就是说我需要一个mysql 去存一些数据进行测试, 这个mysql运行在哪里我根本不care 。 同样的思路告诉docker:

    • I need 负载均衡(haproxy,Nginx)

    • I need 数据库(mysql)

    • I need 文件存储(通过-v , ossfs)

    • I need 缓存服务(redis,kv-store)

    • I need ...

  • 这时,用于编排多个Docker Image 的服务,docker-compose 就出现了,官方文档里用三张最简单的图表明了compose是怎么用的:

screenshot

  • 就是说,我运行一次测试, 需要mysql, 那我就启动一个mysql容器就行,通过link 的方式将我的app链接上,配置一个密码即可,至于其他的信息,我根本不需要,或者说不关心。

再举一个例子,假设一个php的Wordpress 应用, 除了应用本身还需要一个db ,他的编排文件(docker-compose.yml)如下:

webimage: registry.aliyuncs.com/acs-sample/wordpress:4.3ports:    - '80'volumes:    - 'wp_upload:/var/www/html/wp-content/uploads'environment:    WORDPRESS_AUTH_KEY: changeme    WORDPRESS_SECURE_AUTH_KEY: changeme    WORDPRESS_LOGGED_IN_KEY: changeme    WORDPRESS_NONCE_KEY: changeme    WORDPRESS_AUTH_SALT: changeme    WORDPRESS_SECURE_AUTH_SALT: changeme    WORDPRESS_LOGGED_IN_SALT: changeme    WORDPRESS_NONCE_SALT: changeme    WORDPRESS_NONCE_AA: changeme  command: run test script   links:    - 'db:mysql'labels:    aliyun.logs: /var/log    aliyun.probe.url: http://container/license.txt    aliyun.probe.initial_delay_seconds: '10'

点赞  0 收藏  0 转发  
全部评论 暂无评论