AWS Cloud Development Kit(AWS CDK)でカスタムリソース(@aws-cdk/custom-resources)をつかってみた

先日AWS Cloud Development Kit(AWS CDK)がGAされたので、なにができるのかソースを眺めたり使ってみたりしていたところ、面白そうなパッケージをみつけたのでお試ししていました。


## AWS Cloud Development Kit(AWS CDK)とは

AWS クラウド開発キット (CDK) – TypeScript と Python 用がご利用可能に | Amazon Web Services ブログ
https://aws.amazon.com/jp/blogs/news/aws-cloud-development-kit-cdk-typescript-and-python-are-now-generally-available/

>Infrastructure as Code によって、手動での実行手順に頼る代わりに、管理者と開発者の両方が構成ファイルを使用し、アプリケーションに必要なコンピューティング、ストレージ、ネットワーク、アプリケーションサービスのプロビジョニングを自動化できるようになります。


AWS CloudFormation(CFn)を利用するとYAMLやJSONでインフラストラクチャの管理ができますが、それを発展させてインフラストラクチャの管理もプログラミングしようぜって感じのやつです。
詳細は上記ブログでだいたいわかるかと思います。


AWS CDKを利用してインフラストラクチャを定義する場合、リソースごとにパッケージをインストールして実装します。
下記は`aws-samples/aws-cdk-examples` リポジトリにあるサンプルでLambda関数とスケジュール起動させるためのAmazon CloudWatch Eventsのリソースを定義しています。`package.json` をみると`@aws-cdk/aws-events`, `@aws-cdk/aws-events-targets`, `@aws-cdk/aws-lambda` が別途インストールされていることが確認できます。~~Python 2.7を指定しているのがむず痒いですね~~

aws-cdk-examples/index.ts at master · aws-samples/aws-cdk-examples
https://github.com/aws-samples/aws-cdk-examples/blob/master/typescript/lambda-cron/index.ts

```ts
import events = require('@aws-cdk/aws-events');
import targets = require('@aws-cdk/aws-events-targets');
import lambda = require('@aws-cdk/aws-lambda');
import cdk = require('@aws-cdk/core');

import fs = require('fs');

export class LambdaCronStack extends cdk.Stack {
  constructor(app: cdk.App, id: string) {
    super(app, id);

    const lambdaFn = new lambda.Function(this, 'Singleton', {
      code: new lambda.InlineCode(fs.readFileSync('lambda-handler.py', { encoding: 'utf-8' })),
      handler: 'index.main',
      timeout: cdk.Duration.seconds(300),
      runtime: lambda.Runtime.PYTHON_2_7,
    });

    // Run every day at 6PM UTC
    // See https://docs.aws.amazon.com/lambda/latest/dg/tutorial-scheduled-events-schedule-expressions.html
    const rule = new events.Rule(this, 'Rule', {
      schedule: events.Schedule.expression('cron(0 18 ? * MON-FRI *)')
    });

    rule.addTarget(new targets.LambdaFunction(lambdaFn));
  }
}

const app = new cdk.App();
new LambdaCronStack(app, 'LambdaCronExample');
app.synth();
```

```json:package.json
{
  "name": "lambda-cron",
  (略)
  "dependencies": {
    "@aws-cdk/aws-events": "*",
    "@aws-cdk/aws-events-targets": "*",
    "@aws-cdk/aws-lambda": "*",
    "@aws-cdk/core": "*"
  }
}
```

こんな感じでこれまでYAMLやJSONでテンプレート定義してスタック管理していたのをプログラミングに落とし込めるのがAWS CDKです。


## 面白そうなパッケージ

`aws-cdk` リポジトリのソースを眺めていたら`@aws-cdk/custom-resources` というパッケージがありました。

aws-cdk/packages/@aws-cdk/custom-resources at master · aws/aws-cdk
https://github.com/aws/aws-cdk/tree/master/packages/%40aws-cdk/custom-resources


名前のとおりカスタムリソースのパッケージですが、CFnのいわゆるカスタムリソースのパッケージは別にあり、利用用途が少し異なるものでした。

こちらがCFnのいわゆるカスタムリソースのパッケージ
aws-cdk/packages/@aws-cdk/aws-cloudformation at master · aws/aws-cdk
https://github.com/aws/aws-cdk/tree/master/packages/%40aws-cdk/aws-cloudformation


単にカスタムリソースの使い方を調べていて先に`@aws-cdk/custom-resources` に目がいったという。。。


`@aws-cdk/custom-resources` のREADMEの冒頭で

>This is a developer preview (public beta) module. Releases might lack important features and might have future breaking changes.
>
>This API is still under active development and subject to non-backward compatible changes or removal in any future version. Use of the API is not recommended in production environments. Experimental APIs are not subject to the Semantic Versioning model.

とあり、AWS CDKはGAされたにも関わらず、絶賛開発中のパッケージです。(2019/07/23時点)

サンプルをみてみるとCFnのカスタムリソースをより簡単に実装できるようにするためのパッケージみたいです。
Lambda関数がでてこない！

``` ts:サンプル
const verifyDomainIdentity = new AwsCustomResource(this, 'VerifyDomainIdentity', {
  onCreate: {
    service: 'SES',
    action: 'verifyDomainIdentity',
    parameters: {
      Domain: 'example.com'
    },
    physicalResourceIdPath: 'VerificationToken' // Use the token returned by the call as physical id
  }
});

new route53.TxtRecord(zone, 'SESVerificationRecord', {
  recordName: `_amazonses.example.com`,
  recordValue: verifyDomainIdentity.getData('VerificationToken')
});
```


