运维 GitLab, Docker, Ruby on Rails CI/CD 实践

freefishz · October 17, 2018 · Last by HDJ replied at October 21, 2018 · 12200 hits
Topic has been selected as the excellent topic by the admin.

前言

一直以来公司的开发、测试及生产环境都基于实体机,CI/CD 通过Jenkins完成。

最近公司的运维工程师离职了,新的还未觅得。另外,公司的业务正朝着多线方向发展,未来计划采用基于SeviceMesh的微服务方式部署到K8S平台。先将环境迁移到Docker,对于零运维经验的人,看上去是一个不错的开始。

本文假设GitLab已成功搭建运行,若想了解如何搭建 GitLab,请参考这篇文章

1. GitLab CI/CD工作流

先来看一张官网的图: 图1.1 GitLab CI/CD流程图

说明:

  • GitLab CI/CD的PIPELINE是由一系列stage构成的,如图中 CI PIPELINE 的BUILDUNIT TESTINTEGRATION TESTS
  • 每个stage又包含一系列任务,如INTEGRATION TESTS包含了 3 个任务;
  • 默认上一个stage的所有任务都成功执行,才会执行下一个stage中的任务(可自定义执行规则);
  • 系统默认设置了 3 个stagebuildtestdeploy(可自定义,见下面配置文件);
  • 主要配置都由项目根目录下的.gitlab-ci.yml设定;

再来看一下 GitLab 的执行过程: 图1.2 GitLab执行过程图

说明:

  • 每个项目根目录都有一个.gitlab-ci.yml配置文件;
  • 配置文件的主要内容包括:
    • 定义一系列任务;
    • 设置任务在哪个stage执行;
    • 设置任务应该由哪个GitLab Runner负责执行;
    • 设置GitLab Runner应该使用什么执行环境执行该任务,如某个 docker 镜像;
    • 设置任务依赖的git分支;
    • 设置任务的触发条件,如代码提交或手工触发;
  • GitLab Runner需要在使用前先在 GitLab 注册:
    • 一般每个GitLab Runner都是相互独立的服务器或虚拟机,如本地办公室的开发服务器、云端的测试服务器、专门用于打包构建 app 的黑苹果电脑、专门用于某个项目的服务器等;
    • GitLab Runner根据任务配置,为任务准备执行环境,如shelldockerk8s等;
    • GitLab Runner注册时可以设置一到多个tag
    • GitLab通过配置文件中任务设置tag,调度相应的GitLab Runner运行任务;
    • 若多个 GitLab Runner匹配执行条件,系统会随机选择一个;
    • 若没有相匹配的GitLab Runner,或所有匹配的GitLab Runner都在忙,则任务会处于等待状态;
    • GitLab Runner可设置同时执行任务的数量;

2. 安装、注册 GitLab Runner

  • 本示例使用Docker运行GitLab Runner
  • 安装完后还需要在 GitLab 里注册,才能使用;
  • 本示例采用alpine-10.7.2

示例脚本如下:

docker run --detach \
  --name gitlab-runner \
  --restart always \
  --volume /opt/data/gitlab-runner/config:/etc/gitlab-runner \ # 配置文件
  --volume /var/run/docker.sock:/var/run/docker.sock \         # 支持dind(Docker in Docker, 在Docker中构建Docker镜像)
  gitlab/gitlab-runner:alpine-v10.7.2

GitLab Runner 跑起来之后,运行以下脚本完成注册。详情参考这里

docker exec -it gitlab-runner gitlab-runner register \
  --name shared-runner \                              # 给GitLab Runner起个名
  --url "https://gitlab.com/" \                       # GitLab服务器地址
  --registration-token "PROJECT_REGISTRATION_TOKEN" \ # GitLab注册Token,可在GitLab管理界面获得
  --description "ruby-2.5" \                          # GitLab Runner的一些描述
  --tag-list nodejs,java,ruby \                       # 给GitLab Runner打上标签,配置文件可根据标签指定某个Runner来执行任务
  --run-untagged true \                               # 是否可以运行未指定标签的任务
  --locked false \                                    # 是否锁定到某个项目
  --executor "docker" \                               # 任务执行环境
  --docker-volumes /opt/data/ws:/share:rw \           # 使用docker执行环境时,自动挂载的目录(可选)
  --docker-image ruby:2.5                             # 使用docker执行环境时,设置默认执行镜像

说明:

  • 任务执行环境:每种环境支持的功能有所区别。详情参考这里
  • 自动挂载目录:根据需求自行决定是否需要,一些通用的脚本和工具可放在这里。

