Preview
Skip to content
CodingDiary
返回

我的博客是如何集成CICD的?

编辑页面
导读

静态博客发布文章太繁琐?CI/CD 来解放双手!本文分享博客部署演进:v1.0 Jenkins + Travis-CI 自动构建推送 Pages;v2.0 升级 GitLab CI + Docker Swarm 容器化部署,Traefik 反向代理实现 0 配置负载均衡。附完整 Jenkinsfile、.gitlab-ci.yml 配置。

1. 前言

博客现在貌似成为了一个技术人的标配,从最初的炫技到单纯的记录自己的成长历程。我的博客也经历了很多次「蜕变」。

当下博客的选型十分多,这里我列举一些:

关于动态和静态的区别主要有以下几点:

本站使用的就是静态博客 hexo 加上 Next 主题搭建的,同时也自定义了一些样式和功能。上面说到静态博客发布更新文章比较繁琐,那么有没有什么方法可以简化发布流程,提升整个写作体验呢?答案就是 CICD

2. 什么是 CICD?

CI:持续集成

CD:持续交付、持续部署

具体概念,大家可以自行百度查阅。

3. 「代码日记」的心路历程

这里就略过了从 QZone、WordPress 的那些经历,直接从 hexo 开始

3.1. 原始阶段

最开始接触 hexo,还是懵懂的学生阶段,因为是静态博客,同时又可以托管到 GitHub,使用 GitHub Pages 服务发布网站,避免了大额的服务器花销,这让我深深的喜欢上了 hexo。

那时候我每次写完文章,就本地 hexo g 一下,将打包生成的 public 目录,放置在 pages 分支,然后提交到 GitHub,就算一次博文的发布。

缺点

将博客托管在 GitHub,没有了服务器的开销,但是因为网络环境的问题,明明已经是静态资源的博客,居然经常性的访问不到。

后面通过在 coding.net 部署解决国内访问慢的问题,通过阿里云 DNS 解析配置,国外默认到 xkcoding.github.io,国内默认到 xkcoding.coding.me

3.2. 折腾 v1.0

16 年工作之后,在 618 入手了一台大米云服务器。那时,coding.net 强制要求 Pages 服务底部增加版权,否则会弹出广告信息,于是就想着将博客部署在自己服务器上。

因为博客都是静态资源,仅需要安装 nginx 即可解决。但是部署在自己服务器,如果每次写完文章都需要 ftp 传到指定目录,那也太麻烦了。v1.0 就是在这样的前提下诞生了。

我在自己的服务器搭建了 Jenkins,同时在 GitHub 开启 webhook,当有新的 push 操作的时候,就会触发 Jenkins 任务。Jenkins 流水线任务主要就是获取代码、下载依赖、打包压缩、最后将打包好的资源文件放在 nginx 目录。

同时集成了 Travis-ci,自动往 pages 分支提交内容,为了维护 pages 的站点,避免偶尔自己服务器宕机导致我自己无法查阅博客内容。

Jenkins 和 Travis 都会在部署完成之后触发邮件提醒,通知我博客是否部署成功,如果失败,会附带失败日志。

缺点

v1.0 的版本基本已经可以满足要求了,也陪伴了我将近 3 年的时光。但是不乏还是存在缺点:

分享

Jenkinsfile

源码地址:https://github.com/xkcoding/MyBlog/blob/master/.jenkins/Jenkinsfile

