6
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

GitLab CIを用いてHugoのHP編集をGitLabのWeb UI上だけで完結させる(自動でファイル・テストページ作成/削除・デプロイする)

Last updated at Posted at 2019-12-02

はじめに

今回は、静的サイトジェネレーターであるHugoを、GitLabのWebインターフェイス上でユーザーさんに編集してもらうのが良いのではないかということになり、ホームページの編集を行うユーザーさんに小難しいことを考えさせずに更新してもらえばなぁという思いがあって、GitLab CIを用いて実装してみました。

本記事ではGitLab CIを用いてファイルの作成とコミット・別サーバーへの転送/削除方法もご紹介しますので、Hugoに限らずに参考になればと思います。

背景

Web上で編集出来るものとしてWordpressがあると思いますが、こちらはコンテンツを複数人が同時に編集することが出来ないようなので(私は使ったことはありませんので詳しくは分かりませんが・・・)、静的サイトジェネレーターを使おうという話になりました。
その中でも、私達は個人的にHugoを使っていたのが採用の経緯です。
そして、Hugoのテーマをチームで開発することになった時にGitLabを使ってみたらWeb UI上で編集出来るしコミット出来るしで、尚且GitLab CIも使えばscheduling機能で毎日自動で更新もしてくれる1し、これは結構イケるのでは!?と思って、実装してみました。

前提

  • 今回、intranet上でのみ見えるサーバーhoge-intra.comがあり、ここにGitLab CEを立てています
    • 公開する前のテストページもこちらのサーバーに置くことにします
  • 外から見てもらうためのサーバーhuga.comがあり、本番環境をここに置きます
    • huga.comhoge-intra.comはSSH通信が可能なようにファイアウォールを設定しています
  • 2つのWebサーバーのDocument Rootは共に/foo/bar/htmlとします
  • 予め、サーバーhugaにてgitlab-botユーザーを作成し、RSA認証鍵を新規に作成しておきます
    • DocumentRootディレクトリのPermission設定も行ってあります
  • GitLab CIはDockerで動かします(Imageの名前はhugo:latest)
  • リポジトリの名前はhugo-hp
  • GitLab CE, GitLab Runnerのバージョンは12.1, Gitのバージョンは2.22.0.454, Hugoのバージョンは0.56です。

仕様

  • 公開するコンテンツはmasterブランチで追跡する
  • masterブランチから新たなbranch(draft_xxxというフォーマット)を切ると、自動でhoge-intra.com/draft_xxxというテストページを作る
  • Variablesにファイル名・ディレクトリ名が指定されればそのファイルをGitLab CIを実行させた人でコミットする
  • draft_xxxブランチ内でコミットがあると、これに対応するhoge-intra.com/draft_xxxのページを自動更新
  • draft_xxxがmasterブランチにMergeされると自動でhoge-intra.com/draft_xxxのページを削除し、huga.comに反映される

実装

SSH通信について

まず、初めに作っておいたRSA秘密鍵をGitLab CIに登録します。Settings->CI/CD->Variablesと移動して、Keyの名前をSSH_PRIVATE_KEY, Valueに秘密鍵を書きます。 この値はリポジトリのMaintenerしか触れないもので、pre-commitを用いて.gitlab-ci.ymlを変更されなければセキュアなものだと考えています(gitで特定のファイルの更新を禁止する)。例えば.gitlab-ci.yml内にecho $SSH_PRIVATE_KEYとか書かれたら・・・。ここは本格的にやるなら、サーバー側でセキュリティを設定するなど、もう少し考えなければならないかもしれません(が、今回は性善説に立って2笑)。

image.png

CIにsshrsyncをやってもらうのですが、Dockerを使っている場合、コンテナ内にて

  • ssh-agentが立ち上がっていません
    • 毎回ssh-agentを立ち上げる必要があります
