API Gatewayにカスタムドメインを設定するためのリソースを全てAWS CDKでつくってみた

AWS CDKを用いてRoute53、ACM、API Gateway、Lambdaのリソースを作成し、API Gatewayに設定したカスタムドメインでリクエストを送ってみました
2020.09.18

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

最近TypeScript・AWS CDKでAWSのリソースを作るのにはまっています。

本記事では「APIGatewayのカスタムドメインのエンドポイントに対してGETリクエストを送りバックエンドのLambdaが文字列を返す」という処理を実現するためのリソースを全てAWS CDKでデプロイしてみます。

※ドメイン自体は外部サイトで取得しているため厳密には全てのリソースではありません。

  • Route53 パブリックホストゾーン
  • Amazon Certificate Manager(以下、ACMと記載) 上記ドメインの証明書
  • API Gatewayとカスタムドメイン
  • Lambda(文字列を返すだけの単純なもの)

開発環境

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.15.6
BuildVersion:   19G2021
$ npm -v
6.14.6
$ node -v
v12.18.3
$ cdk --version
1.62.0 (build 8c2d7fc)

なお、本記事に記載しているコードは下記リポジトリに格納しています。一部本記事のコードと異なる部分があるのでご注意ください(ドメイン名を外部ファイルから取得している)。

https://github.com/urawa72/cdk_samples/tree/master/apigw-custom-domain

ドメインを取得する

今回は検証目的のため、freenomという無料でドメインを取得できるサービスを利用します。

freenomでドメインを取得する手順は弊社ブログの下記記事をご参照ください。

無料ドメイン(.tk)とRoute53を利用して0円でHTTPS環境を設定してみた

CDKの準備

AWS CDKのインストールやbootstapの手順については割愛します。

作業用のディレクトリを作成しcdk initで初期リソースを作成します。

また、必要なパッケージもインストールします。

mkdir apigw-domain-sample
cd apigw-domain-sample
cdk init --language=typescript
npm install -D @aws-cdk/aws-apigateway @aws-cdk/aws-lambda @aws-cdk/aws-certificatemanager @aws-cdk/aws-route53 @aws-cdk/aws-route53-targets

Stackを作成する

今回はRoute53、ACM、API Gateway、Lambdaの4つのStackを作成します。

Route53

freenomで取得したドメインのパブリックホストゾーンを作成します。今回はzoneNameにドメイン名を文字列として直書きしていますが、cdk.jsonや環境変数に定義して外部から受け取ることも可能です。

lib/route53-stack.ts

import * as cdk from '@aws-cdk/core';
import * as route53 from '@aws-cdk/aws-route53';

export class Route53Stack extends cdk.Stack {
  public readonly hostedZone: route53.HostedZone;

  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    this.hostedZone = new route53.PublicHostedZone(this, 'HostedZone', {
      zoneName: 'yourdomainname.tk', // freenomで取得したドメイン
      comment: 'created by cdk'
    });
  }
}

ACM

Route53のStackの情報を使用してカスタムドメインとして使用するapi.yourdomainname.tkの証明書を作成します。

lib/cetificate-stack.ts

import * as cdk from '@aws-cdk/core';
import * as certificatemanager from '@aws-cdk/aws-certificatemanager';
import * as route53 from '@aws-cdk/aws-route53';

interface Route53Props extends cdk.StackProps {
  hostedZone: route53.HostedZone
}

export class CertificateStack extends cdk.Stack {
  public readonly certificate: certificatemanager.Certificate;

  constructor(scope: cdk.Construct, id: string, props: Route53Props) {
    super(scope, id, props);

    this.certificate = new certificatemanager.DnsValidatedCertificate(this, 'Certificate', {
      domainName: `api.${props.hostedZone.zoneName}`,
      hostedZone: props.hostedZone,
      validationMethod: certificatemanager.ValidationMethod.DNS
    });
  }
}

API Gateway

Route53とACMのStackのデータを利用してリソースを作成します。本Stack内でRoute53にカスタムドメインをターゲットとするAレコードを設定します。

lib/apigw-stack.ts

import * as cdk from '@aws-cdk/core';
import * as acm from '@aws-cdk/aws-certificatemanager';
import * as apigw from '@aws-cdk/aws-apigateway';
import * as route53 from '@aws-cdk/aws-route53';
import * as alias from '@aws-cdk/aws-route53-targets';

interface Route53Props extends cdk.StackProps {
  hostedZone: route53.HostedZone;
  certificate: acm.Certificate;
}

export class ApigwStack extends cdk.Stack {
  public restApi: apigw.RestApi;

