내 블로그 자동화 프로세스

✍🏼 작성일 2021년 12월 21일    💡 수정일 2022년 01월 19일
❗️ 참고: 이 글이 작성된 지 이미 일이 지났습니다. 시의성에 유의하세요

이전에 어떤 분이 제 블로그가 무엇을 사용하는지 물어보셨을 때, 저는 Github Pages의 Jekyll을 사용하고 빌드된 HTML을 호스팅한다고 말했지만, 이 설명이 다소 간략했고 이 과정에는 여러 서비스 통합 작업이 포함되어 있습니다. 그래서 여기서 자세히 설명하고 동시에 코드를 공개하겠습니다.

전체 프로세스

Markdown 파일의 전체 흐름도:

Image

블로그 이미지의 전체 흐름도:

Image

이미지 처리에 관한 몇 가지 설명:

  1. 로컬 개발 시 이미지 저장소는 블로그 저장소의 서브모듈입니다. 로컬에서 블로그 글을 작성하면서 새로운 이미지를 추가한 경우, 먼저 이미지를 이미지 저장소에 푸시하여 저장소의 액션이腾讯云 COS에 동기화되도록 한 후 블로그 코드를 푸시해야 합니다. 그렇지 않으면 이미지 참조 링크를 찾을 수 없습니다.

  2. 블로그 글을 작성할 때 이미지를 참조할 때는 상대 경로를 사용하며, Jekyll에는 로컬 개발, .com 웹사이트 참조, .cn 웹사이트 참조에 각각 사용되는 세 가지 구성 파일이 있습니다. 세 yml 파일의 유일한 차이는 정적 리소스 참조 방식입니다:

  3. 로컬 구성 파일: static_url: /static

  4. .com 구성 파일: static_url: https://static.xheldon.com

  5. .cn 구성 파일: static_url: https://static.xheldon.cn

  6. 이미지 리소스를 참조해야 할 때 markdown에서의 작성 방식은 다음과 같습니다: \!\[\]\(https://static.xheldon.cn/img/in-post/qing-zheng-lu-yu/IMG_3789.png) (Jekyll은 Markdown 파일을 처리할 때 Liquid 변수를 먼저 대체한 후 HTML로 빌드합니다).

시작 계기

이전에 Notion을 데이터 소스로 사용하여 블로그를 업데이트하고 싶었기 때문에, 이 작업을 위해 서버가 필요했습니다. 그래서 저는腾讯云의 경량 서버를 구매하여 Notion 데이터를 가져오는 서버로 사용했습니다. 결과는 여기에서 확인할 수 있습니다:

구독&유료 소프트웨어 - Xheldon Blog

두 번째 이유는 블로그에는 당연히 이미지를 올려야 하는데, 처음에는 jsDelivr의 Github 저장소 가속 서비스를 사용했습니다:

jsDelivr - 오픈 소스를 위한 무료, 빠르고 안정적인 CDN

하지만 안타깝게도 jsDelivr의 가속 서비스는 각 저장소마다 50MB의 제한이 있어, 공개 js/css 파일 등은 가속이 가능하지만 이미지에는 적합하지 않았습니다.

이를 바탕으로 제가 서버를 보유하고 있으니 해답은 명확했습니다: 도메인을 추가로 구매하고 CDN 가속 서비스를 활성화하면 됩니다.

그리고 한 걸음 더 나아가, 이미 도메인을 구매했는데 내 블로그를 접속하는 대부분의 IP가 중국 내부인 만큼, 국내 버전 블로그를 추가로 만들면 어떨까 생각했습니다. 도메인 이름은 https://xheldon.cn로 정했습니다.

이렇게 생각한 대로 바로 실행에 옮겼고, 아래는 이 과정을 정리한 내용입니다.

서버와 도메인

구매

서버는 텐센트 클라우드의 경량 서버를 구매했는데, 4코어 8GB 메모리에 4Mbps 대역폭입니다. 할인 행사(거의 0.몇% 수준의 가격) 때 구매해서 매우 저렴했습니다.

도메인을 구매한 후 중국 본토에서는 반드시 ICP 비안(备案)을 해야 합니다. 비안을 하지 않으면 도메인에 대한 DNS 해석이 제공되지 않으며, 해당 도메인에 접속하면 ‘이 도메인은 비안되지 않아 해석이 중지되었습니다’ 등의 메시지가 표시됩니다. 다행히 텐센트 클라우드에서 무료로 비안 서비스를 제공하며, 현재는 절차도 많이 간소화되었습니다. 텐센트 클라우드의 비안 신청서를 작성하기만 하면 되는데, 가정 주소, 연락처, 웹사이트 용도, 신청 사유 등의 정보를 기입해야 합니다. 만약 MIIT(공신부)의 요구 사항에 맞지 않게 작성하면, 예를 들어 신청 사유란에 '빨리 승인해 달라’는 등의 내용을 기재하면 당연히 반려될 것입니다. 작성 내용에 문제가 있는 경우(텐센트에서 도메인을 관리하는 정부 기관을 '관국’이라고 함), 담당자가 연락하여 수정 사항을 확인한 후 제출할 수 있도록 도와줍니다.

