CDKv2 を触ってみた

December 12, 2021

CDKv2 について

以下のブログでアナウンスされているように、2021/12/02 に Cloud Development Kit Version 2 (a.k.a CDKv2) がリリースされました。

CDKv1 との主な変更点は以下の通りです:

  • パッケージが aws-cdk-lib に統一され、1 つ 1 つインストールする必要がなくなった
  • 明示的にインストールしない場合は非安定板のクラスが使用できなくなった
  • deprecated なプロパティやメソッドが削除された

今回は忘備録もかねて、Python 版のワークショップ を CDKv2 でおさらいしたいと思います。

CDKv2 のインストール

以下のコマンドで一発です。

$ npm i -g aws-cdk

added 13 packages, removed 13 packages, changed 185 packages, and audited 199 packages in 4s

found 0 vulnerabilities

ちゃんと 2.1.0 になってることが確認できますね。

$ cdk --version
2.1.0 (build f4f18b1)

プロジェクトの初期化

まずはプロジェクト用のディレクトリを作成して、そこに移動します。

$ mkdir cdk_workshop && cd cdk_workshop

次に cdk init でプロジェクトの初期化をします。

$ cdk init sample-app --language python
Applying project template sample-app for python

...

✅ All done!

Virtualenv の起動

次に、virtualenv を起動し、システム上とは隔離された環境を構築します。

$ source .venv/bin/activate

virtualenv の起動が終わったら必要な Python モジュールをインストールします。

$ pip install -r requirements.txt 

CDK プロジェクトの構造について

プロジェクトのディレクトリ構造は以下のようになっていると思います。

$ tree -L 2 -a
.
├── .venv
│   ├── bin
│   ├── include
│   ├── lib
│   └── pyvenv.cfg
├── README.md
├── app.py
├── cdk.json
├── cdk_workshop
│   ├── __init__.py
│   └── cdk_workshop_stack.py
├── requirements-dev.txt
├── requirements.txt
├── source.bat
└── tests
    ├── __init__.py
    └── unit

それぞれざっくり以下のような役割を持ちます。

ファイルもしくはディレクトリ 説明
.venv Python venv 用の設定ファイル群が格納されるディレクトリ
cdk_workshop/cdk_workshop_stack.py CDK アプリケーションの CDK Stuck Construct
tests/unit ユニットテスト格納用ディレクトリ
app.py アプリケーションのエントリポイント
cdk.json CDK 用の設定ファイル
requirements.txt pip によって利用される、依存関係を記述したファイル
setup.py Python パッケージがどう構築されるか、また依存関係を定義する

エントリポイントとなる app.py は以下のような構造をしています。

app.py
#!/usr/bin/env python3

import aws_cdk as cdk

from cdk_workshop.cdk_workshop_stack import CdkWorkshopStack


app = cdk.App()
CdkWorkshopStack(app, "cdk-workshop")

app.synth()

cdk_workshop.cdk_workshop_stack から CdkWorkshopStack をインポートしているが、このメインとなるスタックは以下で記述されます。

cdk_workshop.cdk_workshop_stack
from constructs import Construct
from aws_cdk import (
    Duration,
    Stack,
    aws_iam as iam,
    aws_sqs as sqs,
    aws_sns as sns,
    aws_sns_subscriptions as subs,
)


class CdkWorkshopStack(Stack):

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

        queue = sqs.Queue(
            self, "CdkWorkshopQueue",
            visibility_timeout=Duration.seconds(300),
        )

        topic = sns.Topic(
            self, "CdkWorkshopTopic"
        )

        topic.add_subscription(subs.SqsSubscription(queue))

本サンプルだと SNS トピックのサブスクリプションとして SQS キューを追加している感じですね。

cdk synth

cdk synth コマンドを用いることで、アプリケーション内のスタックに対応する CloudFormation テンプレートを出力できます。

$ cdk synth

出力としては以下のような CloudFormation テンプレートが得られます。

