AWS-CDK for TypeScriptで色んなサービスをデプロイする

この記事の内容は古いものとなっています…
こちらの[GA記念！AWSCDK for TypeScriptで色んなサービスをデプロイする](https://qiita.com/is_ryo/items/8e6787a457a84447461a)を参考にしてください！

# はじめに

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

* コンソール画面からGUIで操作して…
* AWS-CLIで利用して…
* CloudFormationのテンプレートファイルを書いて…
* TerraformやServerlessFrameworkなどの構成管理ツールを使って…

などの方法があると思います。
デプロイするサービスが多くなればなるほど、構成管理ツールを用いたほうが管理コストがかからなくなるので良いですよね。
どのサービスをどれだけ、どのサービスと紐づけているのかをソースコードベースで確認することができます。
また不必要なサービスを減らすことができるのも利点の一つかと思います。

今回は `awslabs` がOSSとして開発している `AWS-CDK` を紹介していこうと思います。

## AWS-CDKとは？

AWS-CDK(Cloud Development Kit)はCloudForamationのテンプレートファイルを、TypeScriptやJavaScript、Javaなどで書くことができるフレームワークです。
CloudForamationのテンプレートファイルはJSONまたはYAMLで書く必要があり、馴染みのないエンジニアにとっては学習コストが高く感じることがあると思います。
AWS-CDKではtsファイルやjsファイルをビルドするとCloudFormationのYAMLファイルが生成されます。
リリースされたのは去年の夏頃で、OSSで開発が進められています。(2019/02/28時点の最新バージョンは `0.24.1`)
ただまだ開発者プレビューという状態なので、「これから大きな変更があるかもしれないよ」とドキュメント内でも書かれています。

* [Github リポジトリ](https://github.com/awslabs/aws-cdk)
* [公式ドキュメント](https://awslabs.github.io/aws-cdk/)

この記事ではTypeScriptでの書き方を紹介していきます。
この記事で紹介したソースコードなどは [こちらのGithubリポジトリ](https://github.com/ryo-is/cdk-templates)にpushしています。こちらも合わせてご確認ください。

## 前提条件

* Node.js >= 8.11.x
* TypeScript => 2.7
* AWSのCredentailの設定([参考](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

// CloudFormationのテンプレートファイル生成してファイルに書き出す
$ cdk synth --output ./output

// 差分を確認
$ cdk diff
```

## 今回デプロイしていくサービス

* S3
* DynamoDB
* APIGateway
* Lambda
* IAM
* Cognito

## S3

### Install Package

```
$ npm i @aws-cdk/aws-s3
```

### SourceCode

<details>
<div>

```ts

import cdk = require("@aws-cdk/cdk");
import s3 = require("@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 s3.Bucket(this, id, {
      bucketName: "bucketName"
    })
  }
}
```
</div>
</details>

## DynamoDB

### Install Pakcage

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

### SourceCode

<details>
<div>

```ts

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

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

    /**
     * Create DynamoDB Table
     */
    new dynamodb.Table(this, id, {
        tableName: "tableName",
        partitionKey: {
          name: "HashKey",
          type: dynamodb.AttributeType.String // String or Number or Binary
        },
        sortKey: {
          name: "SortKey",
          type: dynamodb.AttributeType.String // String or Number or Binary
        }
      }
    )
  }
}
```
</div>
</details>

## APIGateway & Lambda & IAM

### Install Package

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

### SourceCode
Webアプリケーションからのリクエストを受け取るRESTの受け口としてAPIGatewayを立てます。
また、CORSとCognitoUserPool認証を有効にしています。
LambdaはDynamoDBへの権限をいくつか付与し、APIGatewayからinvokeされるようにしています。

<details>
<div>

```ts
import cdk = require("@aws-cdk/cdk");
import apigateway = require("@aws-cdk/aws-apigateway");

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

    /**
     * Create APIGateway
     */
    const api: apigateway.RestApi = new apigateway.RestApi(this, id, {
      restApiName: "apiName",
      description: "apiDescription"
    });

    /**
     * Create APIGateway Authorizer
     * Cognito認証の設定
     */
    const apiAuthorizer: apigateway.CfnAuthorizer = new apigateway.CfnAuthorizer(this, id, {
      restApiId: api.restApiId,
      name: "authorizerName",
      identitySource: "method.request.header.Authorization",
      providerArns: ["cognitoArn"],
      type: "COGNITO_USER_POOLS"
    });

    /**
     * Create Lambda Function
     */
    const getHandler: lambda.Function = new lambda.Function(this, id, {
      functionName: "functionName",
      runtime: lambda.Runtime.NodeJS810, // Lambdaのランタイム
      code: lambda.Code.directory("resources"), // Lambdaのソースコードを置いているディレクトリ
      handler: index.handler, // Lambdaのハンドラー
      environment: {
        TZ: "Asia/Tokyo" // 環境変数
      }
    });

    /**
     * Create IAM Policy Statement
     */
    const statement: iam.PolicyStatement = new iam.PolicyStatement().allow()
      .addActions(
        "dynamodb:PutItem",
        "dynamodb:UpdateItem",
        "dynamodb:Query",
        "dynamodb:Scan"
      )
      .addResource(
        "DynamoDBTableARN" // 操作の対象となるDynamoDBTableのARN
      );

    /**
     * Attach role to Lambda
     */
    getHandler.addToRolePolicy(statement);

    /**
     * Add GET method to APIGateway
     */
    const integration = new apigateway.LambdaIntegration(getHandler);
    const option: apigateway.MethodOptions = {
      authorizationType: apigateway.AuthorizationType.Cognito,
      authorizerId: authorizer.authorizerId
    }
    api.root.addMethod("GET", integration, option);

    /**
     * Active APIGateway CORS Setting
     * CORSの設定をするために OPTION メソッドを追加します
     */
    const options = api.root.addMethod("OPTIONS", new apigateway.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: apigateway.PassthroughBehavior.Never,
      requestTemplates: {
        "application/json": "{\"statusCode\": 200}"
      }
    }));
    const methodResource = (options as cdk.Construct).node.findChild("Resource") as apigateway.CfnMethod;
    methodResource.propertyOverrides.methodResponses = [{
      statusCode: "200",
      responseModels: {
        "application/json": "Empty"
      },
      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,
      }
    }];
  }
}
```
</div>
</details>

## Cognito

### Install Package

```
$ npm i @aws-cdk/aws-cognito
```

### SourceCode

<details>
<div>

```ts
import cdk = require("@aws-cdk/cdk");
import cognito = require("@aws-cdk/aws-cognito");

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

    /**
     * Create Cognito UserPool
     */
    const userPool: cognito.CfnUserPool = new cognito.CfnUserPool(this, id, {
      userPoolName: "UserPoolName",
      // パスワードポリシー
      policies: {
        passwordPolicy: {
          minimumLength: 8,
          requireLowercase: true,
          requireNumbers: true,
          requireUppercase: true,
          requireSymbols: false
        }
      },
      // 必須の標準属性やカスタム属性
      schema: [
        {
          name: "email",
          attributeDataType: "String",
          required: true
        }
      ],
      // 自動検証する項目
      autoVerifiedAttributes: ["email"],
      // Eメール検証メッセージの件名
      emailVerificationSubject: "Your verification code",
      // Eメール検証メッセージの内容
      emailVerificationMessage: "Your verification code is {####}"
    });

    /**
     * Create Cognito UserPool Client
     */
    new cognito.CfnUserPoolClient(this, id, {
      clientName:"UserPoolClientName",
      userPoolId: userPool.userPoolId
    });
}
```
</div>
</details>

## まとめ

「TypeScriptからCloudFormationのYAMLが生成されるなんて素敵過ぎる！！！」と興奮気味な感じで色々触ってみましたが、実際最高でした。
YAMLファイルを書くのにあまり抵抗はないのですが、AWS-CDKで生成すると、自動で設定を保管してくれる部分もあったりするので、一からすべてを書かないといけないというわけではないです。
またドキュメントもまだまだ少なく、ドキュメント読むよりソースコード読んだほうが早い。みたいなことがあったりします。
前述したとおり、まだ開発者プレビューの段階ということなので、今後のアップデートをしっかり追って行くことが大事だと思います。
今後も他のサービスのデプロイを確認したりしていこうと思っています！
ではまた！！！