## つかってみた

CFnのカスタムリソースといえば、先日散々苦労してCFnのテンプレートを作成したAmazon Managed Blockchain(AMB)しかおもい浮かばなかったのでお題にしてみます。

苦労したやつはこちら

Amazon Managed BlockchainでHyperledger Fabricのブロックチェーンネットワークをさくっと構築するAWS CloudFormationのテンプレートを作ってみた(使い方編) - Qiita
https://qiita.com/kai_kou/items/dc7dbe947a7cc6d0a8db

Amazon Managed BlockchainでHyperledger Fabricのブロックチェーンネットワークをさくっと構築するAWS CloudFormationのテンプレートを作ってみた(解説編) - Qiita
https://qiita.com/kai_kou/items/ab51cb7d16c680f591d2


### 前提

- AWSアカウントがある
- AWS CLIが利用できる
- Node.jsがインストール済み


### AWS CDKのインストール

AWS CDKのコマンドが利用できるようにするため、`aws-cdk` をインストールします。

```console
> node -v
v10.11.0

> npm -v
6.10.1


> npm i -g aws-cdk

# fishの場合
> exec fish -l

> cdk --version
1.1.0 (build 1a11e96)
```


### AWS CDKプロジェクト作成

`cdk` コマンドでプロジェクトを作成します。言語はTypeScriptを利用します。

```console
> mkdir use-cdk-custom-resources
> cd use-cdk-custom-resources

> cdk init app --language=typescript

Applying project template app for typescript
Initializing a new git repository...
Executing npm install...
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN use-cdk-custom-resources@0.1.0 No repository field.
npm WARN use-cdk-custom-resources@0.1.0 No license field.

# Useful commands

 * `npm run build`   compile typescript to js
 * `npm run watch`   watch for changes and compile
 * `cdk deploy`      deploy this stack to your default AWS account/region
 * `cdk diff`        compare deployed stack with current state
 * `cdk synth`       emits the synthesized CloudFormation template
```


`cdk init` コマンドを実行すると以下のようにファイルが自動生成されました。
コマンド実行したディレクトリの名前が反映されました。

```console
> tree . -L 2
.
├── README.md
├── bin
│   └── use-cdk-custom-resources.ts
├── cdk.json
├── lib
│   └── use-cdk-custom-resources-stack.ts
├── node_modules
(略)
├── package-lock.json
├── package.json
└── tsconfig.json

3 directories, 5 files
```

### `@aws-cdk/custom-resources` のインストール

`@aws-cdk/custom-resources` をインストールして利用できるようにします。

```console
> npm i @aws-cdk/custom-resources

+ @aws-cdk/custom-resources@1.1.0
added 19 packages from 4 contributors and audited 939 packages in 170.2s
found 0 vulnerabilities
```

### 実装する

`@aws-cdk/custom-resources` パッケージの`AwsCustomResource` を利用して既存のAMBネットワーク情報を取得するカスタムリソースを実装してみました。
取得した情報を`cdk.CfnOutput` でCFnスタックのアウトプットとします。

AMBのネットワークがないって方は下記を参考に構築してみてください。

Amazon Managed BlockchainでHyperledger Fabricのブロックチェーンネットワークをさくっと構築するAWS CloudFormationのテンプレートを作ってみた(使い方編) - Qiita
https://qiita.com/kai_kou/items/dc7dbe947a7cc6d0a8db


実装は`cdk init` コマンドで自動生成された`lib/use-cdk-custom-resources-stack.ts` に行います。

```ts:lib/use-cdk-custom-resources-stack.ts
import cdk = require('@aws-cdk/core');
import { AwsCustomResource } from '@aws-cdk/custom-resources/lib';

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

    const getNetworkTest = new AwsCustomResource(this, 'getNetworkTest', {
      onCreate: {
        apiVersion: '2018-09-24', // 指定しなくてもOK
        service: 'ManagedBlockchain',
        action: 'getNetwork',
        parameters: {
          NetworkId: 'n-XXXXXXXXXXXXXXXXXXXXXXXXXX'
        },
        physicalResourceIdPath: 'Network.Id'
      }
    });

    new cdk.CfnOutput(this, 'BcNetworkId', {
      description: 'The message that came back from the Custom Resource',
      value: getNetworkTest.getData('Network.Id').toString()
    });
  }
}

```

### ビルドしてデプロイする

実装できたらビルドしてデプロイします。

```console
> npm run build

 > use-cdk-custom-resources@0.1.0 build /Users/kai/dev/aws/cdk/use-cdk-custom-resources
 > tsc
```

AWS CDKで初回デプロイ時に`cdk bootstrap` コマンドを実行する必要がありました。
実行するとCFnに`CDKToolkit` というスタックが作成されてリソースとしてS3バケットが作成されました。
`--profile` オプションでAWSアカウントを指定する必要があります。こちらはAWS CLIと同じ設定をみてくれます。

```console
> cdk bootstrap --profile default

 ⏳  Bootstrapping environment aws://xxxxxxxxxxxx/us-east-1...
CDKToolkit: creating CloudFormation changeset...
 0/2 | 16:11:40 | CREATE_IN_PROGRESS   | AWS::S3::Bucket | StagingBucket
 0/2 | 16:11:41 | CREATE_IN_PROGRESS   | AWS::S3::Bucket | StagingBucket Resource creation Initiated
 1/2 | 16:12:02 | CREATE_COMPLETE      | AWS::S3::Bucket | StagingBucket
 2/2 | 16:12:05 | CREATE_COMPLETE      | AWS::CloudFormation::Stack | CDKToolkit
 ✅  Environment aws://xxxxxxxxxxxx/us-east-1 bootstrapped.
```

