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

# はじめに

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

コンソール画面からGUIで操作して…
AWS-CLIで利用して…
CloudFormationのテンプレートファイルを書いて…
TerraformやServerlessFrameworkなどの構成管理ツールを使って…
などの方法があると思います。
デプロイするサービスが多くなればなるほど、構成管理ツールを用いたほうが管理コストがかからなくなるので良いですよね。
どのサービスをどれだけ、どのサービスと紐づけているのかをソースコードベースで確認することができます。
また不必要なサービスを減らすことができるのも利点の一つかと思います。

## AWSCDKとは？

AWSCDK(Cloud Development Kit)はCloudFormationのテンプレートファイルを、TypeScriptやJavaScript、Javaなどで書くことができるフレームワークです。
CloudFormationのテンプレートファイルはJSONまたはYAMLで書く必要があり、馴染みのないエンジニアにとっては学習コストが高く感じることがあると思います。
AWS-CDKではtsファイルやjsファイルをビルドするとCloudFormationのYAMLファイルが生成されます。

以前、[AWS-CDK for TypeScriptで色んなサービスをデプロイする](https://qiita.com/is_ryo/items/f04e05768c91c21f3699)という記事を書いたのですが、先日ついにGAとなりバージョン `1.0.0` がリリースされました :tada:  :tada:  :tada: 

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

## 前提条件

* Node.js >= 8.11.x
* TypeScript => 2.7
* AWSのCredentialの設定([参考](https://docs.aws.amazon.com/CDK/latest/userguide/install_config.html#credentials))(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にあたります。

```ts: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が用意されています。

```ts: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

<details>
<div>

```ts
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"
    })
  }
}
```

</div>
</details>

## APIGateway + Lambda

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

### Install Package

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

### SourceCode

<details>
<div>

```ts
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()
        },
      }]
    })
  }
}

```
</div>
</details>

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

<details>
<div>

```ts
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
      }
    })
  }
}
```

</div>
</details>

## AppSync + DynamoDB

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

### Install Package

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

### SourceCode

<details>
<div>

```ts
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)
  }
}
```

</div>
</details>

## さいごに

GAになる前から注目していたので、実際にCDKを使って色々なサービスをデプロイしています。
やっぱり慣れている言語(ここではTypeScript)でCFｎのテンプレートを書くことができるのは最高です！
GAになる前は、かなり激しいアップデートが繰り返されていましたが、GAになったことでそういったことも落ち着くはずです。
この機会にみなさんもCDK使ってみてはいかがでしょうか？
ではまた！！！