CDK Construct Library を使ってみる

December 21, 2021

CDK Construct Library とは

CDK Construct Library (カスタム Construct) とは、CDK で作成した Construct を一般に公開し、ライブラリとして読み込めるようにしたもののことを言います。これにより、普段使用することの多い構成等を Construct としてインポートし使い回す、といったことが可能となります。
今回は cdk-dynamo-table-view を使って、HitCounter が DynamoDB テーブルに書きこんだ値を Viewer から見てみたいと思います。

cdk-dynamo-table-view について

利用する前に cdk-dynamo-table-view のソースを少し覗いて、具体的に何をやっているのか把握してみたいと思います。なおソースは ここ からアクセス可能です。

まず、src/index.ts ですが、これは ./table-viewer を呼んでるだけですね。

src/index.ts
export * from './table-viewer';

次に、src/table-viewer.ts ですが、こちらは Lambda 及び API Gateway のリソースが定義されており、シンプルな REST API を作るだけの構成になってます。なお、Construct の引数として table を受け取るようになっているので、利用するときはそこに DynamoDB テーブル名を渡してあげれば良さそうです。

src/table-viewer.ts
export class TableViewer extends Construct {

  public readonly endpoint: string;

  constructor(parent: Construct, id: string, props: TableViewerProps) {
    super(parent, id);

    const handler = new lambda.Function(this, 'Rendered', {
      code: lambda.Code.fromAsset(path.join(__dirname, '..', 'lambda')),
      runtime: lambda.Runtime.NODEJS_12_X,
      handler: 'index.handler',
      environment: {
        TABLE_NAME: props.table.tableName,
        TITLE: props.title || '',
        SORT_BY: props.sortBy || '',
      },
    });

    props.table.grantReadData(handler);

    const home = new apigw.LambdaRestApi(this, 'ViewerEndpoint', {
      handler,
      endpointConfiguration: props.endpointType
        ? { types: [props.endpointType] }
        : undefined,
    });
    this.endpoint = home.url;
  }
}

では、実際に Lambda 関数はどうなっているかというと、lambda/index.js は DynamoDB テーブルをスキャンして、それを元にレンダされた HTML を返しているだけです。レンダ処理自体は lambda/render.js 側で行ってますね。

lambda/index.js
const { DynamoDB } = require('aws-sdk');
const render = require('./render');

