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 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
Infrastructure as Code!
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
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.
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.
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:
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 @
Top comments (0)