AWS CDK for Lambda and DynamoDB
Tags: awscdk, aws, lambda, dynamodb
Published: 2023-02-13 07:04:19

This post is second post after [this(Part1)](https://dev.to/kkkensuke/starting-aws-cdk-with-typescript-aws-lambda-api-gateway-3jhn).

## Environment

- AWS Cloud9
- CDK Version : 2.63.2

## Image Diagram
Create a HitCounter Lambda in front of the Lambda (Hello Lambda) created in Part 1. The Hit Counter Lambda accesses DynamoDB and counts up the number of hits.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zf5uoze9x6z6tgmwa3tk.png)

So let's get your hands dirty!

## Create HitCounter
### Define HitCounter Construct
Create a hitcounter.ts file under the lib folder and write the following code.

```
import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { Construct } from 'constructs';

export interface HitCounterProps {
  /** the function for which we want to count url hits **/
  downstream: lambda.IFunction;
}

export class HitCounter extends Construct {
  constructor(scope: Construct, id: string, props: HitCounterProps) {
    super(scope, id);

    // TODO
  }
}
```
- A new construct class HitCounter is defined.
- The constructor arguments are scope, id, and props as usual, and we expand it into a cdk.Construct base class.
- A new construct class HitCounter is defined.
- The constructor arguments are scope, id, and props as usual, and we expand it into a cdk.Construct base class.The props argument is of the same type as HitCounterProps and contains one property, downstream of lambda.IFunction, to "plug in" the Hello Lambda function created in Part 1 to this props and count hits.

### Create HitCounter Lambda Function
Next, we will create a Lambda function for the hit counter: lambda/hitcounter.js.

```
const aws = require('aws-sdk')

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

  // create AWS SDK clients
  const dynamo = new aws.DynamoDB();
  const lambda = new aws.Lambda();

  // update dynamo entry for "path" with hits++
  await dynamo.updateItem({
    TableName: process.env.HITS_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);
};
```
- HITS_TABLE_NAME is the name of the DynamoDB table.
- DOWNSTREAM_FUNCTION_NAME is the downstreamed AWS Lambda function name.

Since the table names and downstream function names are determined when the app is deployed, these values must be tied to the constructor code. We will implement this in next sections.

### Add resource to Hit Counter constructor
Now we will add the AWS Lambda function and DynamoDB we created to the Hit Counter constructor. lib/hitcounter.ts will look like this

```
import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import { Construct } from 'constructs';

export interface HitCounterProps {
  /** the function for which we want to count url hits **/
  downstream: lambda.IFunction;
}

export class HitCounter extends Construct {

  /** allows accessing the counter function */
  public readonly handler: lambda.Function;

  constructor(scope: Construct, id: string, props: HitCounterProps) {
    super(scope, id);

    const table = new dynamodb.Table(this, 'Hits', {
        partitionKey: { name: 'path', type: dynamodb.AttributeType.STRING }
    });

    this.handler = new lambda.Function(this, 'HitCounterHandler', {
        runtime: lambda.Runtime.NODEJS_14_X,
        handler: 'hitcounter.handler',
        code: lambda.Code.fromAsset('lambda'),
        environment: {
            DOWNSTREAM_FUNCTION_NAME: props.downstream.functionName,
            HITS_TABLE_NAME: table.tableName
        }
    });
  }
}
```
- Partition keys for DynamoDB tables are defined as 'path'.
- Defines a Lambda function associated with lambda/hitcounter.handler.
- Lambda environment variables functionName and tableName are associated with this resource.

### Add Hit Counter constructor to out stack
Now that the Hit Counter constructor is ready, the next step is to add the constructor to the stack.

```
import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as apigw from 'aws-cdk-lib/aws-apigateway';
import { HitCounter } from './hitcounter';

export class CdkWorkshopStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const hello = new lambda.Function(this, 'HelloHandler', {
      runtime: lambda.Runtime.NODEJS_14_X,
      code: lambda.Code.fromAsset('lambda'),
      handler: 'hello.handler'
    });

    const helloWithCounter = new HitCounter(this, 'HelloHitCounter', {
      downstream: hello
    });

    // defines an API Gateway REST API resource backed by our "hello" function.
    new apigw.LambdaRestApi(this, 'Endpoint', {
      handler: helloWithCounter.handler
    });
  }
}
```
The API Gateway handler has been changed to helloWithCounter.handler. Now when a URL is hit, the Hit Counter Lambda is called first, and the Hit Counter Lambda specifies the helloLambda function in the downstream.

### Deploy

```
cdk deploy
```
It will take some time. You should see something like the following as an output

```
Outputs:
CdkWorkshopStack.Endpoint8024A810 = https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/
```

### Test

```
curl -i https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/
```
A 502 Bad Gateway error was returned. We will fix what went wrong.

```
HTTP/2 502 
...

{"message": "Internal server error"}
```
When I refer to the error log from CloudWatch, I see that Lambda is throwing an AccessDeniedException against DynamoDB. It is true that we did not give Lambda permission to access DynamoDB. I will add the following to hitcounter.ts.

```
    // grant the lambda role read/write permissions to our table
    table.grantReadWriteData(this.handler);
```
Now, retest!
I got a 502 error again... Another 502 error. This time, I get an AccessDeniedException and an error saying "You don't have enough privileges to invoke!". Add the following to hitcounter.ts.

```
    // grant the lambda role invoke permissions to the downstream function
    props.downstream.grantInvoke(this.handler);
```
Now the third time's the charm! We finally succeeded!
(It may take a while for API Gateway to flip the endpoint, so if you get the 502 error again, wait a bit and try again.)

```
HTTP/2 200 OK
...

Hello, CDK! You've hit /
```
Try it several times.

```
curl https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/
curl https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/
curl https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/hello
curl https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/hello/world
curl https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/hello/world
```
Let's check dynamoDB table.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6s4y87kbppgpd08s5vv8.png)
It looks good! It counts how many hits against each path.

## Summary
I think the Hit Counter created this time is a useful service that can be attached to various Lambdas to count how many URLs have been hit for each URL. I think it can be used in actual operations as well!

## Reference
https://cdkworkshop.com/20-typescript/40-hit-counter.html

Thank you!