Resources:
  CdkWorkshopQueue50D9D426:
    Type: AWS::SQS::Queue
    Properties:
      VisibilityTimeout: 300
    UpdateReplacePolicy: Delete
    DeletionPolicy: Delete
    Metadata:
      aws:cdk:path: cdk-workshop/CdkWorkshopQueue/Resource
  CdkWorkshopQueuePolicyAF2494A5:
    Type: AWS::SQS::QueuePolicy
    Properties:
      PolicyDocument:
        Statement:
          - Action: sqs:SendMessage
            Condition:
              ArnEquals:
                aws:SourceArn:
                  Ref: CdkWorkshopTopicD368A42F
            Effect: Allow
            Principal:
              Service: sns.amazonaws.com
            Resource:
              Fn::GetAtt:
                - CdkWorkshopQueue50D9D426
                - Arn
        Version: "2012-10-17"
      Queues:
        - Ref: CdkWorkshopQueue50D9D426
    Metadata:
      aws:cdk:path: cdk-workshop/CdkWorkshopQueue/Policy/Resource
  CdkWorkshopQueuecdkworkshopCdkWorkshopTopicA7BCA841EC3B13D1:
    Type: AWS::SNS::Subscription
    Properties:
      Protocol: sqs
      TopicArn:
        Ref: CdkWorkshopTopicD368A42F
      Endpoint:
        Fn::GetAtt:
          - CdkWorkshopQueue50D9D426
          - Arn
    Metadata:
      aws:cdk:path: cdk-workshop/CdkWorkshopQueue/cdkworkshopCdkWorkshopTopicA7BCA841/Resource
  CdkWorkshopTopicD368A42F:
    Type: AWS::SNS::Topic
    Metadata:
      aws:cdk:path: cdk-workshop/CdkWorkshopTopic/Resource
  CDKMetadata:
    Type: AWS::CDK::Metadata
    Properties:
      Analytics: v2:deflate64:H4sIAAAAAAAA/1WNywqDMBBFv8V9nL6g0LU/YLX7ojGlU+1EMwlFQv69JoFCN3PvPRyYIxxgX3QfLuUwlhP24FvbyVFs6O55YfBXp5wS1YNySbfWE8r1B/MMgmnzW9ezNDhb1BSNv33TM8pIUwkh1kaxdkamH5WmAaMZRL3ap6bdCS5wLl6MWBpHFt8KmpxfPD+hTbwAAAA=
    Metadata:
      aws:cdk:path: cdk-workshop/CDKMetadata/Default
    Condition: CDKMetadataAvailable

...

cdk deploy

いよいよデプロイです。ただ、その前に初めてそのアカウント及びリージョンにリソースをデプロイする場合は cdk bootstrap コマンドを実行する必要があります。

$ cdk bootstrap
⏳  Bootstrapping environment aws://xxxxxxxxxxxx/us-west-2...
Trusted accounts for deployment: (none)
Trusted accounts for lookup: (none)

...

完了したら cdk deploy コマンドを実行します。

$ 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                                               │
├───┼─────────────────────────┼────────┼─────────────────┼───────────────────────────┼─────────────────────────────────────────────────────────┤
│ + │ ${CdkWorkshopQueue.Arn} │ Allow  │ sqs:SendMessage │ Service:sns.amazonaws.com │ "ArnEquals": {                                          │
│   │                         │        │                 │                           │   "aws:SourceArn": "${CdkWorkshopTopic}"                │
│   │                         │        │                 │                           │ }                                                       │
└───┴─────────────────────────┴────────┴─────────────────┴───────────────────────────┴─────────────────────────────────────────────────────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)

Do you wish to deploy these changes (y/n)? y

CloudFormation からもデプロイの様子が確認できます。

Hello, CDK!

それでは実際にリクエストを投げると { body: "hello" } を返してくるような簡単な Web API を作成します。

サンプルのお片付け

まずは最初にデプロイした SNS / SQS から構成されるサンプルのリソースを削除します。cdk_workshop/cdk_workshop_stack.py を以下のように変更します。

cdk_workshop/cdk_workshop_stack.py
from constructs import Construct
from aws_cdk import (
    Stack,
)


class CdkWorkshopStack(Stack):

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

