Aengus 的技术小筑

Aengus | Blog

使用 Drone CI 自动部署服务

1567
2022-10-03

前言

平时写了几个服务自己用,在此之前一直都是手工方式部署,也就是本地打包、上传服务器、停止原有服务、运行新的服务,整个流程虽然就几个步骤,但是上传等待、每次手动敲命令行都是很麻烦的一件事,本着能自动就不要手动的原则,就决定搞一套CI/CD流程自动完成打包、部署的工作。看了网上介绍的一些CI平台,结合自己服务器性能有限、需求比较简单的情况下,最终选择了Drone

整个流程涉及到的框架及工具:

  • Nginx作为代理服务器;
  • SpringBoot;
  • Gradle作为构建工具;

安装Drone

Drone可以与多个Git平台进行搭配使用,包括Github、GitLab、Gitea、Gogs等,这里以Github为例。

创建OAuth Application

Drone搭配Github使用首先需要创建Github OAuth Application,在Github的Settings - Developer settings - OAuth Apps中创建新应用。注意Authorization callback URL一定要是http(s)://域名/login的格式,示例如下:

Create OAuth Application

创建完成后需要记住ClientIdClientSecret,在后面需要用到;

安装Drone与Drone-Runner

配置完成后正式安装Drone。Drone需要使用Docker安装,使用如下命令:

# 拉取镜像
docker pull drone/drone:2
# 运行
docker run \
  --volume=/var/lib/drone:/data \
  --env=DRONE_USER_CREATE=username:${GITHUB_USERNAME},admin:true \
  --env=DRONE_GITHUB_CLIENT_ID=${CLIENT_ID} \
  --env=DRONE_GITHUB_CLIENT_SECRET=${CLIENT_SECRET} \
  --env=DRONE_SERVER_HOST=${URL} \
  --env=DRONE_SERVER_PROTO=http \
  --env=DRONE_RPC_SECRET=d1dcf4f192da23plpm25k126zh87otv0 \
  --publish=7000:80 \
  --restart=always \
  --detach=true \
  --name=drone \
  drone/drone:2
  • DRONE_USER_CREATE是管理员用户名,username后面跟Github的用户名,如果没有设置就无法对仓库进行高级设置;
  • DRONE_GITHUB_CLIENT_ID是创建OAuth Application后的ClientId;
  • DRONE_GITHUB_CLIENT_SECRET是创建OAuth Application后的ClientSecret;
  • DRONE_SERVER_HOST需要与创建OAuth Application时的Authorization callback URL的域名相同(无/loginpath);
  • DRONE_SERVER_PROTO需要与创建OAuth Application时的Authorization callback URL的proto相同;
  • DRONE_RPC_SECRET将在后面配置Drone-Runner时使用,作为Drone与Drone-Runner通信的“凭证”,可以用openssl rand -hex 16生成一个;
  • --publish设置Docker的端口映射规则,这里将Drone的80端口映射到主机的7000端口;

接着安装Drone-Runner,命令如下:

# 拉取镜像
docker pull drone/drone-runner-docker:1
# 运行
docker run --detach \
  --volume=/var/run/docker.sock:/var/run/docker.sock \
  --env=DRONE_RPC_PROTO=http \
  --env=DRONE_RPC_HOST=drone \
  --env=DRONE_RPC_SECRET=d1dcf4f192da23plpm25k126zh87otv0 \
  --env=DRONE_RUNNER_CAPACITY=2 \
  --env=DRONE_RUNNER_NAME=drone-runner \
  --publish=7001:3000 \
  --restart=always \
  --name=runner \
  --link drone:drone \
  drone/drone-runner-docker:1

需要注意DRONE_RPC_SECRET需要与运行Drone时的配置保持一致;此外还要注意这里的DRONE_RPC_PROTO, DRONE_RPC_HOST以及--link,由于我们这里的Drone与Drone-Runner在同一台主机上运行,需要它们之间可以相互通信,因此采用了--link选项,使得Runner的DRONE_RPC_HOST能够访问到Drone;

上面的Runner叫做Docker Runner,是Drone-Runner最常用的一种,主要为在无状态容器中编译及测试的代码提供了优化。除了Docker Runner以外,还有Kubernetes Runner, Exec Runner, SSH Runner等,由于与本文内容无关,不在此详细介绍