注册完成后可以 GitLab 管理界面看到注册成功的 GitLab Runner,如下图所示: 图2.1 GitLab Runner 列表

同时,在/opt/data/gitlab-runner/config/目录下,可以找到config.toml配置文件:

concurrent = 1           # 任务并发数
check_interval = 0

[[runners]]
  name = "rails builder"
  url = "https://gitlab.com/"
  token = "PROJECT_REGISTRATION_TOKEN"
  executor = "docker"
  clone_url = "https://gitlab.com/"
  [runners.docker]
    tls_verify = false
    image = "ruby:2.5"
    privileged = false
    disable_cache = false
    volumes = ["/var/run/docker.sock:/var/run/docker.sock", "/opt/data/ws:/share:rw"]
    shm_size = 0
  [runners.cache]

3. 定义.gitlab-ci.yml

# 重新定义stages,可选,也可以使用默认的;
stages:
  - compile
  - build
  - deploy

# 将一些通用设置抽出来;
.general: &general
  only:
    - dev                           # 设置任务依赖的 git 分支
  when: manual                      # 设置手工触发
  tags:
    - ror                           # 设置哪个GitLab Runner来执行任务
  image: gitlab.com/builder:ror-v1  # 设置任务的执行环境,这里为docker镜像
  script:                           # 设置任务具体内容,依次列出shell脚本
    - /share/script/$CI_JOB_NAME.sh

# 编译任务,任务名称可任意设置
# 修订:将.bundle目录加入artifacts,build阶段就不需要再次bundle install了
compile:
  <<: *general        # 引用通用设置
  stage: compile      # 设置任务在哪个stage执行
  artifacts:          # 任务执行完毕后,哪些内容需要打包,供下载或给下一个任务使用
    expire_in: 12h    # 过期时间,过期后自动删除打包内容
    paths:
    - public/assets/  # rails项目编译后的assets
    - public/packs/   # rails项目中用到了react,这是编译后的react内容
    - .bundle/           # bundle install后的配置文件 < 修订:新增>

# 构建docker镜像任务
build:
  <<: *general
  stage: build
  image: docker:latest  # 使用dind(Docker in Docker)的方式来构建镜像 

# 部署任务
deploy:
  <<: *general
  stage: deploy
  dependencies: []      # 依赖任务列表

配置文件提交到GitLab后,在管理界面 -> CI/CD -> Pipelines可以看到如下所示:

图3.1 GitLab CI/CD Pipeline

图3.2 Pipleline详情

3.1 图例说明

  • 每次代码提交都会产生一条新的Pipeline,每条都有一个编号,如图中 1 标注;
  • 点击Pipeline编号可以看到详情,如图 3.2 所示。在图中可以手工触发相应的任务;
  • 图中第一条已经手工触发运行过了,状态是passed,第二条状态是skipped(还未手工触发);
  • 配置文件中设置了 3 个stage,如图中 2 标注;
  • 由于compile任务设置了artifacts,图中 3 标注有可以点击下载的选项;
  • 图中 3 标注的左边可以手工触发任务执行;

3.2 script 说明

将 shell 脚本依次列在script的优缺点:

  • 优点:可以将脚本变更记录纳入版本控制;
  • 缺点:不方便调试,每次修改都需要先提交;

为了方便调试,示例中将所有脚本都写在单独的 shell 文件中。

前面提到运行GitLab Runner时,我们配置了/opt/data/ws:/share:rw。该配置会自动将主机的/opt/data/ws目录自动挂载到任务运行环境(Docker)的/share目录。因此,可以将所有 shell 脚本都放在本地/opt/data/ws

GitLab自带了一些环境变量供配置文件使用。示例中的$CI_JOB_NAME就是其中的一个,该变量会自动赋值为任务名称。例如,在compile任务中,该变量为compile,执行compile.sh。因此,可以在主机的/opt/data/ws目录下创建三个 shell 文件compile.shbuild.shdeploy.sh,分别用于执行相应的任务。

3.3 artifacts 与 dependencies 说明

  • 每个任务都可以通过artifacts声明,任务执行完毕后,哪些内容需要打包暂存,供下载或给下一个任务使用;
  • 若没有特别声明,每个任务都会默认继承前面任务的所有artifacts
  • 可以通过dependencies声明,依赖哪些任务的的artifacts
  • 若不想继承任何artifacts,可声明dependencies为空,如deploy任务所示;

运行compile任务,在任务结束时,可以看到如下关于artifacts的信息:

