Walkthrough - Part 2 - AWS Solutions Constructs

Walkthrough - Part 2

Note

AWS Solutions Constructs is supported on AWS CDK versions ≥ 1.46.0.

This tutorial walks you through how to modify the "Hello Constructs" app created in part 1. Our modification will add a site hit counter using the AWS Lambda to DynamoDB pattern from AWS Solutions Constructs. Modifying the Hello Constructs app will result in the following solution:

Hit Counter Lambda code

Let's get started by writing the code for the Hit Counter AWS Lambda function. This function will:

  • increment a counter related to the API path in a Amazon DynamoDB table,

  • invoke the downstream Hello AWS Lambda function,

  • and return the response to end user.

TypeScript

Add a file called lambda/hitcounter.js with the following contents:

const { DynamoDB, Lambda } = require('aws-sdk'); exports.handler = async function(event) { console.log("request:", JSON.stringify(event, undefined, 2)); // create AWS SDK clients const dynamo = new DynamoDB(); const lambda = new Lambda(); // update dynamo entry for "path" with hits++ await dynamo.updateItem({ TableName: process.env.DDB_TABLE_NAME, Key: { path: { S: event.path } }, UpdateExpression: 'ADD hits :incr', ExpressionAttributeValues: { ':incr': { N: '1' } } }).promise(); // call downstream function and capture response const resp = await lambda.invoke({ FunctionName: process.env.DOWNSTREAM_FUNCTION_NAME, Payload: JSON.stringify(event) }).promise(); console.log('downstream response:', JSON.stringify(resp, undefined, 2)); // return response back to upstream caller return JSON.parse(resp.Payload); };
Python

Add a file called lambda/hitcounter.py with the following contents:

import json import os import boto3 ddb = boto3.resource('dynamodb') table = ddb.Table(os.environ['DDB_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)

Install the new dependencies

Note

Remember to substitute the correct, matching version to be used for both AWS Solutions Constructs and the AWS CDK into the VERSION_NUMBER placeholder fields for each command. This should be identical to the version number used for dependencies in the first part of this walkthrough. Mismatching versions between packages may cause errors.

As usual, we first need to install the dependencies we need for our solution update. First, we need to install the DynamoDB construct library:

TypeScript
npm install -s @aws-cdk/aws-dynamodb@VERSION_NUMBER
Python
pip install aws_cdk.aws_dynamodb==VERSION_NUMBER

Finally, install the AWS Solutions Constructs aws-lambda-dynamodb module and all its dependencies into our project:

TypeScript
npm install -s @aws-solutions-constructs/aws-lambda-dynamodb@VERSION_NUMBER
Python
pip install aws_solutions_constructs.aws_lambda_dynamodb==VERSION_NUMBER

Define the resources

Now, let's update our stack code to accomodate our new architecture.

First, we are going to import our new dependencies and move the "Hello" function outside of the aws-apigateway-lambda pattern we created in part 1.

TypeScript

Edit the file lib/hello-constructs.ts with the following:

import * as cdk from '@aws-cdk/core'; import * as lambda from '@aws-cdk/aws-lambda'; import * as api from '@aws-cdk/aws-apigateway'; import * as dynamodb from '@aws-cdk/aws-dynamodb'; import { ApiGatewayToLambda, ApiGatewayToLambdaProps } from '@aws-solutions-constructs/aws-apigateway-lambda'; import { LambdaToDynamoDB, LambdaToDynamoDBProps } from '@aws-solutions-constructs/aws-lambda-dynamodb'; export class HelloConstructsStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); // The code that defines your stack goes here const helloFunc = new lambda.Function(this, 'HelloHandler', { runtime: lambda.Runtime.NODEJS_12_X, code: lambda.Code.fromAsset('lambda'), handler: 'hello.handler' }); const api_lambda_props: ApiGatewayToLambdaProps = { lambdaFunctionProps: { code: lambda.Code.fromAsset('lambda'), runtime: lambda.Runtime.NODEJS_12_X, handler: 'hello.handler' }, apiGatewayProps: { defaultMethodOptions: { authorizationType: api.AuthorizationType.NONE } } }; new ApiGatewayToLambda(this, 'ApiGatewayToLambda', api_lambda_props); } }
Python

Edit the file hello_constructs/hello_constructs_stack.py with the following:

from aws_cdk import ( aws_lambda as _lambda, aws_apigateway as apigw, aws_dynamodb as ddb, core, ) from aws_solutions_constructs import ( aws_apigateway_lambda as apigw_lambda, aws_lambda_dynamodb as lambda_ddb ) class HelloConstructsStack(core.Stack): def __init__(self, scope: core.Construct, id: str, **kwargs) -> None: super().__init__(scope, id, **kwargs) # The code that defines your stack goes here self._handler = _lambda.Function( self, 'HelloHandler', runtime=_lambda.Runtime.PYTHON_3_7, handler='hello.handler', code=_lambda.Code.asset('lambda'), ) apigw_lambda.ApiGatewayToLambda( self, 'ApiGatewayToLambda', deploy_lambda=True, lambda_function_props=_lambda.FunctionProps( runtime=_lambda.Runtime.PYTHON_3_7, code=_lambda.Code.asset('lambda'), handler='hello.handler', ), api_gateway_props=apigw.RestApiProps( default_method_options=apigw.MethodOptions( authorization_type=apigw.AuthorizationType.NONE ) ) )

Next, we are going to add the aws-lambda-dynamodb pattern to build out the hit counter service for our updated architecture.

The next update below defines the properties for the aws-lambda-dynamodb pattern by defining the AWS Lambda function with the Hit Counter handler. Additionally, the Amazon DynamoDB table is defined with a name of Hits and a partition key of path.

TypeScript

Edit the file lib/hello-constructs.ts with the following:

import * as cdk from '@aws-cdk/core'; import * as lambda from '@aws-cdk/aws-lambda'; import * as api from '@aws-cdk/aws-apigateway'; import * as dynamodb from '@aws-cdk/aws-dynamodb'; import { ApiGatewayToLambda, ApiGatewayToLambdaProps } from '@aws-solutions-constructs/aws-apigateway-lambda'; import { LambdaToDynamoDB, LambdaToDynamoDBProps } from '@aws-solutions-constructs/aws-lambda-dynamodb'; export class HelloConstructsStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); // The code that defines your stack goes here const helloFunc = new lambda.Function(this, 'HelloHandler', { runtime: lambda.Runtime.NODEJS_12_X, code: lambda.Code.fromAsset('lambda'), handler: 'hello.handler' }); // hit counter, aws-lambda-dynamodb pattern const lambda_ddb_props: LambdaToDynamoDBProps = { lambdaFunctionProps: { code: lambda.Code.asset(`lambda`), runtime: lambda.Runtime.NODEJS_12_X, handler: 'hitcounter.handler', environment: { DOWNSTREAM_FUNCTION_NAME: helloFunc.functionName } }, dynamoTableProps: { tableName: 'Hits', partitionKey: { name: 'path', type: dynamodb.AttributeType.STRING } } }; const hitcounter = new LambdaToDynamoDB(this, 'LambdaToDynamoDB', lambda_ddb_props); const api_lambda_props: ApiGatewayToLambdaProps = { lambdaFunctionProps: { code: lambda.Code.fromAsset('lambda'), runtime: lambda.Runtime.NODEJS_12_X, handler: 'hello.handler' }, apiGatewayProps: { defaultMethodOptions: { authorizationType: api.AuthorizationType.NONE } } }; new ApiGatewayToLambda(this, 'ApiGatewayToLambda', api_lambda_props); } }
Python

Edit the file hello_constructs/hello_constructs_stack.py with the following:

from aws_cdk import ( aws_lambda as _lambda, aws_apigateway as apigw, aws_dynamodb as ddb, core, ) from aws_solutions_constructs import ( aws_apigateway_lambda as apigw_lambda, aws_lambda_dynamodb as lambda_ddb ) class HelloConstructsStack(core.Stack): def __init__(self, scope: core.Construct, id: str, **kwargs) -> None: super().__init__(scope, id, **kwargs) # The code that defines your stack goes here self.hello_func = _lambda.Function( self, 'HelloHandler', runtime=_lambda.Runtime.PYTHON_3_7, handler='hello.handler', code=_lambda.Code.asset('lambda'), ) # hit counter, aws-lambda-dynamodb pattern self.hit_counter = lambda_ddb.LambdaToDynamoDB( self, 'LambdaToDynamoDB', deploy_lambda=True, lambda_function_props=_lambda.FunctionProps( runtime=_lambda.Runtime.PYTHON_3_7, code=_lambda.Code.asset('lambda'), handler='hitcounter.handler', environment={ 'DOWNSTREAM_FUNCTION_NAME': self.hello_func.function_name } ), dynamo_table_props=ddb.TableProps( table_name='Hits', partition_key={ 'name': 'path', 'type': ddb.AttributeType.STRING } ) ) apigw_lambda.ApiGatewayToLambda( self, 'ApiGatewayToLambda', deploy_lambda=True, lambda_function_props=_lambda.FunctionProps( runtime=_lambda.Runtime.PYTHON_3_7, code=_lambda.Code.asset('lambda'), handler='hello.handler', ), api_gateway_props=apigw.RestApiProps( default_method_options=apigw.MethodOptions( authorization_type=apigw.AuthorizationType.NONE ) ) )

Next, we need to grant the Hit Counter function created from the aws-lambda-dynamodb pattern added above permission to invoke our Hello function.

TypeScript

Edit the file lib/hello-constructs.ts with the following:

import * as cdk from '@aws-cdk/core'; import * as lambda from '@aws-cdk/aws-lambda'; import * as api from '@aws-cdk/aws-apigateway'; import * as dynamodb from '@aws-cdk/aws-dynamodb'; import { ApiGatewayToLambda, ApiGatewayToLambdaProps } from '@aws-solutions-constructs/aws-apigateway-lambda'; import { LambdaToDynamoDB, LambdaToDynamoDBProps } from '@aws-solutions-constructs/aws-lambda-dynamodb'; export class HelloConstructsStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); // The code that defines your stack goes here // hello function responding to http requests const helloFunc = new lambda.Function(this, 'HelloHandler', { runtime: lambda.Runtime.NODEJS_12_X, code: lambda.Code.fromAsset('lambda'), handler: 'hello.handler' }); // hit counter, aws-lambda-dynamodb pattern const lambda_ddb_props: LambdaToDynamoDBProps = { lambdaFunctionProps: { code: lambda.Code.asset(`lambda`), runtime: lambda.Runtime.NODEJS_12_X, handler: 'hitcounter.handler', environment: { DOWNSTREAM_FUNCTION_NAME: helloFunc.functionName } }, dynamoTableProps: { tableName: 'Hits', partitionKey: { name: 'path', type: dynamodb.AttributeType.STRING } } }; const hitcounter = new LambdaToDynamoDB(this, 'LambdaToDynamoDB', lambda_ddb_props); // grant the hitcounter lambda role invoke permissions to the hello function helloFunc.grantInvoke(hitcounter.lambdaFunction); const api_lambda_props: ApiGatewayToLambdaProps = { lambdaFunctionProps: { code: lambda.Code.fromAsset('lambda'), runtime: lambda.Runtime.NODEJS_12_X, handler: 'hello.handler' }, apiGatewayProps: { defaultMethodOptions: { authorizationType: api.AuthorizationType.NONE } } }; new ApiGatewayToLambda(this, 'ApiGatewayToLambda', api_lambda_props); } }
Python

