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

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();

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関数がでてこない!

サンプル

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://cloudpack.media/48077

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

前提

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

AWS CDKのインストール

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

> 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を利用します。

> 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コマンドを実行すると以下のようにファイルが自動生成されました。
コマンド実行したディレクトリの名前が反映されました。

> 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をインストールして利用できるようにします。

> 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://cloudpack.media/48077

実装はcdk initコマンドで自動生成されたlib/use-cdk-custom-resources-stack.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()
    });
  }
}

ビルドしてデプロイする

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

> 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と同じ設定をみてくれます。

> 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バケットが作成されたのを確認できます。

> 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にスタック作成されてイベントログが出力されます。わかりやすくていいですね。

> 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特有のリソースがあったりします。

> 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関連のファイルが作成されています。

> 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関数のファイルになるみたいです。

> 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://cloudpack.media/48318

AWS CloudFormationのLambda-BackedカスタムリソースでネストされてるっぽいJSONを返す方法 – Qiita
https://cloudpack.media/48329

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

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

となります。

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関数の実行ログをみてみます。

> 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で得られる情報となりフラット化されているのが確認できます。

ログ一部抜粋

{
    "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コマンドでスタック削除ができます。

> 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://cloudpack.media/48077

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

AWS CloudFormationのLambda-BackedカスタムリソースでネストされたJSONを返しても参照できない – Qiita
https://cloudpack.media/48318

AWS CloudFormationのLambda-BackedカスタムリソースでネストされてるっぽいJSONを返す方法 – Qiita
https://cloudpack.media/48329

元記事はこちら

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