2014年以降、更新が途絶えてしまっていましたが、また一念発起して静的サイトジェネレータである hugo を使って、また、今っぽくパイプラインを組んで aws 上にブログサイトを実装してみました(このサイトです)。その時の aws 側と hugo 側のポイントなんかを記録として残しておきたいと思います。

(aws の各サービスの使い方や hugo の使い方などには触れていませんのでご了承ください。もっとシンプルに構築したい場合は、AWS Amplify Console を使っていただくと良いと思います。)

記事投稿から公開までの流れ

まずはざっくり。

  1. Markdown で記事を書く
  2. AWS CodeCommit へ push する
  3. AWS CodePipeline により AWS CodeBuild のビルドプロジェクトが実行される
    1. hugo を実行しコンテンツを生成
    2. 生成されたコンテンツを s3 バケットへ格納
    3. 各フェーズの結果は AWS Chatbot により Slack へ通知
  4. 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>/*"
    }
  ]
}

参考 : https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html

Amazon CloudFront

ブログへのアクセスは常に CloudFront 経由としたかったので、Restrict Bucket Access を有効にして、S3 のバケットポリシーに設定する Origin Access Identity を作成しておきます。S3 でパブリックアクセスをすべてブロックをオンにしておくと、Grant Read Permissions on BucketYes, ...にしてもバケットポリシーを更新できないので、手動で更新します。

Default Root Objectindex.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>&ensp;
    {{ end }}
    
  • layouts/partials/single/tags.html
    {{ range .Params.tags }}
    <a href="{{ (urlize (printf "tags/%s.html" .)) | absLangURL }}">#{{ . }}</a>&ensp;
    {{ end }}
    

そんなわけでできあがったのが今のこのブログです。更新頻度低めですがちょこちょこと更新していきたいと思います。

最後に・・・

この投稿は個人的なものであり、所属組織を代表するものではありません。ご了承ください。