エムスリーテックブログ

エムスリー(m3)のエンジニア・開発メンバーによる技術ブログです

M3 USA 出張記 #3: SPA を CloudFront + S3 でシンプルにデプロイしてみました

こんにちは、エンジニアリングGの矢崎です。

最近は M3 USA で仕事をしており、下記の前回の記事でご紹介した Headless CMS Contentful を利用するアプリケーションを React の Single Page Application (SPA)で作っています。

www.m3tech.blog

コードはほぼ全てフロントエンドの SPA で実装するアーキテクチャなのですが、そのようなアプリケーションを CloudFront でデプロイする際に工夫した点や知見のご共有です。

CloudFront

f:id:Saiya:20181029221716p:plain

今回、以下のような利点があるため AWS の提供する CDN である CloudFront を利用しました:

  • 初期投資・固定費用が小さい (HTTPS を利用する場合でも, 後述)
  • AWS のエコシステムに乗ることが出来て便利
  • 凝った機能(画像処理など)がない代わり仕様がシンプルであり、挙動や特性が比較的把握しやすい

やったこと

主な作業はこれだけです:

  1. terraform の cloudfront_distribution および cloudfront_origin_access_identity を構築
  2. CI/CD でデプロイ時に SPA アプリをビルド && aws s3 sync && aws cloudfront create-invalidation

筆者は CloudFront を初めて触ったのですが、本番リリースにも利用できる構成が 1 日程度でセットアップできました。実際は既存のオンプレミス環境との DNS の調整や GuardDuty *1のセットアップなどの本件に直接関係ない仕込みもあったのですが、それでも数日程度です。

既に多数の解説記事が世の中にあるので、上記のセットアップのやり方そのものについてはここでは深入りせず、本記事ではセットアップ時に工夫した点・気をつけた方が良いと思われる点をご紹介します。

CloudFront の設定項目について

Infrastructure as a Code 用の自動化ツールである terraform を使う場合は、cloudfront_distribution のドキュメント にある設定項目を一通り見ることで CloudFront の設定項目が一通りわかります。

terraform を使わない場合も、AWS 公式のドキュメントよりもこういったツールの設定項目の一覧を見るほうが把握しやすい気が個人的にはします。

上記の cloudfront_distribution にある設定を一通りやれば動くのですが、実際に使ってみて気づいた興味深い点や工夫した点・留意が必要そうな点について以下に記します:

Price Class (料金クラス) を絞るとお得

CloudFront の料金ページの中央あたりに記載があるのですが、CloudFront には Price Class (料金クラス) という概念があります。

提供する Edge Location (CDN の拠点)を絞ると安くなるというものです。例えば医療業界のように地域・国による差が大きいサービス特性の場合、US 向けのサービスを全ロケーションにデプロイする意義は小さいので、このように地域を絞ることで安くなるのは良いことですし地球環境にも少しだけやさしいことでしょう。

皆様も CloudFront を使う際は必要最小限の Price Class にすると良いかと思います。

Geo Restriction (地域制限) は初期構築時に検討しておけると吉

CloudFront には Geo Restriction (地域制限) 機能があります。

昨今の個人情報に関する規制の情勢などを踏まえても、サービスの本来の対象地域以外からのアクセスを可能にすることにはデメリットがある時代になってきているのではないでしょうか。Geo Restriction 機能を用いることでそういったリスクを減らせます。

加えて、礼儀正しくないクローラー・アタッカーによる大量アクセスを減らす一定の効果もあります*2ので、その意味でもこの機能も設定しておく価値があるかと思います。

IP アドレスから地域を判定しており精度が 100% ではない(上記ドキュメント曰く 99.8% の精度)ですが、その点が許容できれば良いかと思います。サービス開始後から制限を掛けようとすると思わぬ影響*3が出る心配があり辛いため、サービスの開始前に予め設定しておくと良いでしょう。

HTTPS は SNI にすれば安い