运行后,使用docker logs runner命令查看日志,如果有successfully pinged the remote server信息,代表Runner运行成功。

配置Nginx

Nginx的配置比较简单,只需要在Nginx的配置文件中添加以下内容,然后重启Nginx即可:

# /etc/nginx/nginx.conf
server {
    listen 80;
    # URL与Authorization callback URL保持一致,如drone.test.com
    server_name ${URL};

    location / {
        # 与Docker配置的端口号保持一致
        proxy_pass http://localhost:7000;
        proxy_set_header HOST $host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        client_max_body_size 100m;
    }
}

重启Nginx:

systemctl restart nginx

对于比较新的Nginx版本,对于站点的配置可能在sites-enabled目录下,这时候在/etc/nginx/sites-enabled目录下创建drone.conf文件,然后将上面的配置复制到drone.conf文件中即可;

完成配置后,访问Drone URL,登录Github账号,针对仓库进行配置。

配置Repository

在Drone后台首页,可以看到Github账号下的所有仓库,点击我们想要启用CI的仓库,进入Settings Tab下,点击Activate Repository后,可以看到如下配置页面:

Repository Settings

我们需要将Trusted开关打开,以保证后续pipeline中的命令可以正常执行。此外还有Timeout和Configuration等配置项,Timeout指定Pipeline脚本超时时间,而Configuration则对应了脚本名称。

如果Settings中没有这么多配置项,可能是运行Drone时没有设置Github用户名或者用户名不对

Settings Tab下还有Secrets等配置,可以在Sercets中新建secret,比如密码等,其值可以在后续的pipeline中获取到。

然后切换到Builds Tab下,点击右上角的NEW BUILD按钮,在弹出的对话框中输入目标分支名,然后点击Create。当目标分支有新的commit时,Drone会自动根据脚本运行Pipeline。

配置Pipeline

完成配置仓库后,我们需要手工编写Drone脚本,告诉Drone当有新提交时需要做什么。对于大部分服务,都需要 构建 - 部署 步骤。我们分开进行介绍。

构建任务

在需要开启CI的仓库的根目录下,创建.drone.yml文件,输入以下内容:

kind: pipeline
type: docker
name: MyService