Edit the file hello_constructs/hello_constructs_stack.py with the following:

from aws_cdk import ( aws_lambda as _lambda, aws_apigateway as apigw, aws_dynamodb as ddb, core, ) from aws_solutions_constructs import ( aws_apigateway_lambda as apigw_lambda, aws_lambda_dynamodb as lambda_ddb ) class HelloConstructsStack(core.Stack): def __init__(self, scope: core.Construct, id: str, **kwargs) -> None: super().__init__(scope, id, **kwargs) # The code that defines your stack goes here self.hello_func = _lambda.Function( self, 'HelloHandler', runtime=_lambda.Runtime.PYTHON_3_7, handler='hello.handler', code=_lambda.Code.asset('lambda'), ) # hit counter, aws-lambda-dynamodb pattern self.hit_counter = lambda_ddb.LambdaToDynamoDB( self, 'LambdaToDynamoDB', deploy_lambda=True, lambda_function_props=_lambda.FunctionProps( runtime=_lambda.Runtime.PYTHON_3_7, code=_lambda.Code.asset('lambda'), handler='hitcounter.handler', environment={ 'DOWNSTREAM_FUNCTION_NAME': self.hello_func.function_name } ), dynamo_table_props=ddb.TableProps( table_name='Hits', partition_key={ 'name': 'path', 'type': ddb.AttributeType.STRING } ) ) # grant the hitcounter lambda role invoke permissions to the hello function self.hello_func.grant_invoke(self.hit_counter.lambda_function) apigw_lambda.ApiGatewayToLambda( self, 'ApiGatewayToLambda', deploy_lambda=True, lambda_function_props=_lambda.FunctionProps( runtime=_lambda.Runtime.PYTHON_3_7, code=_lambda.Code.asset('lambda'), handler='hello.handler', ), api_gateway_props=apigw.RestApiProps( default_method_options=apigw.MethodOptions( authorization_type=apigw.AuthorizationType.NONE ) ) )

