GeekPipe-基于 Gogs+Drone的持续集成之项目实战



  • 在DevOps 项目中,在 Pipeline 流水线中通常包含克隆代码、测试、构建、发布、部署、通知等步骤。基本流程如下,当然不同的语言或不同的需求下流程会有所差异:

    clone -> test -> build -> publish -> deploy -> notify

    包含开发的完整流程为:

    • 开发项目代码,包括 .drone.yml 文件和 Dockerfile 文件
    • 提交代码至 Gogs,通过 Gogs 的 webhook 触发 Drone 的 Pipeline
    • Drone 开始 Pipeline 的执行
    • clone 代码至容器
    • 测试
    • 编译代码,构建可执行文件(Java、Golang 等编译型语言需要,PHP 之类的脚本语言则不需要)
    • 将项目和运行环境打包成镜像,发布到 Registry(当然也可以使用 rsync 将编译后的文件Golang 等)或源码(PHP 等)部署到服务器,此时需要在目标服务器提前安装运行环境
    • 部署至测试环境/预生产环境/生产环境
    • 发送通知信息

    本章节基于 Gogs代码仓库 + 持续集成组件Drone 来演示 Drone 的工作流程。

    阅读要求

    • 熟悉 Docker 以及 Docker Compose

    • 熟悉 Git 基本命令

    • CI/CD 有一定了解

    新建 GitHub 应用

    登录 Gogs 后,从 Web 页面创建项目。例如本章节使用的 Gogs 项目地址是 https://gogs.finogeeks-test.com,创建完成后,Gogs允许在 Web管理页面添加文件,但是会比较麻烦。在这里,我们把仓库克隆到本地:

    git clone https://gogs.finogeeks-test.com/liangyi/test.git
    

    然后,在目录中创建 .drone.yml 和 app.go 两个文件。

    编写 app.go 文件

    package main
     
    import "fmt"
     
    func main(){ 
        fmt.Printf("Hello World!");
    }
    

    编写 .drone.yml 文件

    这里直接使用 Go 的官方镜像:

    
    workspace:
      base: /srv/drone-demo
      path: .
     
    pipeline:
      build:
         image: golang:alpine
         # pull: true
         environment:
           - KEY=VALUE
         secrets: [key1, key2]
         commands:
           - echo $$KEY
           - pwd
           - ls
           - CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
           - ./app
    

    其中,workspace 定义了可以在构建步骤之间共享的 volume 和工作目录。建议设置 workspace 以使用所需的 GOPATH。其中:

    • base :定义了一个可用于所有 Pipeline 步骤的共享的 volume,确保源代码、依赖项和编译的二进制文件在步骤之间持久保存和共享。
    • path :定义了用于构建的工作目录。代码会克隆到这个位置,并且构建过程中每个步骤都会使用这个工作目录作为默认的工作目录。
      注意:path 必须是相对路径,并且可以与 base 相结合,本例中 git 源代码将被克隆到 golang 容器中的 /srv/drone-demo目录中。。

    pipeline 指明构建所需的 Docker 镜像,环境变量,编译指令等。

    现在目录结构如下

    .
    ├── .drone.yml
    └── app.go
    

    提交代码

    $ git add ./
    $ git commit -m "test drone ci"
    [master 3425916] test drone ci
     2 files changed, 26 insertions(+)
     create mode 100644 .drone.yml
     create mode 100644 app.go
    $ git push origin master
    Counting objects: 4, done.
    Delta compression using up to 4 threads.
    Compressing objects: 100% (4/4), done.
    Writing objects: 100% (4/4), 638 bytes | 319.00 KiB/s, done.
    Total 4 (delta 0), reused 0 (delta 0)
    To https://git.finogeeks.club/liangyi/test.git
       668ec50..3425916  master -> master
    

    查看项目构建过程及结果

    打开 Drone 管理页面,即可看到构建结果。

    clone 阶段:
    + git init
    Initialized empty Git repository in /srv/drone-demo/.git/
    + git remote add origin https://gogs.finogeeks-test.com/liangyi/test.git
    + git fetch --no-tags origin +refs/heads/master:
    From https://git.finogeeks.club/liangyi/test
     * branch            master     -> FETCH_HEAD
     * [new branch]      master     -> origin/master
    + git reset --hard -q fb9405390d27e3a3ac58c1ae68869d42774a821e
    + git submodule update --init --recursive
    
    build 阶段:
    + go test
    PASS
    ok      gogs.finogeeks-test.com/liangyi/test  0.002s
    + go build
    

    当然我们也可以把构建结果上传到 Gogs,Docker Registry,云服务商提供的对象存储,或者生产环境中。

    参考链接

    以某工程为样例解析DRONE PIPELINE写法

    为Gogs Repository 添加 Drone Webhook

    标准配置样例 .drone.yml文件

    clone: # 克隆 (默认为git)
      git:
        image: plugins/git
        tags: true # git clone --tags 克隆tag
    pipeline: #管道
       docker_step1: #第一阶段,自上而下执行
        image: plugins/docker #采用插件
        repo: docker.finogeeks.club/business/xxx-knowledge # 容器image-name 需完整拼写
        registry: docker.finogeeks.club
        secrets: [ docker_username, docker_password ] #存储在repostiroy数据库中的密钥,由drone cli生成(drone secret add ...)
        when:
          branch: master
          event: [push, pull_request]
       docker_step2: #第二阶段
        registry: docker.finogeeks.club
        repo: docker.finogeeks.club/business/xxx-knowledge
        image: plugins/docker
        default_tags: true
        when: 
         event: tag  # step的条件化执行,仅当 tag 事件时执行这个阶段     
    

    JVM 工程配置样例(以 gradle 为例,适用于 maven/cargo)

    作为一个各方面很友好的 JVM 语言构建工具,gradle 支持手动指定任意绝对路径作为 jar 包的 cache 目录。只需要约定好各 JVM 项目在 drone-agent 宿主机的路径即可,并且可以跨工程复用,以下样例路径约定为 /tmp/gradle_cache

    在 CI 环境下,gradle 命令需要加--no-daemon参数
    通过 volumes 关键字映射全局缓存目录,冒号左侧是宿主机路径,右侧是 build 容器内的映射路径
    通过 --gradle-user-home 参数指定冒号右侧的路径,让构建可以利用缓存

    pipeline:
      build:
        image: docker.finogeeks.club/build/gradle:4.1
        pull: true
        volumes:
          - /mnt/data/drone/cache/gradle:/my_awesome_gradle_cache
        commands:
          - gradle --no-daemon --gradle-user-home=/my_awesome_gradle_cache clean assemble
      docker_latest:
        image: docker.finogeeks.club/drone/docker
        repo: docker.finogeeks.club/xxx/auth-center
        dockerfile: Dockerfile
        when:
          branch: master
          event: [push, pull_request]
      docker_tag:
        image: docker.finogeeks.club/drone/docker
        repo: docker.finogeeks.club/xxx/auth-center
        dockerfile: Dockerfile
        default_tags: true
        when:
          branch: master
          event: [tag]
    

    Node.js 工程配置样例

    npm 同样是一个优秀的 javascript 依赖管理与构建工具。唯一缺憾是 node_modules 目录无法全局跨工程复用。因此,对于 js 工程,可以通过 volumns 关键字把 node_modules 目录在宿主机上持久化。

    通过 volumes 关键字映射当前项目的依赖缓存目录,冒号左侧是宿主机路径,右侧是当前 drone 的工作空间(默认绝对路径是 /drone/src/<gogs_domain>/<gogs_group_name>/<gogs_repo_name>),我们只需要把右侧指向我们当前项目根目录下的 node_modules 路径即可。

    左侧路径因为上述原因,无法像 gradle 那样设置为全局唯一。各个项目之间需要约定好一个宿主机映射路径,以达到项目隔离的目的。以下样例约定为 /mnt/data/drone/cache/<lang>/<gogs_group_name>/<gogs_repo_name>
    构建成功一次后,依赖缓存到 node_modules 路径下,后续 cnpm i 这一步都可以飞速跳过

    pipeline:
      build:
        image: docker.finogeeks.club/base/node-base:8.9
        pull: true
        volumes:
          - /mnt/data/drone/cache/business/xxx-admin:/drone/src/git.finogeeks.club/business/xxx-admin/node_modules
        commands:
          - cnpm i # 所有 node-base/node-base-alpine 镜像下都预装有 cnpm 用于加速依赖解析
          - npm run build
      docker_latest:
        image: docker.finogeeks.club/drone/docker
        repo: docker.finogeeks.club/business/xxx-admin
        dockerfile: Dockerfile
        when:
          branch: master
          event: [push, pull_request]
      docker_tag:
        image: docker.finogeeks.club/drone/docker
        repo: docker.finogeeks.club/business/xxx-admin
        dockerfile: Dockerfile
        default_tags: true
        when:
          branch: master
          event: [tag]
    

    golang 工程配置样例

    2012 年才诞生的编程语言 golang,无论是工程,或者是工具链上,一如既往的保持跟语法同样糟糕的构建体验。鸡肋的 go get 命令结合谜一般的 GOPATH 设计,看起来像是 maven 拿掉依赖的版本管理,又阉割掉项目隔离的混合产物。

    首先官方的思路是让 GOPATH 作为开发工作目录,但这样的话做不到以项目为粒度的依赖隔离,无法解决多工程中某个共同依赖的版本冲突。如果构建方式选择把整个gopath目录包括依赖都提交到 git 也很不现实

    my-gopath/ (使用全局唯一的 gopath 目录)
    └── src/
    ├── github.com/
    │ └── project1/
    └── golang.com/
    └── project2/

    引入项目隔离后,工作路径只能设计成

    my-projects/ (GIT根目录兼GOPATH,每个项目使用独立的GOPATH)
    └── src/
    │ ├── github.com/ (公共库1 通过 .gitignore exclude)
    │ │ └── lib1 (公共库1下面的依赖1)
    │ │ └── lib2 (公共库1下面的依赖2)
    │ └── golang.com/ (公共库2 通过 .gitignore exclude)
    │ │ └── lib1 (公共库2下面的依赖1)
    │ │ └── lib2 (公共库2下面的依赖2)
    │ └── my_project/ (工程源码目录/go 源码)
    │ └── service/
    │ │ └── a.go
    │ │ └── b.go
    │ │ └── c.go
    │ └── main.go
    └── .drone.yml (其他工程文件与 src 目录同级)
    └── Dockerfile
    └── readme.md

    由于 src 目录既混有项目源码,也混有外部依赖。难以像 node 工程那样通过一行 volumns 关键字设置缓存。只能用 drone 提供的 cache 插件进行更细致的配置才能做到。过程中难免要感知很多细节:

    • volumns 关键字左侧是缓存插件用到的宿主机路径,右侧可以随意
    • mount关键字是一个数组,可以指向工作目录(workspace)下的若干个目录,进行缓存
    • pipeline 执行到 restore-cache 步骤时,从宿主机的缓存目录回复缓存到 mount 声明的位置
    • pipeline 执行到 rebuild-cache 步骤时,将本次构建对 mount 下文件的变更,同步到宿主机缓存目录(rsync)

    对于 golang 等语言,建议在 rebuild-cache 加 when: status: failure 条件,当失败时候也能缓存已经拉下的依赖

    pipeline:
      restore-cache:
        image: drillster/drone-volume-cache
        restore: true
        mount:
          - src/github.com
          - src/golang.org
          - src/gopkg.in
        volumes:
          - /mnt/data/drone/cache/go/xxx/netdisk:/cache
      build:
        image: docker.finogeeks.club/build/golang:1.9.2
        pull: true
        environment: # gopkg.in 下面的包需要挂代理才能拉回本地
          - http_proxy=http://127.0.0.1:3128
          - https_proxy=http://127.0.0.1:3128
        commands:
          - export GOPATH=`pwd`
          - cd src/netdisk
          - go get -v
          - go build
          - mv netdisk ../../  # build成功后,可执行文件位于当前路径的下两层子目录(./src/netdisk/) 需要mv向上移动两层。或者在 Dockerfile 里感知具体子路径,然后 COPY 到目标镜像里。
      rebuild-cache:
        image: drillster/drone-volume-cache
        rebuild: true
        mount:
          - src/github.com
          - src/golang.org
          - src/gopkg.in
        volumes:
          - /mnt/data/drone/cache/go/xxx/netdisk:/cache
        when:
          branch: master
          status:  [ failure, success ] # 增加 failure 状态是因为,在 golang 工程 80%+ 大概率不能一次成功拉回所有依赖。就算失败也要缓存掉已拉回的依赖免除失败后从头再来的痛苦。