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

1. 前言

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

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

  • 动态博客
    • WordPress:http://www.wordpress.com/
    • Ghost:https://ghost.org/
    • Halo:https://halo.run/
  • 静态博客
    • jekyll:http://jekyllcn.com/
    • hexo:https://hexo.io/
    • hugo:https://gohugo.io/

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

  • 资源占用:静态博客相比动态博客占用服务器资源更少;
  • 发布更新:由于静态博客没有管理后台,所以发布更新内容要比动态博客繁琐;
  • 访问速度:静态博客没有数据库的访问连接,所以静态博客访问速度更快;

本站使用的就是静态博客 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 年的时光。但是不乏还是存在缺点:

  • 随着我的不断折腾,服务器跑的服务越来越多,导致服务器资源不够,有时候会出现 Jenkins 挂掉的情况,导致部署失败
  • 因为对博客做了写定制化改造,集成了一些第三方插件,博客配置文件携带了比较敏感的 key/secret 信息,开始变得不太适合放在 GitHub

分享

Jenkinsfile

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
#!/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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
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
1
2
3
4
5
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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,如果我找到一个我喜欢的主题的话。

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

-------------本文结束  感谢您的阅读-------------
xkcoding wechat
欢迎来我的公众号「xkcoding小凯扣丁」逛逛
o(╯□╰)o 赞助一杯咖啡 ~~