설정

설정에는 다음과 같은 단계가 있으며, 자세한 내용은 생략합니다:

  1. 무료 HTTPS 인증서를 신청하고 HTTPS를 활성화합니다.

  2. CDN 가속 도메인을 설정합니다.

  3. 이미지 저장 공간으로 COS를 구매합니다(신규 사용자에게는 무료로 제공됩니다).

  4. Express를 사용하여 Node 서비스를 시작하고, Nginx 리버스 프록시를 통해 80 포트를 외부에 노출합니다. 서버는 Gitee에서 정적 HTML 리소스를 가져오며, 일부 API 요청에 응답합니다. 왜 Github에서 직접 가져오지 않고 이러한 API 요청을 통해 번거롭게 가져오는지에 대한 이유는 다음과 같습니다:

  5. Gitee의 웹훅에서 오는 요청에 응답하여 서버가 Gitee의 최신 HTML 파일을 가져오도록 알립니다.

  6. 블로그의 Notion 조회 요청에 응답하며, 서버는 notion 서버에 요청을 보내 조회합니다.

몇 가지 설명이 필요한 부분이 있습니다:

  1. 경량 서버로는 Node + Nginx의 Docker 이미지를 선택했습니다. Java나 사용자 정의 이미지를 선택해도 무방합니다.

  2. 경량 서버에 기본 설치된 Node와 Nginx는 lighthouse 사용자 권한으로 설치되어 있어, 패키지 설치 시 권한 부족 메시지가 자주 발생했습니다. 그래서 편의를 위해 기존의 Nginx와 Node를 삭제하고 root 사용자로 다시 설치했습니다.

  3. 때로는 서버에 FTP로 파일을 업로드해야 할 때가 있습니다. 앞서 언급한 SSL 인증서와 같은 파일을 업로드할 때 FTP 관련 설정이 필요하며, 이에 대한 문서는 Tencent Cloud에서 찾아볼 수 있습니다.

  4. ICP(인터넷 콘텐츠 제공자) 비안은 최소 일주일 이상 기다려야 하며, 저는 약 2주 정도 기다렸습니다.

저장소 설정

Github Actions로 정적 파일 빌드 설정

블로그의 소스 코드 파일을 노출하고 싶지 않았고, Github Pages에서 지원하는 Jekyll 플러그인에는 제한이 있어 요구 사항을 충족할 수 없었기 때문에(예: 홈페이지뿐만 아니라 카테고리 페이지에서도 페이징을 원하는 경우 Github Pages에서 지원하는 플러그인으로는 불가능), 직접 소스 코드를 HTML로 빌드하기로 결정했습니다.

