前言
由于我们公司最近在准备将我们的服务运行环境切换到 k8s 以方便我们的微服务相关的管理, 因为之前都是通过 shell 脚本进行部署的, 如果想要实现一个比较安全稳定并且能够回滚的自动发布工具是比较困难并且成本比较高的, 所以在这之前我们并没有太多的去思考自动发布方面的问题, 但是切换到 k8s 环境后, deployment 天然提供滚动无服务终端发布, 而且基于镜像的发布也非常容易回滚; 切换环境后自动化最大的两个问题都能比较好的解决, 所以持续发布当然也是势在必行的了, 毕竟人的操作比机器更容易出错, 还没有机器效率高;
思考
因为我们使用 git 来作为代码版本管理, 所以我们肯定是希望我们的持续集成与持续发布与我们的版本管理集成在一起, 我们通过在 git 代码仓库中的操作来触发我们的 CI/CD 操作, 其实差不多 GitOps https://www.weave.works/blog/category/gitops/ 这样子一个概念;
我清理了一下我们的业务场景和开发相关的流程, 提出了一下三个我们需要解决的问题:
能够持续的集成到主干分支并检查代码 (代码风格, 常见 bug 检查, 单元测试) 待测试并发布
能够紧密的把发布与我们的代码库结合起来, 保证我们能够回溯我们发过的每一个版本的代码
正式环境运行的镜像必须是经过测试的镜像
利用 gitlab-ci 解决问题
1.
我们的第一个问题其实利用 gitlab-ci 能够比较容易得解决我们的问题, 我们可以通过配置 `.gitlab-ci.yaml` 中配置我们的在代码提交之后需要进行什么样的代码检查, 保证我们的代码都能通过我们制定的一些规范; 比如我们基于 java 的配置文件是这样子的:
- # 设置我们的 maven 仓库的缓存, 加快我们的构建流程
- variables:
- MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2"
- cache:
- key: "java-mvn-repo-cache"
- paths:
- - .m2
- # 检查是否能通过编译
- build:
- stage: compile
- script:
- mvn clean compile
- only:
- - branches
- # 检查代码风格
- check-style:
- stage: check
- script:
- mvn install checkstyle:check
- only:
- - branches
- # 检查是否有比较明显的 bug
- find-bugs:
- stage: check
- script:
- mvn install findbugs:check
- only:
- - branches
- # 进行项目单元测试
- unit-test:
- stage: unit-test
- script:
- - mvn test -DskipTests=false
- only:
- - branches
这样子我们在提 mr 之前就能够看到我们的请求合并的代码是否能够通过相关的代码质量检查, 我们通过设置 build 和 deploy 操作在我们的保护分支即可达到我们的 cd 流程; 我们的项目分为三步:
打包我们的运行时 jar 包
build 我们的 docker image , 并上传到我们的 docker register ;
根据我们的 build 出来的镜像发布打我们的相关环境中
大致的配置如下:
- # 打包 jar 包
- maven-build:
- stage: build-jar
- script:
- mvn clean package
- - cp -r $SERVER_DIR/target target
- - ls target
- # 存放我们的应用 jar 包, 方便我们下载和在之后的一次构建中使用
- artifacts:
- expire_in: 1 week
- paths:
- - target/*.jar
- # 只在我们的主干分支中进行 build 操作
- only:
- - test
- - master
- # build docker image
- docker-build:
- image: docker:git
- stage: build-image
- script:
- # docker register 的一些信息和一些公共变量我们可以在 group 的 ci/cd 参数中设置, 方便我们同一个 group 不同项目公用
- - export DOCKER_DIR=$SERVER_DIR/deploy/docker
- - export DOCKER_IMAGE=$CI_DOCKER_REGISTRY_SERVER/$CI_DOCKER_REGISTRY_SERVER_NAMESPACE/$SERVER_NAME
- - export DOCKER_TAG=$(git rev-parse --short HEAD)
- - export DOCKER_FULL_NAME=$DOCKER_IMAGE:$DOCKER_TAG
- - echo $DOCKER_FULL_NAME
- - cp target/*.jar $DOCKER_DIR/server.jar
- docker build -t $DOCKER_FULL_NAME $DOCKER_DIR
- docker login -u $CI_DOCKER_REGISTRY_SERVER_NAME -p $CI_DOCKER_REGISTRY_SERVER_PASS $CI_DOCKER_REGISTRY_SERVER
- - docker push $DOCKER_FULL_NAME
- # 将我们构建镜像用的相关参数存到一个文件里面并通过 cache 传递到 deploy 相关的操作操作中
- - echo version=$DOCKER_TAG>> cache.properties
- - echo branch=$CI_COMMIT_REF_NAME>> cache.properties
- - echo image=$DOCKER_IMAGE>> cache.properties
- - echo registry=$CI_K8S_REGISTRY_NAME>> cache.properties && echo "">> cache.properties
- - cat cache.properties
- - echo $CI_PIPELINE_ID
- # 设置 cache key 为 $CI_PIPELINE_ID, 方便我们在同一个 pipeline 中传递信息
- cache:
- key: "$CI_PIPELINE_ID"
- paths:
- - cache.properties
- dependencies:
- - maven-build
- only:
- - test
- - master
- # deploy beta k8s
- k8s-deploy-beta:
- image: prophet/kubectl
- stage: deploy
- script:
- # 把 kubectl 相关的配置参数拷贝到我们的 deploy 容器中, 这里是通过 gitlab-ci 的 volume 实现的, 这种方式不好, 最好是通过配置 ci 的 secret 参数来进行配置
- - cp -r /cache/.kube ~/.kube
- # go-tpl-replacer 是一个实现了 go template 的命令行工具, 我们的项目中提供的 k8s 配置 yaml 只需要利用模板参数, 然后在 deploy 阶段自动替换
- # 该项目实现比较简单, 但是感觉还是挺实用的, 项目地址是 https://github.com/fudali113/go-tpl-replacer
- - go-tpl-replacer -tpl-files $SERVER_DIR/deploy/k8s/deploy.yaml -args "env=beta" -arg-files cache.properties> deploy.yaml
- - cat deploy.yaml
- kubectl apply -f deploy.yaml
- cache:
- key: "$CI_PIPELINE_ID"
- paths:
- - cache.properties
- # 这里配置我们的环境, 这样子在 ci/cd 的 environments 目录下可以看到我们 deploy 的相关的信息
- environment:
- name: beta
- url: https://47.95.143.20:31320
- dependencies:
- - docker-build
- only:
- - test
- - master
上面的 environment 是一个比较好的东西, 可以帮助我们回顾发布历史, 并且配置 url 能够快速帮我们连接到发布项目的地址, 配置运行过后的 environment 大致是这样子的:
ci/cd environments
从上图我们可以看到 re-deploy 和 rollback 的按钮, 点击我们可以再次运行我们配置了 environment 参数的 job , 同时会保留但是 git 分支的相关上下文, 我们基于 commit id 来作为 tag 和版本的 deploy 流程可以被正确的处理;
2.
上面的流程我们通过提前代码来触发 ci , 通过 mr 的合并来触发 deploy , 因为我们的发布正式的代码肯定是应该通过测试的, 所以我们通过在 git 仓库中打 tag 来触发发布正式版本(同时还可以通过不同的 tag 名字规则来发布不同的环境);
3.
我们需要我们线上运行的镜像都必须是进过测试的, 所以我们需要在打 tag 发布正式版的时候找到我们我们打 tag 之前在的相关的镜像, 因为我们的 image tag 都是通过 commit id 来获得的, 同时我们的 git tag 也是一个 commit id 的引用, 所以我们可以很方便的找到我们 git tag 关联的 commit id 并找到相关的镜像, 找到并重新更加 tag 重新推送到我们的 docker register; 如果没有找到相关的 image tag, 则说明我们之前没有过相关的镜像, 既没有被测试过, 我们的 cd 流程将被终端; 同时我们限定只有 ci/cd 才能 push image 到我们的 docker register , 保证在我们的 docker register 上的镜像都是正常流程发布到我们的环境中的, 避免权限在松让一些不遵循流程的开发人员钻空子; 大致的配置如下:
- # build docker image in tag
- docker-build-in-tag:
- image: docker:git
- stage: build-image
- script:
- - export DOCKER_DIR=$SERVER_DIR/deploy/docker
- - export DOCKER_IMAGE=$CI_DOCKER_REGISTRY_SERVER/$CI_DOCKER_REGISTRY_SERVER_NAMESPACE/$SERVER_NAME
- - export DOCKER_TAG=$(git rev-parse --short HEAD)
- - export DOCKER_FULL_NAME=$DOCKER_IMAGE:$DOCKER_TAG
- - export GIT_TAG=$(git describe --tags)
- - echo $DOCKER_FULL_NAME
- # pull git tag 相关 commit id 的镜像并重新打 image tag 然后再上传到我们的 docker register, 之所以不直接使用 commit id 作为版本是为了在正式环境保证可读性
- docker login -u $CI_DOCKER_REGISTRY_SERVER_NAME -p $CI_DOCKER_REGISTRY_SERVER_PASS $CI_DOCKER_REGISTRY_SERVER
- docker pull $DOCKER_FULL_NAME
- docker tag $DOCKER_FULL_NAME $DOCKER_IMAGE:$GIT_TAG
- docker push $DOCKER_IMAGE:$GIT_TAG
- - export DOCKER_TAG=$GIT_TAG
- - echo version=$DOCKER_TAG>> cache.properties
- - echo branch=$CI_COMMIT_REF_NAME>> cache.properties
- - echo image=$DOCKER_IMAGE>> cache.properties
- - echo registry=$CI_K8S_REGISTRY_NAME>> cache.properties && echo "">> cache.properties
- - cat cache.properties
- - echo $CI_PIPELINE_ID
- - echo $CI_COMMIT_REF_NAME
- cache:
- key: "$CI_PIPELINE_ID"
- paths:
- - cache.properties
- dependencies: []
- # 在所有名字以 v 开头的引用中执行该 job
- only:
- - /^v.*$/
- # 排除所有分支, 所以这样子配置只会处理以 v 开头的 tag
- except:
- - branches
- # deploy prod k8s
- # 跟之前发布的操作差不多, k8s 的配置和相关的参数应该是正式环境的相关东西
- k8s-deploy-prod:
- image: prophet/kubectl
- stage: deploy
- script:
- - cp -r /cache/.kube ~/.kube
- - go-tpl-replacer -tpl-files $SERVER_DIR/deploy/k8s/deploy.yaml -args "env=prod" -arg-files cache.properties> deploy.yaml
- - cat deploy.yaml
- kubectl apply -f deploy.yaml
- cache:
- key: "$CI_PIPELINE_ID"
- paths:
- - cache.properties
- environment:
- name: prod
- url: https://api-ytree.23mofang.com
- # 什么时候执行, 配置为 manual 会需要手动在 gitlab 界面点击执行按钮
- when: manual
- dependencies:
- - docker-build-in-tag
- only:
- - /^v.*$/
- except:
- - branches
结语
目前这一套流程在我们的测试环境还是能够比较好的工作, 同事还能够比较好的满足回滚操作和能够很好的与 git 集成在一起; 但是他在面对诸如灰度发布等场景还是有一些局限性, 这个还需要一定的思考来进行相关的优化;
来源: https://juejin.im/entry/5b0cd5b46fb9a009ef47b3a7