...
Uploading artifacts...
public/assets/: found 631 matching files           
public/packs/: found 15 matching files             
Uploading artifacts to coordinator... ok            id=7282 responseStatus=201 Created token=ExCbBThh

运行build任务,在任务开始前,可以看到如下关于artifacts的信息:

Downloading artifacts for compile (7282)...
Downloading artifacts from coordinator... ok        id=7282 responseStatus=200 OK token=ExCbBThh
...

4. 构建 Rails 编译环境

  • 将编译环境和运行环境分开,主要是想得到一个小而干净的镜像;
  • 使用ubuntu 18.04作为编译环境,默认可安装ruby 2.5
  • 安装编译工具包需要配置时区,因此顺道安装设置了时区;
  • 安装nodejsyarn(开发用到两者了);
FROM ubuntu:18.04
MAINTAINER jacky.zhang <[email protected]>

# 安装并配置ruby、bundler
RUN apt update && \
    apt install -y ruby && \
    gem sources --add https://gems.ruby-china.com/ --remove https://rubygems.org/ && \
    gem install bundler --no-rdoc --no-ri && \
    bundle config mirror.https://rubygems.org https://gems.ruby-china.com

ENV DEBIAN_FRONTEND=noninteractive # 避免设置时区有交互,打断安装过程

# 安装必备软件包(根据业务要求裁剪),并设置时区
RUN apt-get install -y build-essential libpq-dev libmysqlclient-dev imagemagick ghostscript apt-transport-https curl git ruby-dev tzdata && \
    ln -fs /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
    dpkg-reconfigure -f noninteractive tzdata

# 安装并配置nodejs、yarn
RUN curl -sL https://deb.nodesource.com/setup_8.x | bash - && \
    curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
    echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
    apt-get update && \
    apt-get install -y nodejs yarn && \
    sh -c 'echo https://registry.npm.taobao.org > ~/.npmrc'    

5. 构建 Rails 运行环境

一直以来都使用mina部署 Rails 服务,服务器环境为:Ubuntu + Nginx + Passenger。该环境稳定运行了好多年,因此想继续沿用。

几点说明:

  • 没有使用ruby:2.5-alpine来做基础镜像的原因:
    • 构建Passenger过程相对复杂,需要从源码编译;
    • 构建完的镜像也没小多少(也许有优化空间?);
    • ubuntu环境相比较熟悉;
  • 设置系统时区:上海
  • 安装msyqlpostgresql驱动(业务同时需要连接两个数据库);
  • 安装imagemagick支持图像处理;
  • 安装nodejs支持(应该可以去掉,未验证);
  • 安装cron定时任务服务(业务需要);
  • nginx需要单独安装,否则Pas
  • Passenger官方安装文档中说明,需要先安装ruby。经验证,最新Passenger自带ruby 2.5运行环境。若满足业务需求,可以不用单独安装ruby
  • 构建完镜像大小约400M,若清理一下/var/lib/apt/lists/,还可以减掉40M

Dockerfile如下:

FROM ubuntu:18.04
MAINTAINER jacky.zhang <[email protected]>

ENV DEBIAN_FRONTEND=noninteractive # 避免设置时区有交互,打断安装过程

# 安装必备软件包(根据业务要求裁剪),并设置时区;
RUN apt-get update && \
    apt-get install -y nginx cron imagemagick ghostscript libpq-dev libmysqlclient-dev nodejs tzdata && \
    ln -fs /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
    dpkg-reconfigure -f noninteractive tzdata

# 安装Passenger,自带ruby 2.5;
RUN apt-get install -y dirmngr gnupg && \
    apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 561F9B9CAC40B2F7 && \
    apt-get install -y apt-transport-https ca-certificates && \
    sh -c 'echo deb https://oss-binaries.phusionpassenger.com/apt/passenger bionic main > /etc/apt/sources.list.d/passenger.list' && \
    apt-get update && \
    apt-get install -y libnginx-mod-http-passenger && \
    apt-get remove -y dirmngr gnupg && \
    apt-get autoremove -y && \
    apt-get clean

# 安装并设置bundle
RUN gem sources --add https://gems.ruby-china.com/ --remove https://rubygems.org/ && \
    gem install bundler --no-rdoc --no-ri && \
    bundle config mirror.https://rubygems.org https://gems.ruby-china.com

EXPOSE 80

# 默认nginx和cron服务不开机启动;
# ubuntu 18设置开机启动相对复杂,简单起见,就写在入口脚本里了;
ENTRYPOINT service nginx start && service cron start && tail -f /dev/null