cdk diff コマンドを利用することで現在デプロイされているリソースと CDK アプリケーションとの差分をみることができます。

$ cdk diff
Stack cdk-workshop
IAM Statement Changes
┌───┬─────────────────────────────────┬────────┬─────────────────┬───────────────────────────┬─────────────────────────────────────────────────────────────────┐
│   │ Resource                        │ Effect │ Action          │ Principal                 │ Condition                                                       │
├───┼─────────────────────────────────┼────────┼─────────────────┼───────────────────────────┼─────────────────────────────────────────────────────────────────┤
│ - │ ${CdkWorkshopQueue50D9D426.Arn} │ Allow  │ sqs:SendMessage │ Service:sns.amazonaws.com │ "ArnEquals": {                                                  │
│   │                                 │        │                 │                           │   "aws:SourceArn": "${CdkWorkshopTopicD368A42F}"                │
│   │                                 │        │                 │                           │ }                                                               │
└───┴─────────────────────────────────┴────────┴─────────────────┴───────────────────────────┴─────────────────────────────────────────────────────────────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)

Resources
[-] AWS::SQS::Queue CdkWorkshopQueue50D9D426 destroy
[-] AWS::SQS::QueuePolicy CdkWorkshopQueuePolicyAF2494A5 destroy
[-] AWS::SNS::Subscription CdkWorkshopQueuecdkworkshopCdkWorkshopTopicA7BCA841EC3B13D1 destroy
[-] AWS::SNS::Topic CdkWorkshopTopicD368A42F destroy

上記で削除されるリソースが確認できたら cdk deploy でお片付けをします。

$ cdk deploy

Hello Lambda

それではまずは Lambda 関数のコードを書きます。プロジェクトルートに lambda というディレクトリを作成し、さらに lambda/hello.py というファイルにハンドラーの処理を記述していきます。

$ mkdir lambda
lambda/hello.py
import json

def handler(event, context):
  print('request {}'.format(json.dumps(event)))
  return {
    'statusCode': 200,
    'headers': {
      'Content-Type': 'text/plain'
    },
    'body': 'Hello, CDK! You have hit {}\n'.format(event['path'])
  }

あとはメインスタックに Lambda 関数の記述を行うだけです。なお、ここで Lambda 用に Construct Library をインストールする必要はありません。CDKv2 では全てが 1 つにまとまり、利用するサービス毎のインストールが不要になりました。

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


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',
        )

cdk diff で確認すると Lambda 関数が追加されていることがわかります。

$ cdk diff
Stack cdk-workshop
IAM Statement Changes
┌───┬─────────────────────────────────┬────────┬────────────────┬──────────────────────────────┬───────────┐
│   │ Resource                        │ Effect │ Action         │ Principal                    │ Condition │
├───┼─────────────────────────────────┼────────┼────────────────┼──────────────────────────────┼───────────┤
│ + │ ${HelloHandler/ServiceRole.Arn} │ Allow  │ sts:AssumeRole │ Service:lambda.amazonaws.com │           │
└───┴─────────────────────────────────┴────────┴────────────────┴──────────────────────────────┴───────────┘
IAM Policy Changes
┌───┬─────────────────────────────┬────────────────────────────────────────────────────────────────────────────────┐
│   │ Resource                    │ Managed Policy ARN                                                             │
├───┼─────────────────────────────┼────────────────────────────────────────────────────────────────────────────────┤
│ + │ ${HelloHandler/ServiceRole} │ arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole │
└───┴─────────────────────────────┴────────────────────────────────────────────────────────────────────────────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)

Resources
[+] AWS::IAM::Role HelloHandler/ServiceRole HelloHandlerServiceRole11EF7C63 
[+] AWS::Lambda::Function HelloHandler HelloHandler2E4FBA4D 

あとはデプロイするだけです。

