マルチレベルでベースパスマッピングするAPI GatewayをAWS CDKで構築する

AWS CDKでAPI Gatewayでマルチレベルなベースパスマッピングしたカスタムドメイン環境を構築しようとしたところ、大いにハマったので注意点と実例を共有します。
2021.06.03

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

先日、API Gatewayがマルチレベルのベースマッピングに対応して、パスに「/」を複数設定できるようになりました。 詳しくは弊社ブログを御覧ください。

このAWS環境をAWS CDKで構築しようとしたところ、大いにハマったので注意点と実例を共有します。

CDKでAPI Gatewayのカスタムドメイン環境を構築する

まずはマルチレベルでないAPI Gatewayのカスタムドメイン環境の構築を考えてみます。

弊社ブログで解説されているので、合わせてこちらもご参照ください。

カスタムドメイン環境構築の前提として、ドメインの取得とRoute53でホストゾーンを登録してください。

本ブログでは便宜上、 mydomain.example.com のドメインを持ち、Route53に登録しているものとして説明をします。

AWS CDKを使って次のようなAPI Gatewayのカスタムドメイン環境を構築していきます。

API GatewayのMockを使って、/hello にGETでアクセスしたら {"message": "Hello, API Gateway!!"} を返すだけのシンプルなAPI Gatewayです。 これにカスタムドメインを利用して、 https://customdomainapi.mydomain.example.com/api/ でAPI Gatewayにアクセスできるように設定します。

以下がサンプルコードです。 実際に動かすときは、 <<AWSのアカウントID>><<Route53に登録しているドメイン名>> をご自身のものに書き換えてください。

cdk.ts

#!/usr/bin/env node
import "source-map-support/register";
import * as core from "@aws-cdk/core";
import * as route53 from "@aws-cdk/aws-route53";
import * as route53Targets from "@aws-cdk/aws-route53-targets";
import * as acm from "@aws-cdk/aws-certificatemanager";
import * as agw from "@aws-cdk/aws-apigateway";

const region = 'ap-northeast-1';
const accountId = <<AWSのアカウントID>>;
const domainName = <<Route53に登録しているドメイン名>>;
const hostName = 'customdomainapi';
const apiDomain = `${hostName}.${domainName}`;

class TestStack extends core.Stack {
  constructor(scope: core.Construct, id: string, props?: core.StackProps) {
    super(scope, id, props);

    // Route53のホストゾーンを取得する
    const hostedZone = route53.HostedZone.fromLookup(this, 'HostedZone', {
      domainName: domainName,
    });

    // AWS Certificate Managerの構築
    const certificate = new acm.Certificate(this, 'Certificate', {
      domainName: apiDomain,
      validation: acm.CertificateValidation.fromDns(hostedZone),
    });

    // API Gatewayの作成
    const api = new agw.RestApi(this, 'RestApi', {
      restApiName: 'Rest API Sample',
      endpointTypes: [
          agw.EndpointType.REGIONAL,
      ],
    });

    // helloメソッドの作成
    const method = api.root.addResource('hello').addMethod('GET', new agw.MockIntegration({
      integrationResponses: [{
        statusCode: '200',
        responseTemplates: {'application/json': '{"message": "Hello, API Gateway!!"}'}
      }],
      passthroughBehavior: agw.PassthroughBehavior.NEVER,
      requestTemplates: {
        'application/json': '{ "statusCode": 200 }',
      },
    }), {
      methodResponses: [{ statusCode: '200' }],
    });

    // API Gatewayのカスタムドメインの設定
    const domainNameApi = new agw.DomainName(this, 'CustomDomain', {
      certificate: certificate,
      domainName: apiDomain,
      endpointType: agw.EndpointType.REGIONAL,
      securityPolicy: agw.SecurityPolicy.TLS_1_2,
    });
    // ベースパスマッピングの設定
    domainNameApi.addBasePathMapping(
        api,
        {
          basePath: 'api',
        });

    // Route53にApiGatewayのエイリアスレコードを設定する
    const apiRecord = new route53.ARecord(this, 'ApiRecord', {
      zone: hostedZone,
      target: route53.RecordTarget.fromAlias(
          new route53Targets.ApiGatewayDomain(domainNameApi)
      ),
      recordName: hostName,
    });
  }
}

const env: core.Environment = {
  region: region,
  account: accountId,
};

const app = new core.App();
new TestStack(app, 'TestStack', {env: env});

CDKで環境構築ができたら、次のコマンドを実行して動作確認します。

$ MY_DOMAIN=mydomain.example.com
$ curl "https://customdomainapi.${MY_DOMAIN}/api/hello"
{"message": "Hello, API Gateway!!"}

