Sending DB migration Alerts To Slack Using AWS Serverless stack and AWS CDK
Tags: cdk, serverless, aws, typescript
Published: 2021-10-04 19:51:35

---
title: Sending DB migration Alerts To Slack Using AWS Serverless stack and AWS CDK
published: true
description: Implementing small alerting system for the PRs with DB migration using AWS API Gateway and Lambda constructing by AWS CDK
tags: CDK, serverless, AWS, Typescript
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/51ksf90tyf0t3wsgnxtk.jpg
canonical_url: https://www.farmin.dev/posts/2412d2a5-70a7-4377-ab22-4d0aa38078bf
---


As we were developing more features and scaling the product, our data guy was more confused with the changes on DB and wanted to catch up with the migrations that we were doing.

So I thought why not just let him know in Slack if there's a new DB migration every time that we release to production?

It should be easy right? We're using Prisma as our DB driver so any changes on the DB are tracked with `schema.prisma` file and we just deploy to production when we merge a PR to master branch on GitHub. So I just need to know when a PR gets merged if there's a change in the `schema.prisma` file, if so, then I can call Slack API and notify our data guy that we're having a new migration.

### Let's do this!
I'm a Serverless and [AWS-CDK](https://docs.aws.amazon.com/cdk/latest/guide/home.html) fan and one of the main reasons is doing something like this, takes only a couple of hours!

So I need to:
- Set up a Slack App to provide me a webhook URL to send messages to a specific channel
- Build an endpoint that accepts `Post` and set it on webhook settings in the Github repo.
- Write a Lambda function, which checks for DB migration change and calls the Slack API

![digram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k93codpjy4itlpm0mmal.jpg)


