2
1

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 3 years have passed since last update.

API GatewayでCloudFrontの真似事をしてSPAを動作させてみる

Posted at

はじめに

とある理由でSPAのアプリケーションの配信にS3+CloudFrontを使うことができなかったので、API Gateway+S3を使ってみることにしました。

S3を定義する

API Gatewayのチュートリアル「API Gateway で REST API を Amazon S3 のプロキシとして作成する」にあるように、API GatewayとS3を直接連携して、バケットのCRUDができるAPIとして公開する機能がAPI Gatewayにあります。この機能を利用して今回はAPI GatewayからS3内にある静的サイト(index.html)にアクセスできるように実装してみます。

まずはs3を定義して静的サイトのホスティングができるようにします。

import * as cdk from '@aws-cdk/core'
import * as s3 from '@aws-cdk/aws-s3'

export class SpaHostingStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props: WebStackProps) {
    super(scope, id)
    new s3.Bucket(this, 's3', {
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      websiteIndexDocument: 'index.html',
      websiteErrorDocument: 'index.html',
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
    })
  }
}

IAMロールを作成する

S3は公開設定をOFFにした状態です。API Gatewayにのみバケットの読み込み権限を与えるようにIAMロールを定義します。

import * as cdk from '@aws-cdk/core'
import * as s3 from '@aws-cdk/aws-s3'
import * as iam from '@aws-cdk/aws-iam'

export class SpaHostingStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props)

    const bucket = new s3.Bucket(this, 's3', {
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      websiteIndexDocument: 'index.html',
      websiteErrorDocument: 'index.html',
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
    })

    const credentialsRole = new iam.Role(this, 'role', {
      assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'),
    })

    bucket.grantRead(credentialsRole)
  }
}  

grantRead()メソッドを用いると特定のS3バケットに対しての読み込み権限をIAMロールのポリシーに上書きすることが簡単にできます。

API Gatewayを設定する

SPAの配信を行うために、APIの/{何かしらのパス}へのアクセスに対してはindex.html、ブラウザ自身が行う/bundle.js/favicon.icoへのアクセスに対しては静的なファイルを返すようにします。

import * as cdk from '@aws-cdk/core'
import * as s3 from '@aws-cdk/aws-s3'
import * as apigateway from '@aws-cdk/aws-apigateway'
import * as iam from '@aws-cdk/aws-iam'

