因为我非常啰嗦,所以写的分享也太长,全部内容被内部同学review后的反馈是: 像看小说一样……
所以为了防止大家看了开头就去逛别的小网站了,开篇我先点个题, 这篇文件最终的目的是讲清楚下面这张图:

就是一个完整的,应用Docker化持续交付需要做的事情。
并且,这篇文章不是硬广, 图中涉及到的服务也是基础服务,提供便捷的配置方式,最佳实践的推荐。
我们并不去定义标准和规范,会兼容业内所有的规范和标准的玩法。
下面开始正文:
在干货之前,先要引导一下为什么要做持续交付,任何一家互联网或者软件公司,随着产品规模的扩大,市场需求的变化,都会逐步的发现产品版本管理混乱,运维人员总是在兜底, 不知道开发/测试/集成/预发布/生产等等环境到底经历过几代运维人员之手,所以环境压根没人敢动。
因为市场永远在变化, 需求一定在变化,人员也在变化,导致了研发过程中遇到的这样那样的问题。 因此,大多数企业都用CI/CD 这个解决方案来应对 , 如下图:
额外说一下,我们认为的持续交付概念总结如下:
在一起
就是集成,每次集成都应该有反馈
。代价越大
。多次集成
产生一次交付。有效反馈
和持续
,因为CI就像体检服务一样,好比我这个胖子要减肥,体检服务不能让我吃的更少动的更多,但我如果每天都称一下体重,就能随时知道自己身体的状态,随时知道我每天该干什么, 这就是持续的重要性。有效反馈
也很重要,每次集成都应该给出准确的问题定位和建议, 谁的代码merge出现冲突,谁提交的commit导致UT失败, 谁应该立刻去解决什么样的问题, 这都是有效的反馈。 就好比我中午没吃饭,去称一下体重,体重秤告诉我:还凑合。 那这个反馈让我晚饭是吃。。还是不吃呢? 。。 这就是无效反馈。简单来说,持续交付的pipeline就像下面的管道图一样:
当然这个图里的每个节点(stage)的定义并不适用于所有应用,每个stage 是不同角色,运行需要耗费不同的成本,那么只要保证每个Stage 是一个独立有效的反馈就是正确的持续交付pipeline 。
那么,构建出能够运行这样pipeline的一个环境,都需要什么东西:
如上图, 你需要有代码托管服务(存储),运行CI中的单元测试,编译打包服务(环境), 如果你的应用已经托管在公共云上,还要涉及到网络问题。 也就是你核心要解决的除了需要服务本身,关键是解决“存储,环境和网络”这三个问题。
现在,当你辛辛苦苦做好了这些过程之后,仍然会遇到一些问题:
每次build,是需要不同的build环境的
每次集成 Test,是需要依赖其他环境,被依赖的环境不受提交者的控制
每个package, 在不同的环境, run的结果是不一样的
每个package ,是无法回溯的
每个环境,是不同的维护者(开发环境,测试环境,生产/产品环境)
每个环境,除了维护者,是无法清楚知道环境的搭建过程的
Why ? 为什么会遇到这样那样的问题? 为什么开发人员经常抱怨: “明明我的程序在测试环境已经调试好了,为什么一上生产环境就运行不了 ? ”
变革软件交付方式的技术。
回到第一章节的问题, 我们找到了开发和运维之间问题的关键,找到了写代码和维护生产环境之间的核心差别, 那么我们YY一下。
如果我们能像描述代码依赖关系一样,描述代码运行所需的环境依赖呢? 如果又能像描述应用之间的依赖关系一样,描述环境之间的依赖呢?
假定,我们的代码中有一个文件,定义了运行需要的环境依赖栈(就像pom.xml文件中定义了java应用的jar包依赖一样)
构建时,我们能根据整个文件,将所有软件依赖栈安装到一个镜像中,镜像是只读的。任何变更都会新产生一个新的镜像而不会更改原先的镜像。
如果我们能轻松的交付整个软件依赖栈, 是不是刚才说到的在不同环境调试的问题就能大大减少或者不复存在了?
这个YY过程正好被Docker 技术所覆盖, 我们看一下Docker 提供什么样的能力,能满足刚才的YY:
描述环境的能力
分层文件系统
Docker Registry
屏蔽Host OS 差异
这几种能力天然的帮助我们解决环境描述和传递的问题, 因此docker 能够做到Build Once, Run EveryWhere !
这是本文最核心的一章, 首先先看个例子,用docker做持续交付能带来的好处, 避免广告嫌疑,我用docker官方网站上的案例: BBC News
简单来说,一个全球新闻中心,内容的变化是最快的, BBC 公司内部的第一个问题是涉及10几种CI环境,26000 Jobs,500Dev人员
第二个核心问题是,CI任务需要等待,无法并行
最明显的改变,开发可以自己定义自己的开发语言,自己所需的build,集成测试环境,以及应用运行所需的依赖环境。
基本思路如下:
这个现在已经非常非常的简便了:
配置安装
安装云驱动
创建Docker运行环境
- `docker-machine create --driver aliyunecs mytest eval "$(docker-machine env mytest)"`
- `docker run -d nginx `
这句话可以翻译为: 如何将我的应用环境通过Dockerfile描述出来?
假如我的应用是一个Java Web 应用,需要Java运行环境和Tomcat 容器 ,那么大概我的环境所需下面这些东西:
启动tomcat
转化为成Dockerfile 的语言大致如下:
FROM buildpack-deps:jessie-curl
RUN apt-get update && apt-get install -y unzip openjdk-7-jre-headless=“$JAVA_DEBIAN_VERSION”
ENV LANG C.UTF-8
ENV JAVA_VERSION 7u91
ENV JAVA_DEBIAN_VERSION 7u91-2.6.3-1~deb8u1
ENV CATALINA_HOME /usr/local/tomcat
ENV PATH $CATALINA_HOME/bin:$PATH
RUN mkdir -p "$CATALINA_HOME"
WORKDIR $CATALINA_HOMEENV TOMCAT_VERSION 7.0.68
ENV TOMCAT_TGZ_URL https://xxxx/apache-tomcat-$TOMCAT_VERSION.tar.gz
RUN set -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*
EXPOSE 8080
CMD ["catalina.sh", "run"]
我们再看一个Nodejs的环境:
FROM ubuntu:14.04
COPY sources.list /etc/apt/sources.list
COPY .npmrc /root/.npmrc
RUN apt-get update && apt-get -y install curl automake tar libtool make wget xz-utils supervisor
ENV NODE_VERSION 0.12.5
ENV NPM_VERSION 2.11.3
RUN 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 clear
RUN 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/installVersion
CMD ["node"]
那么通过这两个例子,我们发现Dockerfile 还是写起来很麻烦的(其实也不麻烦,就是刚刚说的装要装的东西,配置,运行这三步)。 那么,刚刚说到每一个Dockerfile的第一行都是FROM另一个镜像, 那么思考一下:
通过这些思考,得到如下寻找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 的参数挂载进去运行就好了, 这样世界就变的很简单和清楚了。
编译/CI环境往往在公司规模越来越大的时候, 变得越来越麻烦, 因为不同语言,不同类型的应用对编译环境的要求都不一样。 就像刚才说到的BBC News的例子,一个大公司几十种编译环境的存在是很正常的。
那么,编译环境Docker化最大的好处是: 自定义,可扩展,可复制
试想一下, 假如你的应用编译只需要依赖标准的Jdk 1.7 和 Maven 2, 或者你是python应用编译过程其实只是需要安装依赖, 那么你可以跟很多人共用编译镜像。
但假如你的应用是Nodejs ,编译依赖特定的C库, 或者是C++之类的编译环境一定要和运行环境一致等等,那就需要定制自己的编译环境了。
这里我做一个最简单的用于编译java的镜像示例:
FROM registry.aliyuncs.com/acs-sample/centos:7
RUN yum update yum install -y open-jdk-1.7.0_65-49
COPY build.sh /build.sh
COPY settings.xml /home/apache-maven-2.2.1/conf/
ENTRYPOINT [“./build.sh"]
cd /ws ; mvn -e -U clean package -Dmaven.test.skip=true $@
cp target/*.war docker/ || exit 0
git clone [email protected]:tangrong.lx/myproject.git ~/myprj ; cd ~/myprj
docker run --rm -v `pwd`:/ws -v ~/.m2/repo:/buf build_maven:1.0
解释一下这个过程:
这里提两个小提示,都是经验之谈:
建议: build app 和 build docker image 建议分开进行, 即先进行应用本身的编译,再将输出物拷贝到镜像内(但脚本语言可以例外) 因为:
建议: Docker file 不要放到代码根目录下
简单思路: 运行Docker 镜像环境,安装测试所需依赖 , 运行Docker容器, 运行测试命令/脚本
用一个travis-ci 官方的例子来说明容器测试这件事,先看下面一个ruby的镜像:
FROM ubuntu:14.04
MAINTAINER carlad "https://github.com/carlad"
# Install packages for building ruby
RUN apt-get update
RUN apt-get install -y --force-yes build-essential wget git
RUN apt-get install -y --force-yes zlib1g-dev libssl-dev libreadline-dev libyaml-dev libxml2-dev libxslt-dev
RUN apt-get clean
RUN wget -P /root/src http://cache.ruby-lang.org/pub/ruby/2.2/ruby-2.2.2.tar.gz
RUN cd /root/src; tar xvf ruby-2.2.2.tar.gz
RUN cd /root/src/ruby-2.2.2; ./configure; make install
RUN gem update --system
RUN gem install bundler
RUN git clone https://github.com/travis-ci/docker-sinatra /root/sinatra
RUN cd /root/sinatra; bundle install
EXPOSE 4567
sudo: required
language: ruby
services:
- docker
before_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部分内可以看到过程是:
再来看一个python的例子,也很好理解:
language: python
python:
- 2.7
services:
- docker
install:
- docker build -t blog .
- docker run -d -p 127.0.0.1:80:80 --name blog blog
before_script:
- pip install -r requirements.txt
- pip install mock
- pip install requests
- pip install feedparser
script:
- 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语法结构
刚刚说了单独一个容器运行测试的情况, 但实际情况可能是即便是运行测试,也需要依赖proxy,依赖db,依赖redis等。 简单来说一般web应用会需要下面的结构:
这个结构很简单也很常见, 那在传统思想里,要运行UT或者集成测试,需要依赖的组件,都是去搭建。 搭一个mysql,配置mysql ,运行mysql 这种思路。
但是在docker的思想里,是声明的概念,就是说我需要一个mysql 去存一些数据进行测试, 这个mysql运行在哪里我根本不care 。 同样的思路告诉docker:
再举一个例子,假设一个php的Wordpress 应用, 除了应用本身还需要一个db ,他的编排文件(docker-compose.yml)如下:
web:
image: registry.aliyuncs.com/acs-sample/wordpress:4.3
ports:
- '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'
aliyun.routing.port_80: http://wordpress
aliyun.scale: '3'
db:
image: registry.aliyuncs.com/acs-sample/mysql:5.7
environment:
MYSQL_ROOT_PASSWORD: password
restart: always
labels:
aliyun.logs: /var/log/mysql
tips1: compose的好处还在于将配置从Dockerfile中提取出来,比如在测试/生产环境所需要的配置差别, 就可以放到compose里,在不同环境运行的时候换不同的compose文件即可,不用重复的编出不同环境用的docker image
tips2: 上面的wordpress示例里,启动多个应用容器之上,并没有用nginx做代理,因为阿里云容器服务提供了routing,省去了这部分, 如果是在企业内部,当启动三个应用,还是需要在compose里再声明一个nginx 或者 haproxy 在前面做应用代理和负载均衡的。
对一个公司/企业来说,将自身应用docker化,编译服务,测试集群docker化之后, 要跑通整个的过程,达到BBC News 这样的效果, 整个流程就如图中所示:
当然,如果你拥抱阿里云的服务, 并且自己的业务都在阿里云上。 你可以省去搭建这些基础设施, 使用云Code(https://code.aliyun.com) 来托管代码, 使用阿里云持续交付平台(https://crp.aliyun.com ) 来做CI/CD, 使用镜像仓库服务( https://dev.aliyun.com ),作为远程docker registry来管理镜像, 使用阿里云容器服务搭建和管理自己的运行环境 。 让一切变的更简单,让你更专注于自己应用的开发和docker化。
Docker技术是 DevOps 的最好诠释, DevOps不是开发去做运维的事情, 而是:
举例来说: Immutable,Copy on Write 这些思想在研发领域是耳熟能详的,好处大家秒懂。而在运维领域的Immutable,传统是怎么做的? 靠组织架构,权限管理。各种人为订制的机制,规范。 而docker 是用技术来解决了这个问题, 官方文档的介绍docker是 An open platform for distributed applications for developers and sysadmins, 很明显看到了DevOps有木有?
另外,从资源的角度上讲, docker化能够大大减少开发/测试环境的成本,测试或者调试的场景是当发起测试的时候才需要, 其他时候测试环境并不承担业务, 如果用虚拟机则白白的在那里空跑。 Docker 化之后可以在需要的时候随时拉起来整个环境,很快,并且不会出错, 因此阿里云持续交付平台CRP在会在将来提供集成测试环境,作为一项基础服务, 如果没有容器化,那提供整个服务的成本和可行性都是无法想象的
本文基本上表达了我在Qcon2016北京站上的分享,和阿云其他同事在各种运维,容器大会上分享里关于容器化持续交付的思想, 表达方式不同,但思想基本上是一致的。
本来想写成step by step的教程, 但考虑到docker 技术还是要了解它的特性和思想, 才能熟练运用,否则会出现把容器当vm来用等形式, 并且既然说了是完整版,所以分别用了java,ruby, nodejs, pyhton 和php 举例,当然都涉及的很浅,都是很简单的示例。
还是希望大家能对docker的特性有更深的理解, 讲其长处运用在自己的公司,自己的项目和自己的应用中。