February 1, 2020

Hugo 배포 계획

기존에는 tumblr를 사용해서 블로그를 띄웠었는데, 자체 쿠버네티스도 구축되어 있으므로 이제부터는 Hugo를 사용해서 해 보려고 한다. 그래서 hugo에 대한 이런저런 사용법을 확인해보고 있는데… hugo… 꽤 괜찮은 느낌이다. 어차피 변하지 않는 컨텐츠를 배포하려는 것이므로 뭔가 복잡하게 배우거나 그럴 필요도 없이 적당히 Markdown으로 작성하면 html이 출력된다니 이 얼마나 간편한가.

그럼에도 불구하고 사용법을 보며 언뜻 떠오르지 않는 것들이 있었다. 과역 어떻게 컨텐츠를 배포하는 것이 효율적일까? 하는 것이 바로 그것들 중 하나가 되겠다. 글쓰기 폼이 따로 있는 것이 아니니 어딘가 다른 곳에서 글을 작성해야 하는 것이고, 글이 어느 정도 써지면 이게 자동으로 배포가 되었으면 하는 것이다. 글이 실제로 반영되려면 hugo를 통한 정적 컨텐츠로의 빌드 과정이 포함되어야 하는데, 이걸 누가 트리거링 해줄 것인가도 문제가 된다.

이 블로그는 휴고로 생성한 템플릿인데 이 템플릿 전체가 git에 통째로 올라갈 것이다. git에 커밋이 새로 추가되는 경우 정적 컨텐츠를 새로 빌드하여야 하는 신호로 생각하고 컨텐츠를 새로 빌드할 것이다. 그렇다면 구성을 어떻게 해야 할까? 우선 트리거링을 주는 것은 git webhook을 사용하면 충분히 가능할 것 같은데…

그래서 필요한 것들을 한번 순서대로 생각해 보았다.

  1. hugo로 빌드된 정적 컨텐츠를 보여줄 수 있는 서버가 필요하다.
  2. git 저장소에 저정된 hugo 데이터를 빌드하는 서버가 필요하다.
  3. git의 데이터가 업데이트되었다면 git webhook을 사용해 어딘가에 알려야 한다.
  4. git webhook을 받아 뭔가를 실행할 수 있는 서버가 필요하다.

먼저 1번, 정적 컨텐츠를 보여주는 것은 nginx로 비롯되는 서버들로 간단히 처리할 수 있다. 사용중인 git서버도 있으므로 webhook이벤트를 어딘가로 던지는 것도 문제는 없다. 문제는 webhook 이벤트를 받는 대몬이 없다는 것이다. 그렇다고 이걸 막 또 만들기도 그렇고. 그래도 조금 찾아보니 이런 상황에 간단하게 사용할 수 있도록 webhook이라는 것이 있다는 것을 알게 되었다. 다양한 웹훅 이벤트에 대한 처리를 하기 위해서 이런저런 정의를 json으로 정의할 수 있는데, 이게 상세한 hook-example 예제가 있어서 여러 상황에서 적당히 맞추어가면서 쓸 수 있지 않을까? 마지막으로 필요한 것이 있다면 webhook 이벤트를 받은 후 빌드 명령을 날릴 스크립트나 코드가 필요한데, 이건 어쩔 수 없이 코드를 작성하여야 하겠다…

따라서 쿠버네티스 위에서 돌린다고 했을 때 다음과 같은 그림이 나온다.

kubernetes-architecture

  1. 유저가 글을 쓴 후 git에 해당 내용을 업로드한다.
  2. git에서 발생한 webhook이 webhook 서버에 이벤트를 보낸다.
  3. 받은 이벤트를 통해 git 데이터를 다운로드한 후 정적 데이터로 빌드한다.
  4. 빌드된 데이터는 PVC에 저장한다.

뭐 이렇다.

webhook 설정

Webhook은 앞에서 말한 대로 webhook을 사용했다. go로 작성된 간단한 대몬이고, -hooks 옵션으로 웹훅 설정 파일을 읽어 해당 조건에 맞는 웹훅을 발생시킨다. 웹훅 이벤트를 보내는 주체는 이미 구축된 Gogs인데, 설정이 매우 간단했다. 웹훅 이벤트 발생시 들어오는 이벤트 정보는 Gogs의 가이드에서 확인할 수 있다. 이 이벤트를 보고 webhook의 설정을 만들면 되는데, 다음과 같은 식으로 설정하여 이를 쿠버네티스의 ConfigMap으로 작성하였다.

Name:         hook-information
Namespace:    hook-namespace
Labels:       <none>
Annotations:  <none>