exports.handler = async function(event) {
  console.log('request:', JSON.stringify(event, undefined, 2));

  const dynamo = new DynamoDB();

  const resp = await dynamo.scan({
    TableName: process.env.TABLE_NAME,
  }).promise();

  const html = render({
    items: resp.Items,
    title: process.env.TITLE,
    sort: process.env.SORT_BY,
  });

  return {
    statusCode: 200,
    headers: {
      'Content-Type': 'text/html'
    },
    body: html
  };
};
lambda/render.js
module.exports = function(props) {
  const title = props.title;
  let sort = props.sort;

  let items = props.items || [];
  if (sort) {
    let desc = false;
    if (sort.startsWith('-')) {
      sort = sort.substr(1);
      desc = true;
    }

    items = items.sort((i1, i2) => {
      const result = compare(i1, i2);
      return desc ? -1 * result : result;
    });

    function compare(i1, i2) {
      const v1 = i1[sort];
      const v2 = i2[sort];
      if (v1 && v2 && v1.N && v2.N) {
        return v1.N - v2.N;
      }

      return getAttribute(i1, sort).localeCompare(getAttribute(i2, sort));
    }
  }

  const headers = collectHeaders()

  return `
  <!DOCTYPE html>
  <html>
  <head>
    ${ title ? `<title>${title}</title>` : '' }
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta http-equiv="refresh" content="10">
    <style>${stylesheet}</style>
  </head>
  <body>
    <div class="nice-table">
        ${ title ? `<div class="header">${title}</div>` : '' }
        <table cellspacing="0">
          ${ renderHeaderRow() }
          ${ items.map(item => renderItemRow(item)).join('\n') }
        </table>
    </div>
  </body>
  </html>
  `;

なので、まとめると cdk-dynamo-table-view「 DynamoDB テーブルをスキャンして得られた結果をもとに HTML を生成しそれをクライアントに返すような API Gateway + Lambda で構成された HTTPS エンドポイントを提供する」 というものになりそうです。

ライブラリのインストール

何をやってるのかがなんとなくわかったところで、早速つかってみましょう。まずはインストールです。

$ pip install cdk-dynamo-table-view
Collecting cdk-dynamo-table-view
  Downloading cdk_dynamo_table_view-0.2.26-py3-none-any.whl (28 kB)
Requirement already satisfied: jsii<2.0.0,>=1.47.0 in ./.venv/lib/python3.9/site-packages (from cdk-dynamo-table-view) (1.47.0)
Requirement already satisfied: aws-cdk-lib<3.0.0,>=2.0.0.rc28 in ./.venv/lib/python3.9/site-packages (from cdk-dynamo-table-view) (2.1.0)
Requirement already satisfied: constructs<11.0.0,>=10.0.5 in ./.venv/lib/python3.9/site-packages (from cdk-dynamo-table-view) (10.0.12)
Requirement already satisfied: publication>=0.0.3 in ./.venv/lib/python3.9/site-packages (from cdk-dynamo-table-view) (0.0.3)
Requirement already satisfied: cattrs~=1.8.0 in ./.venv/lib/python3.9/site-packages (from jsii<2.0.0,>=1.47.0->cdk-dynamo-table-view) (1.8.0)
Requirement already satisfied: typing-extensions<5.0,>=3.7 in ./.venv/lib/python3.9/site-packages (from jsii<2.0.0,>=1.47.0->cdk-dynamo-table-view) (4.0.1)
Requirement already satisfied: python-dateutil in ./.venv/lib/python3.9/site-packages (from jsii<2.0.0,>=1.47.0->cdk-dynamo-table-view) (2.8.2)
Requirement already satisfied: attrs~=21.2 in ./.venv/lib/python3.9/site-packages (from jsii<2.0.0,>=1.47.0->cdk-dynamo-table-view) (21.2.0)
Requirement already satisfied: six>=1.5 in ./.venv/lib/python3.9/site-packages (from python-dateutil->jsii<2.0.0,>=1.47.0->cdk-dynamo-table-view) (1.16.0)
Installing collected packages: cdk-dynamo-table-view
Successfully installed cdk-dynamo-table-view-0.2.26

Table Viewer をスタックに追加する

インストールが完了したらいよいよ cdk-dynamo-table-view をインポートして使用します。が、その前に cdk-dynamo-table-view は引数に table を指定してあげなきゃいけないので、HitCounter Construct でテーブルを外部から参照可能にしてあげないといけません。そのためにクラスのメンバとして DynamoDB テーブルを定義し直してあげます。具体的には以下のように cdk_workshop/hitcounter.py を変更します。

cdk_workshop/hitcounter.py
from constructs import Construct
from aws_cdk import (
  aws_lambda as _lambda,
  aws_dynamodb as ddb,
)

class HitCounter(Construct):

  @property
  def handler(self):
    return self._handler

  @property
  def table(self):
    return self._table

  def __init__(self, scope: Construct, id: str, downstream: _lambda.IFunction, read_capacity: int = 5, **kwargs):
    if read_capacity < 5 or read_capacity > 20:
      raise ValueError("read_capacity must be greater than 5 or less than 20")
    
    super().__init__(scope, id, **kwargs)

    self._table = ddb.Table(
      self, 'Hits',
      partition_key={'name': 'path', 'type': ddb.AttributeType.STRING},
      encryption=ddb.TableEncryption.AWS_MANAGED,
      read_capacity=read_capacity,
    )

    self._handler = _lambda.Function(
      self, 'HitCountHandler',
      runtime=_lambda.Runtime.PYTHON_3_9,
      handler='hitcount.handler',
      code=_lambda.Code.from_asset('lambda'),
      environment={
        'DOWNSTREAM_FUNCTION_NAME': downstream.function_name,
        'HITS_TABLE_NAME': self._table.table_name,
      }
    )

    self._table.grant_read_write_data(self.handler)
    downstream.grant_invoke(self.handler)

これでようやくメインのスタックで cdk-dynamo-table-view のインスタンスが作成可能になります。

cdk_workshop/cdk_workshop_stack.py
from constructs import Construct
from aws_cdk import (
    Stack,
    aws_lambda as _lambda,
    aws_apigateway as apigw,
)

from cdk_dynamo_table_view import TableViewer
from .hitcounter import HitCounter

class CdkWorkshopStack(Stack):

    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        my_lambda = _lambda.Function(
            self, 'HelloHandler',
            runtime=_lambda.Runtime.PYTHON_3_9,
            code=_lambda.Code.from_asset('lambda'),
            handler='hello.handler',
        )

        hello_with_counter = HitCounter(
            self, 'HelloHitCounter',
            downstream=my_lambda,
        )

        apigw.LambdaRestApi(
            self, 'Endpoint',
            handler=hello_with_counter._handler
        )

        TableViewer(
            self, 'ViewHitCounter',
            title='Hello Hits',
            table=hello_with_counter.table,
        )

デプロイ

ここまで来たらあとはデプロイするだけです。

$ cdk deploy

...

 ✅  cdk-workshop

Outputs:
cdk-workshop.Endpoint8024A810 = https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/prod/
cdk-workshop.ViewHitCounterViewerEndpointCA1B1E4B = https://yyyyyyyyyy.execute-api.us-west-2.amazonaws.com/prod/

デプロイが完了したら API を叩いてみます。

$ curl https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/prod/hoge
Hello, CDK! You have hit /hoge
$ curl https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/prod/foo
Hello, CDK! You have hit /foo
$ curl https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/prod/bar
Hello, CDK! You have hit /bar
$ curl https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/prod/bar
Hello, CDK! You have hit /bar
$ curl https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/prod/bar
Hello, CDK! You have hit /bar
$ curl https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/prod/bar
Hello, CDK! You have hit /bar

実際に TableViewer のエンドポイントにアクセスするとちゃんとカウントされていることがわかりますね!

cdk-construct-libraries-1.png


 © 2023, Dealing with Ambiguity