作为一个全栈(全沾)工程师,你一个人负责着产品的方方面面,从前端的切图,到后端的数据库,你无所不惧,无所不能,可是日理万机的你,日以继夜的做出一个优秀的产品了之后,终于要生产环境部署服务,然后准备向用户开放了,可是你却开始开始犯难了,怎么才能够无痛持续部署呢?其实任何程序的运行都是基于特定环境,那么只要保证环境稳定,那么程序正常运行的基础才能保证。而且环境稳定的最佳保证方式我觉得是环境独立,而保证环境独立的最佳方式当然是容器技术了,这篇文章我将介绍我自己怎么实用 Docker 进行无痛部署的。
准备条件
本文针对的场景是: 个人或者小团队正在开发一个小产品。这样的场景下假设我们是没有使用任何的 CI, CD 服务,比如 Jenkins, Bamboo 等工具的。我所拥有的就是我的开发本(一台Mac, 或者 *nix 机器), 一台等待部署的生产环境云主机. 就这么简单.
- 开发环境的 Docker 环境配置
现在 Docker 已经了 Mac 版的,详细的安装方式可以参考这篇官方文档。 docker for mac, 安装好了启动好了。简单检查一下你的 Docker 是否安装妥当。
$ docker info # 看看整体信息
$ docker --version
$ docker-compose --version
$ docker-machine --version
项目分解
我们的产品终于开发好了,假设我们很好的对我们的产品进行了基于服务的结构分离,我们的整个产品 break down 成为了下面的几个部分:
- Mongo 数据库
- API Server 后端API
- 前端
基于这样的架构,我们需要的部署的服务将是这样的:
- Mongo
- API Server
- Nginx
这三者分别作为独立的服务以 Docker Container 的形式存在,其中 Mongo 作为数据库服务,API Server 显然是承担着后端 API 服务,Nginx 负责 Proxy 网站前端以及 API Server, 也就是 Nginx 基于 location 或者 server_name 来决定应该转发到我们的 Web 前端还是后端 API Server. 所以我们的 nginx.conf 可能是这样的:
user nginx;
worker_processes auto;
pid /run/nginx.pid;
events {
worker_connections 768;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE
ssl_prefer_server_ciphers on;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
gzip on;
gzip_disable "msie6";
server {
listen 80;
location / {
root /opt/sira/web/public;
}
location /v1/api {
proxy_pass http://api:5001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
}
如果我们是用 server_name 来控制的,那么 server section 可能是这样的。
server {
listen 80;
server_name example.com;
}
server {
listen 80;
server_name api.example.com;
}
准备 dockerfile
dockerfile 的最佳文档当然是官方文档了,docker file,当然如果你的技术栈和我的差不多的话: Mongo + Koa + React,全都是 ES6 的话,你可以参考我的dockerfile.
- Mongo 的dockerfile
FROM mongo
Mongo 的 dockerfile 其实什么也没有做,就是基于官方的 Mongo 镜像而已,独立写一份只是为了后面的拓展方便.
- API Server 的dockerfile
FROM alpine:3.4
RUN apk add --update nodejs
RUN apk add --update python
RUN apk add --update make
RUN apk add --update gcc
RUN apk add --update g++
RUN mkdir -p /opt/laivei-server
WORKDIR /opt/laivei-server
ADD server .
# Fix bug https://github.com/npm/npm/issues/9863
RUN cd $(npm root -g)/npm \
&& npm install fs-extra \
&& sed -i -e s/graceful-fs/fs-extra/ -e s/fs\.rename/fs.move/ ./lib/utils/rename.js
RUN npm install
RUN npm run transpile
EXPOSE 5001
CMD ["npm", "run", "start"]
API Server 的 dockerfile 基于的是 alpine 这个超轻量级的 linux, 然后编译运行我们的服务,并且暴露出特定端口.
- Nginx 的dockerfile
FROM nginx:alpine
COPY devops/nginx.conf /etc/nginx/nginx.conf
RUN apk add --update nodejs
RUN apk add --update python
RUN apk add --update make
RUN apk add --update gcc
RUN apk add --update g++
RUN mkdir -p /opt/sira/web
WORKDIR /opt/sira/web
COPY web .
RUN npm install
RUN npm run build
EXPOSE 80
Nginx 这个docker file 做了两件事情,设置 nginx.conf,然后编译前端。当然我们也可以把编译前端的这一部分工作放到单独的服务里面去做。至此,我们的整个产品的 docker 化就算完成了. 你可以验证你的 dockerfile 是否已经准备妥当了.
docker build -f devops/dockerfile.mongo .
docker run <image>
其他的服务类似.
docker-compose
至此呢,我们已经完成了所有的 dockerfile 了, 当然我们可以每一个服务都单独去运行,但是有一种更好的方式去组织我们的整个产品: Docker Compose. docker-compose 做两件事情:
- 定义整个产品的构成,也就是有哪些 container 来组成
- 运行整个产品
docker-compose 的输入是 docker-compose.yml 文件, 假设我们的产品代码的目录结构是这样子的:
$ ls
drwxr-xr-x crawler
drwxr-xr-x devops
drwxr-xr-x server
drwxr-xr-x web
基于我们的产品架构,我们的 compose file 是这样子的.
version: '2'
services:
mongo:
build:
context: ..
dockerfile: devops/dockerfile.mongo
volumes:
- /data/sira/db:/data/db
api:
build:
context: ..
dockerfile: devops/dockerfile.server
ports:
- '5001:5001'
depends_on:
- mongo
links:
- mongo
nginx:
build:
context: ..
dockerfile: devops/dockerfile.nginx
ports:
- '80:80'
depends_on:
- api
links:
- api
由此你可以大致了解到,我们可以在 compose file 中定义整个产品的各个组成部分的依赖逻辑,定义各个服务的构建信息,各个服务所需的 volumns, ports 等等。当然能做的还有更多,你可以参考这个这个官方文档: compose file.
一键部署
到此,我们整个产品就已经完成了 docker-based 的部署方案了,敲下一下命令,然后愉快的访问你的网站把.
docker-compose -f devops/compose.yml build && docker-compose -f devops/compose.yml up -d
当然,在实际实践中,我们可以写一个简单的部署脚本,然后准备一个简单的Makefile:
#!/bin/bash
env=$1
dev_host='root@106.185.xxx.yyy'
prod_host='root@139.162.aaa.bbb'
target_host=$dev_host
if [[ $env = "prod" ]];then
target_host=$prod_host
fi
ssh ${target_host} <<END
docker rmi \$(docker images --filter "dangling=true" -q --no-trunc)
rm -rf /tmp/Sira
git clone git@github.com:metrue/Sira.git /tmp/Sira
cd /tmp/Sira
docker-compose -f devops/compose.yml down
docker-compose -f devops/compose.yml build && docker-compose -f devops/compose.yml up -d
END
# Makefile
deploy-dev:
devops/deploy.sh dev
deploy-prod:
devops/deploy.sh prod
你可以看到我们将生产环境和开发环境进行了分离,而且每一次的部署都经历三步:
- 删掉无用的旧 docker images
- 重新clone代码做clean build
- 停止之前的services
- 最后build各项服务,而且启动
这其实就是一个很小但是五脏俱全的pipeline了,这样我们就可以在本地的部署了
make deploy-dev # 部署到 dev 环境
make deploy-prod # 部署到 prod 环境