12
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Cloudfront + S3 + API GatewayでSPAとAPIを公開する時のTips

Last updated at Posted at 2022-06-04

良くあるこのパターンですが、自分なりに辿り着いたプラクティスを紹介しておきます。

image.png

先に結論

  • SPA: Cloudfront Functionsを使って拡張子付きのパスを/index.htmlに変更する
  • API: Behaviorで/apiをAPI Gatewayに向ける

背景

今まで、Cloudfront + S3 でVueやAngular等のSPAアプリケーションを公開する場合、
存在しないパスにリクエストすることで、S3が403もしくは404を返す。
そのため、Cloudfront側でカスタムエラーページを作成し、/index.htmlにリダイレクトするような設定をしていた。

よくある悩み1(SPAのパス問題)

本来キャッチすべき404があった場合でも気にせずリダイレクトされてしまう。
例えばCSSやJSのアップロードミスがあった場合は気づきにくい。

よくある悩み2(Cloudfrontで特定のパスを振り分けたい問題)

/api のパスはAPI Gatewayに振り分けたい。
CORSの設定が不要となるので、セキュリティ的にもよさそうな方法。

SPAのパス問題

Cloudfront Functionsを使うと、エラーページを待たずして良い感じで/users/xxxのようなリクエストを /index.html に振り分けることができます。

  • Cloudfront Functionsとは、エッジでイベントを処理するコードをかけるものになります。
  • 同じようなものにlambda@edgeがありますが、違いについてはこちらをご参照ください。
  • lambda@edgeから機能を限定して簡単な処理のみ行えるようになったものと理解すればよさそうです。

また、Cloudfront Functionsを手っ取り早く試したい方はこちらを試してみると良いです。
Cloudfront Functions tutorial

terraformの例

Cloudfront Functionsの設定

このようにリソースを定義します。(AWS Providerの設定等、直接関係のない部分は省略しています)
パラメータの説明はドキュメントを見てください。

  • code に実際の処理を書きます。ここでは別ファイルにして読み込んでます。
  • publish = true にすることで、公開状態にすることができます。
cloudfront_functions.tf
resource "aws_cloudfront_function" "spa_redirect" {
  name    = "${var.product_name}-${var.env}-spa-redirect"
  runtime = "cloudfront-js-1.0"
  comment = "${var.product_name}-${var.env}-spa-redirect"
  publish = true
  code    = file("cloudfront_functions/spa-redirect.js")
}

関数部分

かなり古い書き方をしていますが、なぜかECMAScript 5.1 準拠である必要があります。
これについては、ぜひ改善していただけるとありがたいですね。

  • 拡張子の有無はドットの有無で判断しています。if(request.uri.indexOf(".") === -1)
cloudfront_functions/spa-redirect.js
var index = '/index.html';
function handler(event) {
    var request = event.request;
    // 拡張子が見つからない場合(実ファイルではないアクセス)
    if(request.uri.indexOf(".") === -1) {
        request.uri = index;
    }
    return request;
}

Cloudfront FunctionsをCloudfrontに適応する設定

長々と書いていますが、注目するのは下の方のfunction_associationだけです。

  • behavior毎に設定することができます!
  • event_type は リクエスト時なので viewer-request と書いておきます。
  • event_typeについてはこちらを参考にしてください。
    ※Cloudfront Functionsではviewer-requestviewer-responseにのみ対応しています。
cloudfront.tf
resource "aws_cloudfront_distribution" "front" {
  origin {
    domain_name = aws_s3_bucket.front.bucket_regional_domain_name
    origin_id   = local.s3_origin_id

    s3_origin_config {
      origin_access_identity = aws_cloudfront_origin_access_identity.origin.cloudfront_access_identity_path
    }
  }
  enabled             = true
  is_ipv6_enabled     = true
  comment             = "${var.product_name}-${var.env}"
  default_root_object = "index.html"
  aliases             = ["test.${data.terraform_remote_state.network.outputs.domain}"]

  default_cache_behavior {
    allowed_methods  = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
    cached_methods   = ["GET", "HEAD"]
    target_origin_id = local.s3_origin_id

    forwarded_values {
      query_string = true

      cookies {
        forward = "none"
      }
    }

    function_association {
      event_type   = "viewer-request"
      function_arn = aws_cloudfront_function.spa_redirect.arn
    }

    viewer_protocol_policy = "allow-all"
    min_ttl                = 0
    default_ttl            = 0
    max_ttl                = 0

  }
...省略
}

/apiを分けたい

前述したとおり、 cloudfrontのbehaviorで振り分ければOKです
serverless frameworkやCDKを使用してAPI Gatewayを作成する人も多いと思いますが、
Cloudformationから値を取得すると便利です。

ポイント

  • origindomain_nameにcloudformationからエンドポイントとステージを取得しています
  • behaviorpath_patternapi/*を指定しています
  • APIGatewayへのアクセスに/apiは不要なので、lambda@edgeで除去しています(ここもcloudfront functionsに置き換えられそう)
cloudfront.tf
resource "aws_cloudfront_distribution" "front" {
...
  origin {
    custom_origin_config {
      http_port              = "80"
      https_port             = "443"
      origin_protocol_policy = "https-only"
      origin_ssl_protocols   = ["TLSv1.2"]
    }
    # ServiceEndpointから xxx.execute-api.ap-northeast-1.amazonaws.com と v1 を抜き出す
    domain_name = split("/", data.aws_cloudformation_stack.api_test.outputs["ServiceEndpoint"])[2]
    origin_id   = var.product_name
  }
...
  ordered_cache_behavior {
    path_pattern     = "api/*"
    allowed_methods  = ["HEAD", "DELETE", "POST", "GET", "OPTIONS", "PUT", "PATCH"]
    cached_methods   = ["GET", "HEAD"]
    target_origin_id = var.product_name

    forwarded_values {
      headers      = ["Authorization"]
      query_string = true
      cookies {
        forward = "none"
      }
    }

    min_ttl                = 0
    default_ttl            = 10
    max_ttl                = 10
    viewer_protocol_policy = "https-only"
    lambda_function_association {
      event_type = "origin-request"
      lambda_arn = aws_lambda_function.redirect_trim_context.qualified_arn
    }

  }
...
}

以上、当たり前でしょ!と思う方もいるかもしれませんが、インフラ構成時に迷った時の参考になると嬉しいです。

参考:
AWS CloudFront FunctionsでSPAのルーティング処理

12
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?