#!/usr/bin/env groovy Jenkinsfile
pipeline {
    agent any
    environment {
        CONFIG_FILE = "_config.yml"
        BLOG_DIR = "/data/xkcoding.com"
        GIT_REPO = "https://github.com/xkcoding/MyBlog.git"
        USERMAIL = "237497819@qq.com"
    }

    stages {
        stage("获取/更新代码代码") {
            steps {
                echo "Git仓库代码: ${GIT_REPO}"
                echo "校验是否已存在代码"
                script {
                    if (fileExists(file:"${CONFIG_FILE}")) {
                        echo "项目存在,准备更新代码"
                        sh "git pull origin master"
                    }else{
                        echo "项目不存在,准备拉取代码"
                        git 'https://github.com/xkcoding/MyBlog.git'
                    }
                }
            }
        }

        stage("下载依赖") {
            steps {
                echo "开始下载NPM依赖"
                sh "npm install"
            }
        }

        stage("打包/压缩博客") {
            steps {
                echo "开始打包Hexo博客"
                sh "hexo clean && hexo g"
                echo "开始压缩Hexo博客"
                sh "export PATH=/apps/node-v8.0.0-linux-x64/bin:$PATH && gulp"
            }
        }

        stage("部署") {
            steps {
                echo "删除旧目录"
                sh "rm -rf ${BLOG_DIR}"
                echo "部署静态页面到nginx目录"
                sh "mv public ${BLOG_DIR}"
                echo "删除依赖目录"
                sh "rm -rf node_modules"
            }
        }

        stage("上线") {
            steps {
                echo "重启Nginx"
                sh "nginx -s reload"
            }
        }
    }

    post {
        always {
            emailext (
                subject: '''构建通知:${JOB_NAME} [${BUILD_NUMBER}] ${BUILD_STATUS}''',
                body: '''
                <!DOCTYPE html>
                    <html>
                    <head>
                    <meta charset="UTF-8">
                    <title>${JOB_NAME}-第${BUILD_NUMBER}次构建日志</title>
                    </head>
                    <body leftmargin="8" marginwidth="0" topmargin="8" marginheight="4" offset="0">
                        <table width="95%" cellpadding="0" cellspacing="0" style="font-size: 11pt; font-family: Tahoma, Arial, Helvetica, sans-serif">
                            <tr>
                                <td>(本邮件是程序自动下发的,请勿回复!)</td>
                            </tr>
                            <tr>
                                <td><h2>
                                        <font color="#0000FF">构建结果 - ${BUILD_STATUS}</font>
                                    </h2></td>
                            </tr>
                            <tr>
                                <td><br />
                                <b><font color="#0B610B">构建信息</font></b>
                                <hr size="2" width="100%" align="center" /></td>
                            </tr>
                            <tr>
                                <td>
                                    <ul>
                                        <li>项目名称: ${JOB_NAME}</li>
                                        <li>构建编号: 第${BUILD_NUMBER}次构建</li>
                                        <li>构建状态: ${BUILD_STATUS}</li>
                                        <li>构建日志: <a href="${BUILD_URL}console">${BUILD_URL}console</a></li>
                                        <li>构建Url: <a href="${BUILD_URL}">${BUILD_URL}</a></li>
                                        <li>工作目录: <a href="${BUILD_URL}ws">${BUILD_URL}ws</a></li>
                                        <li>项目Url: <a href="${PROJECT_URL}">${PROJECT_URL}</a></li>
                                    </ul>
                                </td>
                            </tr>
                            <tr>
                                <td><b><font color="#0B610B">Changes Since Last Successful Build:</font></b>
                                <hr size="2" width="100%" align="center" /></td>
                            </tr>
                            <tr>
                                <td>
                                    <ul>
                                        <li>历史变更记录 : <a href="${PROJECT_URL}changes">${PROJECT_URL}changes</a></li>
                                    </ul> ${CHANGES_SINCE_LAST_SUCCESS,reverse=true, format="Changes for Build #%n:<br />%c<br />",showPaths=true,changesFormat="<pre>[%a]<br />%m</pre>",pathFormat="    %p"}
                                </td>
                            </tr>
                            <tr>
                                <td><b>Test Informations</b>
                                <hr size="2" width="100%" align="center" /></td>
                            </tr>
                            <tr>
                                <td><pre style="font-size: 11pt; font-family: Tahoma, Arial, Helvetica, sans-serif">Total:${TEST_COUNTS,var="total"},Pass:${TEST_COUNTS,var="pass"},Failed:${TEST_COUNTS,var="fail"},Skiped:${TEST_COUNTS,var="skip"}</pre>
                                    <br /></td>
                            </tr>
                            <tr>
                                <td><b><font color="#0B610B">构建日志 (最后 100行):</font></b>
                                <hr size="2" width="100%" align="center" /></td>
                            </tr>
                            <tr>
                                <td><textarea cols="80" rows="30" readonly="readonly" style="font-family: Courier New">${BUILD_LOG, maxLines=100}</textarea>
                                </td>
                            </tr>
                        </table>
                    </body>
                </html>
                ''',
                to: "${USERMAIL}",
                recipientProviders: [[$class: 'DevelopersRecipientProvider']]
            )
        }
    }
}
.travis.yml

源码地址:https://github.com/xkcoding/MyBlog/blob/master/.travis.yml

language: node_js
node_js:
  - "8.16.0"
cache:
  directories:
    - node_modules
before_install:
  - npm install -g hexo-cli
  - npm install -g gulp