Data
====
hooks.json:
----
[
  {
    "id": "update-blogs",
    "execute-command": "/mnt/builder",
    "command-working-directory": "/mnt",
    "trigger-rule": {
      "and": [
        {
          "match": {
            "type": "payload-hash-sha256",
            "secret": "SECRET_XXXXXX",
            "parameter": {
              "source": "header",
              "name": "X-Gogs-Signature"
            }
          }
        },
        {
          "match": {
            "type": "value",
            "value": "refs/heads/master",
            "parameter": {
              "source": "payload",
              "name": "ref"
            }
          }
        }
      ]
    }
  }
]
Events:  <none>
<Paste>

다음과 같은 조건을 노리고 이를 작성했는데,

  1. X-Gogs-Signature로 간단한 인증 절차를 밟도록 하고,
  2. 브랜치 master에 새로운 커밋이 발생한다면
  3. 이미 준비된 /mnt/builder를 실행시킨다.

이런 식으로 동작한다. webhook이 동작하는 서버에서는 /mnt/builder를 실행해서, 직접 hugo를 사용해 컨텐츠를 빌드하지 않고 정적 컨텐츠를 빌드하는 쿠버네티스의 Jobs를 생성하도록 하였다. 굳이 이유를 정하자면 각 서버가 하는 역할을 분리하고 싶어서였다. 특히나 Jobs가 그런 용도에 더 적합해 보이기도 했고. 하여간 Jobs를 생성하는 코드는 goclient-go라이브러리를 를 사용해서 매우 간단하게 처리했다.

Jobs 생성하기

정적 컨텐츠를 빌드하는 Jobs는 다음의 역할을 필요로 한다.

  1. git 저장소로부터 최신 컨텐츠를 가져온다.
  2. hugo를 사용하여 컨텐츠를 빌드한다.

단순히 생각해도 두 가지 작업을 해야 한다. Pod에서 두 가지 작업을 하게 되었으므로 이를 컨테이너 두 개로 나눠야 한다.

  1. initContainers: git 저장소로부터 데이터를 git clone한다. 이 때 서브모듈로 등록된 테마 등을 같이 가져온다.
  2. containers: hugo --source CLONE_DIRECTORY --dest VOLUME을 실행한다.

이렇게 작성하면 될 것 같다. 그래서 이와 똑같이 동작하는 go 코드를 작성했다. 이 코드의 결과물은 위에 작성한 webhook 팟에서 동작하게 되는데, 권한을 위해 쿠버네티스의 ServiceAccount, Role, RoleBinding을 함께 작성하였다. Role에는 간단하게 Jobs를 생성만 가능한 권한을 부여했는데, 다음과 같이 작성했다.

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: hook-namespace
  name: batch
rules:
- apiGroups: ["batch"]
  resources: ["jobs"]
  verbs: ["create"]

RoleRoleBinding을 사용하여 ServiceAccount와 엮은 후 webhook 팟에 ServiceAccount를 제공한다. 이제 webhook 컨테이너에서 client-gorest.InClusterConfig()를 호출하면 해당 권한을 가진 쿠버네티스 클라이언트를 사용할 수 있다.

rolebinding

문제점

이렇게 하면 이제 원하는 대로 동작하게 된다. 새로운 브랜치를 생성해서 글을 쓰다가 다 작성한 후 master에 머지하여 이를 커밋한다면 webhook 이벤트가 발생하고, 이 이벤트를 받은 webhook 대몬은 Jobs리소스를 생성하게 될 것이고, Jobs가 정적 컨텐츠를 빌드한다. 하지만 항상 원하는 대로 되는 것은 아니니…

nfs chtimes permission denied

$ kubectl logs -f hugo-static-builder
Building sites … Total in 1231 ms
Error: Error copying static files: chtimes /mnt/css/style.css: permission denied

한 번씩 빌드가 끝난 후 PersistentVolume으로 결과를 복사하질 못한다. 동시 쓰기 문제등으로 인해 발생하는 문제로 추정되는데 같은 볼륨을 사용하지 않도록 해야 하나 고민중이다.

Jobs 히스토리 관리

쿠버네티스 CronJob의 경우 Jobs의 히스토리 관리가 가능한데, Jobs 자체는 그런 관리가 불가능한 것 같다. 완료된 잡이 히스토리에 남아 새로운 잡을 실행할 경우 동일한 이름의 잡이 있으므로 새로 생성되지 않는다. 우선 GenerateName을 사용해서 랜덤한 이름을 부여하도록 하여 이를 우회했는데, 더 간단한 방법이 있을지 찾아봐야 할 것 같다.

이후 할 일

데이터를 백업하는 방법도 강구해야 할 것 같다. dropbox-uploader를 사용해서 간단히 처리할 수 있을 것이다. initContainer를 통해 빌드 전에 처리하도록 해야겠다.