before_script:
  - eval `ssh-agent`
  - ssh-add <(echo "$SSH_PRIVATE_KEY")
  • known_hostsが破棄されます(つまり毎回、初めて繋ぐ時にYesと打つやつが出てきます)
    • 今回通信するサーバー先は自前のDNSサーバーで管理されているものなので、sshコマンドのオプション-oStrictHostKeyChecking=noをつけます。
      • rsyncの場合はsshプロトコルが出来るオプション-eを使います
      • gitの場合は、git 2.1.0以降からgit -c core.sshCommandを用いてSSHコマンドを指定出来るようになった(参考)ので、これを用いてFinger printの確認を回避しています

masterブランチからdraft_xxxブランチを切ると、自動でテストページを作る

GitLab CIではCI用の変数作業をしているブランチの名前を$CI_COMMIT_REF_NAMEにて取得することが可能です。これを利用します。
また、GitLab Runner 11以降では、正規表現にも対応しています。これで、draft_(ホニャララ)というブランチに対してのみ操作を行うといったことが可能です。
また、Hugoではhugo -bにて、configファイルで指定した以外のBaseURLをコマンドライン上で変更してGenerateしてくれるので、BaseURLにブランチの名前を引っ張って、独自にURLを振ってあげます。

  script:
    - 'hugo -b https://hoge-intra.com/test/${CI_COMMIT_REF_NAME}/'
    - 'rsync -au --delete -e "ssh -oStrictHostKeyChecking=no" public/ gitlab-bot@hoge:/foo/bar/html/test/${CI_COMMIT_REF_NAME}'
  only:
    - /^draft_.*$/

CIを通じてファイルを新規作成する

Hugoにて新規記事を作成する場合、普通はshellにて

$ hugo new (ディレクトリ名)/(ページ名).md

と打たなければなりません。また、下図のようにPipeline実行時に独自に変数を作ってRunnerのshellに投げられるので、これを用いてファイルを作成するようにします。
image.png

  script:
    - if [[ $CI_COMMIT_REF_NAME =~ ^draft_.*$ ]]; then
    - if [ -n "$FILENAME" ]; then
    - if [ -z "$DIRNAME" ]; then
    - DIRNAME="post"
    - fi
    - hugo new $DIRNAME/$FILENAME.md
    - git config --global user.email $GITLAB_USER_EMAIL
    - git config --global user.name $GITLAB_USER_NAME
    - git add content/$DIRNAME/$FILENAME.md
    - 'git commit -m "new content file creation: $DIRNAME/$FILENAME.md"'
    - git -c core.sshCommand="ssh -oStrictHostKeyChecking=no" push gitlab@hoge-intra/hugo-hp.git HEAD:${CI_COMMIT_REF_NAME}
    - fi
    - fi
  only:
    - web

このスクリプトはdraft_(ホニャララ)ブランチのみで、かつWeb UIでCI/CD -> Run Pipelineを押して実行されるとき(上図の画面経由)のみ動作させたいです。
前者はshell scriptのif文で、後者はonly: webとすればOKです3

Merge request時にテストページを削除する

draftブランチが作成される度にコンテンツが作られまくると流石にストレージを圧迫するので、ここでは編集が終わってmasterブランチにMergeしてもらう時に自動でファイルを消してもらうということをします。

delete_testpage:
  stage: del_test
  script:
    - if [[ $CI_COMMIT_REF_NAME =~ ^draft_.*$ ]]; then
    - ssh -oStrictHostKeyChecking=no gitlab-bot@hoge rm -r /foo/bar/test/${CI_MERGE_REQUEST_SOURCE_BRANCH_NAME}
    - fi
  only: 
    refs:
      - merge_requests

ここでのポイントは

only:
  refs:
    - merge_requests

です。Merge requestを送った時にもPipelineが走るのですが、これも分岐の条件として用いる事ができます。もちろんexceptなどでも使えます。

最終形態

最終的に、こんな感じで実装出来ました。

.gitlab-ci.yml
image: hugo:latest

variables:
  GIT_SUBMODULE_STRATEGY: recursive

stages:
  - creation
  - test 
  - del_test
  - deploy
  
before_script:
  - eval `ssh-agent`
  - ssh-add <(echo "$SSH_PRIVATE_KEY")

