Granting permissions

Allow Lambda to read/write our DynamoDB table #

Let’s give our Lambda’s execution role permissions to read/write from our table.

Go back to hitcounter.ts and add the following highlighted lines:

import { Construct } from "constructs";
import {
  Code,
  LambdaFunction,
  IFunction,
  Runtime,
} from "terraconstructs/lib/aws/compute";
import { AttributeType, Table } from "terraconstructs/lib/aws/storage";

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

export class HitCounter extends Construct {
  /** allows accessing the counter function */
  public readonly handler: LambdaFunction;

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

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

    this.handler = new LambdaFunction(this, "HitCounterHandler", {
      runtime: Runtime.NODEJS_22_X,
      handler: "hitcounter.handler",
      code: Code.fromAsset("lambda"),
      environment: {
        DOWNSTREAM_FUNCTION_NAME: props.downstream.functionName,
        HITS_TABLE_NAME: table.tableName,
      },
    });

    // grant the lambda role read/write permissions to our table
    table.grantReadWriteData(this.handler);
  }
}

Deploy #

Save & deploy:

cdktf deploy

Test again #

Okay, deployment is complete. Let’s run our test again (either use curl or your web browser):

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

Again?

HTTP/2 502 Bad Gateway
...

{"message": "Internal server error"}

😢 #

Still getting this pesky 5xx error! Let’s look at our CloudWatch logs again (click “Refresh”):

{
    "errorType": "AccessDeniedException",
    "errorMessage": "User: arn:aws:sts::694710432912:assumed-role/cdk-workshop-dev-cdrHandlerServiceRole20250715042121401600000001/cdk-workshop-dev-cderHitCounterHandler is not authorized to perform: lambda:InvokeFunction on resource: arn:aws:lambda:us-east-1:694710432912:function:cdk-workshop-dev-cdorkshopHelloHandler because no identity-based policy allows the lambda:InvokeFunction action",
    "name": "AccessDeniedException",
    "message": "User: arn:aws:sts::694710432912:assumed-role/cdk-workshop-dev-cdrHandlerServiceRole20250715042121401600000001/cdk-workshop-dev-cderHitCounterHandler is not authorized to perform: lambda:InvokeFunction on resource: arn:aws:lambda:us-east-1:694710432912:function:cdk-workshop-dev-cdorkshopHelloHandler because no identity-based policy allows the lambda:InvokeFunction action",
    "stack": [
        "AccessDeniedException: User: arn:aws:sts::694710432912:assumed-role/cdk-workshop-dev-cdrHandlerServiceRole20250715042121401600000001/cdk-workshop-dev-cderHitCounterHandler is not authorized to perform: lambda:InvokeFunction on resource: arn:aws:lambda:us-east-1:694710432912:function:cdk-workshop-dev-cdorkshopHelloHandler because no identity-based policy allows the lambda:InvokeFunction action",
        "    at throwDefaultError (/var/runtime/node_modules/@aws-sdk/node_modules/@smithy/smithy-client/dist-cjs/index.js:867:20)",
        "    at /var/runtime/node_modules/@aws-sdk/node_modules/@smithy/smithy-client/dist-cjs/index.js:876:5",
        "    at de_CommandError (/var/runtime/node_modules/@aws-sdk/client-lambda/dist-cjs/index.js:4061:14)",
        "    at process.processTicksAndRejections (node:internal/process/task_queues:105:5)",
        "    at async /var/runtime/node_modules/@aws-sdk/node_modules/@smithy/middleware-serde/dist-cjs/index.js:35:20",
        "    at async /var/runtime/node_modules/@aws-sdk/node_modules/@smithy/core/dist-cjs/index.js:193:18",
        "    at async /var/runtime/node_modules/@aws-sdk/node_modules/@smithy/middleware-retry/dist-cjs/index.js:320:38",
        "    at async /var/runtime/node_modules/@aws-sdk/middleware-logger/dist-cjs/index.js:33:22",
        "    at async exports.handler (/var/task/hitcounter.js:25:23)"
    ]
}

Another access denied, but this time, if you take a close look:

User: <VERY-LONG-STRING> is not authorized to perform: lambda:InvokeFunction on resource: <VERY-LONG-STRING>"