CloudFront で自分のドメイン*4を利用しかつ HTTPS を利用する場合、昔は Dedicated IP (専用 IP) が必須であったため毎月 600 ドル/の固定費*5が必要だったのですが、今では SNI (Server Name Indicaton) によって追加費用なしで HTTPS の利用が可能です。

SNI と言われても TLS プロトコルの知識がないとピンと来にくいと思いますが、これは以下のような仕組みです:

  1. TLS 通信を開始する際、まずブラウザからサーバー(CloudFront)へ通信をする。
    • その際に、ブラウザがどのドメインに接続しようとしているかを宣言する
      • これが SNI - Server Name Indication
  2. サーバー(CloudFront)は SNI で指定されたドメインの証明書を返す
    • Dedicated IP (専用 IP) の場合は、SNI が無くてもブラウザが接続してきた IP アドレスを見ることでどのドメインの証明書を返すべきか分かります
  3. ブラウザ側で証明書の検証が行われる等のプロセスを経て、TLS の接続が確立される
  4. HTTP の通信が TLS 上で開始される (HTTPS 通信)

IP アドレスを共有利用している場合は SNI がないと 2. のステップでどのドメインの証明書を返すべきかが分からなくなってしまいます。なので Dedicated IP (専用 IP) なしで運用するためにはブラウザが SNI を送ってくる必要があります *6Windows XP や IE 6 では SNI に対応していない といった対応ブラウザの制約はありますが、その点が問題なければ SNI で運用したほうが固定費が削減できますし、固定 IP の確保に伴うセットアップの手間も減るので良いでしょう。

Lambda@Edge による認証・認可

内部用のページや開発環境など、ページによっては不特定多数からアクセスさせたくないケースがあります。そのような場合、Lambda@Edge を使うことで、リクエストに介入し認証状態をチェック、アクセスを認可する処理を実現できます。

認証の要件は場合によって様々であるためここでは深く触れませんが、例えば basic 認証 lambda edge で事例を調べてみると実現イメージがつかみやすいかと思います。

CloudFront / Lambda@Edge の良いところとして、CDN 固有の独自言語などではなく普通の JavaScript や Python などなどが利用でき、また処理の内容もかなり自由に出来るのでその点が素晴らしいです。

CloudFront の Origin の S3 は public にせず、CloudFront だけにアクセスを開放する

世の中のチュートリアル記事などは S3 を public ACL でセットアップしていますが、筆者は以下の点で好ましくないと考えています:

  • CloudFront のアクセスログが信じられなくなる (S3 直接アクセスが記録に残らない)
    • リソースの利用状況の調査をするといったことが難しくなります。例えば不要ファイルをアクセスログから不要と断定することが難しくなる。
  • Lambda@Edge での認証などもすり抜けてしまう
    • 後から認証を追加した際に S3 直接アクセスが抜け穴になってしまう、といった将来に対するリスクもあります

あとになってから S3 へのアクセスを遮断すると予期せぬアクセス断*7が出るリスクがあり厄介なので、最初から S3 への直接アクセスは不可能にしておくと良いでしょう。

やり方としては以下の流れになります (AWS 公式のドキュメントはこちら):

  1. CloudFront の Origin Access Identity を作成する
    • CloudFront が S3 にアクセスする際に使う擬似的な IAM ユーザーのようなものです
  2. CloudFront Distribution が上で作成した Origin Access Idendity を使うように設定する
  3. S3 側では、Origin Access Identity の principal に対して s3:GetObject, s3:ListBucket *8 を許可する
    • S3 の仕様として ListBucket"arn:aws:s3:::bucket名" に、GetObject"arn:aws:s3:::bucket名/*" に対して許可する必要がある (S3 で誰もが一度はハマる点)

terraform であれば cloudfront_origin_access_identity のドキュメントにある通りにやるだけなので簡単です、手動でセットアップするより楽なのではないでしょうか *9

ACM 証明書は DNS 認証でセットアップすると楽

CloudFront の話ではないですが、ACM で無料の SSL 証明書を発行する場合、メール認証ではなく DNS 認証でセットアップするのがおすすめです。

