はじめに
今回は、静的サイトジェネレーターである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.com
とhoge-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笑)。
CIにssh
やrsync
をやってもらうのですが、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の確認を回避しています
-
- 今回通信するサーバー先は自前のDNSサーバーで管理されているものなので、
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に投げられるので、これを用いてファイルを作成するようにします。
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
などでも使えます。
最終形態
最終的に、こんな感じで実装出来ました。
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が走ります。
このようにPipelineが成功します。
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とクリックすると以下のような画面になりますので、FILENAME
にdraft_test
を設定します。
すると、以下のようにfile_creation
ステージ・draft
ステージが走ります。
肝心のコミットも、私のアカウントにて適切に行われています4。
HugoもMarkdownで記事を記述するので、実質的にedit画面上でこのようにPreviewも出来るのが嬉しいですね。
変更をコミットすると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を作ります。
すると、Pipelineが走ります。
Pipelineの中身を見ると
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で管理しようと思っています。
そうした時に
- テーマの編集が終わったらリポジトリ管理者がmasterブランチに新しいコミットに移しておける(submodule_update)
- ページ編集する人が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
-
HugoはExpire dateなどを指定して、ある日にちを経過したら記事を表示させる/させないなどの指定が出来るのですが、この日付は静的サイトへ変換した瞬間の日付・時間を見て表示させるかを判断します。そこで、毎日同じ時間にCIを走らせて変換させてWebサーバーに同期させれば、表示させる/させない記事をコンテンツ編集時だけでなく、1日毎に自動更新できることになります。 ↩
-
性善説に立てる理由を補足します。本番Webサーバ
huga.com
とイントラのhoge-intra
内のGitLab CIとの間にしかSSH通信出来ないようになってします。また、仮に他のプロジェクトとRunnerをshareしても(一応specificにしておりますが)、RSA認証秘密鍵は各プロジェクトのOwnerのみ触れるものなのでここで(脆弱性がない限り)秘密鍵は守られます。最後に、ホームページを広報さんたちが変更するという性質上、プロジェクトは広報の方と開発側のみメンバーになっているprivateリポジトリとして設定しています。また、限定的人数に秘密鍵が漏れる可能性があってもその人が出来ることはウェブ改竄のみです。このようにして、性善説を一定の領域内でのみ信じることが出来ます。 ↩ -
現バージョンだと、
only
にand条件がかけられないみたいです)
また、新しくファイルが作られるので、このファイルをコミットしないとリモートリポジトリ(つまりGitLab上)に反映されません。そこで、新しく作られたファイルをコミットするのですが、ここでGitLab CIでは、このパイプラインを実行したかユーザーの情報を変数として持っていることを利用します。$GITLAB_USER_EMAIL
,$GITLAB_USER_NAME
としてメールアドレス・名前を取得し、設定します。Docker containerがbuildされるたびにgitの設定も初期化されるので、一先ずglobalで変えておきます。 ↩ -
ここのCIは失敗してしまいます。何故なら、ファイルを作成した時に起こるコミットによって走らされるCIはAdministratorで実行されるからです。しかし、Administratorはこのリポジトリにアクセス権限を持たせていないので、エラーが起こるという原理です。が、空ファイルを作っただけで変換テストを行う必要はないだろうという考えです。
そして、ユーザーはWeb UI上でMarkdownを編集します。
↩