또한 Github Pages 무료 버전의 제한으로 인해 비공개 저장소에 Github Pages를 활성화할 수 없기 때문에, 저는 다른 저장소를 공개로 설정하고 소스 코드 저장소는 비공개로 설정했습니다. 코드는 비공개 저장소에 커밋된 후 Github Action을 통해 빌드되어 해당 공개 저장소로 푸시됩니다.

Github Pages와 Github Action 사용에 관해서는 제가 이전에 작성한 다음 글을 참고하실 수 있습니다:

비공개 저장소를 무료로 사용하여 GitHub Pages 배포하기 - Xheldon 블로그

하지만 국내 도메인을 추가로 구입했기 때문에 설정 파일도 일부 수정되었습니다. 아래는 새로운 설정 파일입니다:

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
name: 博客打包任务

# 代码 push 到 master 分支的时候运行该 workflow
# TODO:不运行 commit 信息中包含特定关键词的 push
on:
push:
branches: [ master ]

jobs:
Build:
runs-on: ubuntu-latest

steps:
- name: 检出分支
uses: actions/checkout@v2
with:
persist-credentials: fasle # false 是用 personal token,true 是使用 GitHub token
fetch-depth: 0 # 保证能够 push 成功

# 设置 ruby 环境
- name: 设置 Ruby 环境
uses: ruby/setup-ruby@v1
with:
bundler-cache: true
ruby-version: 2.6

# 安装依赖
- name: bundle 安装依赖
run: bundle install

# 打包静态资源
- name: 构建 xheldon.com 页面
if: ${{ startsWith(github.event.head_commit.message, 'com') || startsWith(github.event.head_commit.message, 'all') }}
run: bundle exec jekyll build

- name: xheldon.com 写入信息
if: ${{ hashFiles('./_site') }}
working-directory: ./_site
run: |
echo "www.xheldon.com" > CNAME
echo -e "# [Xheldon's blog](https://www.xheldon.com)" > README.md

- name: 推送到 x_blog 仓库
if: ${{ hashFiles('./_site') }}
working-directory: ./_site
run: |
pwd
git init
git checkout -b master
git add -A
git -c user.name='github actions by ${{github.actor}}' -c user.email='NO' commit -m '${{github.event.head_commit.message}}'
git push "https://${{github.actor}}:${{secrets.X_BLOG_SITE}}@github.com/Xheldon/x_blog.git" HEAD:master -f -q

- name: 构建 xheldon.cn 页面
if: ${{ startsWith(github.event.head_commit.message, 'cn') || startsWith(github.event.head_commit.message, 'all') }}
run: bundle exec jekyll build --config=_config.cn.yml -d _site_cn

# gitee 和 github 的用户名一样
- name: 推送到 x_blog_cn 仓库
if: ${{ hashFiles('./_site_cn') }}
working-directory: ./_site_cn
run: |
pwd
git init
git checkout -b master
git add -A
git -c user.name='gitub actions by ${{github.actor}} push to gitee' -c user.email='NO' commit -m '${{github.event.head_commit.message}}'
git push "https://${{github.actor}}:${{secrets.X_BLOG_SITE_CN}}@gitee.com/Xheldon/x_blog_cn.git" HEAD:master -f -q

다른 저장소 구성 및 Github Pages 활성화

이 단계는 설명이 필요 없을 정도로 간단합니다. Github 저장소의 설정을 켜기만 하면 됩니다.

Conding Webhooks 구성

Gitee 저장소 관리-Webhooks에서 WebHook 주소를 구성합니다. 제 경우는 https://www.xheldon.cn/hooks_cn_push。입니다.

서버 Webhooks 응답 구성

