大公司的程序员,容易产生的错觉之一就是,误将平台能力当作自己的能力。在大团队,我们不应仅关注自己的一亩三分地,更需要了解平台的各个环节。一方面,有助于更好地利用平台相关特性,另一方面,也为了自我技术更好地成长。本文,介绍了如何使用 Jekins、Docker、GitLab 搭建 Django 自动化开发部署流程。相关工具都是开源、可以拿来即用的。
1. 开发流程
在生产环境,Web 应用采用的是 K8S 多实例部署,状态服务 MySQL、RabbitMQ 采用的是集群部署。同时,还搭建了监控和日志采集、检索等周边。相比较于生产环境,这里的开发流程,我希望能尽量模拟生产环境,但也不需要太完善。毕竟,个人的时间和精力有限,当有需求时,逐步完善是一个不错的选择。这里使用 GitLab 作为开发仓库,使用 Jenkins 作为自动化引擎,部署使用的是 Docker 镜像。下面是一个简单的部署流程:当满足触发条件时,Jenkins 就会自动从 GitLab 拉取代码,制作 Docker 镜像,最终在服务器上运行 Django 实例。这样基本就可以,模拟整个部署流程。
2. GitLab 配置
选用 GitLab 是因为,其允许创建私有仓库。
首先得创建一个 GitLab 仓库,例如: ProjectA
在本地执行命令,生成远程访问需要的 SSH-KSY
1
|
ssh-keygen -o -t rsa -b 4096 -C "[email protected]"
|
在 https://gitlab.com/profile 页面,找到 【SSH Keys】,添加上面生产的 Key 值。
在 https://gitlab.com/profile 页面,找到 【Access Tokens】,完善信息,点击生成 PersonToken。
3. Jenkins 配置
Jenkins 可以对外直接提供 API ,也支持插件扩展。对于熟悉 Java 的团队,Jenkins 具有很强的吸引力。使用 Jenkins ,可以满足 CI、CD 各种各样的需求。这里的 Jenkines 主要用于部署服务。通过接收 GitLab 发送的提交信息,拉取最新的代码,执行脚本,完成部署。
这里需要使用到的 Jenkins 插件主要有:
Gitlab Authentication plugin
Gitlab Hook Plugin
Gitlab Plugin
在 【Jenkins】-> 【插件管理】 里面,搜索并安装插件,重启 Jenkins 生效。
在 【Jenkins】-> 【凭据】-> 【系统】-> 【全局凭据 (unrestricted)】 中,【添加凭据】 ,类型选择 【Gitlab API token】,API token 即为在第二章节中生成的 PersonToken。
创建【构建一个自由风格的软件项目】,如上图填入项目 ProjectA 的仓库地址。点击新增 SSH-Key 访问凭证。
如上图,在 【Build Triggers】中,勾选 【Build when a change is pushed to GitLab. GitLab webhook URL:】,获取 GitLab Webhook 地址。点击【Advanced】,生成 Token。
上一步中,获取到了两个值,GitLab Webhook 和 Token。如上图,在 Gitlab 项目仓库 【Settings】-> 【Integrations】 中填入相关信息。如果你的 Webhook 不是 https 链接,还需要去掉 【Enable SSL verification】 勾选。
构建配置实际上就是 Jenkins 拉取仓库代码之后,执行的脚本命令。这里直接执行项目下的 start.sh
脚本,即可。
4. Docker 镜像制作
使用 Docker 进行部署,有利于打包环境依赖,对服务进行水平扩展。在生产环境中,通常会采用多实例+集群的方式进行部署,以保障服务高可用。这里主要使用 docker-compose 编译镜像,编排 Django 运行时需要的容器。上图是整个仓库的目录结构,分为四个部分。
4.1 Django 项目代码
Django 使用的是默认目录结构,有两点需要注意:
通过环境变量,区分不同环境
在启动时,需要传入一个环境变量,提供给 Django 区分环境。在 settings.py 文件中:
1
2
3
4
|
if os.getenv('Env') == 'Production':
DEBUG = False
else:
DEBUG = True
|
Django DEBUG=False 模式下,无法转发静态文件,需要配置 WhiteNoise
1
2
3
4
5
6
7
8
|
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfile')
#
STATICFILES_DIRS = (
os.path.join(BASE_DIR, 'static'),
)
WHITENOISE_STATIC_PREFIX = '/static/'
MIDDLEWARE.append('whitenoise.middleware.WhiteNoiseMiddleware')
STATICFILES_STORAGE = 'whitenoise.storage.CompressedStaticFilesStorage'
|
4.2 数据存放
通过卷的形式,将数据目录挂载到 Docker,可以保存运行状态,避免因容器重启而丢失数据。
4.3 镜像配置
Python 镜像,需要安装基本的依赖包。Dockerfile 文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
FROM python:3.7-alpine
RUN apk update
&& apk add --no-cache --virtual bash
&& apk add gcc
&& apk add musl-dev
&& apk add linux-headers
&& apk add jpeg-dev
&& apk add zlib-dev
&& apk add mariadb-dev
&& apk add libffi-dev
COPY requirements.txt /requirements.txt
RUN pip install --upgrade pip
&& pip install -r requirements.txt
&& rm /usr/bin/mysql*
RUN mkdir /code
WORKDIR /code
|
requirements.txt 文件
1
2
3
4
5
6
7
8
9
|
django==2.1.2
gunicorn==19.9.0
mysqlclient==1.3.13
pymysql==0.9.2
whitenoise==4.1.2
celery==4.2.1
django-celery-results==1.0.4
django-celery-beat==1.3.0
redis==2.10.6
|
MySQL 镜像,用于提供 DB 访问服务。Dockerfile 文件:
1
2
|
FROM mysql:5.7
COPY my.cnf /etc/mysql/conf.d/my.cnf
|
my.cnf 文件:
1
2
3
4
|
[mysqld]
character-set-server=utf8
[client]
default-character-set=utf8
|
这里复用了 Python 镜像,提供 django 和 celery 容器环境。Django 通过 gunicorn 启动。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
|
version: '2'
services:
python:
build: ./docker/python
container_name: django
ports:
- 7900:7900
volumes:
- ./code:/code
command: >
bash -c "pip install -r requirements.txt
&& python manage.py migrate
&& python manage.py collectstatic --no-input
&& gunicorn news.wsgi -b 0.0.0.0:7900"
environment:
- Env=Production
depends_on:
- mysql
- redis
- rabbitmq
- celery
- mongo
networks:
- django-networks
mysql:
build: ./docker/mysql
container_name: mysql
ports:
- 3306:3306
volumes:
- ./data/mysql:/var/lib/mysql
environment:
- MYSQL_ROOT_PASSWORD=root
- MYSQL_DATABASE=news
networks:
- django-networks
redis:
image: redis:latest
container_name: redis
expose:
- "6379"
networks:
- django-networks
rabbitmq:
image: rabbitmq:3-management
container_name: rabbitmq
environment:
- RABBITMQ_DEFAULT_USER=guest
- RABBITMQ_DEFAULT_PASS=guest
ports:
- "5673:5673"
networks:
- django-networks
celery:
build: ./docker/python
container_name: celery
environment:
- Env=Production
depends_on:
- rabbitmq
- mysql
volumes:
- ./code:/code
command: >
bash -c "pip install -r requirements.txt
&& celery -A news.celery worker -l INFO
&& celery -A news.celery beat -l INFO --scheduler django_celery_beat.schedulers:DatabaseScheduler"
networks:
- django-networks
mongo:
image: mongo:latest
container_name: mongo
ports:
- "27018:27017"
volumes:
- ./data/mongo:/data/db
networks:
- django-networks
networks:
django-networks:
driver: "bridge"
|
start.sh 脚本,用于编译镜像,重启容器。
1
2
3
4
|
#!/bin/bash
docker-compose build
docker-compose stop
docker-compose up -d
|
5. 运行测试
向 Gitlab 仓库提交代码之后,Jenkins 流水线自动触发执行。至此,通过 7900 端口就可以访问服务了。如果,需要绑定域名,新增一条 Nginx Server 配置:
1
2
3
4
5
6
7
8
9
10
11
|
server {
listen 80;
server_name yourdomain.com;
location / {
proxy_pass http://127.0.0.1:7900;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
|
通过触发 Celery 后台任务,可以确定 Celery、RabbitMQ、MySQL 都能正常提供服务。
6. 与生成环境的对比
高并发的关键是无状态,利用集群为状态提供高性能、高可用的服务。这里的 MySQL、RabbitMQ、Redis 都是单实例,生产环境需要采用集群部署。另一方面,采用单机单实例部署是十分不可靠的,最好能使用多机多实例部署。
对于线上服务,日志是审计和排查错误的重要信息。使用 ELK + Filebeat 采集不同链路阶段、不同级别的日志,并将其按时间顺序串起来,十分有必要。
生产环境的一台主机可能会运行很多实例,需要对每个实例使用的资源,CPU、内存、IO 等进行隔离,避免相互影响。
这里我们通过 Nginx 新增一条 Server 配置,来新增一个服务。这里可以,通过 Etcd + Confd 自动化这一流程,可以参考之前的一篇文章。 如果采用 K8S,借助于 Ingress 能实现类似效果。
生产环境,当然也少不了对各种服务状态的监控和告警。可以使用 Prometheus + Grafana 等开源监控工具,快速搭建监控系统。