AWS lambda로 slack + github 연동하기(수많은 error를 곁들인)

jellyjw·2023년 1월 30일
2

진행하는 프로젝트에서 github PR, commit, issue 등 중요한 알림들은 slack으로 받아볼 수 있도록 slack webhookAWS lambda, node.js를 이용해서 github와 slack을 연동해 보기로 했다.

lambda는 커녕 AWS와 node.js도 제대로 사용해본 적이 없었기에 처음엔 너무 막막했지만, 차근차근 진행해 보았다.

lambda를 사용한 이유

진행하기 전, 우리 팀에서 slack 내에 github, jira와 연동할 수 있는 봇이 존재함에도 불구하고 lambda 를 이용해 직접 연동시킨 이유는 다음과 같다.

  • github subscribe를 통해서 연결할 수 있지만 제공하는 event 자체가 한정적이고, issue close 이벤트의 알림이 오지 않는 등 이벤트 지원이 일부만 가능한 점
  • message custom 을 하기 위해!
  • server에서 발생하는 알림이나 jira, jenkins 등의 알림도 받을 수 있도록 확장성을 고려하기 위해

slack webhook API 생성하기

가장 먼저 slack으로 메세지를 수신하기 위해 webhook URL 생성이 필요하다.
URL은 다음과 같은 형태로 만들어진다.

https://hooks.slack.com/services/{slack hook URL}

웹훅 설정 페이지는 여기를 참고

Postman으로 테스트

URL을 생성했으니, POST 요청을 보내 테스트를 해볼 수 있다.

Headers 입력

  • key: Content-type
  • value: application/json

Body > raw 입력

{
  "text": "Hello, World"
}

그리고 요청을 보내보니 200 ok 응답과 함께 정상적으로 내가 만든 채널에 알림이 왔다.


AWS lambda function 생성하기

처음 function을 생성하면 기본적으로 index.mjs 파일이 생성되어 있다.

node.js 코드를 입력하고 Test를 하는데 자꾸
exports is not defined in ES module scope 에러가 발생했다.

Before (에러 코드)

'use strict';

const url = require('url');
const https = require('https');

let hookUrl = `<slack webhook URL>`;

function sendMessage(message) {
  const body = JSON.stringify(message);
  const options = url.parse(hookUrl);
  options.method = 'POST';
  options.headers = {
    'Content-Type': 'application/json',
    'Content-Length': Buffer.byteLength(body)
  };
  return new Promise((resolve) => {
    const req = https.request(options, (res) => {
      res.on('end', () => {
        resolve({
          statusCode: res.statusCode
        });
      });
    });
    req.write(body);
    req.end();
  });
}

exports.handler = (event, context, callback) => {
  const message = {
    channel: '#github-event-lucy',
    text: 'This is a test message!'
  };
  sendMessage(message)
  .then((response) => {
    callback(null, response);
  }).catch((error) => {
    callback(error);
  });
};

node 버전 이슈로 exports 사용이 안되는 것 같아
export const handler = () => {} 형식으로 변경해주고,
require 문법도 전부 import로 변경 해 주었다.
저장만 하고 테스트를 하면 안되고, 꼭 deploy 를 한 뒤 test 해 봐야 한다!

After (에러 해결 코드)

import * as url from "url";
import * as https from "https";

let hookUrl = `<slack webhook URL>`;

function sendMessage(message) {
  const body = JSON.stringify(message);
  const options = url.parse(hookUrl);
  options.method = "POST";
  options.headers = {
    "Content-Type": "application/json",
    "Content-Length": Buffer.byteLength(body),
  };

  return new Promise((resolve) => {
    const req = https.request(options, (res) => {
      res.on("end", () => {
        resolve({
          statusCode: res.statusCode,
        });
      });
    });
    req.write(body);
    req.end();
  });
}

export const handler = async(event) => {
  const message = {
    channel: '#jj-알림',
    text: 'test message'
  };
  sendMessage(message).then((response) => {
    // callback(null, response);
  }).catch((error) => {
    // callback(error);
  });
};


수정해주니 정상적으로 test message가 slack으로 보내졌다.

다음 할일은?

  • http 요청은 axios로 변경할 것
  • severless framework + node.js 를 이용해 로컬에서 작업한 뒤 lambda로 배포하기 (lambda에서 코드를 작성하고 테스트하는건 비효율적)
  • github event 연동하기(Issue, PR, push 정도만)

Serverless로 코드 작성하기

코드리뷰 받은 부분을 반영하여 Serverless 프레임워크를 이용해서 코드를 다시 작성한 뒤 lambda에 배포하려고 한다.

서버리스 프레임워크는, AWS lambda에서 node.js로 작업할 때 사용되는 프레임워크이다.

먼저 AWS IAM에서 사용자와 권한을 추가해준 뒤, 액세스 키 발급받고 환경설정을 해주자.

// serverless 설치
npm install -g serverless