  constructor(scope: cdk.Construct, id: string, props: Route53Props) {
    super(scope, id, props);

    this.restApi = new apigw.RestApi(this, 'SampleRestApi', {
      restApiName: 'sample',
      deployOptions: {
        stageName: 'dev'
      },
      defaultCorsPreflightOptions: {
        allowOrigins: apigw.Cors.ALL_METHODS,
        allowMethods: ['GET', 'OPTIONS'],
        statusCode: 200,
      }
    });

    const customDomain = new apigw.DomainName(this, 'CustomDomain', {
      domainName: `api.${props.hostedZone.zoneName}`,
      certificate: props.certificate,
      securityPolicy: apigw.SecurityPolicy.TLS_1_2,
      endpointType: apigw.EndpointType.REGIONAL
    });

    new route53.ARecord(this, 'SampleARecod', {
      zone: props.hostedZone,
      recordName: `api.${props.hostedZone.zoneName}`,
      target: route53.RecordTarget.fromAlias(new alias.ApiGatewayDomain(customDomain))
    });

    customDomain.addBasePathMapping(this.restApi, {
      basePath: 'dev',
    });
  }
}

Lambda

samplesというリソースにGETリクエストを送れるようにします。

lib/lambda-stack.ts

import * as cdk from '@aws-cdk/core';
import * as apigw from '@aws-cdk/aws-apigateway';
import * as lambda from '@aws-cdk/aws-lambda';

interface ApigwProps extends cdk.StackProps {
  apigw: apigw.RestApi
}

export class LambdaStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props: ApigwProps) {
    super(scope, id, props);

    const sampleFunction = new lambda.Function(this, 'SampleFunction', {
      code: lambda.Code.fromAsset('lambda'),
      functionName: 'sampleFunction',
      handler: 'sample.handler',
      runtime: lambda.Runtime.NODEJS_12_X,
      memorySize: 256
    });

    const sampleIntegration = new apigw.LambdaIntegration(sampleFunction);
    const sampleResource = props.apigw.root.addResource('samples');
    sampleResource.addMethod('GET', sampleIntegration);
  }
}

関数自体は以下のようなシンプルなものです。

lambda/sample.ts

export const handler = async (event: any): Promise<any> => {
  return {
    'statusCode': 200,
    'body': 'hello! sample api gateway with lambda!',
    'isBase64Encoded': false
  };
}

Stackの依存関係の設定

依存しているStackを後続のStackのpropsで渡します。

bin/apigw-custom-domain.ts

#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from '@aws-cdk/core';
import { Route53Stack } from '../lib/route53';
import { CertificateStack } from '../lib/certificate';
import { ApigwStack } from '../lib/apigw';
import { LambdaStack } from '../lib/lambda';

const app = new cdk.App();

const route53 = new Route53Stack(app, 'Route53Stack');
const acm = new CertificateStack(app, 'CertificateStack', {
  hostedZone: route53.hostedZone
});
const apigw = new ApigwStack(app, 'ApigwStack', {
  hostedZone: route53.hostedZone,
  certificate: acm.certificate
});
const new LambdaStack(app, 'LambdaStack', {
  apigw: apigw.restApi
});

デプロイ

cdk lsでデプロイ可能なStack一覧を確認します。エラーなく4つのStackが表示されればOKです。

$ cdk list
Route53Stack
CertificateStack
LambdaStack
ApigwStack

あとはApigwStackをデプロイすれば、他の3つのStackも自動でデプロイされます。

ただし、今回の場合はRoute53のホストゾーンを作成した後に確認できるネームサーバーをfreenomに設定しないと、ACMの証明書のDNS検証が完了しません

そのため、まずはRoute53Stackを個別にデプロイします。

$ cdk deploy Route53Stack

続いて、freenomにてネームサーバを設定します。

freenomのトップページ > Services > My Domains > (取得したドメインの行の) Manage Domain > Management Tools > Nameservers と遷移すればネームサーバの設定画面にたどり着きます。前掲の弊社ブログ記事に画面キャプチャ付きで解説があります。

ネームサーバは4つ全て設定します。

feenom側の設定が完了したら、ApigwStackをデプロイします。ACMの証明書のDNS検証には数分かかるため、cdkによるデプロイも一時的にとまります。

$ cdk deploy ApigwStack
Including dependency stacks: LambdaStack, CertificateStack, Route53Stack
# IAM関連の変更内容が出力される
LambdaStack: deploying...

最後までエラーなくデプロイが完了したら、以下のようにGETリクエストを送ってみます。Lambda関数自体に記載した文字列が返ってくれば成功です。

$ curl -X GET https://api.yourdomainname.tk/dev/samples
'hello! sample api gateway with lambda!'

まとめ

Route53とACMの間で手作業が必要になるため完全自動化はできていませんが、それでも簡単にAPI Gatewayをカスタムドメインで試せるので便利です。

今後もAWS CDKのマイコードスニペットを増やしていき、検証などでリソースを作成する際に活用していきたいと思います。