Edited at

GA記念!AWSCDK for TypeScriptで色んなサービスをデプロイする


はじめに

皆様はAWSのサービスをデプロイするのをどうされていますか?

コンソール画面からGUIで操作して…

AWS-CLIで利用して…

CloudFormationのテンプレートファイルを書いて…

TerraformやServerlessFrameworkなどの構成管理ツールを使って…

などの方法があると思います。

デプロイするサービスが多くなればなるほど、構成管理ツールを用いたほうが管理コストがかからなくなるので良いですよね。

どのサービスをどれだけ、どのサービスと紐づけているのかをソースコードベースで確認することができます。

また不必要なサービスを減らすことができるのも利点の一つかと思います。


AWSCDKとは?

AWSCDK(Cloud Development Kit)はCloudForamationのテンプレートファイルを、TypeScriptやJavaScript、Javaなどで書くことができるフレームワークです。

CloudForamationのテンプレートファイルはJSONまたはYAMLで書く必要があり、馴染みのないエンジニアにとっては学習コストが高く感じることがあると思います。

AWS-CDKではtsファイルやjsファイルをビルドするとCloudFormationのYAMLファイルが生成されます。

以前、AWS-CDK for TypeScriptで色んなサービスをデプロイするという記事を書いたのですが、先日ついにGAとなりバージョン 1.0.0 がリリースされました :tada: :tada: :tada:

だいぶ書き方がスッキリしたので、現在の書き方を紹介していこうと思います。


前提条件


  • Node.js >= 8.11.x

  • TypeScript => 2.7

  • AWSのCredentailの設定(参考)(AWS-CLIの初期設定ができていたらOK)


インストール

$ npm i -g aws-cdk

$ cdk --version
0.24.1 (build 67fcf6d)


初期設定

$ mkdir hello-cdk

$ cd hello-cdk
$ cdk init app --language=typescript


コマンド

// デプロイ

$ cdk deploy

// スタックを指定してデプロイ
$ cdk deploy ${StackName}

// CloudFormationのテンプレートファイル生成
$ cdk synth

// 差分を確認
$ cdk diff


基礎知識

CDKの単位は App / Stack / Construct に分かれています。


  • App


    • 実行可能なプログラム

    • CloudFormation(以下CFn)テンプレートに生成とデプロイに利用



  • Stack


    • デプロイ可能な単位

    • リージョンとアカウントを保持



  • Construct


    • AWSリソースを表現

    • 階層的なツリー構造を構成可能



雛形を用意したときに ./bin/cdk-app.ts が用意されています。この cdk-app.ts 自体がAppとなります。

cdk-app.ts でインスタンスを生成している HelloCdkStack というClassがStackにあたります。


cdk-app.ts

#!/usr/bin/env node

import "source-map-support/register"
import cdk = require("@aws-cdk/core")
import { HelloCdkStack } from "../lib/stacks/hello-cdk-stack"

const app: cdk.App = new cdk.App()
new HelloCdkStack(app, "HelloCdkStack") // Stack
app.synth()


hello-cdk-stack.ts でインスタンスを生成している s3.Bucket がConstructにあたります。

このConstructは各サービスごとにClassが用意されています。


hello-cdk-stack.ts

import core = require('@aws-cdk/core');

import s3 = require('@aws-cdk/aws-s3');

export class HelloCdkStack extends core.Stack {
constructor(scope: core.App, id: string, props?: core.StackProps) {
super(scope, id, props);
// Construct
new s3.Bucket(this, 'MyFirstBucket', {
versioned: true
});
}
}


こういった感じで、Stackの中にConstructを用意していき、その単位でCFnにスタックを作成しデプロイしていくという流れになります。


今回デプロイするパターン


  • S3

  • APIGateway + Lambda

  • IoTCore + DynamoDB

  • AppSync + DynamoDB


S3

S3バケットを作成する


Install Package

$ npm i @aws-cdk/aws-s3


SourceCode



import cdk = require("@aws-cdk/cdk")

import { Bucket } from "@aws-cdk/aws-s3"

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

// Create S3 Bucket
new Bucket(this, id, {
bucketName: "bucketName"
})
}
}






APIGateway + Lambda

APIGatewayとLambdaを作成して、RESTAPIのインターフェースをデプロイする


Install Package

$ npm i @aws-cdk/aws-lambda @aws-cdk/aws-apigateway


SourceCode



import cdk = require("@aws-cdk/core")

import { Function, Runtime, Code } from "@aws-cdk/aws-lambda"
import { RestApi, Integration, LambdaIntegration, Resource,
MockIntegration, PassthroughBehavior, EmptyModel } from "@aws-cdk/aws-apigateway"

export class QiitaAPILambda extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props)

// Lambda Function 作成
const lambdaFunction: Function = new Function(this, "qiita_demo", {
functionName: "qiita_demo", // 関数名
runtime: Runtime.NODEJS_10_X, // ランタイムの指定
code: Code.asset("lambdaSoruces/demo_function"), // ソースコードのディレクトリ
handler: "index.handler", // handler の指定
memorySize: 256, // メモリーの指定
timeout: cdk.Duration.seconds(10), // タイムアウト時間
environment: {} // 環境変数
})

