Github Webhook 이용하여 사이트관리
By Hyuksoo Kim, CTO & Co-Founder at ReturnValues (hyuksoo.kim@returnvalues.com)
ReturnValues에서 운영하고 있는 사이트가 점점 늘어서 변경사항이 생기거나 할 때, 업데이트 하는 것도 조금의 수고스러운 일이 되어버렸다. 이젠 자동화를 하지 않으면 업무에 방해가 될 상황인 것 같다.
… 등등 (현재 관리되고 있는 웹사이트가 10개가 넘고, 빠르게 증가되고 있다)
어떤 방식으로 해야할까. Jenkins 같은 ci 도구를 잠깐 생각했지만, 그냥 현재 우리에게 가장 간단한 방식으로 하는 것으로 결정.
현재 ReturnValues 모든 프로젝트 소스는 github으로 유지되고 있고, github에서는 webhook 이라는 서비스를 제공하고 있어서 이걸 사용하기로 한다.
대략적인 흐름은 아래와 같이 설명할 수 있겠다.
- remote repository(github)에 push
- gh-release 모듈 이용해서 tag 로 생성
- tag 생성 이벤트 받아서 webhook 서비스로 tag 정보 전달
- webhook service에서 tag 정보를 이용하여 해당 스크립트 실행(repository에 대응하는 shell script)
- web server application 소스를 업데이트 하는 스크립트 실행하여 서버 재시작
- Webhook service: 관리하고 있는 사이트별로 스크립트를 관리한다.
- Web Server: 각 사이트별로 애플리케이션이 존재하고, github 과 연결되어 있다.
- Github: 각 프로젝트에 setting>webhook 에 webhook service를 등록해둔다.
gh-release 사용 예
“scripts”: {
“release”: “node_modules/.bin/gh-release -t $VER -n $VER -c master -b init”
}
위와 같이 작성된 스크립트를 ‘VER=1.0.1 npm run release’ 로 실행하면 아래 이미지처럼 github 에 Tags 로 등록된다.
중요: gh-release 사용 시, Root 경로에, CHANGELOG.md 파일이 있어야 합니다.
Webhook 설정하기
github에 webhook 세팅하기
Webhooks 에 들어가서 Add webhook 진행하면, 아래처럼 Branch or tag creation 에 체크해야 한다, 참고로 기본 세팅은Just the push event
로 되어 있다.
Payload URL은 webhook 을 이벤트를 처리할 서비스 URL이다. 아래에 webhook service 소스를 참고하면 된다.
secret
입력항목은 webhook 서비스 인증 시, 필요로 한다. 우리 경우에는 소스에 직접 하드코딩해서 넣어둠.
github 에 webhook 세팅결과 위의 정보를 입력하면, 아래처럼 등록이 된 결과를 확인할 수 있다.
Webhook 서비스 생성
index.js (webhook service application)
소스에서 관심있게 볼 부분은 highlight 처리해둔 부분인데, 각 서버 애플리케이션별로 webhook을 라우팅하게 되는데요. 각자의 환경에 맞게 별도의 스크립트를 가져가면 webhook 서비스 수정없이 다른 사이트들도 처리할 수 있게 됩니다.
const http = require('http');const spawn = require('child_process').spawn;const crypto = require('crypto');const url = require('url');const secret = '<secret word>';const port = 18081;
http.createServer((req, res) => { const { headers, method, url: reqUrl } = req; console.log("request received"); res.writeHead(400, {"Content-Type": "application/json"}); const ip = req.headers["X-Forwarded-For"] || req.connection.remoteAddress; const path = url.parse(reqUrl).pathname; if(process.env.NODE_ENV !== 'development') { if(path !== '/push' || req.method !== 'POST'){ const data = JSON.stringify({"error": "invalid request"}); return res.end(data); } }let payloadString = '';req.on('data', (data) => { payloadString += data;});req.on('end', () => { const hash = "sha1=" + crypto.createHmac('sha1', secret).update(payloadString).digest('hex'); if(process.env.NODE_ENV !== 'development') { if(hash != req.headers['x-hub-signature']){ const data = JSON.stringify({"error": "invalid key", key: hash}); return res.end(data); } }payloadString = decodeURIComponent(payloadString).substring(8);const payload = JSON.parse(payloadString);const { ref, ref_type } = payload;const { name: repoName } = payload.repository;const shellFile = `${repoName}.sh`;if (ref_type === 'tag') { console.log(`Execute Shell Script: sh ${shellFile} ${ref}`); const runSh = spawn('sh', [shellFile, ref]); runSh.stdout.on('data', (data) => { const buff = new Buffer(data); console.log('stdout::', buff.toString('utf-8')); });} const responseBody = { headers, method, reqUrl, payload }; res.writeHead(200, {"Content-Type": "application/json"}); return res.end(JSON.stringify(responseBody)) });}).listen(port);console.log("Server listening at " + port);
샘플1) returnvalues-academy.sh(Webhook Service 애플리케이션)
#!/bin/bash
# DIRECTORY TO THE REPOSITORY
REPOSITORY=”/var/www/virtualhosts/returnvalues-academy/frontend”
echo $1 >> logs/returnvalues-academy-$1.txt
cd $REPOSITORY
VER=$1 npm run www:webhook
샘플2) package.json (returnvalues-academy 애플리케이션)
“scripts”: {
“www:webhook”: “git fetch origin refs/tags/$VER:refs/tags/$VER && git checkout $VER”
},
샘플1의 VER=$1 npm run www:webhook
을 통해서, 샘플2의 www:webhook 을 실행하게 된다.
전체 소스는 webhook-public에서 확인하실 수 있습니다.