6. 部署脚本

6.1 compile.sh

#!/bin/bash

echo 'compiling starts ...'

echo 'bundle: link and install '
# 为了避免每次都安装所有gem,将bundle缓存在公共目录;
ln -fs /share/env/bundle vendor/bundle 
bundle install --deployment --clean

echo 'compile assets'
# 为了避免每次都所有安装npm包,将npm包缓存在公共目录;
# 注意:
# 这里不能使用link,否则nodejs编译会报错,或出现莫名其妙的bug;
# 具体原因应该是某些npm包的路径规则引起的;
mv /share/env/node_modules node_modules 
RAILS_ENV=production bundle exec rails assets:precompile
mv node_modules /share/env/node_modules

echo 'compiling ends.'

6.2 build.sh

#!/bin/sh

echo 'building docker image starts ...'

echo 'copy bundle'
# 将缓存的bundle拷贝过来
cp -rf /share/env/bundle vendor/bundle 

echo 'build start ...'
docker build -t test:latest .

echo 'remove untaged images' 
# 如有必要移除未打标签的镜像
docker rmi -f $(docker images | grep none | awk '{print $3}')

echo 'building ends.'

项目根目录的 Dockerfile 如下:

FROM gitlab.com/passenger:latest
MAINTAINER jacky.zhang <[email protected]>

# passenger 工作目录
ENV APP_ROOT=/var/www/app
RUN mkdir -p $APP_ROOT

 # passenger默认使用www-data用户
COPY --chown=www-data . $APP_ROOT
WORKDIR $APP_ROOT

# 再运行一次bundle安装,会在项目根目录生成一些配置文件(可以在编译时缓存,以后优化) 
# 如果用到whenver,就更新一下吧
# RUN RAILS_ENV=production bundle install --deployment && \
#     RAILS_ENV=production bundle exec whenever --update-crontab
# 修订:删除RAILS_ENV=production bundle install --deployment
RUN RAILS_ENV=production bundle exec whenever --update-crontab

6.3 deploy.sh

部署过程主要通过 ssh 到远程服务器来完成:

  • 先做备份;
  • 移除旧的docker容器;
  • 用新的镜像重新部署,使用本地的配置文件,如 nginx、项目的环境变量等;
  • 部署完毕,根据需要运行db:migration,或重启sidekiq服务等;

上述任务可以写在一个 shell 脚本中完成,过程相对简单这里略过;

7. 结束

本文记录了从零经验开始学习使用 GitLab 搭建 CI/CD 的一些经验,希望能帮到新入门的运维人员。 后续,正在进行 rancher + k8s + istio 的 ServiceMesh 实践,有时间话再来分享。

huacnlee mark as excellent topic. 17 Oct 17:53

我想问下日志文件是怎么处理的

Reply to Terry.Shi

因为 Docker 自带 Fluented 驱动,我们用 EFK(ElasticSearch + Fluented + Kibana)组合。先将日志文件重定向到标准输出,然后运行容器时指定 log-driver 为 fluented 即可。

好像流利说和 Strikingly 内部也用的是 gitlab-ci

问一下 gitlab ci 可以脱离 gitlab 使用吗

Reply to huacnlee

谢谢指正! 刚做了一下测试,对 compile 和 build 部分做了 2 处修订:

  • 编译时将.bundle 目录加入缓存(这是 bundle 的配置文件目录);
  • 构建镜像时去掉 bundle install(没有必要了);

也许可以先将vendor/bundle先 ADD 进去,利用 Docker build cache 缩短时间。

Reply to freefishz

不能把 vendor/bundle 加进去,gem 安装的时候和系统有关

很赞!我很喜欢这种 docker+ 自己写脚本部署的方式

Reply to huacnlee

有 c 扩张就 gg 了吧。

12 Floor has deleted
13 Floor has deleted
Reply to hooopo

可以的,debug 的时候在本地跑 gitlab runner http://dsh.li/blog/15029410970549.html

Reply to pynix

不会的,编译环境和运行环境几乎一样,只是运行环境少了编译工具,因此运行环境打出的镜像会小很多(大概小 300M)。我们的项目里有很多 gem 都依赖 c 扩张,都可以无障碍运行。

Reply to freefishz

好吧。。那应该是在 docker 里面进行 build。而不是本地开发机。。

freefishz in 缩减 Docker 镜像体积历程总结 mention this topic. 06 Feb 21:33
You need to Sign in before reply, if you don't have an account, please Sign up first.