프로그래밍/Android

[Bitrise Cli] 리눅스 서버에서 CI/CD pipeline 직접 구축해보기 (2)

Lou Park 2022. 8. 7. 00:36
CI/CD pipeline 직접 구축해보기 (1편) https://jizard.tistory.com/405
CI/CD pipeline 직접 구축해보기 (2편, 현재 포스트) https://jizard.tistory.com/410

 

지난 포스팅에서는 linux 서버에 Bitrise Cli를 설치하고 구동하는 과정을 완성했다. 하지만 "Continuos"가 빠졌다. 지속적 배포를 위해서는 주기적으로 자동으로 돌아가거나 코드를 푸쉬할때 수행되는 장치가 필요하다. 그래서 Node.js Express를 이용해 Github Webhook을 받을 수 있는 웹 서버를 구축해볼 것이다.

 

앞선 Bitrise 과정도 Docker로 시작할걸...이라는 후회와함께 일단 Docker로 node.js를 구동할 준비를 한다. 전체 프로젝트 파일 트리는 다음과 같다.

githook/
├── dkc/
│   ├── docker-compose.yaml
│   └── nginx.conf
├── node_modules
├── app.js
├── Dockerfile
├── entrypoint.sh
├── package.json
├── start_bitrise.sh
└── yarn.lock

 

Express로 웹서버 만들기

필요한 패키지는 express와 express-github-webhook뿐이다. 설치해주자!

#yarn이 설치되어있지 않은 경우 npm i -g yarn
yarn add express express-github-webhook

 

먼저 간단하게 서버를 실행시켜보자.

node app.js 명령어로 구동 후 http://localhost:3000에 들어가서 Hello!가 뜨면 성공이다.

const express = require('express');

const app = express();

app.use(express.json());
app.use(express.urlencoded({ extended: false }));

app.get('/', (req, res) => {
    return res.send("Hello!")
})

app.listen(process.env.PORT || 3000, () => {
    console.log('Server started')
})

 

이제는 express-github-webhook을 이용해 Webhook 이벤트를 처리해주는 Webhook Handler를 추가해줄 것이다. path는 webhook을 받을 곳이며, 그리고 secret을 통해 신뢰할 수 없는 출처의 요청이 오지 않도록 설정해줄 수도 있다.  자세한 내용은 코드에 주석으로 적어두었다.

const GithubWebHook = require('express-github-webhook');

const webhookHandler = GithubWebHook({
    path: '/webhook', secret: 'YOUR SECRET'
});

app.use(webhookHandler);

webhookHandler.on('*', (event, repo, data) => {
    // 모든 이벤트에서 호출된다.
    // @param repo 리포지토리 이름
    // @param event 발생한 이벤트 ex) push, pull_request, release, star...
})

webhookHandler.on('push', (repo, data) => {
    // 푸시를 받을 때마다 호출된다.
})

webhookHandler.on('LouAutoCi', (event, data) => {
    // Github에서 LouAutoCi라는 리포지토리에 대해서 이벤트가 왔을때 호출된다.
})

webhookHandler.on('error', (err, req, res) => {
    // 에러가 있을때마다 호출된다.
})

 

나는 특정 branch로 push가 될때 빌드를 작동시키려고하는데, 따라서 이렇게 처리해두었다.

webhookHandler.on('push', (repo, data) => {
	// repo가 내 안드로이드 리포지토리가 아니면 수행하지 않는다.
    if (repo != 'LouAutoCi') {
        return
    }
    // data는 Webhook의 Payload가 그대로 옴
    // branch이름에 따라 하는일을 다르게 한다.
    let branch = data.ref.replace("refs/heads/", "")
    switch (branch) {
        case "test/production":
            executeScript('gn_production', branch, 'productionRelease')
            break;
        case "test/apptester":
            executeScript('gn_apptester', branch, 'productionRelease')
            break;
        case "test/apptester_debug":
            executeScript('gn_apptester', branch, 'productionDebug')
            break;
    }
})

webhookHandler.on('error', (err, req, res) => {
    console.error(err);
})

data 부분은 Github 리포지토리에서 Settings > Webhooks > Recent Deliveries에서도 확인할 수 있고, 대략 아래처럼 생겼으며 이벤트마다 형식이 약간 다르다. push 이벤트에 대한 Payload 전체는 여기에서 확인할 수 있다. 필요한 부분이있다면 보고 골라쓰면된다.

Webhook payload example

자, 다음 executeScript()는 shell script를 실행하는 함수다. 

현재 돌고있는 자식 프로세스에 대한 정보를 runningChild에 담아두고 있다가, 이미 실행중인 프로세스가 있다면 Kill하고 최신으로 들어온 요청에 대한 process만 돌아가도록 처리해두었다. 

const path = require('path');
const { spawn } = require('child_process');

var runningChild = null;

