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
は以下のような構造をしています。
#!/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
をインポートしているが、このメインとなるスタックは以下で記述されます。
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
を以下のように変更します。
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
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 つにまとまり、利用するサービス毎のインストールが不要になりました。
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 も作成します。といってもメインスタックを更新するだけです。
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
というファイルを以下の内容で作成する。
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 関数の実装を記述します。
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
として、環境変数に渡してます。
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
を以下のように変更します。
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
を以下のように編集します。
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
を編集します。
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
以下のようにテーブルの内容がアップデートされていれば成功です。
お片付け
チュートリアルが完了したら cdk destroy
コマンドでスタックを削除しましょう。
$ cdk destroy
Are you sure you want to delete: cdk-workshop (y/n)? y
cdk-workshop: destroying...
...
✅ cdk-workshop: destroyed