이제 GitHub 웹훅을 처리할 수 있게 됐습니다. 이전 글에서 생성한 URL을 GitHub에 설정해줍니다.

GitHub 저장소의 Settings 탭에 가면 Webhooks 메뉴가 있습니다. 여기서 웹훅을 추가할 수 있습니다.

Add webhook

아래 부분에서 웹훅을 통해 받을 이벤트를 설정할 수 있습니다. 다른 이벤트는 이미 받고 있으므로 여기서는 Gollum(GitHub 위키 엔진) 이벤트만 체크해줍니다.

Select events

위키에 변경을 가하면 'Recent Deliveries' 섹션에 그 내용이 보여집니다. Lambda 함수가 GitHub 위키 이벤트를 의도한대로 처리하는지 테스트하기 위해 이 내용을 사용할 있습니다. AWS Lambda 편집화면에서 Actions -> Configure test event 에서 입력할 수 있습니다.

다음은 최종 Lambda 코드입니다.

'use strict';

const AWS = require('aws-sdk');
const url = require('url');
const https = require('https');

let hookUrl;

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();
  });
}

function decryptHookUrl() {
  if (hookUrl) {
    return Promise.resolve(hookUrl);
  } else {
    const kms = new AWS.KMS();
    return new Promise((resolve, reject) => {
      const blob = new Buffer(process.env.encryptedHookUrl, 'base64');
      kms.decrypt({ CiphertextBlob: blob }, (error, data) => {
        if (error) {
          return reject(error);
        }
        hookUrl = data.Plaintext.toString('ascii');
        resolve(hookUrl);
      });
    });
  }
}

function processGithubPayload(payload) {
  const page = payload.pages[0];
  const repo = payload.repository.full_name;
  const page_link = page.html_url + '|' + page.title;
  const sender_link = payload.sender.html_url + '|' + payload.sender.login;
  return {
    username: 'github wiki',
    text: `[${repo}] <${page_link}> is ${page.action} by <${sender_link}>`
  }
}

exports.handler = (event, context, callback) => {
  decryptHookUrl()
  .then(() => {
    const message = processGithubPayload(event);
    message.channel = '#auto-github';
    sendMessage(message)
    .then((response) => {
      callback(null, 'success');
    });
  }).catch((error) => {
    callback(error);
  });
};

이제 위키를 수정하고 슬랙에 메시지가 표시되는 것을 확인하면 끝입니다. 수정내역이나 커밋 메시지를 표시해주면 좋은데 아쉽게도 해당 데이터는 없는 것 같습니다.

GitHub 이벤트를 받으려면 GitHub에서 접근가능한 URL이 필요합니다. 이전 글에서 만든 Lambda 함수를 외부에서 접근가능하게 하려면 AWS API Gateway를 사용하면 됩니다.

API Gateway 콘솔에서 새 API를 생성합니다.

Create API

생성된 직후에는 어떤 메소드도 없습니다. GitHub 이벤트는 POST 메소드로 전달되므로 Resources에서 POST 메소드를 하나 생성합니다.

Actions 메뉴에서 Create Method를 누른 후 POST를 선택하면 메소드가 생성됩니다.

Created POST Method

이 메소드에 우리가 생성한 Lambda 함수를 연결할 수 있습니다.

Integration with Lambda Function

테스트 버튼을 누르면 API 테스트를 할 수 있습니다. 슬랙에 메시지가 오는지 확인해보세요.

이제 이 API를 활성화할 차례입니다. Actions에서 Deploy API를 선택합니다. 현재 stage가 없으므로 '[New State]'를 선택하고 prod라고 이름을 줍니다.

Setup deploy

이제 외부에서 접근가능한 URL이 생성됐습니다. 해당 URL에 POST 메소드로 접근할 수 있습니다.

$ curl -X POST https://<your-invoke-url>/prod
null

이번에도 역시 슬랙에 메시지가 표시되면 제대로 설정된 것입니다.

이전 글에서는 AWS Lambda 함수를 생성해서 슬랙에 메시지를 보내는데 성공했습니다. 그런데 이때 사용하는 훅 URL이 그대로 코드에 들어가 있는게 마음에 걸립니다. 이 URL을 알면 외부에서 우리 슬랙 채널에 스팸을 보낼 수 있습니다.

이번 글에서는 AWS KMS(Key Management Service)를 이용해서 이 URL을 보호하는 방법에 대해서 얘기합니다.

AWS Lambda 생성시 blueprint에 slack-echo-command를 선택하면 이미 URL을 보호하는 절차에 대해서 설명하고 있습니다.

To encrypt your secrets use the following steps:

  1. Create or use an existing KMS Key - http://docs.aws.amazon.com/kms/latest/developerguide/create-keys.html

  2. Click the "Enable Encryption Helpers" checkbox

  3. Paste into the kmsEncryptedToken environment variable and click encrypt

하나씩 따라해 봅시다.

AWS IAM 화면에 들어가면 Encryption keys 메뉴가 있습니다. 다른 메뉴와는 달리 여기는 지역 선택이 필터 부분에 있습니다. 여기서 원하는 지역을 선택하고 'Create Key'를 선택합니다.

Create key

Alias만 입력하고 나머지는 특별히 선택하지 않아도 됩니다.

키가 생성되면 생성한 Lambda 함수로 이동합니다. 코드 섹션 밑에 보면 'Enable encryption helpers' 체크박스가 있습니다. 활성화하고 'Encryption key'에 생성한 키를 설정합니다.

'Environment variables'에 훅 URL을 넣고 'Encrypt'를 누르면 URL이 암호화됩니다.

Before encrypt

After encrypt

변경사항을 저장하면 암호화된 URL이 표시됩니다.

Encrypted URL

이제 이 값을 이용하도록 Lambda 코드를 수정합니다.

'use strict';

const AWS = require('aws-sdk');
const url = require('url');
const https = require('https');

let hookUrl;

function sendMessage(message) {
  // ... 이전과 같음
}

function decryptHookUrl() {
  if (hookUrl) {
    return Promise.resolve(hookUrl);
  } else {
    const kms = new AWS.KMS();
    return new Promise((resolve, reject) => {
      const blob = new Buffer(process.env.encryptedHookUrl, 'base64');
      kms.decrypt({ CiphertextBlob: blob }, (error, data) => {
        if (error) {
          return reject(error);
        }
        hookUrl = data.Plaintext.toString('ascii');
        resolve(hookUrl);
      });
    });
  }
}

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

저장하고 테스트를 실행하면 이전과 같이 슬랙 채널에서 메시지를 볼 수 있습니다.