最近因为公司业务需要,部门所有人都要转型做前端(还不知道以后做不做native相关的东西…),所以大家都开始学习react,原因是react的写法更像native app的写法,比如组件的生命周期方法,class等等…
既然做了react项目,CI是一定要配好的,之前做ios app开发的时候,由于没有多余的mac电脑,所以没法做CI,CI可以把开发流程规范很多,还可以自动做语法检查,但是配置CI需要一定的docker
知识,不了解的同学最好还是先去了解一下。
在项目中启用CI很简单,只要在项目根目录加上文件.gitlab-ci.yml
就行了。然后在.gitlab-ci.yml
文件里面写配置。
想要知道怎么写配置,需要先了解CI中的一些概念。
在CI中,执行一次CI的流程或步骤称为pipeline。
比如,我们可以配置项目的CI流程为: 第一步,安装项目依赖。 第二步,执行语法检查。 第三步,构建项目。 第四部,部署在测试服务器。
以上的四步称为一次pipeline,每一步称为一个stage(阶段)。
在CI中可以自定义stage,比如我可以在.gitlab-ci.yml
文件中写这么一段
stages:
- install_deps
- eslint
- build
- deploy
这就代表着把一次pipeline分为四个stage。每个stage只有在前一个stage执行成功后才会执行。
接下来,那怎么告诉CI具体每个stage怎么执行?
这就需要定义job,job是stage的执行单元,一个stage可以定义多个job。
比如一个job可以这样定义:
prepare:
stage: install_deps
script:
- npm install
prepare
是job的名字,可以自定义。
stage: install_deps
表示这个job属于install_deps
这个stage。
script
定义这个job具体执行的脚本。就是执行这个job的时候就会执行npm install
这个命令。
总结一下就是:在.gitlab-ci.yml
文件里面定义pipeline,pipeline中有多个stage,stage是按顺序执行的,如果某个stage执行出错,那么之后的stage不会执行。每个stage又对应多个job,相同的stage中的多个job是并行执行的,只有所有的job都返回成功才算成功。
假设我们已经写好了一个.gitlab-ci.yml
文件,是这样的
image: zacksleo/node
stages:
- install_deps
- build
cache:
key: ${CI_BUILD_REF_NAME}
paths:
- node_modules/
install_deps:
stage: install_deps
when: manual
script:
- npm install
testing-server:
stage: build
script:
- if [ ! -d "node_modules" ]; then
- npm install
- fi
- yarn eslint ./
- npm run build
那么,由谁来具体执行job里面的script脚本呢,就是runner
。
runner
是需要配置的,但是我所在的公司已经配好了一些runner
,有国内和国外的。所以我也没配过。这里就不细讲了,简单就说一下原理。
runner
是用docker
来跑script
脚本的,看到第一行的image: zacksleo/node
了么,这句话的意思是使用zacksleo/node
镜像来构建容器,执行script
脚本,镜像是预装了node的,所以可以执行node脚本,docker的相关知识这里就不赘述了。
还有一个比较重要的点是每次运行job或者stage之前,runner都会把gitignore
里面的文件全部删掉,这就导致了在前一个阶段装的npm install
依赖不能给后面的stage使用,所以这里就用到了cache
,把相关的依赖文件夹node_modules
放到缓存里,这样就能给后面的stage使用了。
还有一点值得注意的是when: manual
这行,这表示此stage
会直接跳过,需要手动执行。因为cache
是一直保存着的,比如之前的pipeline
安装过npm install
的话,是可以在下次的pipeline
直接使用的,所以install_deps
这个stage
是不需要每次都执行的,只有在package.json
里面添加了新的依赖,才需要手动去执行。注意:install_deps
这个stage可能需要执行多遍,执行一遍的话后面的stage
可能还会报找不到对应模块的错误,那是因为每个stage
是跑在不同的runner
上的,只执行一遍的话可能只更新了部分runner
里面的cache
,有些runner
的cache
还是老的,所以最好多执行几次。
好,我们已经了解了CI的一些基础知识。说了那么多,我们直接来解读公司后端大神写的CI文件
image: zacksleo/node
before_script:
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" > ~/deploy.key
- chmod 0600 ~/deploy.key
- ssh-add ~/deploy.key
- mkdir -p ~/.ssh
- '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
- export APP_ENV=testing
- yarn config set registry 'https://registry.npm.taobao.org'
stages:
- prepare
- test
- build
- deploy
variables:
COMPOSER_CACHE_DIR: "/cache/composer"
DOCKER_DRIVER: overlay2
build-cache:
stage: prepare
script:
- yarn install --cache-folder /cache/yarn
cache:
key: "$CI_COMMIT_REF_NAME"
paths:
- node_modules
except:
- docs
- tags
when: manual
tags:
- mainland
eslint:
stage: test
dependencies: []
cache:
key: "$CI_COMMIT_REF_NAME"
policy: pull
paths:
- node_modules
script:
- if [ ! -d "node_modules" ]; then
- yarn install --cache-folder /cache/yarn
- fi
- yarn eslint ./
except:
- docs
- develop
- master
- tags
tags:
- mainland
build-check:
stage: test
dependencies: []
cache:
key: "$CI_COMMIT_REF_NAME"
policy: pull
paths:
- node_modules
script:
- if [ ! -d "node_modules" ]; then
- yarn install --cache-folder /cache/yarn
- fi
- yarn build
except:
- docs
- develop
- master
- tags
tags:
- mainland
build-package:
stage: test
script:
- if [ ! -d "node_modules" ]; then
- yarn install --cache-folder /cache/yarn
- fi
- yarn build
dependencies: []
cache:
key: "$CI_COMMIT_REF_NAME"
policy: pull
paths:
- node_modules
artifacts:
name: "build"
untracked: false
expire_in: 60 mins
paths:
- build
except:
- docs
only:
- develop
- master
- tags
tags:
- mainland
testing-image:
stage: build
image: docker:latest
dependencies:
- build-package
cache: {}
before_script: []
script:
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
- docker build -t $CI_REGISTRY_IMAGE:latest .
- docker push $CI_REGISTRY_IMAGE:latest
- docker rmi $CI_REGISTRY_IMAGE:latest
only:
- develop
tags:
- mainland
staging-image:
stage: build
image: docker:latest
dependencies:
- build-package
cache: {}
before_script: []
script:
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
- docker build -t $CI_REGISTRY_IMAGE:stable .
- docker push $CI_REGISTRY_IMAGE:stable
- docker rmi $CI_REGISTRY_IMAGE:stable
only:
- master
tags:
- mainland
production-image:
stage: build
image: docker:latest
dependencies:
- build-package
before_script: []
script:
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
- docker rmi $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
only:
- tags
tags:
- mainland
testing-server:
stage: deploy
variables:
DEPLOY_SERVER: "xxx.xxx.xxx.xxx"
dependencies: []
cache: {}
script:
- cd deploy/testing
- rsync -rtvhze ssh . root@$DEPLOY_SERVER:/data/$CI_PROJECT_NAME --stats
- ssh root@$DEPLOY_SERVER "docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY"
- ssh root@$DEPLOY_SERVER "export COMPOSE_HTTP_TIMEOUT=120 && export DOCKER_CLIENT_TIMEOUT=120 && cd /data/$CI_PROJECT_NAME && docker-compose pull web && docker-compose stop && docker-compose rm -f && docker-compose up -d --build"
only:
- develop
environment:
name: testing
url: https://testing.example.com:1004
tags:
- mainland
staging-server:
stage: deploy
variables:
DEPLOY_SERVER: "xxx.xxx.xxx.xxx"
dependencies: []
cache: {}
script:
- cd deploy/staging
- rsync -rtvhze ssh . root@$DEPLOY_SERVER:/data/$CI_PROJECT_NAME --stats
- ssh root@$DEPLOY_SERVER "docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY"
- ssh root@$DEPLOY_SERVER "export COMPOSE_HTTP_TIMEOUT=120 && export DOCKER_CLIENT_TIMEOUT=120 && cd /data/$CI_PROJECT_NAME && docker-compose pull web && docker-compose stop && docker-compose rm -f && docker-compose up -d --build"
only:
- master
environment:
name: staging
url: https://preview.example.com:1004
tags:
- mainland
production-server:
stage: deploy
variables:
DEPLOY_SERVER: "xxx.xxx.xxx.xxx"
dependencies: []
cache: {}
script:
- cd deploy/production
- rsync -rtvhze ssh . root@$DEPLOY_SERVER:/data/$CI_PROJECT_NAME --stats
- ssh root@$DEPLOY_SERVER "docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY"
- ssh root@$DEPLOY_SERVER "export COMPOSE_HTTP_TIMEOUT=120 && export DOCKER_CLIENT_TIMEOUT=120 && echo -e '\nTAG=$CI_COMMIT_TAG' >> .env && cd /data/$CI_PROJECT_NAME && docker-compose pull web && docker-compose stop && docker-compose rm -f && docker-compose up -d --build"
only:
- tags
environment:
name: staging
url: https://$DEPLOY_SERVER:1004
tags:
- mainland
我们一点点来分析,首先,我看可以看到这个pipeline
分为四个stage
,分别是prepare
,test
,build
,deploy
。
首先肯定会并行执行stage
为prepare
的job
,这里就一个名为build-cache
的job
,但是这个job是when: manual
的,所以会直接跳过,执行下一个stage。
接下来会并行执行stage
为test
的job
,这里有eslint
和build-check
和build-package
三个,eslint很简单,就是执行代码审查。build-check
和build-package
里面的script是相同的,唯一不同的是build-package
里面的artifacts
字段,artifacts
名为工件(可以理解为附件),工件会把文件上传到gitlab上面,供其他job
下载,我们也可以在gitlab上下载浏览。
既然artifacts
和cache
都可以实现stage和job之间共用数据,那么什么时候用cache
,什么时候用artifacts
呢,他们又有什么区别呢?
1. cache在runner中生成后,可以给下次相同runner执行ci使用。
2. cache可能不是最新的。
3. 多个runner之间的cache可能是不同的。
3. artifacts在创建后会提交到gitlab上,供本次pipeline后面的stage使用。
4. artifacts在创建后会提交到gitlab上,用户可以下载查看。
5. artifacts必须指定保存时间,不然会一直保存在gitlab上。
每个job
开始时会默认去下载artifacts
,如果job没有用到artifacts
,则加入dependencies: []
,表示为禁用artifacts
,以加快执行速度。
好,假设这个stage执行成功,这时我们的项目已经构建好了,并可以在gitlab上下载浏览了。
接下来会并行执行stage
为build
的job
,这里有testing-image
,staging-image
,production-image
三个job
,仔细看的话,其实每次commit
只会执行其中一个job
,因为他们的only字段表示只有commit到only指定的分支时才会触发此job。以testing-image
为例,script中,先登录docker私有仓库,在根据根目录中的Dockerfile构建一个新的镜像,在push到私有仓库中,再从runner中删除镜像。
Dockerfile是这样的:
FROM mops-gitlab.lianluo.com:4567/lianluo/nginx:static-spa
COPY ./build /var/www/html
WORKDIR /var/www/html
VOLUME ["/var/www/html"]
这是一个nginx的镜像,会把我们前一步build出来的工件拷贝到镜像中,所以只要启动这个镜像,就能访问到页面了。
我们还注意到,testing-image
还有tags
一栏,tags
指定了此job
跑在哪个runner
上,runner
一般都是打了多个tag
的,比如公司内部配了很多个runner
,有国内和国外的,国内的会打上mainland
标签,所以我们一般在tag中指定为mainland
,用国内的runner
跑速度更快。
点击Expand,可以看到配置好的所有runner。
接下来会并行执行stage
为deploy
的job
,也是有3个job
,一次commit只会执行其中的一个,以testing-server
为例,先看前两句
- cd deploy/testing
- rsync -rtvhze ssh . root@$DEPLOY_SERVER:/data/$CI_PROJECT_NAME --stats
意思为同步deploy/testing
目录下的文件到服务器上的某个目录中,deploy/testing
目录里面是一些配置文件,比如.env
和docker-compose.yml
文件。
这里有个问题,就是script
是跑在runner
上的,ssh连接到服务器的话runner必须知道私钥,然后服务器有公钥,这样ssh才能成功,那么runner怎么知道私钥呢?
这就需要在gitlab上配置。在gitlab上进入具体的项目,找到setting -> CI / CD -> Secret variables
,点击Expand,看描述的话可以知道这里是配置runner的环境变量的,私钥就是在这里配置的。
配置好了之后,在before_script
中,我们发现这里就是把私钥注入到runner里面,关于ssh的更详细信息可以看这里。
连接上服务器之后,接下来就是登陆私有仓库下载前一步构建好的docker镜像,然后就是根据前面传的docker-compose.yml
文件执行docker-compose up
命令。执行成功后就表示部署好了。