Hugoで作ったサイトをS3 + CloudFrontで独自ドメインで公開する

はじめに

  • Hugoでブログをはじめました
  • 配信環境としてAWSのS3, CloudFront等を利用。aws-cdkで構築しています
  • 無料でホスティングできるサービスも多々ありますがAWSを選んた理由として、
    • 個人的にAWSは慣れてて使いやすい
    • cdkを使うことで再利用(再設定)が容易
    • どうせアクセスは多くならないだろうし利用料金も高くならないだろう

このブログ配信に使っている S3 + CloudFront + 独自ドメイン でのHugoサイトの配信環境構築について紹介します。

前提

DNSゾーン

公開DNSゾーンとしてRoute53のHostedZoneが存在します。
(このブログでいうと kefiwild.com という公開ゾーンが存在している)

証明書

ACMでワイルドカード証明書を取得しています。(このブログでは *.kefiwild.com )
CloudFrontに独自ドメインの証明書を食わすためには証明書をus-east-1リージョンで取得する必要があります。(https://docs.aws.amazon.com/acm/latest/userguide/acm-regions.html)

後述のCloudFrontのデプロイに必要なCertificateArnを、例えば以下のように確認します。

  $ DOMAIN_NAME="*.kefiwild.com"; aws --region us-east-1 acm list-certificates | jq -r --arg domain_name ${DOMAIN_NAME} '.CertificateSummaryList[] | select(.DomainName == $domain_name) | .CertificateArn'
  arn:aws:acm:us-east-1:...

AWSリソースのデプロイ

構成図

AWSリソースのデプロイにはaws-cdkを使っています。
リポジトリは https://github.com/kefi550/s3_website_distribution

CloudFront Distribution

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
    const distribution = new cf.CloudFrontWebDistribution(
      this,
      `${this.stackName}-distribution`,
      {
        priceClass: cf.PriceClass.PRICE_CLASS_100,
        originConfigs: [
          {
            customOriginSource: {
              domainName: bucket.bucketWebsiteDomainName,
              originProtocolPolicy: cf.OriginProtocolPolicy.HTTP_ONLY,
              originHeaders: {
                Referer: crypto
                  .createHash("md5")
                  .update(this.stackId)
                  .digest("hex"),
              },
            },
            behaviors: [
              {
                isDefaultBehavior: true,
              },
            ],
          },
        ],
        viewerCertificate: {
          aliases: [domainName],
          props: {
            acmCertificateArn: acmCertificateArn,
            sslSupportMethod: cf.SSLMethod.SNI,
          },
        },
        errorConfigurations: [
          {
            errorCode: 404,
            responseCode: 200,
            responsePagePath: "/404.html",
          },
        ],
      }
    );

S3

1
2
3
4
5
6
7
    const bucket = new s3.Bucket(this, `${this.stackName}-bucket`, {
      bucketName: `${this.stackName}-origin-bucket`,
      removalPolicy: cdk.RemovalPolicy.RETAIN,
      accessControl: s3.BucketAccessControl.PRIVATE,
      websiteIndexDocument: "index.html",
      versioned: true,
    });
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
    const bucketPolicy = new iam.PolicyStatement({
      effect: iam.Effect.ALLOW,
      actions: ["s3:GetObject"],
      resources: [`${bucket.bucketArn}/*`],
      principals: [new iam.Anyone()],
      conditions: {
        StringEquals: {
          "aws:Referer": crypto
            .createHash("md5")
            .update(this.stackId)
            .digest("hex"),
        },
      },
    });
    bucket.addToResourcePolicy(bucketPolicy);
  • 今回は特にキャッシュTTL等の設定は弄らずデフォルト
    • アクセスとかトラフィック増えるようなことがあればその時にチューニングしようという気持ち
  • viewer policyはSNI only(たぶんデフォルト)に設定
    • 料金を詳しく読まないまま、特に古いクライアントからのアクセスを拒否する必要もないかなみたいな気持ちで sslSupportMethod をVIPにしてたら、約5日後に料金に気づき100ドル近くかかってました…(よく考えればそれはそう)請求
    • マネジメントコンソールで見たら丁寧にお金かかるよ!って書いてあった… マネジメントコンソールでの表示
    • 特にレガシーブラウザ向け等の要件がなければSNIに設定すべき

オリジンにはS3 website hostingによるhttpエンドポイントを設定

CloudFront DistributionはS3バケット自体をオリジンに設定することが可能ですが、今回はs3 website hosting機能によるs3バケットのhttpエンドポイントをオリジンとして設定しています。
Hugoではデフォルトでは各記事へのリンクは /posts/article1/ のような index.htmlを省略したリンクとなるようです。
index.html が省略されている場合、S3バケット自体をオリジンとするdistributionでは index.html へのリダイレクトが機能しません。

S3バケット自体をオリジンとする場合は、OAI(Origin Access Identity)によってS3へのアクセスをCloudFrontからのみに制限することができますが、S3 website hostingを使う場合はS3バケットを公開にする必要があります。
S3直のHTTPでのアクセスされるのはちょっと気持ち悪いので、CloudFrontでカスタムヘッダを付与することでアクセス制限を実現できます。

  • このブログは別に大丈夫なんですが、HTTPでアクセスされるとダメな要件(HTTPSでのアクセスを強制したい場合)ではおそらく必須
  • CloudFrontでログを採る場合とかはS3直アクセスされるとロギングされなくなっちゃうので必要

詳しくはAWSのドキュメント参照

コンテンツのデプロイ

Hugoサイトのビルド

$ hugo

Hugoの public/ 以下をS3バケットにアップロードすることでデプロイされます。

aws cliでのコマンド例

$ aws s3 sync --delete public/ s3://<バケット名>/
  • --delete オプションを付けることで、削除した記事やファイルをS3バケットに反映させるのがポイント

コンテンツを更新した後は、viewerがキャッシュされた古いコンテンツを参照しないようにCloudFrontのキャッシュクリアを行います

$ aws cloudfront create-invalidation --paths "/*" --distribution-id <ディストリビューションID>

おわり

  • 今はまだ手元でビルドして手でS3にあげているが、デプロイまわりもそのうちやる