export class SpaHostingStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props)

    const bucket = new s3.Bucket(this, 's3', {
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      websiteIndexDocument: 'index.html',
      websiteErrorDocument: 'index.html',
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
    })

    const credentialsRole = new iam.Role(this, 'role', {
      assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'),
    })

    bucket.grantRead(credentialsRole)

    const api = new apigateway.RestApi(this, 'api', {
      binaryMediaTypes: ['image/*'],
    })

    const s3RootIntegration = new apigateway.AwsIntegration({
      service: 's3',
      integrationHttpMethod: 'GET',
      path: `${bucket.bucketName}/index.html`,
      options: {
        credentialsRole,
        requestParameters: {
          'integration.request.header.Accept': 'method.request.header.Accept',
          'integration.request.header.Content-Type': 'method.request.header.Content-Type',
        },
        integrationResponses: [
          {
            statusCode: '200',
            responseParameters: {
              'method.response.header.Content-Type': 'integration.response.header.Content-Type',
              'method.response.header.Access-Control-Allow-Headers': 'integration.response.header.Access-Control-Allow-Headers',
              'method.response.header.Access-Control-Allow-Methods': 'integration.response.header.Access-Control-Allow-Methods',
              'method.response.header.Access-Control-Allow-Credentials': 'integration.response.header.Access-Control-Allow-Credentials',
              'method.response.header.Access-Control-Allow-Origin': 'integration.response.header.Access-Control-Allow-Origin',
            },
          },
        ],
      },
    })

    api.root.addMethod('GET', s3RootIntegration, {
      methodResponses: [
        {
          statusCode: '200',
          responseParameters: {
            'method.response.header.Content-Type': false,
            'method.response.header.Access-Control-Allow-Headers': false,
            'method.response.header.Access-Control-Allow-Methods': false,
            'method.response.header.Access-Control-Allow-Credentials': false,
            'method.response.header.Access-Control-Allow-Origin': false,
          },
          responseModels: {
            'text/html': {
              modelId: 'Empty',
            },
          },
        },
      ],
      requestParameters: {
        'method.request.header.Content-Type': false,
        'method.request.header.Accept': false,
      },
    })

    api.root.addResource('build.js').addMethod(
      'GET',
      new apigateway.AwsIntegration({
        service: 's3',
        integrationHttpMethod: 'GET',
        path: `${bucket.bucketName}/build.js`,
        options: {
          credentialsRole,
          requestParameters: {
            'integration.request.header.Accept': 'method.request.header.Accept',
            'integration.request.header.Content-Type': 'method.request.header.Content-Type',
          },
          integrationResponses: [
            {
              statusCode: '200',
              responseParameters: {
                'method.response.header.Content-Type': 'integration.response.header.Content-Type',
                'method.response.header.Access-Control-Allow-Headers': 'integration.response.header.Access-Control-Allow-Headers',
                'method.response.header.Access-Control-Allow-Methods': 'integration.response.header.Access-Control-Allow-Methods',
                'method.response.header.Access-Control-Allow-Credentials': 'integration.response.header.Access-Control-Allow-Credentials',
                'method.response.header.Access-Control-Allow-Origin': 'integration.response.header.Access-Control-Allow-Origin',
              },
            },
          ],
        },
      }),
      {
        methodResponses: [
          {
            statusCode: '200',
            responseParameters: {
              'method.response.header.Content-Type': false,
              'method.response.header.Access-Control-Allow-Headers': false,
              'method.response.header.Access-Control-Allow-Methods': false,
              'method.response.header.Access-Control-Allow-Credentials': false,
              'method.response.header.Access-Control-Allow-Origin': false,
            },
            responseModels: {
              'text/html': {
                modelId: 'Empty',
              },
            },
          },
        ],
        requestParameters: {
          'method.request.header.Content-Type': false,
          'method.request.header.Accept': false,
        },
      },
    )

    api.root.addResource('favicon.ico').addMethod(
      'GET',
      new apigateway.AwsIntegration({
        service: 's3',
        integrationHttpMethod: 'GET',
        path: `${bucket.bucketName}/favicon.ico`,
        options: {
          credentialsRole,
          requestParameters: {
            'integration.request.header.Accept': 'method.request.header.Accept',
            'integration.request.header.Content-Type': 'method.request.header.Content-Type',
          },
          integrationResponses: [
            {
              statusCode: '200',
              responseParameters: {
                'method.response.header.Content-Type': 'integration.response.header.Content-Type',
                'method.response.header.Access-Control-Allow-Headers': 'integration.response.header.Access-Control-Allow-Headers',
                'method.response.header.Access-Control-Allow-Methods': 'integration.response.header.Access-Control-Allow-Methods',
                'method.response.header.Access-Control-Allow-Credentials': 'integration.response.header.Access-Control-Allow-Credentials',
                'method.response.header.Access-Control-Allow-Origin': 'integration.response.header.Access-Control-Allow-Origin',
              },
            },
          ],
        },
      }),
      {
        methodResponses: [
          {
            statusCode: '200',
            responseParameters: {
              'method.response.header.Content-Type': false,
              'method.response.header.Access-Control-Allow-Headers': false,
              'method.response.header.Access-Control-Allow-Methods': false,
              'method.response.header.Access-Control-Allow-Credentials': false,
              'method.response.header.Access-Control-Allow-Origin': false,
            },
            responseModels: {
              'text/html': {
                modelId: 'Empty',
              },
            },
          },
        ],
        requestParameters: {
          'method.request.header.Content-Type': false,
          'method.request.header.Accept': false,
        },
      },
    )

    api.root.addResource('{proxy+}').addMethod('GET', s3RootIntegration, {
      methodResponses: [
        {
          statusCode: '200',
          responseParameters: {
            'method.response.header.Content-Type': false,
            'method.response.header.Access-Control-Allow-Headers': false,
            'method.response.header.Access-Control-Allow-Methods': false,
            'method.response.header.Access-Control-Allow-Credentials': false,
            'method.response.header.Access-Control-Allow-Origin': false,
          },
          responseModels: {
            'text/html': {
              modelId: 'Empty',
            },
          },
        },
      ],
      requestParameters: {
        'method.request.header.Content-Type': false,
        'method.request.header.Accept': false,
      },
    })
  }
}

リソース名を{proxy+}と設定することで、ワイルドカードのような形でパス名を置くことができます。また、htmlのコンテンツを返すためにヘッダーに色々な設定を加えます。

おわりに

とりあえずAPI GatewayでSPAの配信はできないことはないがとても面倒だということがわかりました。また、この方法だと例えばxxxx.svgなどの静的ファイルが加わった時にその都度リソースの設定で対応する必要があります。面倒な場合はwebpackでurl-loaderを設定して全部htmlファイルにまとめてしまうなど、ウェブアプリ側でも対応が必要になったりもします。基本的にはこういった用途にはCloudFrontを使うのが無難かなと思いました。

参考

API Gateway を用いて、S3 で静的ウェブサイトホスティングで公開したVue アプリをHTTPS化してみた。
How to integrate API Gateway with s3 in CDK

2
1
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
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?