CDKを使用してAPI GatewayのHTTP APIを作成するサンプルです。
認証はCognitoを使用して認可コードグラントを利用します。
AWS CDK
% npm install @aws-cdk/aws-apigatewayv2-alpha @aws-cdk/aws-apigatewayv2-authorizers-alpha @aws-cdk/aws-apigatewayv2-integrations-alpha
cognitoドメインのdomainPrefixはユニークな文字列である必要があります。cognitoドメインの代わりにカスタムドメインを使用することも可能です。
bin/sample.ts
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { SampleStack } from '../lib/sample-stack';
const app = new cdk.App();
const contextKey = process.env.ENV === 'prod' ? 'prod' : 'dev';
const context = app.node.tryGetContext(contextKey);
new SampleStack(app, 'SampleStack', { context });
lib/sample-stack.ts
import { Construct } from 'constructs';
import * as cdk from 'aws-cdk-lib';
import * as cognito from 'aws-cdk-lib/aws-cognito';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as lambdaNodeJs from 'aws-cdk-lib/aws-lambda-nodejs';
import * as apigw from '@aws-cdk/aws-apigatewayv2-alpha';
import * as authz from '@aws-cdk/aws-apigatewayv2-authorizers-alpha';
import * as intg from '@aws-cdk/aws-apigatewayv2-integrations-alpha';
export type SampleStackProps = cdk.StackProps & { context: any };
export class SampleStack extends cdk.Stack {
private context: any;
constructor(scope: Construct, id: string, props: SampleStackProps) {
super(scope, id, props);
this.context = props.context;
// Cognito User Pool & User Pool Client
const userPool = this.createUserPool(this.context.cognito.domainPrefix);
const userPoolClient = this.createUserPoolClient(userPool, this.context.cognito.callbackUrls);
// Authorizer
const authorizer = this.createAuthorizer(userPool, userPoolClient);
// Lambda
const func = this.createFunc();
// API Gateway
this.createApiGateway(func, authorizer);
}
/**
* Cognito ユーザープールの作成
* {@link UserPool | https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cognito.UserPool.html}
* {@link UserPoolDomainOptions | https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cognito.UserPoolDomainOptions.html}
*/
private createUserPool(cognitoDomainPrefix: string): cognito.IUserPool {
const userPool = new cognito.UserPool(this, 'UserPool', {
selfSignUpEnabled: false,
standardAttributes: {
email: { required: true, mutable: true },
phoneNumber: { required: false },
},
autoVerify: { email: true },
signInAliases: { email: true },
accountRecovery: cognito.AccountRecovery.EMAIL_ONLY,
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
userPool.addDomain('UserPoolDomain', {
// cognitoドメインまたはカスタムドメインが選択可能
cognitoDomain: { domainPrefix: cognitoDomainPrefix },
// customDomain: {}
});
return userPool;
}
/**
* Cognito ユーザープールクライアントの作成
* {@link UserPoolClient | https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cognito.UserPoolClient.html}
*/
private createUserPoolClient(userPool: cognito.IUserPool, callbackUrls: string[]): cognito.IUserPoolClient {
return userPool.addClient('client', {
generateSecret: true,
oAuth: {
callbackUrls,
scopes: [cognito.OAuthScope.OPENID],
flows: {
authorizationCodeGrant: true,
clientCredentials: false,
implicitCodeGrant: false,
},
},
authFlows: {
adminUserPassword: false,
custom: false,
userPassword: false,
userSrp: false,
},
});
}
/**
* @description API Gateway用オーソライザーの作成
* {@link class HttpUserPoolAuthorizer | https://docs.aws.amazon.com/cdk/api/v2/docs/@aws-cdk_aws-apigatewayv2-authorizers-alpha.HttpUserPoolAuthorizer.html}
*/
private createAuthorizer(
userPool: cognito.IUserPool,
userPoolClient: cognito.IUserPoolClient
): apigw.IHttpRouteAuthorizer {
return new authz.HttpUserPoolAuthorizer('Authorizer', userPool, {
userPoolClients: [userPoolClient],
});
}
/**
* @description Lambdaの作成
* {@link ParamsAndSecretsLayerVersion | https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda.ParamsAndSecretsLayerVersion.html}
* {@link NodejsFunction | https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda_nodejs.NodejsFunction.html}
*/
private createFunc(): lambdaNodeJs.NodejsFunction {
return new lambdaNodeJs.NodejsFunction(this, 'SampleFunc', {
entry: 'src/lambda/sample/index.ts',
handler: 'handler',
runtime: lambda.Runtime.NODEJS_18_X,
timeout: cdk.Duration.minutes(5),
});
}
/**
* @description ラムダの作成
* {@link class HttpApi | https://docs.aws.amazon.com/cdk/api/v2/docs/@aws-cdk_aws-apigatewayv2-alpha.HttpApi.html}
*/
private createApiGateway(
func: lambdaNodeJs.NodejsFunction,
authorizer: apigw.IHttpRouteAuthorizer
): apigw.IHttpApi {
const httpApi = new apigw.HttpApi(this, 'SampleHttpApi');
const integration = new intg.HttpLambdaIntegration('Integration', func);
httpApi.addRoutes({
methods: [apigw.HttpMethod.GET],
path: '/sample',
authorizer,
integration,
});
return httpApi;
}
}
src/lambda/sample/index.ts
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
export async function handler(event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> {
return {
statusCode: 200,
body: '',
};
}
cdk.json
{
// 略
"context": {
// 略
"dev": {
"cognito": {
"domainPrefix": "taaazyyy-sample-230818",
"callbackUrls": ["https://example.com/callback"]
}
},
"prod": {
"cognito": {
"domainPrefix": "taaazyyy-sample-230818",
"callbackUrls": ["https://example.com/callback"]
}
}
}
}
Postmanで検証
認可コードグラントを使用した時のPostmanを使った検証例です。
あらかじめユーザープールにユーザーを作成しておく必要があります。
PostmanのHTTPリクエスト作成ページでのAuthorizationタブで下記のように設定します。
- Grant Type: Authorization Code
- Callback URL: ※ユーザープールクライアント作成時に指定したコールバックURLのいずれか
- Auth URL:
https://{Cognitoドメイン}/oauth2/authorize
- Access Token URL:
https://{Cognitoドメイン}/oauth2/token
- Client ID: Cognitoのアプリケーションクライアントに表示されているクライアントID
- Client Secret: Cognitoのアプリケーションクライアントに表示されているクライアントシークレット
- Scope: openid
「Get New Access Token」ボタンをクリックするとログイン画面が表示されるので、ログインします。
認証に成功したらAPI Gatewayのエンドポイントを入力してSendボタンをクリックして検証を行うことができます。
テスト
test/sample.test.ts
test/sample.test.ts
import * as cdk from 'aws-cdk-lib';
import { Template, Match } from 'aws-cdk-lib/assertions';
import { SampleStack } from '../lib/sample-stack';
const context = {
cognito: {
domainPrefix: 'taaazyyy-sample-test',
callbackUrls: ['https://example.com/callback'],
},
};
test('UserPool', () => {
const app = new cdk.App();
const stack = new SampleStack(app, 'SampleStack', { context });
const template = Template.fromStack(stack);
// User Pool
template.resourceCountIs('AWS::Cognito::UserPool', 1);
template.hasResourceProperties('AWS::Cognito::UserPool', {
Schema: [
{ Mutable: true, Name: 'email', Required: true },
{
Mutable: true,
Name: 'phone_number',
Required: false,
},
],
AutoVerifiedAttributes: ['email'],
UsernameAttributes: ['email'],
AccountRecoverySetting: {
RecoveryMechanisms: [{ Name: 'verified_email', Priority: 1 }],
},
});
// User Pool Domain
template.resourceCountIs('AWS::Cognito::UserPoolDomain', 1);
template.hasResourceProperties('AWS::Cognito::UserPoolDomain', {
Domain: 'taaazyyy-sample-test',
UserPoolId: Match.anyValue(),
});
// User Pool Client
template.resourceCountIs('AWS::Cognito::UserPoolClient', 1);
template.hasResourceProperties('AWS::Cognito::UserPoolClient', {
GenerateSecret: true,
CallbackURLs: ['https://example.com/callback'],
AllowedOAuthScopes: ['openid'],
AllowedOAuthFlows: ['code'],
AllowedOAuthFlowsUserPoolClient: true,
ExplicitAuthFlows: ['ALLOW_REFRESH_TOKEN_AUTH'],
});
});
test('Lambda', () => {
const app = new cdk.App();
const stack = new SampleStack(app, 'SampleStack', { context });
const template = Template.fromStack(stack);
// Lambda
template.resourceCountIs('AWS::Lambda::Function', 1);
template.hasResourceProperties('AWS::Lambda::Function', {
Runtime: 'nodejs18.x',
Timeout: 300,
});
});
test('ApiGateway', () => {
const app = new cdk.App();
const stack = new SampleStack(app, 'SampleStack', { context });
const template = Template.fromStack(stack);
// API Gateway
template.resourceCountIs('AWS::ApiGatewayV2::Api', 1);
template.hasResourceProperties('AWS::ApiGatewayV2::Api', {
Name: 'SampleHttpApi',
ProtocolType: 'HTTP',
});
// Route
template.resourceCountIs('AWS::ApiGatewayV2::Route', 1);
template.hasResourceProperties('AWS::ApiGatewayV2::Route', {
RouteKey: 'GET /sample',
});
// Authorizer
template.resourceCountIs('AWS::ApiGatewayV2::Authorizer', 1);
template.hasResourceProperties('AWS::ApiGatewayV2::Authorizer', {
AuthorizerType: 'JWT',
});
});
エラー
ERESOLVE unable to resolve dependency tree
aws-cdk-libと@aws-cdk/xxxxでバージョンが異なるとnpm installで下記のエラーが発生することがあります。
インストールし直すなどバージョンを揃えることで解消できます。
npm ERR! code ERESOLVE
npm ERR! ERESOLVE unable to resolve dependency tree
npm ERR!
npm ERR! While resolving: sample@0.1.0
npm ERR! Found: aws-cdk-lib@2.91.0
npm ERR! node_modules/aws-cdk-lib
npm ERR! aws-cdk-lib@"2.91.0" from the root project
npm ERR!
npm ERR! Could not resolve dependency:
npm ERR! peer aws-cdk-lib@"2.92.0" from @aws-cdk/aws-apigatewayv2-alpha@2.92.0-alpha.0
npm ERR! node_modules/@aws-cdk/aws-apigatewayv2-alpha
npm ERR! @aws-cdk/aws-apigatewayv2-alpha@"*" from the root project