install:
  - npm install
script:
  - hexo clean
  - hexo g && gulp
after_success:
  - cd ./public
  - git init
  - git config user.name "Yangkai.Shen"
  - git config user.email "237497819@qq.com"
  - git add .
  - git commit -m "TravisCI 自动部署"
  # Github Pages
  - git push --force --quiet "https://${GITHUB_TOKEN}@${GITHUB_PAGE}" master:master
  # Coding Pages
  - git push --force --quiet "https://xkcoding:${CODING_TOKEN}@${CODING_PAGE}" master:master
env:
  global:
    - GITHUB_PAGE: github.com/xkcoding/xkcoding.github.io.git
    - CODING_PAGE: git.dev.tencent.com/xkcoding/xkcoding.git
# 通知
notifications:
  email:
    recipients:
      - 237497819@qq.com
    on_success: always # default: change
    on_failure: always # default: always
③ 相关截图

Jenkins-流水线

travis-ci流水线

3.3. 折腾 v2.0

大米云的服务器将于今年的 6 月份过期,于是掐着这个时间点,我开始了折腾 v2.0 版本。

服务器是去年双十一以新人的身份购入的京东云主机,同时还有返现京豆,羊毛这东西,该薅还得薅,于是一并购入 3 台,这次就趁着无业人员同时又迫于大米云的到期,把它们利用起来。

v2.0 版本主要是做了代码私有化、同时支持容器化的部署。在 3 台服务器上搭建了 Docker Swarm 作为容器集群,架构是 1 主 2 从,然后拿出一台服务器专门搭建了 GitLab,作为后续的个人代码托管平台,在每个节点安装 GitLab Runner 服务,另外在主节点搭建 Traefik 作为容器的反向代理。

Traefik 和 nginx 作用类似,主要用于反向代理,但是在 swarm 模式下,traefik 可以做到 0 配置实现负载均衡,同时可以做到不需要容器在宿主机暴露端口,直接代理到容器里的服务。

这里我选择了 swarm 而没有选择 k8s,主要是因为 k8s 对我来说实在用不上,而且 k8s 占用资源太大,因此选择了更轻量级的 swarm。

主要流程就是,本地写好文章,push 到 GitLab,然后 GitLab 会触发 Pipeline 任务。

pipeline 任务主要有以下几步:

  1. 下载依赖(可选,如果 package.json 文件未改动,则跳过)
  2. 编译构建,同时压缩静态资源
  3. 将静态资源目录打包成 docker 镜像,push 到阿里云镜像仓库
  4. 部署上线,调用 swarm 主节点上的 runner 来执行 docker stack deploy 来部署
  5. 下线(手动操作,如果我想向下,可以直接操作)

分享

.gitlab-ci.yml

源码地址:https://github.com/xkcoding/MyBlog/blob/master/.gitlab-ci.yml

variables:
  DOCKER_REGION: "registry.cn-hangzhou.aliyuncs.com"
  DOCKER_NAMESPACE: "xkcoding"
  APP_NAME: "myblog"
  BUILD_IMAGE: "$DOCKER_REGION/$DOCKER_NAMESPACE/nodebuild:8.17.0-alpine3.11"
  IMAGE_NAME: "$DOCKER_REGION/$DOCKER_NAMESPACE/$APP_NAME:$CI_COMMIT_REF_NAME-$CI_COMMIT_SHORT_SHA"
  DOCKER_FILE_PATH: "./Dockerfile"
  APP_DOMAIN: "xkcoding.com"
  CONTAINER_PORT: 80

stages:
  - 下载依赖
  - 编译构建
  - 打包镜像
  - 部署服务
  - 服务下线

cache:
  key: ${CI_COMMIT_REF_SLUG}
  paths:
    - public/
    - node_modules/

下载依赖:
  stage: 下载依赖
  image: ${BUILD_IMAGE}
  tags:
    - docker
  script:
    - ls -a
    - npm install
    - ls -a
  rules:
    - changes:
        - package.json

编译构建:
  stage: 编译构建
  image: ${BUILD_IMAGE}
  tags:
    - docker
  script:
    - ls -a
    - echo "开始打包Hexo博客"
    - hexo clean && hexo g
    - echo "开始压缩Hexo博客"
    - gulp
    - ls -a public/