Finally, we need to update our original aws-apigateway-lambda pattern to utilize our new Hit Counter function that was provisioned with the aws-lambda-dynamodb pattern above.

TypeScript

Edit the file lib/hello-constructs.ts with the following:

import * as cdk from '@aws-cdk/core'; import * as lambda from '@aws-cdk/aws-lambda'; import * as api from '@aws-cdk/aws-apigateway'; import * as dynamodb from '@aws-cdk/aws-dynamodb'; import { ApiGatewayToLambda, ApiGatewayToLambdaProps } from '@aws-solutions-constructs/aws-apigateway-lambda'; import { LambdaToDynamoDB, LambdaToDynamoDBProps } from '@aws-solutions-constructs/aws-lambda-dynamodb'; export class HelloConstructsStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); // The code that defines your stack goes here // hello function responding to http requests const helloFunc = new lambda.Function(this, 'HelloHandler', { runtime: lambda.Runtime.NODEJS_12_X, code: lambda.Code.fromAsset('lambda'), handler: 'hello.handler' }); // hit counter, aws-lambda-dynamodb pattern const lambda_ddb_props: LambdaToDynamoDBProps = { lambdaFunctionProps: { code: lambda.Code.asset(`lambda`), runtime: lambda.Runtime.NODEJS_12_X, handler: 'hitcounter.handler', environment: { DOWNSTREAM_FUNCTION_NAME: helloFunc.functionName } }, dynamoTableProps: { tableName: 'Hits', partitionKey: { name: 'path', type: dynamodb.AttributeType.STRING } } }; const hitcounter = new LambdaToDynamoDB(this, 'LambdaToDynamoDB', lambda_ddb_props); // grant the hitcounter lambda role invoke permissions to the hello function helloFunc.grantInvoke(hitcounter.lambdaFunction); const api_lambda_props: ApiGatewayToLambdaProps = { existingLambdaObj: hitcounter.lambdaFunction, apiGatewayProps: { defaultMethodOptions: { authorizationType: api.AuthorizationType.NONE } } }; new ApiGatewayToLambda(this, 'ApiGatewayToLambda', api_lambda_props); } }
Python