function executeScript(workflow, branch, variant) {
    if (runningChild != null) {
        // 이미 돌아가고 있는 Process가 있다면 Kill
        console.log(`child process is running, kill ${runningChild.pid}`)
        process.kill(runningChild.pid, 'SIGKILL');
    }

    let command = path.join(__dirname, 'start_bitrise.sh');

    console.log(`=== your command ===\n${command}`)
    runningChild = spawn(command, [workflow, branch, variant]);

    runningChild.stdout.on('data', (data) => {
        console.log(`${data}`)
    })

    runningChild.on('error', (err) => {
        console.trace(err);
    })

    runningChild.on('exit', (code) => {
        console.log(`[${runningChild.pid}] child process exited with code ${code}`);
        runningChild = null;
    })
}

spawn()시에 start_bitrise.sh라는 shell script에 워크 플로우, 브랜치, 배리언트에 대한 인자를 전달하고 있다. start_bitrise.sh는 이전 포스팅에서 run.sh와 동일, Bitrise를 구동시켜주는 내용이다. 

#!/bin/bash
if [ "$1" == "--help" ]; then
	echo "[[ Bitrise Workflow를 구동합니다 ]]"
	echo "이용법: ./start_bitrise.sh <variantName> <branchName>"
	echo ">> 1. workflow: Bitrise Workflow ex) production"
	echo ">> 2. branch: Github Branch이름 ex) release/production"
	echo ">> 3. variant: Build Varaint이름 ex) productionRelease"
	exit 0
fi

if [[ "$1" == "" ]]; then
	echo "workflow가 정의되지 않았습니다."
	exit 0
fi

if [[ "$2" == "" ]]; then
	echo "branch가 정의되지 않았습니다."
	exit 0
fi

if [[ "$3" == "" ]]; then
	echo "variant가 정의되지 않았습니다."
	exit 0
fi

echo "** SELECTED WORKFLOW: $1 **"
echo "** SELECTED BRANCH: $2 **"
echo "** SELECTED VARIANT: $3 **"

cd ~

sudo bitrise envman add --key BUILD --value $1
sudo bitrise envman add --key BRANCH --value $2
sudo bitrise envman add --key VARIANT --value $3
sudo bitrise envman run bitrise run $1

 

중간에 Home 디렉토리로 이동하는데, bitrise .envstore.yml이 있는 장소로 가주면된다. (안그러면 환경변수를 찾지 못하더라...)

 

도메인 등록하기

서버는 개발 됬고, 이제 외부에서 접근 가능하도록 해주어야하는데 만약에 도메인이 있으면 따로 설정해주면되고, 도메인이 없는 작고 소중한 로컬호스트의 경우 ngrok이라는 서비스를 이용하면된다. https://www.npmjs.com/package/ngrok 손쉽게 외부에서 접속가능한 환경으로 만들 수 있다.

 

나는 도메인이 있는 상태라 빠르게 docker 설정 정도만올리고 Github 설정으로 넘어가겠다!

 

Docker로 구동시 설정

- Dockerfile

FROM node:16
RUN apt-get update && \
    apt-get install vim -y
WORKDIR /src
COPY package.json .
RUN yarn install
COPY . .
ENTRYPOINT ["/bin/sh", "/src/entrypoint.sh"]

- entrypoint.sh

node app.js

- dkc/nginx.conf

events {
    worker_connections 4096;
}

http {
    server {
        listen 80;
        listen [::]:80;
        access_log /var/log/nginx/access.log;

        location / {
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;

            proxy_pass http://app:3000;
        }
    }
}

 

- dkc/docker-compose.yaml

version: '3.4'
services:
  nginx:
    depends_on:
      - app
    restart: unless-stopped
    image: nginx:1.19-alpine
    volumes:
     - ./nginx.conf:/etc/nginx/nginx.conf
    ports:
      - 80:80
  app:
    restart: unless-stopped
    build: ../
    working_dir: /src
    volumes:
      - ../:/src
    ports:
      - 3000:3000

 

Github에서 Webhook 설정

자신의 Github Repository로가서 Settings > Webhooks 메뉴에 들어가면 아래와 같이 나올 것이다.

Add webhook을 이용해 새로운 웹 훅을 추가하자.

 

Payload URL

Payload URL에는 방금 만든 서버의 도메인 주소를 적어주면되는데, 끝에는 webhook을 받을 path까지 붙여줘야한다. 예를들어 https://my-webhook.com/webhook 같은식이다.

 

Content type

form-urlencoded와 JSON형식 둘 중 하나를 선택할 수 있다. 우리가 방금 만든 웹서버는 둘 다 지원하므로, 편한걸 고르면된다.

 

Secret

webhook 구현시 Secret를 지정했다면 똑같이 복사하여 붙여넣는다.

다음으로는 아마 어떤 이벤트로 웹훅을 트리거할 것인지 물어보는 창이 나올 것이다. 우리는 push 이벤트에 대해서만 구현해서 Just the push event로도 충분하지만, 추가로 다른 이벤트에 대해서 구현했을 경우 Let me select individual events 옵션에서 따로 설정해준다. Add webhook을 해주면 끝이다. 끝!