크로키에서는 로그를 JSON 문자열로 만들어 일자별(혹은 시간별)로 묶은 후 gzip으로 압축해서 저장하고 있습니다. 그런데 이미 만들어진 로그를 수정해야 하는 일이 생겼습니다.

예를 들면

{"date":"Fri Jan 15 2016 00:00:01 GMT+0000 (UTC)","path":"/foobar"}
{"date":"Fri Jan 15 2016 00:00:03 GMT+0000 (UTC)","path":"/croquis"}
{"date":"Fri Jan 15 2016 00:00:10 GMT+0000 (UTC)","path":"/awesome"}

였던 데이터를

{"date":"2016-01-15T00:00:01.000Z","path":"/foobar"}
{"date":"2016-01-15T00:00:03.000Z","path":"/croquis"}
{"date":"2016-01-15T00:00:10.000Z","path":"/awesome"}

처럼 바꿔야 했습니다.

로그 전체를 읽어와서 줄별로 변환하고 다시 기록하면 되는 일이지만 로그가 커서 잘 동작하지 않았습니다.

그래서 유닉스의 파이프 형태로 처리하기로 했습니다.

위의 작업을 하는 Node.js 프로그램은 다음과 같습니다.

const byline = require('byline');
const stream = byline(process.stdin);
stream.on('data', function (line) {
  const data = JSON.parse(line);
  data.date = new Date(data.date).toISOString();
  console.log(JSON.stringify(data));
});

byline은 스트림을 줄별로 처리할 수 있게 해주는 모듈입니다. 줄별로 들어온 JSON 문자열을 파싱하고 원하는 처리를 한 후 다시 쓰기만 하는 단순한 코드입니다. 이 프로그램을 유닉스 파이프라인에 넣으면 원하는 결과를 얻을 수 있습니다.

$ gunzip -c original/01.data.gz | node convert.js | gzip > converted/01.data.gz

보너스

이 작업을 순수히 Node.js만 가지고도 할 수 있습니다.

const fs = require('fs');
const stream = require('stream');
const zlib = require('zlib');
const byline = require('byline');

class Convert extends stream.Transform {
  constructor(options) {
    super(options);
  }
  _transform(line, encoding, callback) {
    const data = JSON.parse(line);
    data.date = new Date(data.date).toISOString();
    callback(null, JSON.stringify(data)+'\n');
  }
}

fs.createReadStream('original/01.data.gz')
  .pipe(zlib.createGunzip())
  .pipe(byline())
  .pipe(new Convert())
  .pipe(zlib.createGzip())
  .pipe(fs.createWriteStream('converted/01.data.gz'));

하지만 파이프의 특성상 하나의 프로그램이 하나의 작업만 할 수록 응용하기가 편해집니다. 예를 들어 파일이 로컬에 있는게 아니고 S3에 있다면 유닉스 파이프로는 다음과 같이 바꾸면 됩니다.

$ aws s3 cp s3://mybucket/stream.txt - | gunzip | node convert.js | gzip > converted/01.data.gz

Node.js로도 작성할 수 있지만 훨씬 많은 코드를 작성해야겠죠.

이제 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

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