`CDKToolkit` スタック情報を確認するとS3バケットが作成されたのを確認できます。

```console
> aws cloudformation describe-stacks \
  --stack-name CDKToolkit
{
    "Stacks": [
        {
            "StackId": "arn:aws:cloudformation:us-east-1:xxxxxxxxxxxx:stack/CDKToolkit/92106320-ad05-11e9-bffe-0a9fca2e6786",
            "StackName": "CDKToolkit",
            "ChangeSetId": "arn:aws:cloudformation:us-east-1:xxxxxxxxxxxx:changeSet/CDK-63a06f96-830e-4f26-b47a-9d2a79fa6ca4/834d5f97-8053-4974-8091-ce0fbac66f72",
            "Description": "The CDK Toolkit Stack. It was created by `cdk bootstrap` and manages resources necessary for managing your Cloud Applications with AWS CDK.",
            "CreationTime": "2019-07-23T04:51:44.877Z",
            "LastUpdatedTime": "2019-07-23T04:51:52.481Z",
            "RollbackConfiguration": {},
            "StackStatus": "CREATE_COMPLETE",
            "DisableRollback": false,
            "NotificationARNs": [],
            "Capabilities": [
                "CAPABILITY_IAM",
                "CAPABILITY_NAMED_IAM",
                "CAPABILITY_AUTO_EXPAND"
            ],
            "Outputs": [
                {
                    "OutputKey": "BucketName",
                    "OutputValue": "cdktoolkit-stagingbucket-xxxxxxxxxxxx",
                    "Description": "The name of the S3 bucket owned by the CDK toolkit stack"
                },
                {
                    "OutputKey": "BucketDomainName",
                    "OutputValue": "cdktoolkit-stagingbucket-xxxxxxxxxxxx.s3.amazonaws.com",
                    "Description": "The domain name of the S3 bucket owned by the CDK toolkit stack"
                }
            ],
            "Tags": [],
            "EnableTerminationProtection": false,
            "DriftInformation": {
                "StackDriftStatus": "NOT_CHECKED"
            }
        }
    ]
}


> aws cloudformation describe-stack-resources \
  --stack-name CDKToolkit

{
    "StackResources": [
        {
            "StackName": "CDKToolkit",
            "StackId": "arn:aws:cloudformation:us-east-1:xxxxxxxxxxxx:stack/CDKToolkit/92106320-ad05-11e9-bffe-0a9fca2e6786",
            "LogicalResourceId": "StagingBucket",
            "PhysicalResourceId": "cdktoolkit-stagingbucket-xxxxxxxxxxxx",
            "ResourceType": "AWS::S3::Bucket",
            "Timestamp": "2019-07-23T04:52:17.264Z",
            "ResourceStatus": "CREATE_COMPLETE",
            "DriftInformation": {
                "StackResourceDriftStatus": "NOT_CHECKED"
            }
        }
    ]
}
```

準備ができたので`cdk deploy` でデプロイしてみます。
コマンド実行するとスタックで作成されるロールの情報が表示されて実行するか確認されます。
確認後、CFnにスタック作成されてイベントログが出力されます。わかりやすくていいですね。