SSL 証明書を発行する際にドメインの所有権を立証する必要があるのですが、メール認証(初期はこれしかなかった)では以下のような欠点があります:

  • 当該ドメインでメールを受信するためのセットアップが必要
  • 定期的に確認メールが来るので都度対応しないとならない
    • さもないと証明書が更新されず期限切れしてしまう
  • メールの手動確認が必要なため、terraform などで自動化できない

DNS 認証であれば、一回設定するだけで良いですし自動化も容易になので特別な理由がない限りおすすめです。terraform であれば aws_route53_record を使うことで簡単かつ DRYに設定できます。

CloudFront の invalidate と SPA のビルド成果物のファイルパス

CloudFront は S3 側の更新を検知しないので、明示的にキャッシュを invalidate (日本語版で言う 無効リクエスト*10 )する必要があります。

CloudFront の invalidation の説明ページ にある通り、以下の点には留意が必要です:

  • Invalidate するファイル数と回数に比例して課金される
  • あまり大量のファイルは一気に invalidate できない

今回作成した React アプリケーション(create-react-app 利用) の場合、ほとんどのファイル名は build の都度にハッシュ値を含むファイル名が生成されるので、限られたファイルだけを invalidate すれば OK でした。

デプロイ時に CI/CD ツールから invalidate する場合、以下のように AWS CLI コマンド で簡単に invalidate できます:

aws cloudfront create-invalidation \
    --distribution-id "${CF_DISTRIBUTION_ID}" \
    --paths \
        "${CF_PATH_PREFIX}/favicon.ico" \
        "${CF_PATH_PREFIX}/asset-manifest.json" \
        "${CF_PATH_PREFIX}/manifest.json" \
        "${CF_PATH_PREFIX}/service-worker.js" \
        "${CF_PATH_PREFIX}/index.html"

上記のコマンドの結果の JSON に出てくる CallerReference は invalidate の進捗を確認する際などに必要なので、上記のコマンドの出力 JSON はそのまま CI/CD のログに含めておくと良いでしょう。といっても今の所 invalidate はすばやく完了しており、確認する必要が実際に生じたことはないです。

まとめ

CloudFront を使うことで、初期コストと保守負担を両方押さえつつ、運用性や拡張性も高い構成で SPA アプリケーションをデプロイすることができました。

Contentful のような SaaS と今回述べた CloudFront などの CDN サービスを活用することで、コードやロジックはフロントエンドで完結する構成にし、すばやく開発し保守すべきものも最小限にすることが出来ます。フロントエンド完結型のアーキテクチャ設計が採用できそうな局面では、こういったシンプルな構成が取れるという利点を考慮に入れる価値があると思います。

We are hiring

こういったモダンな技術を投入しつつ、しかし安易に技術を使うだけでなく安全性・機能性・コストを両立するかといった試み・チャレンジに積極的なのが M3 の社風です。日本 *11・US どちらでも採用を行なっておりますので、是非お声がけください:

jobs.m3.com

*1:AWS アカウントを作ったら欠かさず全リージョンに設定しておくと良いです

*2:もちろん本気でアタックしてくるものは防げません。しかしこれまでの経験・実績として意外と IP アドレスの地域ベースでの制限は効果があります

*3:システム連携先が他リージョンからアクセスしてました、とか

*4:https://xxxxxx.cloudfront.net ではないドメイン

*5:執筆時点での価格

*6:HTTP には Host: ヘッダーがあり、これを見ても接続先ドメイン名が分かります。しかしそれは 4. のステップで送られてくるため、2. の段階では使えません。

*7:S3 直アクセスに依存する処理が実はありました、的な事案。CloudFront アクセスログでは影響があるか事前に調べることもできない

*8:署名付き Cookie/URL などを使って更新操作も行う場合はそれらのアクションも追加

*9:筆者は全て terraform でやってしまったので推測ですが

*10:この言い回しはちょっと混乱しました。最初にこの単語を見たときは API のリクエストのやり方が間違っていて無効である、という意味かと思いましたが、実際はキャッシュの無効化操作のことでした。

*11:日本にはグローバルチームもあります