### Infrastructure as Code!
First thing first, let's start a CDK project:
```bash
mkdir pr-alert
cd pr-alert
cdk init app --language typescript
```
Ok, great! now I have everything ready to build a simple `POST` endpoint to use as a GitHub Webhook. I'll set the Webhook setting to Post to my endpoint every time we `push`. ("Just the push event.") 
The reason is, in push events, there's a list of files that has been changed and then there's a property that indicated if the push is `PR merged` and there's a branch field too, so I can check if that's `master` or not. ([more info on the hook](https://docs.github.com/en/developers/webhooks-and-events/events/github-event-types#pushevent))

Checking `pr-alert.ts` file in my `bin` folder, CDK initiate a new stack.
```ts
const app = new cdk.App();
new PrAlertStack(app, 'PrAlertStack', {});
```

and I see my stack in `lib/pr-alert-stack.ts` file where I should code my infrastructure.

Cool, cool! but before that, let me write my function which would have my whole logic to receive a webhook payload, finding if PR has been merged to master and then send a Slack message.
Let's create a file `alert.js` in a a new folder calling `resources`.
```js
const main = async function (event, context) {
   console.log(JSON.stringify(event, null, 2));
}
module.exports = { main };
```
Awesome! so right now, every time I call this function, it should just print out the event in Cloudwatch for me. Later I'll write what I need...

Now, let's go back to my stack file and code my API endpoint, but before that, I need to install CDK packages for Lambda and API Gateway:
```bash
yarn add @aws-cdk/aws-apigateway @aws-cdk/aws-lambda
```
then let's jump into the stack
```ts
mport * as cdk from '@aws-cdk/core';

import * as lambda from '@aws-cdk/aws-lambda';
import * as apigateway from "@aws-cdk/aws-apigateway";


export class PRAlertStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // our function
    const handler = new lambda.Function(this, "alertHandler", {
      runtime: lambda.Runtime.NODEJS_12_X, 
      code: lambda.Code.fromAsset("resources"),
      handler: "alert.main",
      environment: {
        SLACK_CHANNEL_URL: "{GET_THIS_FROM_YOUR_SLACK_APP}",
        WEBHOOK_SECRET:"{GET_THIS_FROM_YOUR_GITHUB_REPO_WEBHOOKS_SETTING}"
      }
    });

    // our API
    const api = new apigateway.RestApi(this, "pr-alert-api", {
      restApiName: "PR Alert Service",
    });
    const postPRAlert = new apigateway.LambdaIntegration(handler, {
      requestTemplates: { "application/json": '{ "statusCode": "200" }' }
    });

    api.root.addMethod("POST", postPRAlert); 
  }
}

```
This is AWESOME! now we have our endpoint which is `Post /` in API Gateway, hooked with our lambda function, so every time we call this endpoint, we'll run the lambda function.
As soon as I deploy this, it will spit out the endpoint URL in my console.
```bash
cdk build
cdk deploy
```
*note: for making `cdk deploy` work, you need to set up your AWS credential*

### Setting up a Github Webhook
Having the endpoint, I browse over to Github and make a new webhook: `Repository Setting > webhooks > add webhook`. 
Paste the endpoint URL and choose a secret.

### Send the alert to Slack from a Lambda function!

Let's go back to our function and write the logic. I break down the work into functions as I want to test them later and in general, I like it in this way:

- Github's sending a signed secret over the call that needs to be validated to make sure it is coming from the right source.
```js
const verifyGitHubSignature = (req = {}, secret = "") => {
  const sig = req.headers["X-Hub-Signature"];
  const hmac = crypto.createHmac("sha1", secret);
  const digest = Buffer.from(
    "sha1=" + hmac.update(JSON.stringify(req.body)).digest("hex"),
    "utf8"
  );
  const checksum = Buffer.from(sig, "utf8");
  console.log({ checksum, digest });
  console.log("timing", crypto.timingSafeEqual(digest, checksum));
  if (
    checksum.length !== digest.length
    // || !crypto.timingSafeEqual(digest, checksum)
  ) {
    return false;
  } else {
    return true;
  }
};
```
- As part of the payload, there's commit prop which is an array of commits that a "push event" contains and inside each commit there's a list of files that have been changed in that commit.
```js
const migrationCommit = (commits) => {
  const allModifiedFiles = commits.map((c) => c.modified);
  console.log({ allModifiedFiles: [].concat.apply([], allModifiedFiles) });
  if ([].concat.apply([], allModifiedFiles).includes("prisma/schema.prisma")) {
    return true;
  }
  return false;
};

```

- Great! now I can just write the main body and use my functions:

```js
const main = async function (event, context) {
  console.log(JSON.stringify(event, null, 2));
  const secret = event.headers["X-Hub-Signature"];
  if (!verifyGitHubSignature(event, githubSecret)) {
    return {
      statusCode: 403,
    };
  }
  try {
    var method = event.httpMethod;

    if (method === "POST") {
      if (event.path === "/") {
        const body = JSON.parse(event.body);
        const { ref, commits } = body;
        console.log({ ref, commits });
        if (ref.includes("master") && commits.length !== 0) {
          console.log("Pushed to Master");
          console.log("migrated?", migrationCommit(commits));
          if (migrationCommit(commits)) {
            // send message to the Slack
            await fetch(slackChannel, {
              method: "post",
              body: JSON.stringify({
                text: "<!here> DB Migration Alert: the commit that has been pushed to the master branch includes DB migration",
              }),
              headers: { "Content-Type": "application/json" },
            });
          }
        }
        return {
          statusCode: 200,
          headers: {},
          body: JSON.stringify("success"),
        };
      }
    }

    return {
      statusCode: 400,
    };
  } catch (error) {
    var body = error.stack || JSON.stringify(error, null, 2);
    return {
      statusCode: 400,
      headers: {},
      body: JSON.stringify({ error: body }),
    };
  }
};
```

By the way, I'm using 2 libraries that should be installed and packaged when I deploy to AWS. let's install them inside the `resources` package.
```bash
cd resources
npm init
yarn add crypto node-fetch
```
**crypto** helps me in decoding the Github signature and **node-fetch** enables me to call Slack API.

An exciting moment, let's deploy again:
```bash
yarn cdk deploy
```
Well, that's it, now every time we'd have a PR including DB migration, as soon as we merge it, we'll receive a message in Slack!

Find the whole project @ {% github farminf/PR-Slack-Alert no-readme %}