file_creation:
  stage: creation
  script:
    - if [[ $CI_COMMIT_REF_NAME =~ ^draft_.*$ ]]; then
    - if [ -n "$FILENAME" ]; then
    - if [ -z "$DIRNAME" ]; then
    - DIRNAME="post"
    - fi
    - hugo new $DIRNAME/$FILENAME.md
    - git config --global user.email $GITLAB_USER_EMAIL
    - git config --global user.name $GITLAB_USER_NAME
    - git add content/$DIRNAME/$FILENAME.md
    - 'git commit -m "new content file creation: $DIRNAME/$FILENAME.md"'
    - git -c core.sshCommand="ssh -oStrictHostKeyChecking=no" push gitlab@hoge-intra/hugo-hp.git HEAD:${CI_COMMIT_REF_NAME}
    - fi
    - fi
  only:
    - web

draft:
  stage: test
  script:
    - 'hugo -b https://hoge-intra.com/test/${CI_COMMIT_REF_NAME}/'
    - rsync -au --delete -e "ssh -oStrictHostKeyChecking=no" public/ gitlab-bot@hoge:foo/bar/html/test/${CI_COMMIT_REF_NAME}
  only:
    - /^draft_.*$/

conversion_test:
  stage: test
  script:
    - hugo
  except:
    - /^draft_.*$/
    - master

delete_testpage:
  stage: del_test
  script:
    - if [[ $CI_COMMIT_REF_NAME =~ ^draft_.*$ ]]; then
    - ssh -oStrictHostKeyChecking=no gitlab-bot@hoge rm -r /foo/bar/test/${CI_MERGE_REQUEST_SOURCE_BRANCH_NAME}
    - fi
  only: 
    refs:
      - merge_requests
  
production:
  stage: deploy
  script:
    - hugo -b https://hoge.com/
    - rsync -au --delete -e "ssh -oStrictHostKeyChecking=no" public/ gitlab-bot@hoge:/foo/bar/html/test 
  artifacts:
    paths:
      - public
  only:
  - master

動作確認

ブランチを切ってテストページが出来るか確かめる

まず、masterブランチからdraft_testというブランチを切るとCIが走ります。
image.png
このようにPipelineが成功します。
image.png

Web siteが作られているか確認します。

$ curl https://www.hoge-intra.com/draft_test/
<!DOCTYPE html><html><head><title>https://www.hoge-intra.com/draft_test/ja</title><link rel="canonical" href="https://www.hoge-intra.com/draft_test/ja"/><meta name="robots" content="noindex"><meta charset="utf-8" /><meta http-equiv="refresh" content="0; url=https://www.hoge-intra.com/draft_test/ja" /></head></html>

ただのリダイレクトページですが、適切に作成されています。

ファイルを作成し、コミットする

次に、ファイル作成をやってみます。
左側のCI/CDから右上のRun Pipelineとクリックすると以下のような画面になりますので、FILENAMEdraft_testを設定します。
image.png

すると、以下のようにfile_creationステージ・draftステージが走ります。
image.png
肝心のコミットも、私のアカウントにて適切に行われています4

HugoもMarkdownで記事を記述するので、実質的にedit画面上でこのようにPreviewも出来るのが嬉しいですね。
image.png

変更をコミットするとCIが走り、テストページにも反映されます。

$ curl https://www.hoge-intra.com/ja/draft_test/post/draft_test/
(略)
     <div class="single-title">Draft_test</div>
      <div>
        <article class="content">


<h1 id="編集について">編集について</h1>

<p>ここで編集しますよー</p>

<h2 id="プレビュー">プレビュー</h2>

<p><strong>Markdown</strong>のPreviewも<code>GitLab</code>のWeb UIでできちゃう!</p>

<blockquote>
<p>便利でしょ?</p>
</blockquote>

<p><del>僕はキメ顔でそう言った。</del></p>

        </article>
      </div>
(略)

HugoにてMarkdownを変換できていることが確認出来ました。

Merge requestを送ってテストページを削除

最後にテストページが適切に削除出来るかを確認します。

