CRM with Lambda and Terraform
Tags: aws, programming, terraform, cloud
Published: 2023-07-14 01:57:18

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
```

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
```

`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](https://registry.terraform.io/providers/hashicorp/aws/latest) 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](https://docs.aws.amazon.com/cli/latest/userguide/cli-services-iam-create-creds.html)).

```
# 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
}
```

```
# 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"
}
```

For our lambda function planning, we will be using [terraform lambda module](https://registry.terraform.io/modules/terraform-aws-modules/lambda/aws/latest) 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
  }
}
```

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.
```

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](https://knowledge.hubspot.com/get-started/set-up-your-account) is their documentation on how to setup your HubSpot account.

![Hubspot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/em8cwi20xfi3d52cp02x.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rqdhfoa8oz1u3scxaa93.png)

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

![SSM Parameter store](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/krsvvurnsslhc3w8ubly.png)

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 };
```

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 
```

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 };
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gjfo3yw5st21y2sq63ft.png)


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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0b23u6j4r47bsdlnihl9.png)

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