Comment system: API Gateway & DynamoDB Integration in Pulumi

Inspired by Alex DeBrie’s tweet (👆) to use API Gateway with DynamoDB proxy integration, I built a Comment system in Pulumi.

Background

The idea is to create a comment system in AWS by skipping the Lambda function and using API Gateway and DynamoDB integration. This is possible by using the AWS_PROXY integration type in API Gateway. This integration type allows you to pass the request directly to the backend service without any transformation.

We assume to have a site with 100 pages and each page has 10 recent comments. So, we would create a DynamoDB table which has slug as Hash key and createdAt as Range key. The slug would be the page URL and createdAt would be the timestamp when the comment was created. This way, we can query the table to get the 10 most recent comments for a given page.

For simplicity, we are going to skip the user table etc.**

Pulumi Code

Create a DynamoDB table to store comments:

// Create a DynamoDB table to store comments
const commentsTable = new aws.dynamodb.Table("CommentsTable", {
    attributes: [
        { name: "slug", type: "S" },
        { name: "createdAt", type: "N" },
    ],
    hashKey: "slug",
    rangeKey: "createdAt",
    billingMode: "PROVISIONED",
    readCapacity: 2,
    writeCapacity: 1,
});

We need to define attributes for the fields on which we would query the table. In this case, we are querying based on slug and createdAt.

DB Provisioning and cost calculation:

Next, the billingMode is set to PROVISIONED. We can arrive at the readCapacity and writeCapacity based on the expected traffic. For that we need have some rough assumptions. In this case,

  • we are assuming a site deployed with ISR (Incremental Static Regeneration) and the pages are cached in CDN (Cloudfront) for a day. So, the traffic would be 10 pages x 1 read query per page.
  • for simplicity, the queries will not be parallel. We can distribute the reads over 60 seconds.
  • no additional writes are happening during the build process.

100 reads / 60 seconds ≈ 1.67 reads/second

Once we have our stack running, these values can be adjusted based on the actual traffic.

So the cost will be:

2 RCUs * $0.00013 per RCU-hour = $0.00026 per hour and, Monthly cost = $0.00026 * 24 * 30 = $0.1872

The cost is based on the us-east-1 region. The cost may vary based on the region.

Similar calculation would go in for writeCapacity. For now, we will take it as 1 WCU.

Create a Rest API Gateway:

RestApi Resource is the top-level container for your API. It defines the basic information about your API, such as its name and description.

// Create an API Gateway Rest API
const api = new aws.apigateway.RestApi("Backend", {
    name: "Backend",
}, {
    dependsOn: commentsTable,
});

Once we have the API Gateway, we need to create a resource and method to handle the POST request. The Resource resource is a logical container for the resources that make up your API. In terms of URL path, it corresponds to a collection of API resources. Multiple resources can be created to represent different parts of your API’s URL structure. For our case, we would create a resource for comments under the Backend API Gateway.

// Create a resource for comments
const commentsResource = new aws.apigateway.Resource("CommentsResource", {
    parentId: api.rootResourceId,
    pathPart: "comments",
    restApi: api.id,    // <--- Reference to the Rest API
}, {
    dependsOn: api,     // <--- Make sure the API is created first
});

Similarly, we need to create a method to handle the POST request. The Method resource defines the HTTP method that clients can use to interact with your API. For our case, we would create a POST method under the comments resource.

// Create a method for POST requests to /comments
const commentsMethod = new aws.apigateway.Method("CommentsMethod", {
    restApi: api.id,
    resourceId: commentsResource.id,    // <--- Reference to the comments resource
    httpMethod: "POST",
    authorization: "NONE",
}, {
    dependsOn: commentsResource,
});

Integration with DynamoDB:

Specifying a role is crucial for security and access control while integrating API Gateway with AWS services using the AWS_PROXY integration type. The role specified in the integration is assumed by API Gateway, not the AWS service being called. This role includes permissions that API Gateway needs to invoke actions on the integrated AWS service.with other AWS services.

This approach follows the principle of least privilege, where the role grants only the necessary permissions for API Gateway to perform specific actions. This minimizes the risk of unauthorized access or actions within your AWS environment.

const apigatewayRole = new aws.iam.Role("ApigatewayRole", {
    assumeRolePolicy: {
        Version: "2012-10-17",
        Statement: [{
            Action: "sts:AssumeRole",
            Effect: "Allow",
            Principal: {
                Service: "apigateway.amazonaws.com",
            },
        }],
    },
});
// Create an integration with DynamoDB
const commentsIntegration = new aws.apigateway.Integration("DynamodbIntegration", {
    restApi: api.id,
    resourceId: commentsResource.id,
    httpMethod: commentsMethod.httpMethod,
    type: "AWS",
    integrationHttpMethod: "POST",
    credentials: apigatewayRole.arn,
    uri: `arn:aws:apigateway:${currentRegion}:dynamodb:action/PutItem`,
    requestTemplates: {
        "application/json": pulumi.interpolate`{
            "TableName": "${commentsTable.name}",
            "Item": {
                "commentId": {
                    "S": "$context.requestId"
                },
                "comment": {
                    "S": "$input.body"
                }
            }
        }`,
    },
    passthroughBehavior: "WHEN_NO_MATCH",
}, {
    dependsOn: [commentsMethod, apigatewayRole],
});

The Integration resource defines how API Gateway integrates with other AWS services. In this case, we are integrating with DynamoDB. The uri property specifies the ARN of the service action to be called. The requestTemplates property specifies the mapping templates to transform the request body into the format expected by the backend service.

Deploy the stack:

// Deploy the API
const deployment = new aws.apigateway.Deployment("ApiDeployment", {
    restApi: api.id,
    stageName: "v1",
}, {
    dependsOn: commentsIntegration,
});

Since we would require the recent comments, we need to use ScanIndexForward: false in the query along with Limit: 10.

Checkout the repo for full code.