$ 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 │
├───┼─────────────────────────────────┼────────┼────────────────┼──────────────────────────────┼───────────┤
│ + │ ${HelloHandler/ServiceRole.Arn} │ Allow  │ sts:AssumeRole │ Service:lambda.amazonaws.com │           │
└───┴─────────────────────────────────┴────────┴────────────────┴──────────────────────────────┴───────────┘
IAM Policy Changes
┌───┬─────────────────────────────┬────────────────────────────────────────────────────────────────────────────────┐
│   │ Resource                    │ Managed Policy ARN                                                             │
├───┼─────────────────────────────┼────────────────────────────────────────────────────────────────────────────────┤
│ + │ ${HelloHandler/ServiceRole} │ arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole │
└───┴─────────────────────────────┴────────────────────────────────────────────────────────────────────────────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)

Do you wish to deploy these changes (y/n)? 

API Gateway

先に作成した Lambda 関数をバックエンドとして持つ API Gateway の API も作成します。といってもメインスタックを更新するだけです。

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


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',
        )

        apigw.LambdaRestApi(
            self, 'Endpoint',
            handler=my_lambda
        )

いつものごとく cdk diff で確認です。

$ cdk diff
Stack cdk-workshop
IAM Statement Changes
┌───┬────────────────────────────────┬────────┬───────────────────────┬───────────────────────────────────────────────────────────────┬────────────────────────────────────────────────────────────────┐
│   │ Resource                       │ Effect │ Action                │ Principal                                                     │ Condition                                                      │
├───┼────────────────────────────────┼────────┼───────────────────────┼───────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────┤
│ + │ ${Endpoint/CloudWatchRole.Arn} │ Allow  │ sts:AssumeRole        │ Service:apigateway.amazonaws.com                              │                                                                │
├───┼────────────────────────────────┼────────┼───────────────────────┼───────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────┤
│ + │ ${HelloHandler.Arn}            │ Allow  │ lambda:InvokeFunction │ Service:apigateway.amazonaws.com                              │ "ArnLike": {                                                   │
│   │                                │        │                       │                                                               │   "AWS:SourceArn": "arn:${AWS::Partition}:execute-api:${AWS::R │
│   │                                │        │                       │                                                               │ egion}:${AWS::AccountId}:${EndpointEEF1FD8F}/${Endpoint/Deploy │
│   │                                │        │                       │                                                               │ mentStage.prod}/*/*"                                           │
│   │                                │        │                       │                                                               │ }                                                              │
│ + │ ${HelloHandler.Arn}            │ Allow  │ lambda:InvokeFunction │ Service:apigateway.amazonaws.com                              │ "ArnLike": {                                                   │
│   │                                │        │                       │                                                               │   "AWS:SourceArn": "arn:${AWS::Partition}:execute-api:${AWS::R │
│   │                                │        │                       │                                                               │ egion}:${AWS::AccountId}:${EndpointEEF1FD8F}/test-invoke-stage │
│   │                                │        │                       │                                                               │ /*/*"                                                          │
│   │                                │        │                       │                                                               │ }                                                              │
│ + │ ${HelloHandler.Arn}            │ Allow  │ lambda:InvokeFunction │ Service:apigateway.amazonaws.com                              │ "ArnLike": {                                                   │
│   │                                │        │                       │                                                               │   "AWS:SourceArn": "arn:${AWS::Partition}:execute-api:${AWS::R │
│   │                                │        │                       │                                                               │ egion}:${AWS::AccountId}:${EndpointEEF1FD8F}/${Endpoint/Deploy │
│   │                                │        │                       │                                                               │ mentStage.prod}/*/"                                            │
│   │                                │        │                       │                                                               │ }                                                              │
│ + │ ${HelloHandler.Arn}            │ Allow  │ lambda:InvokeFunction │ Service:apigateway.amazonaws.com                              │ "ArnLike": {                                                   │
│   │                                │        │                       │                                                               │   "AWS:SourceArn": "arn:${AWS::Partition}:execute-api:${AWS::R │
│   │                                │        │                       │                                                               │ egion}:${AWS::AccountId}:${EndpointEEF1FD8F}/test-invoke-stage │
│   │                                │        │                       │                                                               │ /*/"                                                           │
│   │                                │        │                       │                                                               │ }                                                              │
└───┴────────────────────────────────┴────────┴───────────────────────┴───────────────────────────────────────────────────────────────┴────────────────────────────────────────────────────────────────┘
IAM Policy Changes
┌───┬────────────────────────────┬─────────────────────────────────────────────────────────────────────────────────────────┐
│   │ Resource                   │ Managed Policy ARN                                                                      │
├───┼────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────┤
│ + │ ${Endpoint/CloudWatchRole} │ arn:${AWS::Partition}:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs │
└───┴────────────────────────────┴─────────────────────────────────────────────────────────────────────────────────────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)

