12
7

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.

AWS CDKでAPI作成

Last updated at Posted at 2020-03-11

はじめに

AWS上にインフラを構築する場合、ServerlessFramework、Terraform、CloudFormation、手動など色々な手段が存在します。
AWS CDK(Cloud Development Kit)もインフラ構築を目的としたツールキットです。

この記事では、
AWS CDKの理解 → 使い方 → API構築
この流れで記載しています。
AWS CDKでAPIの構築を試みたところ、なかなかうまくいかず手こずったので、特にAWS CDKによるAPI構築について詳しく記載しています。

AWS CDKとは

AWSに展開したいリソースをプログラミング言語を用いて定義し、ローカルからcdkコマンドを用いてCloudFormationのテンプレートとしてデプロイできます。
直接jsonもしくはyamlを書いてCloudFormationのテンプレートを書くと、細かい設定がある場合読みにくいです。クロススタック参照が発生するとさらに厄介です。

AWS CDKでは例えばTypeScriptを用いてリソースを定義したコードをcdk bootstrapでCloudFormationのテンプレートに変換できます。
変換後はcdk deployでAWS上にデプロイできます。

できることを具体的に

  • TypeScript、JavaScript、Python、Javaなどのプログラミング言語で定義できる
  • CloudFormationのテンプレートとしてデプロイするので、スタックによる管理ができる
  • 軽量なLambdaのロジックを、リソースのデプロイと同時にデプロイできる

メリット

  • テンプレートファイルではなくプログラミング言語を用いることで
    • ビルド時にミスに気づくことができる
    • リソースの設定などを定数として置ける
    • オブジェクトとして分割しやすい
    • IDEを利用する場合、IDEの機能を利用できる
  • スタックによる管理となり、cdkコマンドによるスタックの一覧表示、削除、差分検出が容易。cdkコマンドによる操作方法はこちらの記事が参考になります。

デメリット

APIGateway-Lambdaや、DynamoDB-Lambdaといった、リソース間の連携をコード化するのが難しい

導入

最新バージョンである1.27.0(2020-03-12)をインストールします。
利用する言語はTypeScriptです。

$ npm install -g aws-cdk

次にcdk initでプロジェクトを作成します。
AWS CDKのワークショップのサイトがあり、このページと同じ作業をすることになるので、ここでは書きません。

API作成

クライアントからAPIGateway経由でIDを受け取り、DynamoDBからそのIDを用いてデータを取得して、jsonに変換してクライアントに返すシンプルなAPIを作成します。

Lambdaのロジック作成

作成したプロジェクト配下にlambdaディレクトリを作成し、lambdaHandler.tsを作成します。

lambda/lambdaHandler.ts
import * as AWS from 'aws-sdk';

const Region = process.env.REGION!;

const Dynamo = new AWS.DynamoDB(
	{
		apiVersion: '2012-08-10',
		region: Region
	}
);

export async function getCompanyHandler(company: Company) {
	return Dynamo.getItem({
		TableName: 'company',
		Key: {company_id: {S: company.company_id}}
	}).promise();
}

export interface Company {
	company_id: string;
}

getCompanyHandlerは引数として受け取った値(company_id)を元に、DynamoDBからデータを取得し返しています。

Companyというinterfaceを定義し、引数の型として利用し受け取っています。
APIGatewayProxyEvent(APIGatewayEventは古い名称)だと受け取れないので注意が必要です。

ちなみにエラーハンドリングは未実装です。

リソースの定義作成

今回はAPIGateway、Lambda、DynamoDBを利用するので、必要となるライブラリをインストールします。

$ npm install --save @aws-cdk/aws-dynamodb @aws-cdk/aws-lambda @aws-cdk/aws-apigateway

次にcdk init時に生成されているlibディレクトリ配下のファイルを下記のように修正します。

lib/sample-app-stack.ts
import * as dynamodb from '@aws-cdk/aws-dynamodb';
import * as lambda from '@aws-cdk/aws-lambda';
import * as cdk from '@aws-cdk/core';
import * as apigateway from '@aws-cdk/aws-apigateway';

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

const app = new cdk.App();
new SampleAppStack(app, 'SampleAppStack');
app.synth();

これで各リソースの定義を記載する準備が整いました。
これからconstructor()メソッド内に各リソースの定義を記載していきます。

DynamoDBの定義

const companyTable = new dynamodb.Table(this, 'company', {
	partitionKey: {
		name: 'company_id',
		type: dynamodb.AttributeType.STRING
	},
	billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
	tableName: 'company'
});

今回は3つのパラメータを設定していますが、sortKeyなども設定できます。

Lambdaの定義

const getCompanyLambda = new lambda.Function(this, 'getCompanyLambda', {
	// 注意点1
	code: lambda.Code.fromAsset('lambda'),
	// 注意点2
	handler: 'lambdaHandler.getCompanyHandler', 
	runtime: lambda.Runtime.NODEJS_12_X,
	environment: {
		TABLE_NAME: companyTable.tableName,
		REGION: 'ap-northeast-1'
	},
});
// 注意点3
companyTable.grantReadWriteData(getCompanyLambda);
  • 注意点1
    lambda.Code.fromAsset()の引数にLambdaのロジックを置いているパスを指定します。文字列として指定するので間違いやすいです。

  • 注意点2
    発火させたいメソッドの指定します。こちらも文字列として指定するので間違いやすいです。

  • 注意点3
    DynamoDBがLambdaにGrantするように権限を設定する必要があります。