steps:
  - name: build
    image: gradle:jdk11
    volumes:
      - name: service-root
        path: /app/build
      - name: gradle-cache
        path: /root/.gradle
    commands:
      - ./gradlew bootJar
      - cp /drone/src/app-api/build/libs/*.jar /app/build/my-service-latest.jar

volumes:
  - name: service-root
    host:
      path: /root/service
  - name: gradle-cache
    host:
      path: /root/.gradle
  • kind 代表脚本的类型,这里指定为pipeline脚本,除了pipeline脚本还有secret和signature脚本;
  • type 定义pipeline的类型,与runner类型,除了Docker外,还有Kubernetes, Exec, SSH等;
  • name 为pipeline的名称;
  • steps 定义了pipeline的步骤,其由多个如上面形式的步骤组成:
    • name 为此步骤的命名;
    • image 指定了这次步骤所依赖的Docker镜像;
    • volumes 将Docker的路径与主机路径进行映射,以能在文件系统上相互访问;
    • commands 为此步骤需要运行的命令;
  • volumes 挂载了主机的卷,我们可以在stepsvolumes中直接使用这里定义的卷名称;

在上面的脚本中,我们进行了如下的操作:

  1. 定义pipeline名称为MyService
  2. 接着在steps中,定义了名为build的步骤,指定使用gradle:jdk11作为此步骤使用的镜像;
  3. 在volumes中,将主机的/root/service路径与Docker容器内的/app/build路径以及主机的/root/.gradle路径与Docker容器内的/root/.gradle路径做了映射,这样便可以直接访问主机指定目录下的文件系统;
  4. 在commands中,首先使用gradlew命令打包SpringBoot程序,然后将打包后的Jar文件复制到/app/build目录下,由于我们将/app/build路径与主机的/root/service做了映射,因此打包后的Jar文件会被直接复制到主机的/root/service目录下;

在复制命令中,cp的源目录是/drone/src/…,/drone/src是Drone的默认工作目录,代码会直接被clone到此目录下,可以在.drone.yml中通过workspace:path的方式进行配置

完成后,我们将.drone.yml进行提交并推送到Github上,打开Drone后台,可以看到Drone已经自动开始了pipeline任务:

Drone Pipeline

Drone默认为pipeline增加clone step,并在steps之前运行,特殊配置我们会在下文进行介绍

经过如上的操作,我们便完成了构建任务;

部署任务

由于Drone运行在Docker中,无法直接访问主机,因此部署任务需要SSH来“绕行”。

仍然是在.drone.yml文件中,在steps下增加以下内容:

steps:
  - name: build
    # ...
  
  - name: deploy
    image: appleboy/drone-ssh
    settings:
      host: 123.12.34.56
      username: root
      password:
        from_secret: ssh_password
      port: 22
      command_timeout: 5m
      script:
        - cd /root/service
        - mv my-service.jar my-service.backup.jar
        - mv my-service-latest.jar my-service.jar
        - chmod +x run.sh
        - ./run.sh
    when:
      event:
        - promote
  • image 指定此步骤需要使用SSH镜像;
  • settings 针对SSH进行配置:
    • host 指定主机IP地址;
    • username SSH用户名;
    • password SSH密码,这里我们使用from_secret的方式从Secrets中取值(需要在仓库的Settings配置);
    • port SSH端口号;
    • command_timeout 远程操作命令超时时间;
    • script 是需要SSH运行的脚本,类比于steps的commands;
  • when 指定了此步骤运行的时机,event=promote指当构建任务完成后,手动在这次pipeline的后台点击右上角三个点 - Promote时运行;

script中,首先将当前目录切换至构建任务目标Jar包所在目录,然后执行一些重命名操作,并将运行脚本修改为execuable,最后运行脚本;

以运行Jar包为例,脚本内容如下:

#!/bin/sh
originalPid=$(ps x | grep "my-service" | grep -v grep | awk '{print $1}')
if [[ -n "$originalPid" ]]
then
    echo "Running my-service PID=$originalPid"
    echo "Stop Running my-service..."
    kill $originalPid
else
    echo "No Running my-service Found..."
fi
newPid=$(nohup java -jar my-service.jar >/dev/null 2>&1 &)
echo "Start my-service PID=$newPid"

以上便是构建和部署任务的相关配置,下面我们介绍一些针对Pipeline的其他配置。

Clone配置

上文中讲到Drone会自动执行Clone步骤,如果我们需要某些自定义配置,也可以在脚本中进行声明:

kind: pipeline
type: docker
name: default

clone:
  depth: 50
  retries: 3
  • depth 同Git的--depth tag;
  • retries 失败的重试次数,默认不会重试;

Drone默认clone不会fetch tag,如果需要可以通过以下配置:

steps:
- name: fetch
  image: alpine/git
  commands:
  - git fetch --tags

同样,Drone也不会拉取submodules,如需要,则使用以下配置:

steps:
- name: submodules
  image: alpine/git
  commands:
  - git submodule update --init --recursive

如果仍需要比较复杂的Git逻辑,可以直接禁用Drone的clone,自己定义Git逻辑:

clone:
  disable: true

steps:
- name: clone
  image: alpine/git
  commands:
  - git clone https://github.com/octocat/hello-world.git .
  - git checkout $DRONE_COMMIT
  • DRONE_COMMIT 是触发此次pipeline的commit id;

Trigger配置

如果想要pipeline在某些特殊的情景下运行,可以通过Drone提供的trigger进行配置:

steps:
- name: build
  # ...

trigger:
  branch:
  - master
    include:
    - master
    - feature/*
    exclude:
    - alpha/*
  ref:
  - refs/heads/master
  - refs/heads/**
  - refs/pull/*/head
  event:
  - cron
  - custom
  - push
  - pull_request
  - tag
  - promote
  - rollback
  • branch 当某个pull request以此分支作为目标分支时触发;
  • ref 基于Git引用有更新时触发;
  • event 当遇到某个事件时触发;

refevent都可以使用include与exclude

参考文档