So it seems like our hit counter actually managed to write to the database. We can confirm by going to the DynamoDB Console and exploring the table items:

We should see the count for / path is incrementing now:

But, we must also give our hit counter permissions to invoke the downstream lambda function.

Grant invoke permissions #

Add the highlighted lines to lib/hitcounter.ts:

import { Construct } from "constructs";
import {
  Code,
  LambdaFunction,
  IFunction,
  Runtime,
} from "terraconstructs/lib/aws/compute";
import { AttributeType, Table } from "terraconstructs/lib/aws/storage";

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

export class HitCounter extends Construct {
  /** allows accessing the counter function */
  public readonly handler: LambdaFunction;

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

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

    this.handler = new LambdaFunction(this, "HitCounterHandler", {
      runtime: Runtime.NODEJS_22_X,
      handler: "hitcounter.handler",
      code: Code.fromAsset("lambda"),
      environment: {
        DOWNSTREAM_FUNCTION_NAME: props.downstream.functionName,
        HITS_TABLE_NAME: table.tableName,
      },
    });

    // grant the lambda role read/write permissions to our table
    table.grantReadWriteData(this.handler);

    // grant the lambda role invoke permissions to the downstream function
    props.downstream.grantInvoke(this.handler);
  }
}

Diff #

You can check what this did using cdktf diff:

cdktf diff

The aws_iam_role_policy section should look something like this, which shows the IAM statement was added to the role:

cdk-workshop    # aws_iam_role_policy.HelloHitCounter_HitCounterHandler_ServiceRole_DefaultPolicy_ResourceRoles0_B630511F (HelloHitCounter/HitCounterHandler/ServiceRole/DefaultPolicy/ResourceRoles0) will be updated in-place
                ~ resource "aws_iam_role_policy" "HelloHitCounter_HitCounterHandler_ServiceRole_DefaultPolicy_ResourceRoles0_B630511F" {
                      id          = "cdk-workshop-dev-cdrHandlerServiceRole20250715042121401600000001:cdkworkshopHelloHitCounterHitCounterHandlerServiceRoleDefaultPolicy688F79E1"
                      name        = "cdkworkshopHelloHitCounterHitCounterHandlerServiceRoleDefaultPolicy688F79E1"
                    ~ policy      = jsonencode(
                        ~ {
                            ~ Statement = [
                                  # (1 unchanged element hidden)
                                  {
                                      Action   = [
                                          "dynamodb:UpdateItem",
                                          "dynamodb:Scan",
                                          "dynamodb:Query",
                                          "dynamodb:PutItem",
                                          "dynamodb:GetShardIterator",
                                          "dynamodb:GetRecords",
                                          "dynamodb:GetItem",
                                          "dynamodb:DescribeTable",
                                          "dynamodb:DeleteItem",
                                          "dynamodb:ConditionCheckItem",
                                          "dynamodb:BatchWriteItem",
                                          "dynamodb:BatchGetItem",
                                      ]
                                      Effect   = "Allow"
                                      Resource = "arn:aws:dynamodb:us-east-1:694710432912:table/cdkworkshopHelloHitCounterHitsF0859462"
                                  },
                                + {
                                    + Action   = "lambda:InvokeFunction"
                                    + Effect   = "Allow"
                                    + Resource = [
                                        + "arn:aws:lambda:us-east-1:694710432912:function:cdk-workshop-dev-cdorkshopHelloHandler",
                                        + "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:694710432912:function:cdk-workshop-dev-cdorkshopHelloHandler:$LATEST/invocations",
                                      ]
                                  },
                              ]
                              # (1 unchanged attribute hidden)
                          }
                      )
                      # (2 unchanged attributes hidden)
                  }

              Plan: 0 to add, 1 to change, 0 to destroy.

Which is exactly what we wanted.

Deploy #

Okay… let’s give this another shot:

cdktf deploy

Then hit your endpoint with curl or with your web browser:

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

Output should look like this:

HTTP/2 200 OK
...

Hello, CDK! You've hit /
If you still get 5xx, give it a few seconds and try again. Sometimes IAM Policy changes take time to propagate or API Gateway takes a little bit to “flip” the endpoint to use the new deployment.

😲 #




We use analytics to make this content better, but only with your permission.

More information