七 月
4
土曜日
2020

ブログのインフラ構成を改善する

過去記事『HugoとAWSによるブログ構築』で構築した本サイトに以下改善を図った。

  • インフラ構成をTerraformでコード化
  • CI/CD環境を CircleCI から AWS CodeBuild に変更
  • カスタムDockerイメージの管理をDockerHubからECRに変更
  • インフラ、CI/CDの改善をしやすいよう別ドメインで開発環境を用意

システム構成

改善後のシステム構成。

BlogBuilderイメージのビルドフロー

Github上のBlogBuilderリポジトリは静的サイト生成ジョブ用のDockerfileを管理している。
リポジトリが更新されるとWebhookによってCodeBuilder上のBlogBuilderプロジェクトのジョブが実行される。
ジョブは静的サイト生成用DokerイメージをビルドしECR上のBlogBuilder コンテナリポジトリへプッシュする。

ブログ記事の公開フロー

Github上のBlogリポジトリはブログ記事をMarkdownで管理している。
リポジトリが更新されるとWebhookによってCodeBuilder上のBlogプロジェクトのジョブが実行される。
ジョブはHugoでMarkdownから静的サイトを生成、生成した内容をS3バケットへ同期、CloudFrontのキャッシュを無効化し最新状態のブログ記事を公開する。

Terraform化

https://github.com/nijohando/blog-orchestration

今回は実験的に以下の試みを行った。

  • プロジェクトとレイヤー
  • 再利用を目的としない関数としてのモジュール

プロジェクトとレイヤー

プロジェクトレイヤーという概念をTerraformコードに導入してみた。

プロジェクト

プロジェクトは高凝縮、低結合度となるような単位でまとめたTerraformの実行単位。
今回はtf0.d/site(静的サイト用プロジェクト) とtf1.d/ci(CI/CD用プロジェクト)の2プロジェクトで構成。

レイヤー

レイヤーはプロジェクトが属する層のことで上位レイヤーのプロジェクトは下位レイヤーの任意のプロジェクトへの依存関係を持つ。
プロジェクト間の依存関係とは依存先プロジェクトのoutputterraform_remote_state越で参照する事を意味する。

今回の構成ではディレクトリtf0.dはレイヤー0を表し、tf1.dはレイヤー1を表す。番号が小さいほど低レイヤーであり、Applyは低 → 高の順に、Destroyは高 → 低の順に行う。
レイヤーには複数プロジェクトが属することもあるが今回は規模が小さいためレイヤー毎に1プロジェクトという構成になっている。

再利用を目的としない関数としてのモジュール

モジュールは複数リソースから構成される特定の機能をパッケージ化したもので一般的に汎化、再利用性に重きが置かれている事が多い。
一方モジュールには入力と出力があり、リソースや変数のスコープを限定できるため、これをある種の関数と見なすこともできる。

今回はモジュールを再利用を目的としない単なるプライベート関数として扱い、プログラミングにおける

  • main関数に直接処理を書かず処理内容に応じたそれぞれの関数を作成する
  • main関数では各関数を呼び出すフロー制御を行う

という処理構造をTerraformに適用してみた。
プロジェクトルートのmain.tfをmain関数に見立て、ここから各モジュールをプライベート関数として呼び出していく。
プロジェクト内限定のプライベートな関数は汎化を考慮しない分悩むポイントが少なく、処理の単位とその入出力が明確になるためコードの見通しが良くなった。

開発環境

今回のTerraform化により別環境を横に増やす事が容易になったので本番環境に加えて開発環境を構築した。
システムを構成する全てのAWSリソースは環境別に用意されるため、構成変更を行う際は開発環境で事前にテストを行う事ができるようになったが、 以下のような開発環境特有の対応が別途必要になった。

開発環境へのアクセス制限

開発環境は他者から閲覧できる必要はなく、むしろクローラーに補足されると問題になるためアクセス制限を行う必要がある。
アクセス制限の方法としては、

  1. Lambda Edgeをかましてアクセス制限
  2. S3のリソースポリシー側にIP制限
  3. AWS WAFをCloudFrontにアタッチしIP制限

などがあるが、 今回はCloudFront + S3の組み合わせなのでは(2)は不可。
(S3から見るとSource IPがCloudFrontになるため)

WAFv2をTerraformから使ってみたいという事もありAWS WAFをCloudFrontへアタッチしIPベースの制限をかけ、自宅以外からのアクセスを遮断するようにした。

しかしこれによりWAFの ウェブACL x 1ルール x 1 の使用料が月6ドルが掛かってしまうので、そのうち(1)のLambda Edgeでアクセス制限す流方法に変更しようと思う。

インデックスドキュメント

CloudFrontはルートディレクトリへのアクセスに対してインデックスドキュメント(index.html)を返す事ができるが、サブディレクトリへのアクセスに対してはオリジンサーバ側の対応が必要。
オリジンサーバにS3を使用している場合、サブディレクトリのインデックスドキュメントを返すためにはS3の静的ウェブサイトホスティングを有効にする必要がある。
本サイトではサブディレクトリ形式のURLを多用しているので静的ウェブサイトホスティングの有効化は必須なのだが、これが開発環境のWAFによるアクセス制限と絡むと後述のS3への直アクセスをどうブロックする?問題に発展する。

S3への直アクセスをどうブロックする?問題

CloudFrontからカスタムオリジンとして静的ウェブサイトホスティングを有効にしたS3を利用した場合、オリジンアクセスアイデンティティが利用できなくなる。
今回のようにWAFでアクセス制限をかけている場合、CloudFrontを迂回されS3に直接アクセスをされると制限を回避されてしまうのでこれを防ぐ必要がある。

一般的によくある方法としてはCloudFrontとオリジンサーバで秘密鍵を共有、CloudFront側でX-SHARED-SECRETのようなカスタムヘッダーに共有秘密鍵を追加しオリジンサーバ側でチェックするという方法があるが、今回のようにオリジンサーバがS3の場合にリソースポリシーでカスタムヘッダーの値をチェックする術が見当たらなかった。無念。。。

苦肉の策として、S3のリソースポリシーでチェックできる RefererヘッダーをX-SHARED-SECRETヘッダの代替として利用することにした。
TerraformのApply時に共有秘密鍵となるUUIDを生成、CloudFront側では生成したUUIDを値に入れたReferer追加するようにし、S3側では以下のようなポリシーで Referer ヘッダの値をチェックするようにした。

{
  "Version":"2012-10-17",
  "Statement":[
    {
      "Sid":"AllowAccessFromCloudFront",
      "Effect":"Allow",
      "Principal":"*",
      "Action":"s3:GetObject",
      "Resource":"arn:aws:s3:::${bucket_name}/*",
      "Condition":{
        "StringLike":{"aws:Referer":["${site_url}/${cloudfront_token}"]}
      }
    }
  ]
}

いくら使い道がないRefererヘッダーとはいえ、こんな用途に勝手に使ってしまって良いのか若干の心配があったが、 以下の公式サポートでお墨付きを頂いているようなので問題無いようだ。

Amazon S3 でホストされている静的ウェブサイトを提供するために CloudFront をどのように使用したらよいですか?

CloudFront ウェブディストリビューションを作成します。ユースケースに必要なディストリビューション設定に加えて、以下を入力してください。 [Origin Domain Name] で、手順 2 でコピーしたエンドポイントを入力します。 [ヘッダー名] の [Origin カスタムヘッダー] に、[Referer] と入力します。[値] には、オリジンに転送するカスタムヘッダーを入力します (S3 バケット)。オリジンへのアクセスを制限するために、ランダムな値または他の人は知らない秘密の値を入力することができます。