AWS CDKでAPI作成

# はじめに
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コマンドによる操作方法はこちらの[記事](https://dev.classmethod.jp/server-side/serverless/aws-cdk-command-line-interface/)が参考になります。

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

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

```
$ npm install -g aws-cdk
```
次に`cdk init`でプロジェクトを作成します。
AWS CDKのワークショップのサイトがあり、この[ページ](https://cdkworkshop.com/20-typescript/20-create-project/100-cdk-init.html)と同じ作業をすることになるので、ここでは書きません。

# API作成
クライアントからAPIGateway経由でIDを受け取り、DynamoDBからそのIDを用いてデータを取得して、jsonに変換してクライアントに返すシンプルなAPIを作成します。
## Lambdaのロジック作成
作成したプロジェクト配下にlambdaディレクトリを作成し、lambdaHandler.tsを作成します。

```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ディレクトリ配下のファイルを下記のように修正します。

```ts: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の定義
```ts
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の定義
```ts
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の定義
```ts
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](https://docs.aws.amazon.com/apigateway/latest/developerguide/request-response-data-mappings.html)を参考に設定しています。

- 注意点4
APIGatewayからLambdaへのリクエスト時に渡す際、パラメータのテンプレートを設定する必要があります。
[API Gateway Mapping Template and Access Logging Variable Reference](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html#util-template-reference)を参考に設定しています。
`$util`と`$input`という外部変数が登場するのではまりどころです。

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

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

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

```ts: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](https://cdkworkshop.com/)に沿って進めると良いと思います。
僕は闇雲に手を動かしてしんどい思いをしました。

詰まったり、困った時は公式の[API Reference](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-construct-library.html)と、[Developer Guide](https://docs.aws.amazon.com/cdk/latest/guide/home.html)を参考にすることをオススメします。