2014年以降、更新が途絶えてしまっていましたが、また一念発起して静的サイトジェネレータである hugo を使って、また、今っぽくパイプラインを組んで aws 上にブログサイトを実装してみました(このサイトです)。その時の aws 側と hugo 側のポイントなんかを記録として残しておきたいと思います。
(aws の各サービスの使い方や hugo の使い方などには触れていませんのでご了承ください。もっとシンプルに構築したい場合は、AWS Amplify Console を使っていただくと良いと思います。)
記事投稿から公開までの流れ
まずはざっくり。
- Markdown で記事を書く
- AWS CodeCommit へ push する
- AWS CodePipeline により AWS CodeBuild のビルドプロジェクトが実行される
- hugo を実行しコンテンツを生成
- 生成されたコンテンツを s3 バケットへ格納
- 各フェーズの結果は AWS Chatbot により Slack へ通知
- CloudFront が s3 のコンテンツを配信
アーキテクチャ
構成や設定などのポイント(aws 側)
ここからは aws 側での設定のポイントや hugo 側の設定のポイントなどを紹介していきたいと思います。まずは aws 側から。
AWS CodeCommit
CodeCommit では特に考慮するところはありません。大したサイトでもないのでブランチも master のみで。Github でももちろん OK です。CodeCommit は個人で少し使うぐらいであれば無期限に無料で利用できるのが良いです(CodeCommit の無料利用枠の詳細については こちら をご参照ください)。
Github の無料アカウントでは Private Repository の作成に制限があった頃は、CodeCommit の優位性もありましたが今となっては制限もないのでどちらでもお好きな方を使っていただければと。
AWS CodeBuild
CodeBuild で使用する buildspec.yml
は以下の通りです。hugo のバージョンを環境変数 HUGO_VERSION
で渡し、hugo をダウンロード、実行、そして s3 sync
でコンテンツを格納という流れです。hugo によるコンテンツ生成と s3 sync
を行うところをそれぞれ別のビルドプロジェクトに分けるやり方もありそうですが、今回はシンプルに1つのビルドプロジェクト内で完結させることにしました。
(<> の部分は環境に合わせて読み替えてください。)
version: 0.2
phases:
install:
commands:
- echo hugo version is ${HUGO_VERSION}
- curl -Ls https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_${HUGO_VERSION}_Linux-64bit.tar.gz -o /tmp/hugo.tar.gz
- tar zxf /tmp/hugo.tar.gz -C /tmp
- mv /tmp/hugo /usr/local/bin/hugo
- rm -rf /tmp/hugo*
pre_build:
commands:
- echo ${CODEBUILD_SOURCE_VERSION}
build:
commands:
- /usr/local/bin/hugo
post_build:
commands:
- aws s3 sync --exact-timestamps --delete ./public/ s3://<Bucket_Name>/
artifacts:
files:
- 'public/**/*'
CodeBuild に付与するロールに 2つ、ポリシーをアタッチします。
(<> の部分は環境に合わせて読み替えてください。)
1つ目はパイプラインの実行上必要そうなポリシー。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": [
"arn:aws:logs:us-east-1:<AWS_Account_ID>:log-group:/aws/codebuild/<Build_Project>",
"arn:aws:logs:us-east-1:<AWS_Account_ID>:log-group:/aws/codebuild/<Build_Project>:*"
]
},
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:GetObjectVersion",
"s3:GetBucketAcl",
"s3:GetBucketLocation"
],
"Resource": [
"arn:aws:s3:::<S3_Bucket_for_Artifacts>",
"arn:aws:s3:::<S3_Bucket_for_Artifacts>/*"
]
},
{
"Effect": "Allow",
"Action": [
"codecommit:GitPull"
],
"Resource": [
"arn:aws:codecommit:us-east-1:<AWS_Account_ID>:<Repository_Name>"
]
},
{
"Effect": "Allow",
"Action": [
"codebuild:CreateReportGroup",
"codebuild:CreateReport",
"codebuild:UpdateReport",
"codebuild:BatchPutTestCases"
],
"Resource": [
"arn:aws:codebuild:us-east-1:<AWS_Account_ID>:report-group/<Build_Project>-*"
]
}
]
}
2つ目はコンテンツを配信する s3 へ sync するために必要なポリシー。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:DeleteObject"
],
"Resource": "arn:aws:s3:::<S3_Bucket_for_Contents>/*"
},
{
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::*"
}
]
}
AWS CodePipeline
続いて CodePipeline。CodePipeline で前述の CodeCommit や CodeBuild をつなぎ、ワークフローとする事で一連の処理を自動化させることができます。 CodePipeline では特筆すべきことはないかなと思います。強いて言うなら、パイプラインの進捗ステータスや実行結果などを AWS Chatbot を使用して slack に通知したかったので、「設定」-「通知」-「通知のルール」からルールを新規作成し、「通知のターゲット」としてあらかじめ作成しておいた「AWS Chatbot」を指定しています。CodePipeline と Chatbot の間に SNS トピックなどを作成しておく必要はなく CodePipeline から直接 Chatbot を設定できます。
Amazon Simple Storage Service(S3)
続いて S3。S3 はバケットを2つ用意します(1つでできますが、用途も異なりますので2つ用意しましょう)。1つは CodeBuild の Artifact の置き場所として。もう1つは生成されたコンテンツのオリジンサイト用として。オリジン用のバケットは詳しくは後述しますが、S3 バケットをオープンにしたくないというのもあって Static website hosting は使用しないようにします。また、ブロックパブリックアクセスも有効にしてパブリックアクセスをすべてブロック
をオンにしておきます。コンテンツに対して S3 に直接アクセスできないようにするってことですね。
オリジン用バケットには CloudFront でディストリビューション作成後、オリジンアクセスアイデンティティ(OAI)を使用して CloudFront からのみ接続可能になるように、以下のようなバケットポリシーを設定します。
(<> の部分は環境に合わせて読み替えてください。)
{
"Version": "2008-10-17",
"Id": "PolicyForCloudFrontPrivateContent",
"Statement": [
{
"Sid": "1",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity <XXXXXXXXXXXX>"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::<S3_Bucket_for_Contents>/*"
}
]
}
Amazon CloudFront
ブログへのアクセスは常に CloudFront 経由としたかったので、Restrict Bucket Access
を有効にして、S3 のバケットポリシーに設定する Origin Access Identity
を作成しておきます。S3 でパブリックアクセスをすべてブロック
をオンにしておくと、Grant Read Permissions on Bucket
を Yes, ...
にしてもバケットポリシーを更新できないので、手動で更新します。
Default Root Object
は index.html
を指定しますが、ここからが問題。Default Root Object
は URL のルートにアクセスがあった場合に表示するオブジェクトの指定で、サブディレクトリにアクセスがあった場合には機能しません。Apache の DirectoryIndex
がサブディレクトリで使えないイメージです。CloudFront で Origin Access Identity
だと例えば以下のようになります。
http://blog.msysh.me/
にアクセスした場合はhttp://blog.msysh.me/index.html
を表示http://blog.msysh.me/sub/
にアクセスした場合は機能せず、今回のバケットポリシーの状況下では AccessDenied
詳しくは、ドキュメントをご参照ください。
何故問題かというと、hugo で生成されるコンテンツやサイト内のリンクがほぼ全てディレクトリパス(「〜/」)になっている、ということです。
もちろん対策方法はありまして、一般的には以下の2つの方法があります。
- Lambda@Edge を使って、「〜/」で終わる URL にアクセスがあったら、「〜/index.html」に書き変える方法(参考: AWS Compute Blog )
- S3 で静的ウェブサイトホスティングを有効にし、CloudFront にカスタムドメインとして登録する方法
それぞれの検討すべき項目や注意点としては
- Lambda@Edge を使用する場合:
- バージニア北部リージョンに Lambda と S3 を設置しなければならない
- Lambda のコストがかかる
- S3 静的ウェブサイトホスティングを有効にする場合:
- S3 バケットをパブリックアクセス可能にする必要がある
- つまりは、オリジンアクセスアイデンティティを使った CloudFront からのみのアクセス許可にできない
個人ブログなのであまりコストもかけたくない一方、パブリックアクセスを許可したくないというのもあって、どちらも採用しないことにし、hugo 側の設定などでリンク先やコンテンツがディレクトリパス(index.html)にならないように、つまりは全てのパスが「/〜.html」になるようにすることにしました。SEO 的によくないのかもしれませんが、個人ブログだしあまり気にしないのでこれでよしとします。
hugo 側の設定
ここからは hugo 側の設定などです。使用しているテーマは「hugo-theme-okayish-blog」。まずは config.toml
側で以下のような設定を入れておきます(一部の抜粋です)。
UglyURLs = true
[permalinks]
page = "/:slug.html"
# メニューの一部抜粋(url を .html にしている)
[[menu.main]]
identifier = "tags"
name = "Tags"
url = "/tags.html"
今回利用しているテーマの場合はこれだけではだめで、以下のタグに関するファイルについてもカスタマイズさせてもらいました。
- layouts/partials/list/tags.html
{{ range .Params.tags }} <a class="p-link--hashtag" href="{{ (urlize (printf "tags/%s.html" . )) | absLangURL }}"> #{{- . -}} </a>  {{ end }}
- layouts/partials/single/tags.html
{{ range .Params.tags }} <a href="{{ (urlize (printf "tags/%s.html" .)) | absLangURL }}">#{{ . }}</a>  {{ end }}
そんなわけでできあがったのが今のこのブログです。更新頻度低めですがちょこちょこと更新していきたいと思います。
最後に・・・
この投稿は個人的なものであり、所属組織を代表するものではありません。ご了承ください。