```console
> cdk deploy

This deployment will make potentially sensitive changes according to your current security approval level (--require-approval broadening).
Please confirm you intend to make the following modifications:

IAM Statement Changes
┌───┬────────────────────────────────┬────────┬────────────────────────────────┬────────────────────────────────┬───────────┐
│   │ Resource                       │ Effect │ Action                         │ Principal                      │ Condition │
├───┼────────────────────────────────┼────────┼────────────────────────────────┼────────────────────────────────┼───────────┤
│ + │ ${AWS679f53fac002430cb0da5b798 │ Allow  │ sts:AssumeRole                 │ Service:lambda.${AWS::URLSuffi │           │
│   │ 2bd2287/ServiceRole.Arn}       │        │                                │ x}                             │           │
├───┼────────────────────────────────┼────────┼────────────────────────────────┼────────────────────────────────┼───────────┤
│ + │ *                              │ Allow  │ managedblockchain:GetNetwork   │ AWS:${AWS679f53fac002430cb0da5 │           │
│   │                                │        │                                │ b7982bd2287/ServiceRole}       │           │
└───┴────────────────────────────────┴────────┴────────────────────────────────┴────────────────────────────────┴───────────┘
IAM Policy Changes
┌───┬───────────────────────────────────────────────────────────┬───────────────────────────────────────────────────────────┐
│   │ Resource                                                  │ Managed Policy ARN                                        │
├───┼───────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────┤
│ + │ ${AWS679f53fac002430cb0da5b7982bd2287/ServiceRole}        │ arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLam │
│   │                                                           │ bdaBasicExecutionRole                                     │
└───┴───────────────────────────────────────────────────────────┴───────────────────────────────────────────────────────────┘
(NOTE: There may be security-related changes not in this list. See http://bit.ly/cdk-2EhF7Np)

Do you wish to deploy these changes (y/n)? y
UseCdkCustomResourcesStack: deploying...
UseCdkCustomResourcesStack: creating CloudFormation changeset...
 0/6 | 16:09:48 | CREATE_IN_PROGRESS   | AWS::IAM::Role        | AWS679f53fac002430cb0da5b7982bd2287/ServiceRole (AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2)
 0/6 | 16:09:48 | CREATE_IN_PROGRESS   | AWS::CDK::Metadata    | CDKMetadata
 0/6 | 16:09:48 | CREATE_IN_PROGRESS   | AWS::IAM::Role        | AWS679f53fac002430cb0da5b7982bd2287/ServiceRole (AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2) Resource creation Initiated
 0/6 | 16:09:50 | CREATE_IN_PROGRESS   | AWS::CDK::Metadata    | CDKMetadata Resource creation Initiated
 1/6 | 16:09:50 | CREATE_COMPLETE      | AWS::CDK::Metadata    | CDKMetadata
 2/6 | 16:10:01 | CREATE_COMPLETE      | AWS::IAM::Role        | AWS679f53fac002430cb0da5b7982bd2287/ServiceRole (AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2)
 2/6 | 16:10:05 | CREATE_IN_PROGRESS   | AWS::IAM::Policy      | AWS679f53fac002430cb0da5b7982bd2287/ServiceRole/DefaultPolicy (AWS679f53fac002430cb0da5b7982bd2287ServiceRoleDefaultPolicyD28E1A5E)
 2/6 | 16:10:08 | CREATE_IN_PROGRESS   | AWS::IAM::Policy      | AWS679f53fac002430cb0da5b7982bd2287/ServiceRole/DefaultPolicy (AWS679f53fac002430cb0da5b7982bd2287ServiceRoleDefaultPolicyD28E1A5E) Resource creation Initiated
 3/6 | 16:10:13 | CREATE_COMPLETE      | AWS::IAM::Policy      | AWS679f53fac002430cb0da5b7982bd2287/ServiceRole/DefaultPolicy (AWS679f53fac002430cb0da5b7982bd2287ServiceRoleDefaultPolicyD28E1A5E)
 3/6 | 16:10:16 | CREATE_IN_PROGRESS   | AWS::Lambda::Function | AWS679f53fac002430cb0da5b7982bd2287 (AWS679f53fac002430cb0da5b7982bd22872D164C4C)
 3/6 | 16:10:16 | CREATE_IN_PROGRESS   | AWS::Lambda::Function | AWS679f53fac002430cb0da5b7982bd2287 (AWS679f53fac002430cb0da5b7982bd22872D164C4C) Resource creation Initiated
 4/6 | 16:10:16 | CREATE_COMPLETE      | AWS::Lambda::Function | AWS679f53fac002430cb0da5b7982bd2287 (AWS679f53fac002430cb0da5b7982bd22872D164C4C)
 4/6 | 16:10:22 | CREATE_IN_PROGRESS   | Custom::AWS           | getNetworkTest/Resource/Default (getNetworkTest425BEE14)
 4/6 | 16:10:27 | CREATE_IN_PROGRESS   | Custom::AWS           | getNetworkTest/Resource/Default (getNetworkTest425BEE14) Resource creation Initiated
 5/6 | 16:10:27 | CREATE_COMPLETE      | Custom::AWS           | getNetworkTest/Resource/Default (getNetworkTest425BEE14)
 6/6 | 16:10:29 | CREATE_COMPLETE      | AWS::CloudFormation::Stack | UseCdkCustomResourcesStack

 ✅  UseCdkCustomResourcesStack

Outputs:
UseCdkCustomResourcesStack.BcNetworkId = n-XXXXXXXXXXXXXXXXXXXXXXXXXX

Stack ARN:
arn:aws:cloudformation:us-east-1:xxxxxxxxxxxx:stack/UseCdkCustomResourcesStack/ca79efc0-ad18-11e9-8a12-0a3a983b5e88
```

スタック作成されると`Outputs` 情報も出力され、AMBネットワーク情報が取得できたのを確認できます。
CFnスタック情報を確認してみると`Parameters` が設定されていたり、`AWS::CDK::Metadata` リソースなどAWS CDK特有のリソースがあったりします。

```console
> aws cloudformation describe-stacks \
  --stack-name UseCdkCustomResourcesStack

{
    "Stacks": [
        {
            "StackId": "arn:aws:cloudformation:us-east-1:xxxxxxxxxxxx:stack/UseCdkCustomResourcesStack/7d97bb10-ae85-11e9-93b7-0a51b82e168a",
            "StackName": "UseCdkCustomResourcesStack",
            "ChangeSetId": "arn:aws:cloudformation:us-east-1:xxxxxxxxxxxx:changeSet/CDK-8a18b8d3-a2c2-4095-8f99-1c3da69255a2/3ad6c643-6d89-4e96-9cf9-5403bff5b01e",
            "Parameters": [
                {
                    "ParameterKey": "AWS679f53fac002430cb0da5b7982bd2287CodeS3BucketF55839B6",
                    "ParameterValue": "cdktoolkit-stagingbucket-xxxxxxxxxxxx"
                },
                {
                    "ParameterKey": "AWS679f53fac002430cb0da5b7982bd2287CodeS3VersionKey3C45B02F",
                    "ParameterValue": "assets/UseCdkCustomResourcesStackAWS679f53fac002430cb0da5b7982bd2287Code44CF678B/||6d3e1aface6af068a2d1abe48f7aa26b140d9231a6078e03e076239b8189ae2a.zip"
                },
                {
                    "ParameterKey": "AWS679f53fac002430cb0da5b7982bd2287CodeArtifactHash49FACC2E",
                    "ParameterValue": "6d3e1aface6af068a2d1abe48f7aa26b140d9231a6078e03e076239b8189ae2a"
                }
            ],
            "CreationTime": "2019-07-25T02:39:57.405Z",
            "LastUpdatedTime": "2019-07-25T02:40:05.168Z",
            "RollbackConfiguration": {},
            "StackStatus": "CREATE_COMPLETE",
            "DisableRollback": false,
            "NotificationARNs": [],
            "Capabilities": [
                "CAPABILITY_IAM",
                "CAPABILITY_NAMED_IAM",
                "CAPABILITY_AUTO_EXPAND"
            ],
            "Outputs": [
                {
                    "OutputKey": "BcNetworkId",
                    "OutputValue": "n-XXXXXXXXXXXXXXXXXXXXXXXXXX",
                    "Description": "The message that came back from the Custom Resource"
                }
            ],
            "Tags": [],
            "EnableTerminationProtection": false,
            "DriftInformation": {
                "StackDriftStatus": "NOT_CHECKED"
            }
        }
    ]
}


> aws cloudformation describe-stack-resources \
  --stack-name UseCdkCustomResourcesStack \
  --query "StackResources[*].ResourceType"

[
    "AWS::Lambda::Function",
    "AWS::IAM::Role",
    "AWS::IAM::Policy",
    "AWS::CDK::Metadata",
    "Custom::AWS"
]
```