まず、Merge requestを作ります。
image.png
すると、Pipelineが走ります。
image.png
Pipelineの中身を見ると
image.png
delete_testpageステージが呼ばれています。

$ curl https://www.hoge-intra.com/draft_test/
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>404 Not Found</title>
</head><body>
<h1>Not Found</h1>
<p>The requested URL /new_test/draft_test/ was not found on this server.</p>
</body></html>

無事にテストページは削除されました。

さいごに

今回は、GitLabのWeb UIを用いてあまりGitに詳しくない方でもWeb更新が出来るようなシステムをGitLab CIで実装してみました。
これによって、誰かが編集中のときにでも並行して更新作業を行うことが出来ます。ただ、一人の方が一つの記事を作成・編集することを前提としています(コンフリクトが起きてもGitLabで簡単に解消出来ますが)。
また、編集ユーザーさんがローカルリポジトリを持たなくて良いというのは一つの利点だと思います。
何故かというと、我々は現在Hugoのテーマを開発していまして、それをGit submoduleで管理しようと思っています。
そうした時に

  1. テーマの編集が終わったらリポジトリ管理者がmasterブランチに新しいコミットに移しておける(submodule_update)
  2. ページ編集する人がmasterブランチから派生ブランチを作れば最新バージョンのテーマでページを作ってくれる

という流れが出来ます。つまり、ユーザーはローカルリポジトリを持たないことからsubmodule updateをしてもらわなくても良いということです。

また、テストページをHugoで見るには本来shellにて

$ hugo server

というコマンドを打って、localhost上にWebサーバーを立てて確認するということをしなければなりませんが、今回の実装にてこの部分もシームレス化出来たのではないでしょうか。

参考

GitLab CIについて
https://qiita.com/ynott/items/1ff698868ef85e50f5a1
GitLab CIでの変数
https://qiita.com/ynott/items/4c5085b4cd6221bb71c5

  1. HugoはExpire dateなどを指定して、ある日にちを経過したら記事を表示させる/させないなどの指定が出来るのですが、この日付は静的サイトへ変換した瞬間の日付・時間を見て表示させるかを判断します。そこで、毎日同じ時間にCIを走らせて変換させてWebサーバーに同期させれば、表示させる/させない記事をコンテンツ編集時だけでなく、1日毎に自動更新できることになります。

  2. 性善説に立てる理由を補足します。本番Webサーバhuga.comとイントラのhoge-intra内のGitLab CIとの間にしかSSH通信出来ないようになってします。また、仮に他のプロジェクトとRunnerをshareしても(一応specificにしておりますが)、RSA認証秘密鍵は各プロジェクトのOwnerのみ触れるものなのでここで(脆弱性がない限り)秘密鍵は守られます。最後に、ホームページを広報さんたちが変更するという性質上、プロジェクトは広報の方と開発側のみメンバーになっているprivateリポジトリとして設定しています。また、限定的人数に秘密鍵が漏れる可能性があってもその人が出来ることはウェブ改竄のみです。このようにして、性善説を一定の領域内でのみ信じることが出来ます。

  3. 現バージョンだと、onlyにand条件がかけられないみたいです)
    また、新しくファイルが作られるので、このファイルをコミットしないとリモートリポジトリ(つまりGitLab上)に反映されません。そこで、新しく作られたファイルをコミットするのですが、ここでGitLab CIでは、このパイプラインを実行したかユーザーの情報を変数として持っていることを利用します。$GITLAB_USER_EMAIL,$GITLAB_USER_NAMEとしてメールアドレス・名前を取得し、設定します。Docker containerがbuildされるたびにgitの設定も初期化されるので、一先ずglobalで変えておきます。

  4. ここのCIは失敗してしまいます。何故なら、ファイルを作成した時に起こるコミットによって走らされるCIはAdministratorで実行されるからです。しかし、Administratorはこのリポジトリにアクセス権限を持たせていないので、エラーが起こるという原理です。が、空ファイルを作っただけで変換テストを行う必要はないだろうという考えです。
    image.png
    そして、ユーザーはWeb UI上でMarkdownを編集します。
    image.png

6
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?