Edit the file hello_constructs/hello_constructs_stack.py with the following:

from aws_cdk import ( aws_lambda as _lambda, aws_apigateway as apigw, aws_dynamodb as ddb, core, ) from aws_solutions_constructs import ( aws_apigateway_lambda as apigw_lambda, aws_lambda_dynamodb as lambda_ddb ) class HelloConstructsStack(core.Stack): def __init__(self, scope: core.Construct, id: str, **kwargs) -> None: super().__init__(scope, id, **kwargs) # The code that defines your stack goes here self.hello_func = _lambda.Function( self, 'HelloHandler', runtime=_lambda.Runtime.PYTHON_3_7, handler='hello.handler', code=_lambda.Code.asset('lambda'), ) # hit counter, aws-lambda-dynamodb pattern self.hit_counter = lambda_ddb.LambdaToDynamoDB( self, 'LambdaToDynamoDB', deploy_lambda=True, lambda_function_props=_lambda.FunctionProps( runtime=_lambda.Runtime.PYTHON_3_7, code=_lambda.Code.asset('lambda'), handler='hitcounter.handler', environment={ 'DOWNSTREAM_FUNCTION_NAME': self.hello_func.function_name } ), dynamo_table_props=ddb.TableProps( table_name='Hits', partition_key={ 'name': 'path', 'type': ddb.AttributeType.STRING } ) ) # grant the hitcounter lambda role invoke permissions to the hello function self.hello_func.grant_invoke(self.hit_counter.lambda_function) apigw_lambda.ApiGatewayToLambda( self, 'ApiGatewayToLambda', deploy_lambda=False, existing_lambda_obj=self.hit_counter.lambda_function, api_gateway_props=apigw.RestApiProps( default_method_options=apigw.MethodOptions( authorization_type=apigw.AuthorizationType.NONE ) ) )