### のぞいてみる

AWS CDKのデプロイ時に作成されるファイルをのぞいてみました。
`npm run build` コマンドで`js` ファイルが作成されています。
`cdk deploy` コマンドで`cdk.out` フォルダが生成され、こちらにCFn関連のファイルが作成されています。

```console
> tree . -L 2
.
├── README.md
├── bin
│   ├── use-cdk-custom-resources.d.ts
│   ├── use-cdk-custom-resources.js
│   └── use-cdk-custom-resources.ts
├── cdk.json
├── cdk.out
│   ├── UseCdkCustomResourcesStack.template.json
│   ├── asset.01d077466681f165ec462417f525188e4be806c35136c1a5e4bdcd3b71c12942
│   ├── cdk.out
│   └── manifest.json
├── lib
│   ├── use-cdk-custom-resources-stack.d.ts
│   ├── use-cdk-custom-resources-stack.js
│   └── use-cdk-custom-resources-stack.ts
├── node_modules
(略)
├── package-lock.json
├── package.json
└── tsconfig.json

212 directories, 19 files
```

`cdk.out/asset.xxxxx` フォルダに`index.js` ファイルがあり、これがCFnのLambda-Backedカスタムリソースで利用されるLambda関数のファイルになるみたいです。

```console
> tree cdk.out -L 2
cdk.out
├── UseCdkCustomResourcesStack.template.json
├── asset.01d077466681f165ec462417f525188e4be806c35136c1a5e4bdcd3b71c12942
│   ├── index.d.ts
│   └── index.js
├── cdk.out
└── manifest.json
```


`cdk.out/asset.xxxxx/index.js` をみてみると`flatten` メソッドっていうAWS SDKから得られたJSONファイルをフラット化する実装があったりします。
なぜJSONファイルをフラット化する必要があるかは下記が詳しいです。


AWS CloudFormationのLambda-BackedカスタムリソースでネストされたJSONを返しても参照できない - Qiita
https://qiita.com/kai_kou/items/61a2b3c69ae2af4f2e40

AWS CloudFormationのLambda-BackedカスタムリソースでネストされてるっぽいJSONを返す方法 - Qiita
https://qiita.com/kai_kou/items/d030117c694531410203


AWS SDKの利用方法も`const awsService = new AWS[call.service](call.apiVersion && { apiVersion: call.apiVersion });` って呼び方ができるのかぁ。など参考になります。

```js:cdk.out/asset.xxxxx/index.js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
// tslint:disable:no-console
const AWS = require("aws-sdk");
/**
 * Flattens a nested object
 *
 * @param object the object to be flattened
 * @returns a flat object with path as keys
 */
function flatten(object) {
    return Object.assign({}, ...function _flatten(child, path = []) {
        return [].concat(...Object.keys(child)
            .map(key => typeof child[key] === 'object'
            ? _flatten(child[key], path.concat([key]))
            : ({ [path.concat([key]).join('.')]: child[key] })));
    }(object));
}
/**
 * Converts true/false strings to booleans in an object
 */
function fixBooleans(object) {
    return JSON.parse(JSON.stringify(object), (_k, v) => v === 'true'
        ? true
        : v === 'false'
            ? false
            : v);
}
/**
 * Filters the keys of an object.
 */
function filterKeys(object, pred) {
    return Object.entries(object)
        .reduce((acc, [k, v]) => pred(k)
        ? { ...acc, [k]: v }
        : acc, {});
}
async function handler(event, context) {
    try {
        console.log(JSON.stringify(event));
        console.log('AWS SDK VERSION: ' + AWS.VERSION);
        let physicalResourceId = event.PhysicalResourceId;
        let flatData = {};
        let data = {};
        const call = event.ResourceProperties[event.RequestType];
        if (call) {
            const awsService = new AWS[call.service](call.apiVersion && { apiVersion: call.apiVersion });
            try {
                const response = await awsService[call.action](call.parameters && fixBooleans(call.parameters)).promise();
                flatData = flatten(response);
                data = call.outputPath
                    ? filterKeys(flatData, k => k.startsWith(call.outputPath))
                    : flatData;
            }
            catch (e) {
                if (!call.catchErrorPattern || !new RegExp(call.catchErrorPattern).test(e.code)) {
                    throw e;
                }
            }
            physicalResourceId = call.physicalResourceIdPath
                ? flatData[call.physicalResourceIdPath]
                : call.physicalResourceId;
        }
        await respond('SUCCESS', 'OK', physicalResourceId, data);
    }
    catch (e) {
        console.log(e);
        await respond('FAILED', e.message || 'Internal Error', context.logStreamName, {});
    }
    function respond(responseStatus, reason, physicalResourceId, data) {
        const responseBody = JSON.stringify({
            Status: responseStatus,
            Reason: reason,
            PhysicalResourceId: physicalResourceId,
            StackId: event.StackId,
            RequestId: event.RequestId,
            LogicalResourceId: event.LogicalResourceId,
            NoEcho: false,
            Data: data
        });
        console.log('Responding', responseBody);
        const parsedUrl = require('url').parse(event.ResponseURL);
        const requestOptions = {
            hostname: parsedUrl.hostname,
            path: parsedUrl.path,
            method: 'PUT',
            headers: { 'content-type': '', 'content-length': responseBody.length }
        };
        return new Promise((resolve, reject) => {
            try {
                const request = require('https').request(requestOptions, resolve);
                request.on('error', reject);
                request.write(responseBody);
                request.end();
            }
            catch (e) {
                reject(e);
            }
        });
    }
}
exports.handler = handler;
//# sourceMappingURL=data:application/json;base64,eyJ2(略)
```