マルチレベルでないAPI Gatewayのカスタムドメイン環境の構築は問題なくできそうです。

CDKでAPI Gatewayのマルチレベルなカスタムドメイン環境を構築する(NGパターン)

同じようにマルチレベルでベースマッピングするカスタムドメイン環境の構築を考えてみます。

API GatewayのMockを使って、/hello にGETでアクセスしたら {"message": "Hello, API Gateway!!"} を返すだけのシンプルなAPI Gatewayを構築します。 これにカスタムドメインを利用して、 https://customdomainapi.mydomain.example.com/api/a/b/c/ でAPI Gatewayにアクセスできるように設定します。

サンプルコードは次のようになるでしょう。

cdk.ts

#!/usr/bin/env node
import "source-map-support/register";
import * as core from "@aws-cdk/core";
import * as route53 from "@aws-cdk/aws-route53";
import * as route53Targets from "@aws-cdk/aws-route53-targets";
import * as acm from "@aws-cdk/aws-certificatemanager";
import * as agw from "@aws-cdk/aws-apigateway";

const region = 'ap-northeast-1';
const accountId = <<AWSのアカウントID>>;
const domainName = <<Route53に登録しているドメイン名>>;
const hostName = 'customdomainapi';
const apiDomain = `${hostName}.${domainName}`;

class TestStack extends core.Stack {
  constructor(scope: core.Construct, id: string, props?: core.StackProps) {
    super(scope, id, props);

    // Route53のホストゾーンを取得する
    const hostedZone = route53.HostedZone.fromLookup(this, 'HostedZone', {
      domainName: domainName,
    });

    // AWS Certificate Managerの構築
    const certificate = new acm.Certificate(this, 'Certificate', {
      domainName: apiDomain,
      validation: acm.CertificateValidation.fromDns(hostedZone),
    });

    // API Gatewayの作成
    const api = new agw.RestApi(this, 'RestApi', {
      restApiName: 'Rest API Sample',
      endpointTypes: [
          agw.EndpointType.REGIONAL,
      ],
    });

    // helloメソッドの作成
    const method = api.root.addResource('hello').addMethod('GET', new agw.MockIntegration({
      integrationResponses: [{
        statusCode: '200',
        responseTemplates: {'application/json': '{"message": "Hello, API Gateway!!"}'}
      }],
      passthroughBehavior: agw.PassthroughBehavior.NEVER,
      requestTemplates: {
        'application/json': '{ "statusCode": 200 }',
      },
    }), {
      methodResponses: [{ statusCode: '200' }],
    });

    // API Gatewayのカスタムドメインの設定
    const domainNameApi = new agw.DomainName(this, 'CustomDomain', {
      certificate: certificate,
      domainName: apiDomain,
      endpointType: agw.EndpointType.REGIONAL,
      securityPolicy: agw.SecurityPolicy.TLS_1_2,
    });
    // ベースパスマッピングの設定
    domainNameApi.addBasePathMapping(
        api,
        {
          basePath: 'api/a/b/c',
        });

    // Route53にApiGatewayのエイリアスレコードを設定する
    const apiRecord = new route53.ARecord(this, 'ApiRecord', {
      zone: hostedZone,
      target: route53.RecordTarget.fromAlias(
          new route53Targets.ApiGatewayDomain(domainNameApi)
      ),
      recordName: hostName,
    });
  }
}

const env: core.Environment = {
  region: region,
  account: accountId,
};

const app = new core.App();
new TestStack(app, 'TestStack', {env: env});

しかし、このCDKをデプロイしようとすると、次のようなエラーが発生します。 addBasePathMapping はマルチレベルなベースパスマッピングに対応していないようです。

$ cdk deploy TestStack
Error: A base path may only contain letters, numbers, and one of "$-_.+!*'()", received: api/a/b/c

CDKでAPI Gatewayのマルチレベルなカスタムドメイン環境を構築する(OKパターン)

前述の問題は、 @aws-cdk/aws-apigatewayv2 モジュールを使うことで解決しました。

API Gatewayでマルチレベルなベースマッピングするカスタムドメイン環境を構築する際は、 @aws-cdk/aws-apigatewayv2モジュールの CfnApiMapping を利用します。

具体的にCDKのソースコードにすると、次のような感じです。

cdk.ts

#!/usr/bin/env node
import "source-map-support/register";
import * as core from "@aws-cdk/core";
import * as route53 from "@aws-cdk/aws-route53";
import * as route53Targets from "@aws-cdk/aws-route53-targets";
import * as acm from "@aws-cdk/aws-certificatemanager";
import * as agw from "@aws-cdk/aws-apigateway";
import * as agwv2 from "@aws-cdk/aws-apigatewayv2";