서버 측에서는 Express 서비스를 실행하며, 코드는 다음과 같습니다:

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
app.post('/hooks_cn_push', async (req, res) => {
// Note: 收到 x_blog_cn 的 webhooks, 执行 git pull, 将 x_blog_cn 拉取到 public 目录
// Note: 签名验证,暂时略
// Note: 拉取分支
const branch = req.body.repository.clone_url;
const headers = req.headers;
console.log('headers:', headers);
// Note: 验证一下 header 的合法性
if (
headers['x-gitee-event'] === 'Push Hook'
&& headers['x-gitee-token'] === GITEE_WEBHOOKS_SECRET
) {
exec(`sudo rm -rf ./_public && sudo git clone ${branch} ./_public && sudo rsync -chir --delete ./_public/ ./public/ && sudo rm -rf ./_public`, {
cwd: './',

}, (err, stdout, stderr) => {
if (err) {
console.log('err:', err, stderr);
res.status(400).send({
msg: '服务器内部 git clone 仓库失败:',
stderr,
stdout,
err,
})
} else {
// Note: stdout 没有任何输出表示正常
console.log('out:', stdout);
if (!stdout) {
res.json({
msg: '没问题',
status: 200,
stderr,
stdout,
err,
});
} else {
res.json({
msg: 'git clone 的时候返回了一些内容,请过目',
status: 200,
stderr,
stdout,
err,
});
}
}
});
} else {
res.json({
msg: '恶意请求!',
status: 403
});
}
});

이미지 저장소의 Action 구성

이미지가 x_blog-static 저장소에 업로드된 후 Github Action이 트리거되어 Tencent Cloud COS로 증분 업데이트됩니다. 코드는 다음과 같습니다:

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
name: 腾讯云 cos 同步任务

# 代码 push 到本仓库的时候触发该 ci
# 分析提交的 commits(不只是 head commit),将全部的 added modify 归结到一起(去重)调用上传方法
# 将 delete 的,调用删除方法
on:
push:
branches: [master]

jobs:
CheckOutAndUpload:
runs-on: ubuntu-latest

steps:
- name: 检出分支
uses: actions/checkout@v2
with:
persist-credentials: false
fetch-depth: 0

- name: 设置 node 环境
uses: actions/setup-node@v2.4.1
with:
node-version: 14.x
architecture: x64
cache: npm

- name: 安装依赖
run: npm i

- name: 运行上传脚本
uses: actions/github-script@v5
env:
COS_SECRET_ID: ${{secrets.COS_SECRET_ID}}
COS_SECRET_KEY: ${{secrets.COS_SECRET_KEY}}
COS_BUCKET: ${{secrets.COS_BUCKET}}
COS_REGION: ${{secrets.COS_REGION}}
with:
script: |
const script = require('./upload.js')
await script({github, context, core})

물론 COS 등의 비밀키와 같은 일부 환경 변수는 위에서 언급한 Github Action 글을 참고하여 저장소에 직접 구성하시면 됩니다.

위에서는 js 파일을 실행하여 커밋 상황을 확인하고, 해당 커밋에서 추가, 삭제, 수정, 이름 변경된 내용을 확인한 후 일괄 업로드를 수행합니다. 저장소는 공개되어 있으며, 여기에서 확인하실 수 있습니다.

향후 계획

이 글에서 언급했듯이, 이 글 역시 Craft의 플러그인을 통해 Github 저장소에 동기화된 것입니다. 장점들은 이미 해당 글에서 충분히 설명되어 있으니 여기서는 생략하겠습니다. 현재 유일한 문제는 Craft를 통해 삽입된 이미지 처리인데, 비록 공식적으로 CORS 제한이 없는 fetch API(맥 전용)가 공개되었지만, Craft에 업로드된 이미지를 텐센트 클라우드에 어떻게 우아하게 동기화할지 아직 방법을 못 찾았습니다.

현재 계획은 Github에 이미지를 저장하는 단계를 폐기하고, Craft 내의 이미지를 extension을 통해 직접 텐센트 클라우드 COS로 전송하는 것입니다.

- EOF -
이 글의 최초 게시: 내 블로그 자동화 프로세스 - Xheldon Blog