目录

    大公司的程序员,容易产生的错觉之一就是,误将平台能力当作自己的能力。在大团队,我们不应仅关注自己的一亩三分地,更需要了解平台的各个环节。一方面,有助于更好地利用平台相关特性,另一方面,也为了自我技术更好地成长。本文,介绍了如何使用 Jekins、Docker、GitLab 搭建 Django 自动化开发部署流程。相关工具都是开源、可以拿来即用的。

    1. 开发流程

    在生产环境,Web 应用采用的是 K8S 多实例部署,状态服务 MySQL、RabbitMQ 采用的是集群部署。同时,还搭建了监控和日志采集、检索等周边。

    相比较于生产环境,这里的开发流程,我希望能尽量模拟生产环境,但也不需要太完善。毕竟,个人的时间和精力有限,当有需求时,逐步完善是一个不错的选择。

    这里使用 GitLab 作为开发仓库,使用 Jenkins 作为自动化引擎,部署使用的是 Docker 镜像。

    下面是一个简单的部署流程:

    当满足触发条件时,Jenkins 就会自动从 GitLab 拉取代码,制作 Docker 镜像,最终在服务器上运行 Django 实例。这样基本就可以,模拟整个部署流程。

    2. GitLab 配置

    选用 GitLab 是因为,其允许创建私有仓库。

    • 创建仓库

    首先得创建一个 GitLab 仓库,例如: ProjectA

    • 添加远程访问 SSH-KEY

    在本地执行命令,生成远程访问需要的 SSH-KSY

    $ ssh-keygen -o -t rsa -b 4096 -C "mail@chenshaowen.com"
    

    https://gitlab.com/profile 页面,找到 【SSH Keys】,添加上面生产的 Key 值。

    • 生成个人仓库访问的 Token

    https://gitlab.com/profile 页面,找到 【Access Tokens】,完善信息,点击生成 PersonToken。

    3. Jenkins 配置

    Jenkins 可以对外直接提供 API ,也支持插件扩展。对于熟悉 Java 的团队,Jenkins 具有很强的吸引力。使用 Jenkins ,可以满足 CI、CD 各种各样的需求。

    这里的 Jenkines 主要用于部署服务。通过接收 GitLab 发送的提交信息,拉取最新的代码,执行脚本,完成部署。

    • 安装 GitLab 相关插件

    这里需要使用到的 Jenkins 插件主要有:

    1. Gitlab Authentication plugin
    2. Gitlab Hook Plugin
    3. Gitlab Plugin

    在 【Jenkins】-> 【插件管理】 里面,搜索并安装插件,重启 Jenkins 生效。

    • 添加 Gitlab 访问凭据

    在 【Jenkins】-> 【凭据】-> 【系统】-> 【全局凭据 (unrestricted)】 中,【添加凭据】 ,类型选择 【Gitlab API token】,API token 即为在第二章节中生成的 PersonToken。

    • 新建流水线,配置仓库

    创建【构建一个自由风格的软件项目】,如上图填入项目 ProjectA 的仓库地址。点击新增 SSH-Key 访问凭证。

    • 配置 Jenkins 触发规则

    如上图,在 【Build Triggers】中,勾选 【Build when a change is pushed to GitLab. GitLab webhook URL:】,获取 GitLab Webhook 地址。点击【Advanced】,生成 Token。

    • GitLab 配置 Webhook

    上一步中,获取到了两个值,GitLab Webhook 和 Token。

    如上图,在 Gitlab 项目仓库 【Settings】-> 【Integrations】 中填入相关信息。如果你的 Webhook 不是 https 链接,还需要去掉 【Enable SSL verification】 勾选。

    • Jenkins 构建配置

    构建配置实际上就是 Jenkins 拉取仓库代码之后,执行的脚本命令。这里直接执行项目下的 start.sh 脚本,即可。

    4. Docker 镜像制作

    使用 Docker 进行部署,有利于打包环境依赖,对服务进行水平扩展。在生产环境中,通常会采用多实例+集群的方式进行部署,以保障服务高可用。

    这里主要使用 docker-compose 编译镜像,编排 Django 运行时需要的容器。

    上图是整个仓库的目录结构,分为四个部分。

    4.1 Django 项目代码

    Django 使用的是默认目录结构,有两点需要注意:

    1. 通过环境变量,区分不同环境

    在启动时,需要传入一个环境变量,提供给 Django 区分环境。在 settings.py 文件中:

    if os.getenv('Env') == 'Production':
        DEBUG = False
    else:
        DEBUG = True
    
    1. Django DEBUG=False 模式下,无法转发静态文件,需要配置 WhiteNoise
    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 文件:

    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 文件

    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 文件:

    FROM mysql:5.7
    COPY my.cnf /etc/mysql/conf.d/my.cnf
    

    my.cnf 文件:

    [mysqld]
    character-set-server=utf8
    [client]
    default-character-set=utf8
    
    • 容器编排

    这里复用了 Python 镜像,提供 django 和 celery 容器环境。Django 通过 gunicorn 启动。

    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 脚本,用于编译镜像,重启容器。

    #!/bin/bash
    docker-compose build
    docker-compose stop
    docker-compose up -d
    

    5. 运行测试

    向 Gitlab 仓库提交代码之后,Jenkins 流水线自动触发执行。

    至此,通过 7900 端口就可以访问服务了。如果,需要绑定域名,新增一条 Nginx Server 配置:

    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 等开源监控工具,快速搭建监控系统。