DEV Community

kkkensuke
kkkensuke

Posted on

AWS CDK for Lambda and DynamoDB

This post is second post after this(Part1).

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

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
  }
}
Enter fullscreen mode Exit fullscreen mode
  • 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);
};
Enter fullscreen mode Exit fullscreen mode
  • 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
        }
    });
  }
}
Enter fullscreen mode Exit fullscreen mode
  • 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
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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/
Enter fullscreen mode Exit fullscreen mode

Test

curl -i https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/
Enter fullscreen mode Exit fullscreen mode

A 502 Bad Gateway error was returned. We will fix what went wrong.

HTTP/2 502 
...

{"message": "Internal server error"}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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 /
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Let's check dynamoDB table.

Image description
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!

Top comments (0)