// API Gateway 作成
const restApi: RestApi = new RestApi(this, "QiitaDemoAPI", {
restApiName: "QiitaDemoAPI", // API名
description: "Deployed by CDK" // 説明
})

// Integration 作成
const integration: Integration = new LambdaIntegration(lambdaFunction)

// リソースの作成
const getResouse: Resource = restApi.root.addResource("get")

// メソッドの作成
getResouse.addMethod("GET", integration)

// CORS対策でOPTIONSメソッドを作成
getResouse.addMethod("OPTIONS", new MockIntegration({
integrationResponses: [{
statusCode: "200",
responseParameters: {
"method.response.header.Access-Control-Allow-Headers":
"'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'",
"method.response.header.Access-Control-Allow-Origin": "'*'",
"method.response.header.Access-Control-Allow-Credentials": "'false'",
"method.response.header.Access-Control-Allow-Methods": "'OPTIONS,GET,PUT,POST,DELETE'",
}
}],
passthroughBehavior: PassthroughBehavior.NEVER,
requestTemplates: {
"application/json": "{\"statusCode\": 200}"
}
}), {
methodResponses: [{
statusCode: "200",
responseParameters: {
"method.response.header.Access-Control-Allow-Headers": true,
"method.response.header.Access-Control-Allow-Origin": true,
"method.response.header.Access-Control-Allow-Credentials": true,
"method.response.header.Access-Control-Allow-Methods": true,
},
responseModels: {
"application/json": new EmptyModel()
},
}]
})
}
}






IoTCore + DynamoDB(v2)

IoTCoreのRuleとDynamoDBを作成して、DynamoDBv2の方法でデータをPutするインターフェースをデプロイする


Install Package

$ npm i @aws-cdk/aws-dynamodb @aws-cdk/aws-iot @aws-cdk/aws-iam


SourceCode



import cdk = require("@aws-cdk/core")

import { Table, TableProps, AttributeType } from "@aws-cdk/aws-dynamodb"
import { Role, PolicyStatement, Effect, ServicePrincipal } from "@aws-cdk/aws-iam"
import { CfnTopicRule } from "@aws-cdk/aws-iot"

export class CdkIoTDynamo extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props)

const tableNameValue: string = "CdkIoTDemoTable"
const tableParam: TableProps = {
tableName: tableNameValue,
partitionKey: {
name: "id",
type: AttributeType.STRING
}
}
// Create DynamoDB Table
const table: Table = new Table(this, tableNameValue, tableParam)

const roleActions: string[] = ["dynamodb:PutItem"]
const roleResorces: string[] = [table.tableArn]
// Create RoleStatement
const roleStatement: PolicyStatement = new PolicyStatement({
actions: roleActions,
resources: roleResorces
})
roleStatement.effect = Effect.ALLOW
// Create IoTCore ServiceRole
const iotServiceRole: Role = new Role(this, "CdkIoTServiceRole", {
assumedBy: new ServicePrincipal("iot.amazonaws.com")
})
iotServiceRole.addToPolicy(roleStatement)

// sql for topic rule
const sqlBody: string = "SELECT * FROM 'CdkIoTDemo/#'"
// Create TopicRule DynamoDBv2
new CfnTopicRule(this, "CDKIoTDynamoRule", {
ruleName: "CDKIoTDynamoRule",
topicRulePayload: {
actions: [{
dynamoDBv2: {
putItem: { tableName: tableNameValue },
roleArn: iotServiceRole.roleArn
}
}],
ruleDisabled: false,
sql: sqlBody
}
})
}
}






AppSync + DynamoDB

AppSyncとDynamoDBを作成して、GraphQLでテーブルのデータを取得したり、データを書き込んだりするインターフェースをデプロイする


Install Package

$ npm i @aws-cdk/aws-dynamodb @aws-cdk/aws-iot @aws-cdk/aws-iam


SourceCode



import cdk = require("@aws-cdk/core")

import { Table, TableProps, AttributeType } from "@aws-cdk/aws-dynamodb"
import { Role, ManagedPolicy, ServicePrincipal } from "@aws-cdk/aws-iam"
import {
CfnGraphQLApi,
CfnDataSource,
CfnGraphQLSchema,
CfnResolver,
CfnApiKey
} from "@aws-cdk/aws-appsync"

export class CdkAppSync extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props)

const tableNameValue: string = "CDKAppSyncTable"
const tableParam: TableProps = {
tableName: tableNameValue,
partitionKey: {
name: "id",
type: AttributeType.STRING
},
readCapacity: 2,
writeCapacity: 2
}
// Create DynamoDB Table
new Table(this, tableNameValue, tableParam)

const tableRole: Role = new Role(this, "CdkAppSyncServiceRole", {
assumedBy: new ServicePrincipal("appsync.amazonaws.com")
})
tableRole.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName("AmazonDynamoDBFullAccess"))