Review the changes

Let’s review the changes to our resources that will happen when we deploy this:

cdk diff
      Output should look like this:
    
Stack HelloConstructsStack
IAM Statement Changes
┌───┬───────────────────────────────────┬────────┬───────────────────────────────────┬────────────────────────────────────┬───────────┐
│   │ Resource                          │ Effect │ Action                            │ Principal                          │ Condition │
├───┼───────────────────────────────────┼────────┼───────────────────────────────────┼────────────────────────────────────┼───────────┤
│ + │ ${HelloHandler.Arn}               │ Allow  │ lambda:InvokeFunction             │ AWS:${LambdaFunctionServiceRole}   │           │
├───┼───────────────────────────────────┼────────┼───────────────────────────────────┼────────────────────────────────────┼───────────┤
│ + │ ${HelloHandler/ServiceRole.Arn}   │ Allow  │ sts:AssumeRole                    │ Service:lambda.amazonaws.com       │           │
├───┼───────────────────────────────────┼────────┼───────────────────────────────────┼────────────────────────────────────┼───────────┤
│ + │ ${LambdaToDynamoDB/DynamoTable.Ar │ Allow  │ dynamodb:BatchGetItem             │ AWS:${LambdaFunctionServiceRole}   │           │
│   │ n}                                │        │ dynamodb:BatchWriteItem           │                                    │           │
│   │                                   │        │ dynamodb:DeleteItem               │                                    │           │
│   │                                   │        │ dynamodb:GetItem                  │                                    │           │
│   │                                   │        │ dynamodb:GetRecords               │                                    │           │
│   │                                   │        │ dynamodb:GetShardIterator         │                                    │           │
│   │                                   │        │ dynamodb:PutItem                  │                                    │           │
│   │                                   │        │ dynamodb:Query                    │                                    │           │
│   │                                   │        │ dynamodb:Scan                     │                                    │           │
│   │                                   │        │ dynamodb:UpdateItem               │                                    │           │
└───┴───────────────────────────────────┴────────┴───────────────────────────────────┴────────────────────────────────────┴───────────┘
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 
[+] AWS::DynamoDB::Table LambdaToDynamoDB/DynamoTable LambdaToDynamoDBDynamoTable53C1442D 
[+] AWS::IAM::Policy LambdaFunctionServiceRole/DefaultPolicy LambdaFunctionServiceRoleDefaultPolicy126C8897 
[~] AWS::Lambda::Function LambdaFunction LambdaFunctionBF21E41F 
 ├─ [+] Environment
 │   └─ {"Variables":{"DOWNSTREAM_FUNCTION_NAME":{"Ref":"HelloHandler2E4FBA4D"},"DDB_TABLE_NAME":{"Ref":"LambdaToDynamoDBDynamoTable53C1442D"}}}
 ├─ [~] Handler
 │   ├─ [-] hello.handler
 │   └─ [+] hitcounter.handler
 └─ [~] DependsOn
     └─ @@ -1,3 +1,4 @@
        [ ] [
        [+]   "LambdaFunctionServiceRoleDefaultPolicy126C8897",
        [ ]   "LambdaFunctionServiceRole0C4CDE0B"
        [ ] ]

cdk deploy

Okay, ready to deploy?

cdk deploy

Stack outputs

When deployment is complete, you’ll notice this line:

Outputs:
HelloConstructsStack.RestApiEndpoint0551178A = https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/prod/

Testing your app

Let’s try to hit this endpoint with curl. Copy the URL and execute (your prefix and region will likely be different).

curl https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/prod/

Output should look like this:

Hello, AWS Solutions Constructs! You've hit /

Now, let's review the Hits Amazon DynamoDB table.

  1. Go to the DynamoDB console.

  2. Make sure you are in the Region where you created the table.

  3. Select Tables in the navigation pane and select the Hits table.

  4. Open the table and select “Items”.

  5. You should see how many hits you got for each path.

  6. Try hitting a new path and refresh the Items view. You should see a new item with a hits count of one.

If this is the output you received, your app works!