// 프로젝트 환경 세팅
serverless create --template aws-nodejs --path serverless-api --name serverless-api
  • 템플릿 설정 : --template aws-nodejs
  • 프로젝트 폴더 설정 : --path serverless-api
  • 프로젝트 이름 설정 : --name serverless-api

환경 세팅이 완료되면, serverless-api 디렉토리에 파일들이 생성된다.

// handler.js

"use strict";

module.exports.hello = async (event) => {
  return {
    statusCode: 200,
    body: JSON.stringify(
      {
        message: "Go Serverless v1.0! Your function executed successfully!",
        input: event,
      },
      null,
      2
    ),
  };

초기 작성되어 있는 함수를 lambda에서 작성한 함수로 바꿔주고,
발급받은 액세스 키를 입력해서 람다와 연결시켜줬다.

serverless config credentials --provider aws --key {액세스 키 ID} --secret {비밀 액세스 키}

연결이 된 걸 확인하고 배포 명령어 입력

sls deploy

에러가 나지는 않을지 조마조마하며 기다린 결과 배포는 성공했는데,
URL을 열어보니 에러가 떴다.

서버 에러.. 공식문서를 찾아보니, 아무래도 AWS IAM 권한 문제로 함수 호출이 안되는 듯 했다.

API Gateway REST API가 Lambda 호출 권한 없이 Lambda 함수를 호출하려고 하면 API Gateway가 'Invalid permissions on Lambda function'(Lambda 함수에 대한 잘못된 권한) 오류를 반환합니다.

일단 lambda에 내가 작성한 서버리스 코드가 배포된 걸 확인하고,
테스트를 실행시켜 보니 이런 에러가 발생했다

Errormessage : SyntaxError: Cannot use import statement outside a module

이쯤되면 에러가 안뜨면 오히려 서운할 지경인 것 같다.
알고보니 node 버전이 12.xx 버전이어서 뜨는 오류였다.

importrequire 로 바꿔주고,
처음에 exports.handler 함수를 export const handler 로 변경해줬었는데, 이 부분도 다시 exports로 되살려줬다.

const url = require('url');
const https = require('https');

exports.handler = async (event) => {
  const message = {
    channel: "#jj-알림",
    text: "test message",
    ...
  };

다시 테스트를 실행해보자!
이번엔 이런 에러가 떴다!
handler.hello is undefined or not exported

문득 이러다 AWS의 모든 에러메세지를 경험하는 건 아닐까 하는 생각이 들었지만, 정신 차려야 한다..^^

정신이 혼미해지는 스택오버플로우의 답변을 뒤로한채,

This usually occurs when you zip up the directory, instead of zipping up the contents of the directory. When you open your zip file to browse the content, the index.js file should be in the root of the zip file, not in a folder.

yml 파일에 function명이 hello 로 설정되어 있어 handler 함수명을 인식하지 못하는 문제였다. 다시 hello로 변경!

드디어 연동을 성공했다. 아직 갈길이 멀지만, 여기까지 오는 동안에도 너무 많은 시행착오를 겪었기 때문에 아마 올해 가장 성취감을 느꼈던 순간이 아닐까..싶다! 정말 기뻤다 ㅎㅎ

이제 할 일은 axios로 리팩토링 하고 github event마다 분기해서 message custom을 해야 한다!


Github lambda 함수 연결하기

여지껏 고생하며 만들었으니, 이제 깃으로 협업할 때의 알림들을 lambda로 요청이 갈 수 있게 연동해보자.

  • repository > settings > Webhooks > add webhooks

payload URL은 lambda에서 생성한 함수 URL을 넣어주고,
원하는 이벤트만 select할 수 있다.

그리고 Recent Deliveries 탭에서 매번 직접 이벤트를 실행시키지 않고도 redelivery로 테스트도 가능하다. (실제 이벤트가 발생했을 때 log가 남고, 그 이벤트를 다시 호출시키는 방식)

주의할 점은, lambda에서 함수 URL을 생성할 때 꼭 인증 유형을 NONE으로 설정해서 퍼블릭 액세스 권한을 부여해야 한다. 안그러면 함수 호출 불가하다!

깃허브에서 테스트를 돌리면 정상적으로 알림이 가는 것까지 성공했다.
이제 메세지 커스텀 + axios 리팩토링을 해봐야겠다.


axios로 리팩토링 하기

http requestaxios를 이용해서 리팩토링 시켜줬다.

const axios = require("axios");

const hookUrl =
  "https://hooks.slack.com/services/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";

function sendMessage() {
  axios({
    method: "post",
    url: hookUrl,
    headers: {
      "Content-Type": "application/json",
    },
    data: {
      channel: "#jj-알림",
      username: "Github-notify",
      text: "새로운 알림이 있습니다.",
      attachments: [
        {
          color: "#045222",
          fields: [
            {
              title: "Github",
              value: "new issue",
              short: true,
            },
          ],
        },
      ],
    },
  });
}

module.exports.hello = async () => {
  sendMessage()
    .then((response) => {
      console.log(response);
    })
    .catch((error) => {
      console.log(error);
    });
};

serverless에서 test해볼 수 있는 명령어로 로컬에서 테스트를 해보니,

serverless invoke local --function [function name]


이렇게 정상적으로 메세지가 오는 걸 확인할 수 있었다.
그런데 로컬에서는 axios 가 설치되어 있어 정상적으로 요청이 가능했지만 sls deploy를 통해 lambda에 배포 후 테스트를 해보니 axios 모듈을 찾을 수 없다는 에러가 발생했다.

handler.js 파일만 배포가 되다보니 당연한 일이었다. package.json 파일을 추가해준뒤 다시 배포 후 테스트해보니 정상적으로 동작했다.


메세지 커스텀

정말 많이 왔다. 백엔드쪽 지식이 너무 부족하고 정말 사용해본적 없는 환경들을 한번에 다 사용해보려고 하다 보니, 결과적으로는 간단한 과정인데 여기까지 오는데 꽤 많은 시간이 흘렀다. 삽질도 많이 했다 ㅠㅠ

메세지 커스텀을 위해서는 이런 과정이 필요하다.

  • 필요한 github event별로 payload를 분리
  • githubEvent를 includes 로 분기(issue, push, PR ...)

첫번째로 header는 잘 받아와져서

function sendMessage(event) {
const githubEvent = event.headers['x-github-event']
...
}

이렇게 입력했을 시 event의 종류는 잘 들어왔지만, 문제는 body.. payload 부분이었다. 정말 여러번의 테스트를 해봤는데 자꾸 값에 접근 자체가 불가했다. log를 보니 인코딩 되어 들어오고 있었다.

구글링 해보니 headers에 application/json 인지 확인하라는 답변이 있었는데, 분명 POST 요청시에 header를 맞게 설정했기 때문에 직접 디코딩을 해서 접근하려고도 해보고, 정말 많이 헤맸다.

알고보니 Payload URL의 Content type 부분을 잘못 설정해주고 있었다.
위 type을 json으로 설정해주니 다시 객체로 잘 들어왔다.

그리고 기쁜 마음으로 event.body.issue 등으로 바로 접근하려고 하면 역시나 또 에러를 뱉어낸다. JSON.parse() 를 해줘야 하기 때문이다.ㅎㅎㅎㅎ

전체 코드

const axios = require("axios");

const hookUrl = {slack hook url};

function sendMessage(event) {
  const githubEvent = event.headers["x-github-event"];
  const body = JSON.parse(event.body);

  const issue = body.issue;
  const repo = body.repository;
  const pull_request = body.pull_request;

  let message, lucyTitle, titleLink;

  if (githubEvent.includes("issue")) {
    lucyTitle = issue.title;
    titleLink = issue.html.url;
    message = `${issue.user.login} 님의 ${issue.title} ${githubEvent} ${body.action} 이벤트가 발생했어요.`;
  } else if (githubEvent.includes("push")) {
    lucyTitle = `${repo.name} : ${body.head_commit.message}`;
    titleLink = body.commits[0].url;
    message = `${repo.name} repository에서 ${body.pusher.name} 님의 ${githubEvent} 이벤트가 발생했어요.`;
  } else if (githubEvent.includes("pull")) {
    lucyTitle = `${pull_request.head.repo.name} : ${pull_request.title}`;
    titleLink = pull_request.html_url;
    message = `${pull_request.head.repo.name} repository에서 ${pull_request.user.login}님의 ${githubEvent} ${body.action} 이벤트가 발생했어요.`;
  }

  axios({
    method: "post",
    url: hookUrl,
    headers: {
      "Content-Type": "application/json",
    },
    data: {
      channel: "#jj-알림",
      username: "Lucy-event-notifier",
      attachments: [
        {
          color: "#2eb886",
          title: lucyTitle,
          title_link: titleLink,
          fields: [
            {
              title: "Github",
              value: message,
              short: true,
            },
          ],
        },
      ],
    },
  });
}

module.exports.notifier = async (event) => {
  sendMessage(event);
};

이렇게 잘 커스텀까지 성공했다. 다시 시키면 같은 실수는 안할 수 있을 것 같은데, aws log 보는법도 서툴고 node.js도 서툴고 해서 정말 시행착오를 너무 많이 겪었다.. ㅠ

그래도 겪으면서 확실히 머릿속에 남게 된 개념들이 많고 서버쪽 연동이라는 이유로 못할 것만 같았던 일을 해냈을 때의 성취감은 .. 아주아주 기뻤다.
끝!


Reference
서버리스 및 AWS Lambda로 Github 웹후크 핸들러 구축
https://www.serverless.com/blog/serverless-github-webhook-slack/

slack webhook 생성
https://jojoldu.tistory.com/552

서버리스 프레임워크 환경 설정
https://any-ting.tistory.com/102

profile
남는건 기록뿐👩🏻‍💻

0개의 댓글