はじめに
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を作成します。
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ディレクトリ配下のファイルを下記のように修正します。
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
の第三引数であるMethodOptions
にrequestParameters
を設定する必要があります。
注意点3で指定したパラメータをtrue
とします。
全体のソースコード
API構築するためにリソースを定義した全体のソースコードです
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を参考にすることをオススメします。