#cdk
#serverless
#aws
#lambda
#api gateway
Sun Oct 03 2021
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.
I'm a Serverless and AWS-CDK fan and one of the main reasons is doing something like this, takes only a couple of hours!
So I need to:
Post
and set it on webhook settings in the Github repo.First thing first, let's start a CDK project:
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)
Checking pr-alert.ts
file in my bin
folder, CDK initiate a new stack.
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
.
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:
yarn add @aws-cdk/aws-apigateway @aws-cdk/aws-lambda
then let's jump into the stack
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.
cdk build cdk deploy
note: for making cdk deploy
work, you need to set up your AWS credential
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.
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:
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; } };
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; };
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.
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:
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 here.
© Copyright 2022 Farmin Farzin