Resources
[+] AWS::ApiGateway::RestApi Endpoint EndpointEEF1FD8F 
[+] AWS::IAM::Role Endpoint/CloudWatchRole EndpointCloudWatchRoleC3C64E0F 
[+] AWS::ApiGateway::Account Endpoint/Account EndpointAccountB8304247 
[+] AWS::ApiGateway::Deployment Endpoint/Deployment EndpointDeployment318525DA808eeef0ae3eb0745f2faa622f752de3 
[+] AWS::ApiGateway::Stage Endpoint/DeploymentStage.prod EndpointDeploymentStageprodB78BEEA0 
[+] AWS::ApiGateway::Resource Endpoint/Default/{proxy+} Endpointproxy39E2174E 
[+] AWS::Lambda::Permission Endpoint/Default/{proxy+}/ANY/ApiPermission.cdkworkshopEndpoint424A4D39.ANY..{proxy+} EndpointproxyANYApiPermissioncdkworkshopEndpoint424A4D39ANYproxyED9F30E3 
[+] AWS::Lambda::Permission Endpoint/Default/{proxy+}/ANY/ApiPermission.Test.cdkworkshopEndpoint424A4D39.ANY..{proxy+} EndpointproxyANYApiPermissionTestcdkworkshopEndpoint424A4D39ANYproxy4FB922C2 
[+] AWS::ApiGateway::Method Endpoint/Default/{proxy+}/ANY EndpointproxyANYC09721C5 
[+] AWS::Lambda::Permission Endpoint/Default/ANY/ApiPermission.cdkworkshopEndpoint424A4D39.ANY.. EndpointANYApiPermissioncdkworkshopEndpoint424A4D39ANYC722176D 
[+] AWS::Lambda::Permission Endpoint/Default/ANY/ApiPermission.Test.cdkworkshopEndpoint424A4D39.ANY.. EndpointANYApiPermissionTestcdkworkshopEndpoint424A4D39ANYB0C9FB02 
[+] AWS::ApiGateway::Method Endpoint/Default/ANY EndpointANY485C938B 

Outputs
[+] Output Endpoint/Endpoint Endpoint8024A810: {"Value":{"Fn::Join":["",["https://",{"Ref":"EndpointEEF1FD8F"},".execute-api.",{"Ref":"AWS::Region"},".",{"Ref":"AWS::URLSuffix"},"/",{"Ref":"EndpointDeploymentStageprodB78BEEA0"},"/"]]}}

あとは cdk deploy しましょう。

$ cdk deploy

Outputs: の部分で作成した API のエンドポイントが出力されます。

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

curl でリクエストを投げて動作確認しましょう。

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

Construct を書く

それでは次は自分で Construct を定義し、それをメインスタックで呼び出す方法を試してみます。チュートリアルの内容としては HitCounter という、他の任意の Lambda 関数にアタッチすることでそのパスに対するリクエスト数をカウントする Construct を作成します。

HitCounter API の定義

cdk_workshop ディレクトリ配下に hitcounter.py というファイルを以下の内容で作成する。

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

class HitCounter(Construct):

  def __init__(self, scope: Construct, id: str, downstream: _lambda.IFunction, **kwargs):
    super().__init__(scope, id, **kwargs)

新たに downstream という引数を用意し、そこにバックエンドの Lambda 関数を指定できるようにしています。

HitCounter 用の Lambda Handler

lambda/hitcount.py に Lambda 関数の実装を記述します。

lambda/hitcount.py
import boto3
import json
import os

ddb = boto3.resource('dynamodb')
table = ddb.Table(os.environ['HITS_TABLE_NAME'])
_lambda = boto3.client('lambda')