`cdk.out/manifest.json` ファイルがCFnでスタック作成する際のテンプレートになっています。
リソース名にランダムな英数字が含まれているので、扱うリソースが増えるとなかなかに読み応えがありそうです(白目

おもしろいなぁと感じたのは、**AWS SDKのサービス名とアクション(メソッド) = AWS::IAM::Policyのアクション**になる点です。

今回はAMBのgetNetworkを利用しているので、

- AWS SDKのメソッド呼び出し: `new AWS['ManagedBlockchain']['getNetwork'](parameters)`
- AWS::IAM::Policyのアクション: `managedblockchain:GetNetwork`

となります。

```json:cdk.out/manifest.json
{
  "Resources": {
    "getNetworkTest425BEE14": {
      "Type": "Custom::AWS",
      "Properties": {
        "ServiceToken": {
          "Fn::GetAtt": [
            "AWS679f53fac002430cb0da5b7982bd22872D164C4C",
            "Arn"
          ]
        },
        "Create": {
          "service": "ManagedBlockchain",
          "action": "getNetwork",
          "parameters": {
            "NetworkId": "n-XXXXXXXXXXXXXXXXXXXXXXXXXX"
          },
          "physicalResourceIdPath": "Network.Id"
        }
      },
      "UpdateReplacePolicy": "Delete",
      "DeletionPolicy": "Delete",
      "Metadata": {
        "aws:cdk:path": "UseCdkCustomResourcesStack/getNetworkTest/Resource/Default"
      }
    },
    "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2": {
      "Type": "AWS::IAM::Role",
      "Properties": {
        "AssumeRolePolicyDocument": {
          "Statement": [
            {
              "Action": "sts:AssumeRole",
              "Effect": "Allow",
              "Principal": {
                "Service": {
                  "Fn::Join": [
                    "",
                    [
                      "lambda.",
                      {
                        "Ref": "AWS::URLSuffix"
                      }
                    ]
                  ]
                }
              }
            }
          ],
          "Version": "2012-10-17"
        },
        "ManagedPolicyArns": [
          {
            "Fn::Join": [
              "",
              [
                "arn:",
                {
                  "Ref": "AWS::Partition"
                },
                ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
              ]
            ]
          }
        ]
      },
      "Metadata": {
        "aws:cdk:path": "UseCdkCustomResourcesStack/AWS679f53fac002430cb0da5b7982bd2287/ServiceRole/Resource"
      }
    },
    "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleDefaultPolicyD28E1A5E": {
      "Type": "AWS::IAM::Policy",
      "Properties": {
        "PolicyDocument": {
          "Statement": [
            {
              "Action": "managedblockchain:GetNetwork",
              "Effect": "Allow",
              "Resource": "*"
            }
          ],
          "Version": "2012-10-17"
        },
        "PolicyName": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleDefaultPolicyD28E1A5E",
        "Roles": [
          {
            "Ref": "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2"
          }
        ]
      },
      "Metadata": {
        "aws:cdk:path": "UseCdkCustomResourcesStack/AWS679f53fac002430cb0da5b7982bd2287/ServiceRole/DefaultPolicy/Resource"
      }
    },
    "AWS679f53fac002430cb0da5b7982bd22872D164C4C": {
      "Type": "AWS::Lambda::Function",
      "Properties": {
        "Code": {
          "S3Bucket": {
            "Ref": "AWS679f53fac002430cb0da5b7982bd2287CodeS3BucketF55839B6"
          },
          "S3Key": {
            "Fn::Join": [
              "",
              [
                {
                  "Fn::Select": [
                    0,
                    {
                      "Fn::Split": [
                        "||",
                        {
                          "Ref": "AWS679f53fac002430cb0da5b7982bd2287CodeS3VersionKey3C45B02F"
                        }
                      ]
                    }
                  ]
                },
                {
                  "Fn::Select": [
                    1,
                    {
                      "Fn::Split": [
                        "||",
                        {
                          "Ref": "AWS679f53fac002430cb0da5b7982bd2287CodeS3VersionKey3C45B02F"
                        }
                      ]
                    }
                  ]
                }
              ]
            ]
          }
        },
        "Handler": "index.handler",
        "Role": {
          "Fn::GetAtt": [
            "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2",
            "Arn"
          ]
        },
        "Runtime": "nodejs10.x"
      },
      "DependsOn": [
        "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleDefaultPolicyD28E1A5E",
        "AWS679f53fac002430cb0da5b7982bd2287ServiceRoleC1EA0FF2"
      ],
      "Metadata": {
        "aws:cdk:path": "UseCdkCustomResourcesStack/AWS679f53fac002430cb0da5b7982bd2287/Resource",
        "aws:asset:path": "asset.01d077466681f165ec462417f525188e4be806c35136c1a5e4bdcd3b71c12942",
        "aws:asset:property": "Code"
      }
    }
  },
  "Parameters": {
    "AWS679f53fac002430cb0da5b7982bd2287CodeS3BucketF55839B6": {
      "Type": "String",
      "Description": "S3 bucket for asset \"UseCdkCustomResourcesStack/AWS679f53fac002430cb0da5b7982bd2287/Code\""
    },
    "AWS679f53fac002430cb0da5b7982bd2287CodeS3VersionKey3C45B02F": {
      "Type": "String",
      "Description": "S3 key for asset version \"UseCdkCustomResourcesStack/AWS679f53fac002430cb0da5b7982bd2287/Code\""
    },
    "AWS679f53fac002430cb0da5b7982bd2287CodeArtifactHash49FACC2E": {
      "Type": "String",
      "Description": "Artifact hash for asset \"UseCdkCustomResourcesStack/AWS679f53fac002430cb0da5b7982bd2287/Code\""
    }
  },
  "Outputs": {
    "BcNetworkId": {
      "Description": "The message that came back from the Custom Resource",
      "Value": {
        "Fn::GetAtt": [
          "getNetworkTest425BEE14",
          "Network.Id"
        ]
      }
    }
  }
}
```

Lambda-Backedカスタムリソースで利用されてるLambda関数の実行ログをみてみます。

```console
> aws logs get-log-events \
  --log-group-name /aws/lambda/UseCdkCustomResourcesStac-AWS679f53fac002430cb0da5-2LAVFV782HQ4 \
  --log-stream-name '2019/07/25/[$LATEST]3954b08a05354dcf97a63939586c5575' \
  --query "events[*].message"

[
    "START RequestId: 44b992e7-fb71-4c19-bf44-af78d7f79520 Version: $LATEST\n",
    "2019-07-25T02:41:05.271Z\t44b992e7-fb71-4c19-bf44-af78d7f79520\tINFO\t{\"RequestType\":\"Create\",\"ServiceToken\":\"arn:aws:lambda:us-east-1:xxxxxxxxxxxx:function:UseCdkCustomResourcesStac-AWS679f53fac002430cb0da5-2LAVFV782HQ4\",\"ResponseURL\":\"https://cloudformation-custom-resource-response-useast1.s3.amazonaws.com/(略)\",\"StackId\":\"arn:aws:cloudformation:us-east-1:xxxxxxxxxxxx:stack/UseCdkCustomResourcesStack/7d97bb10-ae85-11e9-93b7-0a51b82e168a\",\"RequestId\":\"73e446ee-e9e6-4d7d-9e5f-fd400865dec6\",\"LogicalResourceId\":\"getNetworkTest425BEE14\",\"ResourceType\":\"Custom::AWS\",\"ResourceProperties\":{\"ServiceToken\":\"arn:aws:lambda:us-east-1:xxxxxxxxxxxx:function:UseCdkCustomResourcesStac-AWS679f53fac002430cb0da5-2LAVFV782HQ4\",\"Create\":{\"service\":\"ManagedBlockchain\",\"action\":\"getNetwork\",\"physicalResourceIdPath\":\"Network.Id\",\"parameters\":{\"NetworkId\":\"n-XXXXXXXXXXXXXXXXXXXXXXXXXX\"}}}}\n",
    "2019-07-25T02:41:05.272Z\t44b992e7-fb71-4c19-bf44-af78d7f79520\tINFO\tAWS SDK VERSION: 2.488.0\n",
    "2019-07-25T02:41:06.112Z\t44b992e7-fb71-4c19-bf44-af78d7f79520\tINFO\tResponding {\"Status\":\"SUCCESS\",\"Reason\":\"OK\",\"PhysicalResourceId\":\"n-XXXXXXXXXXXXXXXXXXXXXXXXXX\",\"StackId\":\"arn:aws:cloudformation:us-east-1:xxxxxxxxxxxx:stack/UseCdkCustomResourcesStack/7d97bb10-ae85-11e9-93b7-0a51b82e168a\",\"RequestId\":\"73e446ee-e9e6-4d7d-9e5f-fd400865dec6\",\"LogicalResourceId\":\"getNetworkTest425BEE14\",\"NoEcho\":false,\"Data\":{\"Network.Id\":\"n-XXXXXXXXXXXXXXXXXXXXXXXXXX\",\"Network.Name\":\"TestNetwork\",\"Network.Description\":\"TestNetworkDescription\",\"Network.Framework\":\"HYPERLEDGER_FABRIC\",\"Network.FrameworkVersion\":\"1.2\",\"Network.FrameworkAttributes.Fabric.OrderingServiceEndpoint\":\"orderer.n-XXXXXXXXXXXXXXXXXXXXXXXXXX.managedblockchain.us-east-1.amazonaws.com:30001\",\"Network.FrameworkAttributes.Fabric.Edition\":\"STARTER\",\"Network.VpcEndpointServiceName\":\"com.amazonaws.us-east-1.managedblockchain.n-XXXXXXXXXXXXXXXXXXXXXXXXXX\",\"Network.VotingPolicy.ApprovalThresholdPolicy.ThresholdPercentage\":50,\"Network.VotingPolicy.ApprovalThresholdPolicy.ProposalDurationInHours\":24,\"Network.VotingPolicy.ApprovalThresholdPolicy.ThresholdComparator\":\"GREATER_THAN\",\"Network.Status\":\"AVAILABLE\"}}\n",
    "END RequestId: 44b992e7-fb71-4c19-bf44-af78d7f79520\n",
    "REPORT RequestId: 44b992e7-fb71-4c19-bf44-af78d7f79520\tDuration: 1159.89 ms\tBilled Duration: 1200 ms \tMemory Size: 128 MB\tMax Memory Used: 38 MB\t\n"
]
```

ログに出力されてるCFnへ返すJSONをみてみると`Data` がAWS SDKの`getNetwork` で得られる情報となりフラット化されているのが確認できます。

```json:ログ一部抜粋
{
    "Status": "SUCCESS",
    "Reason": "OK",
    "PhysicalResourceId": "n-XXXXXXXXXXXXXXXXXXXXXXXXXX",
    "StackId": "arn:aws:cloudformation:us-east-1:xxxxxxxxxxxx:stack/UseCdkCustomResourcesStack/7d97bb10-ae85-11e9-93b7-0a51b82e168a",
    "RequestId": "73e446ee-e9e6-4d7d-9e5f-fd400865dec6",
    "LogicalResourceId": "getNetworkTest425BEE14",
    "NoEcho": false,
    "Data": {
        "Network.Id": "n-XXXXXXXXXXXXXXXXXXXXXXXXXX",
        "Network.Name": "TestNetwork",
        "Network.Description": "TestNetworkDescription",
        "Network.Framework": "HYPERLEDGER_FABRIC",
        "Network.FrameworkVersion": "1.2",
        "Network.FrameworkAttributes.Fabric.OrderingServiceEndpoint": "orderer.n-XXXXXXXXXXXXXXXXXXXXXXXXXX.managedblockchain.us-east-1.amazonaws.com:30001",
        "Network.FrameworkAttributes.Fabric.Edition": "STARTER",
        "Network.VpcEndpointServiceName": "com.amazonaws.us-east-1.managedblockchain.n-XXXXXXXXXXXXXXXXXXXXXXXXXX",
        "Network.VotingPolicy.ApprovalThresholdPolicy.ThresholdPercentage": 50,
        "Network.VotingPolicy.ApprovalThresholdPolicy.ProposalDurationInHours": 24,
        "Network.VotingPolicy.ApprovalThresholdPolicy.ThresholdComparator": "GREATER_THAN",
        "Network.Status": "AVAILABLE"
    }
}
```

動作確認ができてスタックが不要になったら`cdk destroy` コマンドでスタック削除ができます。

```console
> cdk destroy
Are you sure you want to delete: UseCdkCustomResourcesStack (y/n)? y
UseCdkCustomResourcesStack: destroying...

 ✅  UseCdkCustomResourcesStack: destroyed
```

`cdk bootstrap` で作成されたスタックを削除するAWS CDKのコマンドは見当たらないのでAWS CDKを今後利用しないのであれば、CFnで`CDKToolkit` スタックを削除すればよさそうです。



## まとめ

実際に動かして出力されるファイルやログを確認することで、AWS CDKがどのように機能しているのか知ることができました。
Infrastructure as Codeが主流になるとするならば、AWS CDKは抑えておいて損はないかなと思います。


## 参考


AWS クラウド開発キット (CDK) – TypeScript と Python 用がご利用可能に | Amazon Web Services ブログ
https://aws.amazon.com/jp/blogs/news/aws-cloud-development-kit-cdk-typescript-and-python-are-now-generally-available/

aws-cdk-examples/index.ts at master · aws-samples/aws-cdk-examples
https://github.com/aws-samples/aws-cdk-examples/blob/master/typescript/lambda-cron/index.ts

aws-cdk/packages/@aws-cdk/custom-resources at master · aws/aws-cdk
https://github.com/aws/aws-cdk/tree/master/packages/%40aws-cdk/custom-resources

aws-cdk/packages/@aws-cdk/aws-cloudformation at master · aws/aws-cdk
https://github.com/aws/aws-cdk/tree/master/packages/%40aws-cdk/aws-cloudformation

Amazon Managed BlockchainでHyperledger Fabricのブロックチェーンネットワークをさくっと構築するAWS CloudFormationのテンプレートを作ってみた(使い方編) - Qiita
https://qiita.com/kai_kou/items/dc7dbe947a7cc6d0a8db

Amazon Managed BlockchainでHyperledger Fabricのブロックチェーンネットワークをさくっと構築するAWS CloudFormationのテンプレートを作ってみた(解説編) - Qiita
https://qiita.com/kai_kou/items/ab51cb7d16c680f591d2

AWS CloudFormationのLambda-BackedカスタムリソースでネストされたJSONを返しても参照できない - Qiita
https://qiita.com/kai_kou/items/61a2b3c69ae2af4f2e40

AWS CloudFormationのLambda-BackedカスタムリソースでネストされてるっぽいJSONを返す方法 - Qiita
https://qiita.com/kai_kou/items/d030117c694531410203