打包镜像:
  stage: 打包镜像
  image: docker:latest
  services:
    - name: docker:dind
  tags:
    - docker
  script:
    - ls -a
    - docker login -u ${DOCKER_USERNAME} -p ${DOCKER_PASSWORD} ${DOCKER_REGION}
    - docker build -t ${IMAGE_NAME} -f ${DOCKER_FILE_PATH} .
    - docker push ${IMAGE_NAME}
    - docker rmi ${IMAGE_NAME}

部署服务:
  stage: 部署服务
  tags:
    - deploy
  script:
    - ls -a
    - sed -i "s#__IMAGE_NAME__#${IMAGE_NAME}#g" ${APP_NAME}.yml
    - sed -i "s#__APP_NAME__#${APP_NAME}#g" ${APP_NAME}.yml
    - sed -i "s#__APP_DOMAIN__#${APP_DOMAIN}#g" ${APP_NAME}.yml
    - sed -i "s#__CONTAINER_PORT__#${CONTAINER_PORT}#g" ${APP_NAME}.yml
    - cat ${APP_NAME}.yml
    - docker login -u ${DOCKER_USERNAME} -p ${DOCKER_PASSWORD} ${DOCKER_REGION}
    - docker stack deploy -c ${APP_NAME}.yml ${APP_NAME}
  cache:
    policy: pull

服务下线:
  stage: 服务下线
  tags:
    - deploy
  script:
    - ls -a
    - sed -i "s#__IMAGE_NAME__#${IMAGE_NAME}#g" ${APP_NAME}.yml
    - sed -i "s#__APP_NAME__#${APP_NAME}#g" ${APP_NAME}.yml
    - sed -i "s#__APP_DOMAIN__#${APP_DOMAIN}#g" ${APP_NAME}.yml
    - sed -i "s#__CONTAINER_PORT__#${CONTAINER_PORT}#g" ${APP_NAME}.yml
    - cat ${APP_NAME}.yml
    - docker login -u ${DOCKER_USERNAME} -p ${DOCKER_PASSWORD} ${DOCKER_REGION}
    - docker stack rm ${APP_NAME}
  when: manual
  cache:
    policy: pull
Dockerfile
FROM nginx:1.18.0
LABEL maintainer="xkcoding <237497819@qq.com>"

COPY public/ /usr/share/nginx/html
COPY default.conf /etc/nginx/conf.d/default.conf
myblog.yml

该文件是 Docker Stack 的部署文件

源码地址:https://github.com/xkcoding/MyBlog/blob/master/myblog.yml

version: "3.8"

services:
  nginx:
    image: __IMAGE_NAME__
    networks:
      - traefik
    deploy:
      mode: replicated
      # 2个副本
      replicas: 2
      # 更新策略
      update_config:
        # 同时只能更新一个节点
        parallelism: 1
        delay: 10s
        order: stop-first
      placement:
        # 每个节点最多副本数量为 1
        max_replicas_per_node: 1
        constraints:
          - "node.labels.deploy==common"
      labels:
        - "traefik.enable=true"
        - "traefik.docker.network=traefik"
        - "traefik.http.routers.__APP_NAME__0.middlewares=https-redirect@file"
        - "traefik.http.routers.__APP_NAME__0.entrypoints=http"
        - "traefik.http.routers.__APP_NAME__0.rule=Host(`__APP_DOMAIN__`)"
        - "traefik.http.routers.__APP_NAME__1.middlewares=content-compress@file"
        - "traefik.http.routers.__APP_NAME__1.entrypoints=https"
        - "traefik.http.routers.__APP_NAME__1.tls=true"
        - "traefik.http.routers.__APP_NAME__1.rule=Host(`__APP_DOMAIN__`)"
        - "traefik.http.services.__APP_NAME__backend.loadbalancer.server.scheme=http"
        - "traefik.http.services.__APP_NAME__backend.loadbalancer.server.port=__CONTAINER_PORT__"
networks:
  traefik:
    external: true
④ 相关截图

GitLab Pipeline 总览

Pipeline 详情

各节点容器运行情况

4. 后记

到这里,我博客算是走上了一个我觉得还行的 CICD 道路,至少目前我只需要关心文章内容即可,其他的交给机器去做吧。

折腾之旅到这儿就结束了,我想应该不会,未来也会去尝试性能更好的 hugo,如果我找到一个我喜欢的主题的话。

总的来说,折腾才是技术人永恒不变的乐趣。


编辑页面
分享到:

上一篇
如何解决 Quartz Job 中无法注入 Spring Bean
下一篇
如何实现一个你自己的 RPC 框架(3)