// Create GraphQL API
const graphqlAPI: CfnGraphQLApi = new CfnGraphQLApi(this, "CDKAppSyncAPI", {
authenticationType: "API_KEY",
name: "CDKAppSyncAPI"
})
// Create API Key
new CfnApiKey(this, "CreateAPIKey", {
apiId: graphqlAPI.attrApiId
})

const definitionString: string = `
type
${tableNameValue} {
id: ID!,
name: String
}
type Paginated
${tableNameValue} {
items: [
${tableNameValue}!]!
nextToken: String
}
type Query {
all(limit: Int, nextToken: String): Paginated
${tableNameValue}!
getOne(id: ID!):
${tableNameValue}
}
type Mutation {
save(name: String!):
${tableNameValue}
delete(id: ID!):
${tableNameValue}
}
type Schema {
query: Query
mutation: Mutation
}
`

// Create Schema
const apiSchema: CfnGraphQLSchema = new CfnGraphQLSchema(this, "CDKGraphQLSchema", {
apiId: graphqlAPI.attrApiId,
definition: definitionString
})

// Create DataSource
const dataSource: CfnDataSource = new CfnDataSource(this, "CDKDataSourse", {
apiId: graphqlAPI.attrApiId,
name: tableNameValue,
type: "AMAZON_DYNAMODB",
dynamoDbConfig: {
awsRegion: this.region,
tableName: tableNameValue
},
serviceRoleArn: tableRole.roleArn
})

const getOneResolverMappingTemplate: string = `
{
"version": "2017-02-28",
"operation": "GetItem",
"key": {
"id": $util.dynamodb.toDynamoDBJson($ctx.args.id)
}
}
`

// Create Get Resolver
const getOneResolver: CfnResolver = new CfnResolver(this, "GetOneQueryResolver", {
apiId: graphqlAPI.attrApiId,
typeName: "Query",
fieldName: "getOne",
dataSourceName: dataSource.name,
requestMappingTemplate: getOneResolverMappingTemplate,
responseMappingTemplate: `$util.toJson($ctx.result)`
})
getOneResolver.addDependsOn(apiSchema)
getOneResolver.addDependsOn(dataSource)

const getAllResolverMappingTemplate: string = `
{
"version": "2017-02-28",
"operation": "Scan",
"limit": $util.defaultIfNull($ctx.args.limit, 20),
"nextToken": $util.toJson($util.defaultIfNullOrEmpty($ctx.args.nextToken, null))
}
`

// Create Scan Resolver
const getAllResolver: CfnResolver = new CfnResolver(this, "GetAllQueryResolver", {
apiId: graphqlAPI.attrApiId,
typeName: "Query",
fieldName: "all",
dataSourceName: dataSource.name,
requestMappingTemplate: getAllResolverMappingTemplate,
responseMappingTemplate: `$util.toJson($ctx.result)`
})
getAllResolver.addDependsOn(apiSchema)
getAllResolver.addDependsOn(dataSource)

const saveResolverMappingTemplate: string = `
{
"version": "2017-02-28",
"operation": "PutItem",
"key": {
"id": { "S": "$util.autoId()" }
},
"attributeValues": {
"name": $util.dynamodb.toDynamoDBJson($ctx.args.name)
}
}
`

// Create Put Resolver
const saveResolver: CfnResolver = new CfnResolver(this, "SaveMutationResolver", {
apiId: graphqlAPI.attrApiId,
typeName: "Mutation",
fieldName: "save",
dataSourceName: dataSource.name,
requestMappingTemplate: saveResolverMappingTemplate,
responseMappingTemplate: `$util.toJson($ctx.result)`
})
saveResolver.addDependsOn(apiSchema)
saveResolver.addDependsOn(dataSource)

const deleteResolverMappingTemplate: string = `
{
"version": "2017-02-28",
"operation": "DeleteItem",
"key": {
"id": $util.dynamodb.toDynamoDBJson($ctx.args.id)
}
}
`

// Create Delete Resolver
const deleteResolver: CfnResolver = new CfnResolver(this, "DeleteMutationResolver", {
apiId: graphqlAPI.attrApiId,
typeName: "Mutation",
fieldName: "delete",
dataSourceName: dataSource.name,
requestMappingTemplate: deleteResolverMappingTemplate,
responseMappingTemplate: `$util.toJson($ctx.result)`
})
deleteResolver.addDependsOn(apiSchema)
deleteResolver.addDependsOn(dataSource)
}
}






さいごに

GAになる前から注目していたので、実際にCDKを使って色々なサービスをデプロイしています。

やっぱり慣れている言語(ここではTypeScript)でCFnのテンプレートを書くことができるのは最高です!

GAになる前は、かなり激しいアップデートが繰り返されていましたが、GAになったことでそういったことも落ち着くはずです。

この機会にみなさんもCDK使ってみてはいかがでしょうか?

ではまた!!!