Docsy を使用して書いたドキュメントを S3 + CloudFront で公開する。
CONTENTS
- S3 + CloudFront で公開する方針
- baseURL の設定
- ルートコンテンツの作成
- favicon の設定
- find-next-version セットアップ
- permalink 調整
- 更新日設定スクリプト
- GitLab Pipeline で S3 にアップロード
- Lambda@Edge セットアップ
- まとめ
- 参考資料
SOURCES
S3 + CloudFront で公開する方針
以下の方針で S3 + CloudFront で公開する。
- バージョンごとに
/x.x.x/*へデプロイして、そのあとは基本的に変更しない /index.htmlと/x.x.x/*.htmlで、最新バージョンにリダイレクトする js を仕込む
これによって、/x.x.x/* のファイルは永遠にキャッシュできる。
ただし、新しいバージョンへのリダイレクトは js で行うので、この絡みで過去のファイルの変更が必要になる可能性もある。
baseURL の設定
内容の確認を、開発時は /dev/* で、本番環境では /x.x.x/* でアクセスできるようにする必要がある。
このために baseURL をうまく設定してやる必要がある。
baseURL = "https://docs.getto.systems/dev/"
baseURL には本番で使用する URL を指定する。
単に "/dev/" だけではうまくいかなかった。
この例では末尾が /dev/ だが、本番の設定ではここは最新のバージョン番号に変更したい。
そこで、デプロイスクリプトで変更することにする。
sed -i \ -e 's|baseURL = "https://\([^/]\+\)/dev/"|baseURL = "https://\1/'"$version"'/"|' \ config.toml
これでバージョンごとのパスを使用できる。
ルートコンテンツの作成
バージョンごとにデプロイするが、サイトのルートにもコンテンツが必要になる。
root ディレクトリを作成して、この中に以下のファイルを作成する。
- index.html
<!doctype html> <html lang="ja"> <head> <meta charset="utf-8"> <title>DOCS | GETTO.systems</title> <script defer src="/js/find-next-version.min.js"></script> <script defer src="/js/redirect.js"></script> </head> <body> <h1>Documents</h1> <footer>GETTO systems</footer> </body> </html>
内容はほとんどないが、js によって最新バージョンにリダイレクトする。 トップページの体裁が気になるなら整えれば良い。
find-next-version.min.js は、以下の手順でインストールする。
npm install --save-dev find-next-version cp node_modules/find-next-version/dist/find-next-version.min.js root/js/
redirect.js は以下の内容で設置する。
(function(info) { FindNextVersion.find({ from: info.version, url: FindNextVersion.url.index }).then(function(version) { location.href = "/" + version + "/" + info.path + location.search; }); })(FindNextVersion.parse_pathname(location.pathname));
- location.pathname から現在のバージョンを取得
/x.x.x/index.htmlが存在するか確認- 新しいバージョンが見つかったらリダイレクト
favicon の設定
本番にデプロイするなら favicon も用意したい。
ドキュメントによれば、適当なジェネレーターで作成して、/static/favicons/ に配置すれば良い。
また、favicon.ico を以下のパスにもコピーしておく。
- root/favicon.ico
- root/favicons/favicon.ico
これはルートの index.html にアクセスすると /favicon.ico が要求されるため。
また、/x.x.x/*.html にアクセスするとなぜか /favicons/favicon.ico が要求されるため。
find-next-version セットアップ
すべての html に、redirect.js を仕込みたい。
テンプレートを見ると、layouts/partials/head-css.html に仕込むのが良さそう。
このファイルに、以下の内容を追記する。
{{ if eq (getenv "HUGO_ENV") "production" }}
<script src="/js/find-next-version.min.js" defer></script>
<script src="/js/redirect.js" defer></script>
{{ end }}
これで、HUGO_ENV が production である場合に、リダイレクトの処理を行うようになる。
ここで、パスはルートからのパスにする。
本来は /x.x.x/js/ にしたいところ。
以下のように書くとできそうに見える。
<script src="{{ "js/find-next-version.min.js" }}" defer></script>
が、/x.x.x/docs/docs/ のようにネストしている場合、相対パスとして解決されてうまくいかなかった。
permalink 調整
デフォルトではパーマリンクに :slug が入っているため、生成されたコンテンツのパスに日本語が含まれてしまう。
S3 にアップロードする際文句を言われるので、この部分を :filename にした。
更新日設定スクリプト
config.toml で以下のように設定する。
enableGitInfo = false
こうすると git の情報から最終更新日が設定されない。
true にして最終更新日がうまく設定できれば使ったのだけれど、何かうまくいっていないようなので OFF にした。
(要検証)
そこで、更新日を設定するスクリプトを設置した。
#!/bin/bash
set_content_date(){
local file
local attr
local date
for file in $(git grep GETTO_DOCS_CONTENT_DATE | sed 's/:.*//'); do
if [ "$(git grep -e "^---\$" -n $file | wc -l)" -gt 1 ]; then
attr=$(git grep -e "^---\$" -n $file | head -2 | tail -1 | cut -d':' -f2)
date=$(git log -1 --format=%ad --date=short $file)
sed -i -e "1,$attr s|GETTO_DOCS_CONTENT_DATE|$date|" $file
fi
done
}
set_content_date
---で区切られている部分の GETTO_DOCS_CONTENT_DATE を git の最終更新日に置換
これで日付部分に GETTO_DOCS_CONTENT_DATE と書いておくと、デプロイ時に置換される。
GitLab Pipeline で S3 にアップロード
まず、root ディレクトリの中身はルート直下にアップロードしておく。
#!/bin/bash
build_main(){
local version
local metadata
export AWS_ACCESS_KEY_ID=<ACCESS-KEY-ID>
export AWS_SECRET_ACCESS_KEY=<SECRET-ACCESS-KEY>
metadata=$(node metadata.js)
cd root
for file in *; do
if [ -d $file ]; then
opt="--recursive"
else
opt=""
fi
aws s3 cp \
--acl private \
--cache-control "public, max-age=86400" \
--metadata "$metadata" \
$opt \
$file s3://DOMAIN/$file
done
}
build_main
本番用にコンテンツを生成するには hugo コマンドを実行する。
ここでは、さらに環境変数 HUGO_ENV も設定する必要がある。
コンテンツを生成したら S3 にアップロードする。
#!/bin/bash
deploy_main(){
local version
local domain
local metadata
local file
version=$(cat .release-version)
./bin/set_content_date.sh
sed -i \
-e 's|baseURL = "https://\([^/]\+\)/dev/"|baseURL = "https://\1/'"$version"'/"|' \
-e 's|GCS-ENGINE-ID|'"$GCS_ENGINE_ID"'|' \
config.toml
domain=$(grep "baseURL" config.toml | sed -e 's|.*baseURL = "https://\([^/]\+\)/.*|\1|')
export HUGO_ENV=production
hugo -EF -e production
metadata=$(node metadata.js)
aws s3 cp \
--acl private \
--cache-control "public, max-age=31536000" \
--metadata "$metadata" \
--recursive \
public s3://$domain/$version
for file in robots.txt sitemap.xml; do
aws s3 cp \
--acl private \
--cache-control "public, max-age=86400" \
--metadata "$metadata" \
public/$file s3://$domain/$file
done
}
deploy_main
/x.x.x/*は1年、ルート直下のファイルは1日キャッシュする
S3 のメタデータは以下の内容。
const headers = { "strict-transport-security": "max-age=31536000", "content-security-policy": [ "default-src 'none'", "object-src 'none'", "base-uri 'none'", "form-action 'self'", "connect-src 'self'", "frame-src 'none'", "frame-ancestors 'none'", "img-src " + [ "https://www.google.com/cse/static/css/", "https://www.google.com/cse/static/images/", "https://www.google.com/images/", "https://www.googleapis.com/", "https://clients1.google.com/", "https://ssl.gstatic.com/ui/", "'self'", ].join(" "), "font-src " + [ "https://fonts.gstatic.com/", "'self'", ].join(" "), "script-src " + [ "https://cdnjs.cloudflare.com/ajax/libs/popper.js/", "https://stackpath.bootstrapcdn.com/bootstrap/", "https://code.jquery.com/", "https://cse.google.com/cse.js", "https://cse.google.com/cse/element/", "https://cse.google.com/adsense/search/", "https://www.google.com/cse/static/element/", "'self'", "'unsafe-eval'", "'sha256-bimIMyRXEP/oybxalWcIAhSYpbLihuUf1RiqrHsg1wA='", ].join(" "), "style-src " + [ "https://fonts.googleapis.com/", "https://www.google.com/cse/static/element/", "https://www.google.com/cse/static/style/look/", "'self'", "'unsafe-inline'", ].join(" "), ].join(";"), "x-content-type-options": "nosniff", "x-frame-options": "DENY", "x-xss-protection": "1; mode=block", "referrer-policy": "same-origin", }; console.log(JSON.stringify(Object.keys(headers).reduce((acc,key) => { acc["header-" + key] = headers[key]; return acc; }, {})));
header-をつけて追加したいヘッダを設定しておく
このデプロイスクリプトを GitLab の Pipeline で実行する。
GitLab でサブモジュールを init するには、以下の変数を設定しておく必要がある。
variables: GIT_SUBMODULE_STRATEGY: recursive
これで、GitLab の Pipeline でデプロイできる。
Lambda@Edge セットアップ
S3 + CloudFront でコンテンツを配信する際に、以下の Lambda が必要。
- メタデータからヘッダを追加する
*/へのリクエストを*/index.htmlとしてオリジンにリクエストする
response-header.js : Origin Response に設定する。
'use strict'; exports.handler = async (event) => { const response = event.Records[0].cf.response; let headers = response.headers; Object.keys(headers).forEach((raw) => { const lower = raw.toLowerCase(); const pattern = /^x-amz-meta-header-/; if (lower.match(pattern)) { const key = lower.replace(pattern, ""); headers[key] = [{ key: key, value: headers[raw][0].value, }]; } }); return response; };
request-directory-index.js : Origin Request に設定する。
'use strict'; exports.handler = async (event) => { let request = event.Records[0].cf.request; request.uri = request.uri.replace(/\/$/, '/index.html'); return request; };
CloudFront にこれらの Lambda を設定して完了。
まとめ
Docsy を使用して書いたドキュメントを S3 + CloudFront で公開する方法をまとめた。
結局、Lambda@Edge が必要なんだな…。