def handler(event, context):
  print('request: {}'.format(json.dumps(event)))
  table.update_item(
    Key={'path': event['path']},
    UpdateExpression='Add hits :incr',
    ExpressionAttributeValues={':incr': 1}
  )

  resp = _lambda.invoke(
    FunctionName=os.environ['DOWNSTREAM_FUNCTION_NAME'],
    Payload=json.dumps(event)
  )

  body = resp['Payload'].read()

  print('downstream response: {}'.format(body))
  return json.loads(body)

HitCounter Construct にリソースを追加する

Lambda 関数の実装ができたので、cdk_workshop/hitcounter.py に DynamoDB テーブル及び Lambda 関数をリソースとして追加します。なお、downstream で受け取った Lambda 関数名を DOWNSTREAM_FUNCTION_NAME として、また DynamoDB テーブルの名前を HITS_TABLE_NAME として、環境変数に渡してます。

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

  def __init__(self, scope: Construct, id: str, downstream: _lambda.IFunction, **kwargs):
    super().__init__(scope, id, **kwargs)

    table = ddb.Table(
      self, 'Hits',
      partition_key={'name': 'path', 'type': ddb.AttributeType.STRING}
    )

    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': table.table_name,
      }
    )

HitCounter を利用する

あとは作成した HitCounter をメインのスタックで呼ぶだけです。cdk_workshop_stack.py を以下のように変更します。

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

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
        )

ここまでできたら一旦 cdk deploy でデプロイします。

$ cdk deploy

デプロイが完了したら curl でテストします。

$ curl -i https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/prod/
HTTP/2 502 

...

{"message": "Internal server error"}

どうやら 502 が返ってきてるようです。トラブルシューティングが必要です。

CloudWatch Logs を確認する

HelloHitCounter 用 Lambda 関数の CloudWatch Logs を確認すると以下のようなエラーが出力されています。

[ERROR] ClientError: An error occurred (AccessDeniedException) when calling the UpdateItem operation: User: arn:aws:sts::xxxxxxxxxxxx:assumed-role/cdk-workshop-HelloHitCounterHitCountHandlerService-KRVRR8ZRWNDO/cdk-workshop-HelloHitCounterHitCountHandler2475EAC-teA0x5lP0SRH is not authorized to perform: dynamodb:UpdateItem on resource: arn:aws:dynamodb:us-west-2:xxxxxxxxxxxx:table/cdk-workshop-HelloHitCounterHits7AAEBF80-1ZS6Y7PVGLJ
Traceback (most recent call last):
  File "/var/task/hitcount.py", line 13, in handler
    table.update_item(
  File "/var/runtime/boto3/resources/factory.py", line 520, in do_action
    response = action(self, *args, **kwargs)
  File "/var/runtime/boto3/resources/action.py", line 83, in __call__
    response = getattr(parent.meta.client, operation_name)(*args, **params)
  File "/var/runtime/botocore/client.py", line 386, in _api_call
    return self._make_api_call(operation_name, kwargs)
  File "/var/runtime/botocore/client.py", line 705, in _make_api_call
    raise error_class(parsed_response, operation_name)

どうやら Lambda 関数が DynamoDB テーブルに対して UpdateItem を行う権限が無いようですね。Lambda 関数に渡す IAM ロールに明示的に権限を付与した覚えはないので当然です。

Lambda 関数に DynamoDB テーブルに対する Read/Write 権限を付与する

Lambda 関数に DynamoDB テーブルに対する Read/Write 権限を付与するために、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

  def __init__(self, scope: Construct, id: str, downstream: _lambda.IFunction, **kwargs):
    super().__init__(scope, id, **kwargs)

    table = ddb.Table(
      self, 'Hits',
      partition_key={'name': 'path', 'type': ddb.AttributeType.STRING}
    )

    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': table.table_name,
      }
    )

    table.grant_read_write_data(self.handler)

再度 cdk deploy でデプロイします。

$ cdk deploy

テストしてみましょう。

