DEV Community

Cover image for CRM with Lambda and Terraform
chester htoo
chester htoo

Posted on

CRM with Lambda and Terraform

Many of us have visited websites, scrolled around and click about. If the website's interesting, you guys have also send inquiry on more details about the product on the website's little form that's called Contact Us.

So, today let's dive into building a minimal working backend service for contact us form and saving that inquiry into our CRM, Hubspot.

Technologies that we'll use are as follows:

  1. Terraform (IaC - Resource provisioning)
  2. AWS Lambda (Compute Infrastructure on the AWS)
  3. HubSpot API (to save inquiries)

Setup

Let's start by creating a new working directory

mkdir contact-us
cd contact-us
Enter fullscreen mode Exit fullscreen mode

The plan here is to create a minimal lambda function that saves the user data from the client application (WordPress website, wix, custom client website, etc) into our HubSpot CRM. So let's get straight into it.

Plan

We'll start off by provisioning our lambda function from Terraform.

touch main.tf providers.tf outputs.tf variables.tf
Enter fullscreen mode Exit fullscreen mode

providers.tf - This file is for defining which Terraform providers that we want to use.
main.tf - Our resource provisioning logic will sit in this file (if it's a huge application, we would create separate module files)
variables.tf - This file is for defining variables that are needed for our terraform module.
outputs.tf - Any output data that we want after provisioning our resources.

We will need to grab our AWS provider Terraform registry into the providers.tf file along with your AWS Access Key and Secret Key (Read more on how to generate these keys here).

# providers.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "5.8.0"
    }
  }
}

provider "aws" {
  region     = var.aws_region
  access_key = var.access_key
  secret_key = var.secret_key
}
Enter fullscreen mode Exit fullscreen mode
# variables.tf
variable "aws_region" {
  type        = string
  description = "AWS Region"
  default     = "eu-west-1"
}

variable "secret_key" {
  type        = string
  description = "AWS Secret Key"
}


variable "access_key" {
  type        = string
  description = "AWS Access Key"
}
Enter fullscreen mode Exit fullscreen mode

For our lambda function planning, we will be using terraform lambda module rather than the resource so that if we were to reuse, we can just use that particular module.

module "lambda" {
  source        = "terraform-aws-modules/lambda/aws"
  version       = "5.2.0"
  function_name = "contact-us"
  architectures = ["arm64"]
  runtime       = "nodejs18.x"
  handler       = "index.handler"

  attach_policy_statements = true

  policy_statements = {
    AmazonSSMReadOnlyAccess = {
      sid       = "AmazonSSMReadOnlyAccess"
      effect    = "Allow"
      actions   = ["ssm:Describe*", "ssm:Get*", "ssm:List*"]
      resources = ["*"]
    }
  }

  source_path = [{
    path = "${path.module}/functions/contact-us"
  }]

  create_lambda_function_url = true

  cors = {
    allowed_credentials = false
    allowed_headers     = ["*"]
    allowed_methods     = ["POST", "OPTIONS", ]
    allowed_origins     = ["*"] # We would only want to allow our domain here
    max_age_seconds     = 3000
  }
}
Enter fullscreen mode Exit fullscreen mode

So let's go through the inputs line by line. First we define what source we are using from terraform module and its version. We then define what sort of architecture, programming language and runtime we're using for the lambda function. For permissions, we're using inline policy statements here, in which we're allowing access to SSM Parameter store, since we don't want to store the HubSpot API keys in the lambda function directly, and will be storing in the Parameter store. We want to create a lambda function url that we can invoke directly so we flag true for create_lambda_function_url (this is not the most ideal way, more on it later), followed by CORS config.

Let's run terraform init and get the required providers.

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
Enter fullscreen mode Exit fullscreen mode

Great stuffs! Now we'll setup HubSpot.

HubSpot

HubSpot is a CRM platform with a lot of integrations and resources for marketing, sales and content management. The product that we want to focus on for this part is their CRM hub contacts. Here is their documentation on how to setup your HubSpot account.

Hubspot

In the hubspot app, we can create our own private app (which is similar to connected app if you've ever used SalesForce or think of it as a client), and then under scopes, choose crm.objects.contacts read/write access. That's it! We then get our own HubSpot Access key.

Private App

We will then store this key in our AWS Parameter store, and store it as an encrypted string.

SSM Parameter store

That's it! Now we get into coding the actual lambda function.

Lambda

We'll create a new folder called functions and then a subfolder called contact-us. This is where the lambda function will sit. In there, we will create a package.json file by called yarn init -y and create a blank index.js with the following content.

const handler = async (event, context) => {};

module.exports = { handler };
Enter fullscreen mode Exit fullscreen mode

We'll install 3 libraries, which are @aws-sdk/client-ssm to get our HubSpot access key and @hubspot/api-client to interact with HubSpot API.

yarn add @aws-sdk/client-ssm @hubspot/api-client 
Enter fullscreen mode Exit fullscreen mode

and this is what our index.js looks like

const AWS = require("@aws-sdk/client-ssm");
const hubspot = require("@hubspot/api-client");

const handler = async (event, context) => {
  const body = JSON.parse(event.body);

  const ssm = new AWS.SSM({
    region: "eu-west-1",
  });

  const hubspot_key = await ssm.getParameter({
    Name: "HUBSPOT_ACCESS_KEY",
    WithDecryption: true,
  });

  const hubspot_access_key = hubspot_key.Parameter.Value;

  const hubspotClient = new hubspot.Client({
    accessToken: hubspot_access_key,
  });

  await hubspotClient.crm.contacts.basicApi.create({
    properties: {
      email: body.email,
      firstname: body.firstname,
      lastname: body.lastname,
      phone: body.phone,
      message: body.message,
    },
  });

  return {
    statusCode: 200,
    body: JSON.stringify({
      message: "Thanks for contacting us! We will be in touch soon.",
    }),
  };
};

module.exports = { handler };
Enter fullscreen mode Exit fullscreen mode

Now before we do terraform plan to see the changes that it's going to make, first we'll need to create main.tfvars and then give the AWS Access key and Secret Key, but personally, I have an IAM Identity Centre enabled on my personal organisation, so I will be skipping this.

Then we do terraform plan. It lists out a bunch of changes that terraform is planning to make, and if everything looks good, we can go ahead and do terraform apply. It will then apply the changes and now your lambda function will be live in no time!

So with our newly created Lambda function, let's test it out on Postman.

Postman

Great stuffs! Now we've successfully deployed our lambda function and if we go check to HubSpot, we'll also see a new contact added there.

Hubspot

So, that's it really. We've successfully built our own little contact us functionality, waiting to be integrated with your client applications!😄

Improvements

Sure, you wouldn't use this lambda function alone when your application grows bigger and bigger. There are definitely ways on how this can be improved.

  1. We wouldn't use the lambda function URL as its own endpoint in big application. Instead, we can create an API Gateway that fronts the lambda function and set them as targets.

  2. If we want the inquiries to be notified to us, we can either add SES service or Slack API to be notified in our own channel.

  3. Setting up CD pipelines for lambda function on deploy is also another thing that can be improved.

  4. In real world application, where there are a bunch of routes in the API gateway, we wouldn't use just a single Terraform file to deploy them. We would have a dedicated file structure on dealing with different terraform states for different functions and resources.

So that's the end of this little lab. I hope you guys enjoy it and I hope to see yous in the next one! Ciao!

Link to Github repo: https://github.com/halchester/contact-us-lambda-hubspot

Top comments (0)