const region = 'ap-northeast-1';
const accountId = <<AWSのアカウントID>>;
const domainName = <<Route53に登録しているドメイン名>>;
const hostName = 'customdomainapi';
const apiDomain = `${hostName}.${domainName}`;

class TestStack extends core.Stack {
  constructor(scope: core.Construct, id: string, props?: core.StackProps) {
    super(scope, id, props);

    // Route53のホストゾーンを取得する
    const hostedZone = route53.HostedZone.fromLookup(this, 'HostedZone', {
      domainName: domainName,
    });

    // AWS Certificate Managerの構築
    const certificate = new acm.Certificate(this, 'Certificate', {
      domainName: apiDomain,
      validation: acm.CertificateValidation.fromDns(hostedZone),
    });

    // API Gatewayの作成
    const api = new agw.RestApi(this, 'RestApi', {
      restApiName: 'Rest API Sample',
      endpointTypes: [
          agw.EndpointType.REGIONAL,
      ],
    });

    // helloメソッドの作成
    const method = api.root.addResource('hello').addMethod('GET', new agw.MockIntegration({
      integrationResponses: [{
        statusCode: '200',
        responseTemplates: {'application/json': '{"message": "Hello, API Gateway!!"}'}
      }],
      passthroughBehavior: agw.PassthroughBehavior.NEVER,
      requestTemplates: {
        'application/json': '{ "statusCode": 200 }',
      },
    }), {
      methodResponses: [{ statusCode: '200' }],
    });

    // API Gatewayのカスタムドメインの設定
    const domainNameApi = new agw.DomainName(this, 'CustomDomain', {
      certificate: certificate,
      domainName: apiDomain,
      endpointType: agw.EndpointType.REGIONAL,
      securityPolicy: agw.SecurityPolicy.TLS_1_2,
    });
    // ベースパスマッピングの設定
    // domainNameApi.addBasePathMapping(
    //     api,
    //     {
    //       basePath: 'api/a/b/c',
    //     });
    const cfnApiMapping = new agwv2.CfnApiMapping(this, 'ApiMapping', {
      apiId: api.restApiId,
      domainName: apiDomain,
      stage: 'prod',
      apiMappingKey: 'api/a/b/c',
    });
    // カスタムドメイン作成後にベースパスマッピングを設定して欲しいので、dependsOn属性を付与する
    const cfnDomainName: agw.CfnDomainName = (domainNameApi.node.defaultChild as agw.CfnDomainName);
    cfnApiMapping.addDependsOn(cfnDomainName);

    // ステージデプロイ後にベースパスマッピングを設定して欲しいので、dependsOn属性を付与する
    const stage = api.node.children.find(
        (child) => child.node.id == 'DeploymentStage.prod'
    ) as core.Construct;
    const cfnStage = stage.node.defaultChild as agw.CfnStage;
    const deployment = api.node.children.find(
        (child) => child.node.id == 'Deployment'
    ) as core.Construct;
    const cfnDeployment = deployment.node.defaultChild as agw.CfnDeployment;
    cfnApiMapping.addDependsOn(cfnStage);
    cfnApiMapping.addDependsOn(cfnDeployment);

    // Route53にApiGatewayのエイリアスレコードを設定する
    const apiRecord = new route53.ARecord(this, 'ApiRecord', {
      zone: hostedZone,
      target: route53.RecordTarget.fromAlias(
          new route53Targets.ApiGatewayDomain(domainNameApi)
      ),
      recordName: hostName,
    });
  }
}

const env: core.Environment = {
  region: region,
  account: accountId,
};

const app = new core.App();
new TestStack(app, 'TestStack', {env: env});

このcdkソースコードであれば、マルチレベルなカスタムドメイン環境がデプロイできます。

次のコマンドを実行して動作確認すれば、問題なく構築できていることがわかります。

$ MY_DOMAIN=mydomain.example.com
$ curl "https://customdomainapi.${MY_DOMAIN}/api/a/b/c/hello"
{"message": "Hello, API Gateway!!"}

終わりに

AWS CDKで、API Gatewayでマルチレベルなベースパスマッピングしたカスタムドメイン環境を構築する方法を紹介しました。

私はこの環境をAWS CDKで作るにあたって大いにハマりました。 @aws-cdk/aws-apigateway モジュールで構築するREST API環境でも、マルチレベルなベースパスマッピングをする場合は、 @aws-cdk/aws-apigatewayv2 モジュールを使わないとうまく行かない。というところに気がつくまで結構時間がかかりました。

本ブログが、同じようなことをしようとしている方の参考になれば幸いです。

参考サイト