Python Flask를 이용해 Bitbucket Webhooks <-> Jenkins 중계하기 (부제: 특정 브랜치만 CI로 트리거하기)

Development / Python / Flask / jenkins / bitbucket

  1. 문제

    우리 회사에서는 소스 저장소로 Bitbucket을 사용하고 있다. (Atlassian 3총사인 Jira와 Confluence를 같이 활용하게 위해.. 결국 이슈 트래킹은 Trello로 넘어갔지만.. 여튼.) 아직은 개발 단계인 우리 프로젝트 상황상, 개발 서버와 실 서버를 분리해 운영하고 있지 않아 메인 개발 브랜치(develop)에 머지된 소스가 자동으로 Jenkins를 통해 단일 서버로 배포되는 CI 시스템이 구축되어 있었다. (이 때 Bitbucket의 Integrations Services와 Jenkins의 Bitbucket Plugin을 활용해 Bitbucket의 레포에 소스가 머지되면 자동으로 Jenkins에 알림이 가도록 하였다.)

    그런데 이제 실 서비스를 준비하며 개발 서버와 실 서버를 나누어 운영하다 보니, 메인 개발 브랜치(develop)에 머지된 소스는 개발 서버로, 릴리즈 브랜치(master)로 머지된 소스는 실 서버로 배포를 해야 하는데 Bitbucket의 Webhook(Serivces)은 어떠한 브랜치에 머지가 되던지간에 Jenkins로 Hook을 보내는 매우 멍청한 상황을 연출해주었다. 가령 develop 브랜치에만 머지가 되었음에도, 괜시리 master용 Jenkins 프로젝트에까지 Hook을 보내서 master를 괜시리 한 번 더 빌드시키게 되었던 것이다. 즉 쉽게 말해, 특정 브랜치만 이벤트를 트리거하는 옵션이 없었다!

    간략히 구글링을 해보니, Relay 서버를 따로 만들고 중간에 분기를 나누어주는 방법이 가장 좋아보여 Python Flask를 이용해 간단한 웹 서버를 구축해보았다.

  2. 프로토콜

    기존에 사용하던 Bitbucket Services(곧 deprecate 될 것이라 한다.)는 POST 방식으로 다음과 같이 메시지를 보내는 것을 확인하였다.

    [URL]
    http://(젠킨스주소)/job/(프로젝트명)/build?token=(토큰)&cause=(트리거된이유)
    
    
    [METHOD]
    POST
    
    
    [BODY]
    없음
    

    아예 브랜치 정보가 담겨가지를 않더라ㅠ.ㅠ..

    그래서 조금 더 고급 버전인 Bitbucket Webhooks을 이용하면서, 트리거를 Choose from a full list of triggers -> Pull Request -> Merged로 선택하였더니 다음과 같은 내용으로 POST 메시지를 보내는 것을 알 수 있었다.

    [URL]
    (Webhook Configuration에서 지정한 주소)
    
    
    [METHOD]
    POST
    
    
    [BODY(application/json)]
    {
    ...(생략)...
        "pullrequest": {
            ...(생략)...
            "destination": {
                ...(생략)...
                "repository": {
                    ...(생략)...
                    "name": (머지된 목적 레포 이름)
                },
                "branch": {
                    ...(생략)...
                    "name": (머지된 목적 브랜치 이름)
                },
            },
            "source": {
                ...(생략)...
                "repository": {
                    ...(생략)...
                    "full_name": (PR을 보내온 원래 레포의 풀 이름 e.g. user_id/repo_name)
                },
            "id": (PR 번호),
            "closed_by": {
                ...(생략)...
                "username": (PR을 머지한 사용자 아이디)
            }
        }
    }
    

    일단 중요한 것들(사용하고 싶은것들)만 추려보니 위와 같았다. 이제 실제 Flask 어플리케이션을 만들어보자.

  3. Flask Application Source

    일단 코드 스니펫만...

    # -*- coding: utf-8 -*-
    
    
    from flask import Flask, request
    import requests
    import json
    from urllib import quote
    import sys
    
    
    reload(sys)
    sys.setdefaultencoding('utf-8')
    
    
    app = Flask(__name__)
    
    
    @app.route('/hook', methods=['POST'])
    def hook():
    
    
    
    resp_code = 200
    resp_str = ''
    resp_header = {'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-cache', 'Pragma': 'no-cache'}
    
    
    try:
    
    
        body = json.loads(request.data.encode('utf-8'))
        if 'pullrequest' in body:
            repository_name = body['pullrequest']['destination']['repository']['name']
            if repository_name == '(레포 이름 확인)':
                branch_name = body['pullrequest']['destination']['branch']['name']
                if branch_name == 'master':
                    job_name = '(해당 브랜치일 때 어디로 보낼지)'
                elif branch_name == 'develop':
                    job_name = '(그럼 이 브랜치일 때는 어디로 보낼지)'
                else:
                    resp_code = 400
                    return
    
    
                cause = quote('*Source Branch: {0} *PR Number: #{1} *Dest. Branch: {2} *Merged by: {3}'.format(
                    body['pullrequest']['source']['repository']['full_name'],
                    body['pullrequest']['id'],
                    branch_name,
                    body['pullrequest']['closed_by']['username']
                ))
                url = 'http://(Bitbucket service를 받는 원래 Jenkins의 주소)/job/{0}/build?token=(토큰)&amp;cause={1}'.format(
                    job_name,
                    cause
                )
                requests.post(url)
                resp_code = 200
            else:
                resp_code = 400
    
    
    except Exception as e:
        resp_code = 500
    
    
    finally:
        return resp_str, resp_code, resp_header
    
    if __name__ == '__main__': app.run(host='0.0.0.0', port=(임의포트, 젠킨스와 같은 서버에 띄워둘 것이라면 중복되지 않도록 한다.), debug=False)

    테스트 코드는 알아서..

  4. Bitbucket 설정

    위와 같은 프로그램을 만들어 서버에 띄웠다면, 이제 Bitbucket에서 Webhook 설정을 해야한다. 레포의 Settings -> INTEGRATIONS -> Webhooks에 들어가, Add Webhook을 한다. Title은 원하는 대로, URL은 위에서 만든 Flask의 End-Point로 설정한다. 그리고 Triggers는 Choose from a full list of triggers를 눌러 나머지는 다 해제하고 Pull Request의 Merged만 체크한다. (우리는 모든 소스가 포크떠간 각 개인의 레포에서 PR을 통해서만 메인 레포에 붙게끔 되어 있기 때문에 이걸 사용했지만, 다른 경우라면.. 혹 바로 푸시하는 환경이라면 각자에 맞게 하면 된다. 단 Webhook의 프로토콜 바디가 바뀔 수 있으니 확인해봐야 함.) 저장을 누르고, 실제 PR을 Merge해서 Flask로 정상 호출이 되는지 확인한다.

  5. Jenkins 설정

    이건 뭐.. 알아서 하면 된다. 설정만 잘 한다면 Bitbucket Webhook -> Realy Server (Flask) -> Bitbucket Intergration service를 가장한 분기 태운 가짜 요청 -> Jenkins -> 빌드 로직을 잘 탈 것이다! 간만에 개발 일기 끝!

Share on : Twitter, Facebook or Google+