GitLab CI/CD实践 - Ruby on Rails

前言

一直以来公司的开发、测试及生产环境都基于实体机,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

# 编译任务,任务名称可任意设置
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 <chenghaoz@gmail.com>

# 安装并配置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 <chenghaoz@gmail.com>

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 <chenghaoz@gmail.com>

# 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实践,有时间话再来分享。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271