$ curl -i https://z0gjt2w0tl.execute-api.us-west-2.amazonaws.com/prod/
HTTP/2 502 

...

{"message": "Internal server error"}

まだダメみたいですね。再度 CloudWatch Logs を見てみましょう。

[ERROR] ClientError: An error occurred (AccessDeniedException) when calling the Invoke operation: User: arn:aws:sts::xxxxxxxxxxxx:assumed-role/cdk-workshop-HelloHitCounterHitCountHandlerService-KRVRR8ZRWNDO/cdk-workshop-HelloHitCounterHitCountHandler2475EAC-teA0x5lP0SRH is not authorized to perform: lambda:InvokeFunction on resource: arn:aws:lambda:us-west-2:xxxxxxxxxxxx:function:cdk-workshop-HelloHandler2E4FBA4D-sjCD7laFKz2l because no identity-based policy allows the lambda:InvokeFunction action
Traceback (most recent call last):
  File "/var/task/hitcount.py", line 19, in handler
    resp = _lambda.invoke(
  File "/var/runtime/botocore/client.py", line 386, in _api_call
    return self._make_api_call(operation_name, kwargs)
  File "/var/runtime/botocore/client.py", line 705, in _make_api_call
    raise error_class(parsed_response, operation_name)

どうやら downstream の Lambda 関数を Invoke する権限が無いようです。これも当然ですね。
再度 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

  def __init__(self, scope: Construct, id: str, downstream: _lambda.IFunction, **kwargs):
    super().__init__(scope, id, **kwargs)

    table = ddb.Table(
      self, 'Hits',
      partition_key={'name': 'path', 'type': ddb.AttributeType.STRING}
    )

    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': table.table_name,
      }
    )

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

cdk diff で違いを見てみます。

$ cdk diff
Stack cdk-workshop
IAM Statement Changes
┌───┬─────────────────────┬────────┬───────────────────────┬────────────────────────────────────────────────────┬───────────┐
│   │ Resource            │ Effect │ Action                │ Principal                                          │ Condition │
├───┼─────────────────────┼────────┼───────────────────────┼────────────────────────────────────────────────────┼───────────┤
│ + │ ${HelloHandler.Arn} │ Allow  │ lambda:InvokeFunction │ AWS:${HelloHitCounter/HitCountHandler/ServiceRole} │           │
└───┴─────────────────────┴────────┴───────────────────────┴────────────────────────────────────────────────────┴───────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)

Resources
[~] AWS::IAM::Policy HelloHitCounter/HitCountHandler/ServiceRole/DefaultPolicy HelloHitCounterHitCountHandlerServiceRoleDefaultPolicy0295D032 
 └─ [~] PolicyDocument
     └─ [~] .Statement:
         └─ @@ -25,5 +25,15 @@
            [ ]         "Ref": "AWS::NoValue"
            [ ]       }
            [ ]     ]
            [+]   },
            [+]   {
            [+]     "Action": "lambda:InvokeFunction",
            [+]     "Effect": "Allow",
            [+]     "Resource": {
            [+]       "Fn::GetAtt": [
            [+]         "HelloHandler2E4FBA4D",
            [+]         "Arn"
            [+]       ]
            [+]     }
            [ ]   }
            [ ] ]

lambda:InvokeFunction が付与されていることがわかりますね。デプロイしてみましょう。

$ cdk deploy

curl でテストします。

$ curl -i https://z0gjt2w0tl.execute-api.us-west-2.amazonaws.com/prod/
HTTP/2 200 

...

Hello, CDK! You have hit /

今度は大丈夫そうです。

HitCounter をテストする

実際に適当なパスにいくつかリクエストを投げてみて、DynamoDB テーブルにカウントがアップデートされているか確認します。

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

以下のようにテーブルの内容がアップデートされていれば成功です。

cdkv2_1

お片付け

チュートリアルが完了したら cdk destroy コマンドでスタックを削除しましょう。

$ cdk destroy
Are you sure you want to delete: cdk-workshop (y/n)? y
cdk-workshop: destroying...

...

 ✅  cdk-workshop: destroyed

 © 2023, Dealing with Ambiguity