APIGatewayの定義

const getCompanyApi = new apigateway.LambdaRestApi(
	this,
	'getCompany',
	// 注意点1
	{handler: getCompanyLambda, proxy: false}
);
const getCompanyApiIntegration = new apigateway.LambdaIntegration(getCompanyLambda, {
	// 注意点2
	proxy: false,
	// 注意点3
	requestParameters: {
		'integration.request.querystring.company_id': 'method.request.querystring.company_id'
	},
	// 注意点4
	requestTemplates: {
		'application/json': JSON.stringify({company_id: "$util.escapeJavaScript($input.params('company_id'))"})
	},
	passthroughBehavior: apigateway.PassthroughBehavior.WHEN_NO_MATCH,
	integrationResponses: [
		{
			statusCode: '200',
			// 注意点5
			responseTemplates: {
				'application/json': '$input.json("$")'
			}
		}
	]
});
getCompanyApi.root.addResource('companyCompanies').addMethod(
	'GET',
	getCompanyApiIntegration,
	{
		// 注意点6
		requestParameters: {'method.request.querystring.company_id': true},
		methodResponses: [{statusCode: '200'}]
	}
);
  • 注意点1
    proxyの設定がないとcdk deploy時にこけるので、設定します。
    ここは深く理解できていないので、どなたか教えてください。

  • 注意点2
    proxyの設定がないとcdk deploy時にこけるので、設定します。
    ここは深く理解できていないので、どなたか教えてください。

  • 注意点3
    APIGatewayからLambdaへのリクエスト時に渡すパラメータを設定します。
    Amazon API Gateway API Request and Response Data Mapping Referenceを参考に設定しています。

  • 注意点4
    APIGatewayからLambdaへのリクエスト時に渡す際、パラメータのテンプレートを設定する必要があります。
    API Gateway Mapping Template and Access Logging Variable Referenceを参考に設定しています。
    $util$inputという外部変数が登場するのではまりどころです。

  • 注意点5
    Lambdaからレスポンスを受け取る際にも、テンプレートを設定する必要があります。

  • 注意点6
    addMethodの第三引数であるMethodOptionsrequestParametersを設定する必要があります。
    注意点3で指定したパラメータをtrueとします。

全体のソースコード

API構築するためにリソースを定義した全体のソースコードです

lib/sample-app-stack.ts
import * as dynamodb from '@aws-cdk/aws-dynamodb';
import * as lambda from '@aws-cdk/aws-lambda';
import * as cdk from '@aws-cdk/core';
import * as apigateway from '@aws-cdk/aws-apigateway';

export class SampleAppStack extends cdk.Stack {
	constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
		super(scope, id, props);
		
		const companyTable = new dynamodb.Table(this, 'company', {
			partitionKey: {
				name: 'company_id',
				type: dynamodb.AttributeType.STRING
			},
			billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
			tableName: 'company'
		});
		
		const getCompanyLambda = new lambda.Function(this, 'getCompanyLambda', {
			code: lambda.Code.fromAsset('lambda'),
			handler: 'lambdaHandler.getCompanyHandler',
			runtime: lambda.Runtime.NODEJS_12_X,
			environment: {
				TABLE_NAME: companyTable.tableName,
				REGION: 'ap-northeast-1'
			},
		});
		
		companyTable.grantReadWriteData(getCompanyLambda);
		
		const getCompanyApi = new apigateway.LambdaRestApi(
			this,
			'getCompany',
			{handler: getCompanyLambda, proxy: false}
		);
		const getCompanyApiIntegration = new apigateway.LambdaIntegration(getCompanyLambda, {
			proxy: false,
			requestParameters: {
				'integration.request.querystring.company_id': 'method.request.querystring.company_id'
			},
			requestTemplates: {
				'application/json': JSON.stringify({company_id: "$util.escapeJavaScript($input.params('company_id'))"})
			},
			passthroughBehavior: apigateway.PassthroughBehavior.WHEN_NO_MATCH,
			integrationResponses: [
				{
					statusCode: '200',
					responseTemplates: {
						'application/json': '$input.json("$")'
					}
				}
			]
		});
		getCompanyApi.root.addResource('companyCompanies').addMethod(
			'GET',
			getCompanyApiIntegration,
			{
				requestParameters: {'method.request.querystring.company_id': true},
				methodResponses: [{statusCode: '200'}]
			}
		);
	}
}

const app = new cdk.App();
new SampleAppStack(app, 'SampleAppStack');
app.synth();

まとめ

  • プログラミング言語を用いてインフラ構築できる
  • インフラ構築と同時にLambdaのロジックもデプロイできる
  • 管理する粒度がStackで、CDKコマンドでそれぞれのStackを操作できる
  • リソース間の連携をコード化するのが難しいが、yamlを書くほど難しくはない

あのyaml地獄から抜け出すことができて幸せです。

まず手を動かしたいという衝動にかられた方はWorkshopに沿って進めると良いと思います。
僕は闇雲に手を動かしてしんどい思いをしました。

詰まったり、困った時は公式のAPI Referenceと、Developer Guideを参考にすることをオススメします。

12
7
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
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?