GitHub Actions로 간단히 CI 서버 대신하기

이 글은 2019년 12월 17일에 작성하였습니다. 시간이 지남에 따라 문서의 내용이 유효하지 않을 수 있습니다.

책상 위에 파이프라인을 구분해둔 보드판이 보임. 각 단계는 Stories, Todo, In Progress, Testing, Done이라고 이름이 붙어있다.

회사에서 CI/CD 도구로 Bamboo를, 코드 저장소로 GitHub를 이용하고 있다. CI 파이프라인을 구축하면서 Bamboo를 GitHub와 연결해서 아래의 흐름을 만들려고 했으나 머지 브랜치를 상대로 CI 빌드를 할 수 있는 방법을 찾지 못하였다.

Pull Request -> Merge Branch 생성 -> Build -> Test 파이프라인을 표현하는 그림

정확하게, GitHub PR의 머지 브랜치를 참조하는 방법을 Bamboo가 지원하지 않는다. Bamboo는 Atlassian 제품(Bitbucket 같은)과 궁합이 잘 맞는 대신, GitHub에 대한 지원은 빈약하다. GitHub을 이용한다면 Bamboo에서 Pull Request 이벤트를 받아서 베이스 브랜치를 기준으로만 빌드를 할 수 있다. GitHub를 모른 척 할 수 있는 패기에 감탄 :(

​ 어쨌든 나는 살아야겠으니 대안을 찾자.

01. 대안을 찾아서…

Jenkins라는 대안이 있지만 직접 설치를 해야 하는 게 부담이다. Bamboo에, 모든 프로젝트의 배포 플랜을 모으는 게 회사 정책이라 CI 서버를 양쪽에 두면 혼란할 것 같다. 손 안대고 코를 풀고 싶다. 방법이 없을까?

고민을 하다가 배포 파이프라인에서 CI 빌드 단계만 GitHub Actions로 이전하는 걸 검토하기로 했다.

02. GitHub Actions, 무엇에 쓰는 물건?

GitHub Actions는 GitHub에서 제공하는 워크플로우(Workflow) 자동화 도구다. 워크플로우라는 단어가 의미하듯 테스트, 빌드, 배포 뿐 아니라 다양한 작업을 만들어서 자동으로 실행하게 할 수 있다. PR에 특정 코멘트를 남기면 자동으로 머지를 하게 만들 수 있고, 데이터를 수집하는 데에 이용할 수도 있다. 누구는 부동산 정보를 수집하기도 하더라.

GitHub hosts Linux and Windows runners on Standard_DS2_v2 virtual machines in Microsoft Azure with the GitHub Actions runner application installed. The GitHub-hosted runner is a fork of the Azure Pipelines Agent. For more information about the Standard_DS2_v2 machine resources, see “DSv2-series” in the Microsoft Azure documentation. - https://bit.ly/2EilXFd

GitHub Actions는 Runner 위에서 실행이 되고, Runner는 가상 머신 위에서 실행이 된다. GitHub는 두 종류의 Runner를 제공한다. GitHub-hosted Runner, 그리고 Self-Hosted Runner.

GitHub-Hosted Runner는 MS Azure의 가상 머신 위에서 돌아간다. 아래에 보이듯이 아직 가상 머신의 사양이 그렇게 좋지는 않다.

로컬에서 9분이 걸리는 CI 빌드를, GitHub-Hosted에서 수행하는 데에 14분이 걸렸다. 만족스럽지는 않지만 쓸만한 정도랄까. Self-Hosted Runner를 이용하면 직접 가상 머신을 임대해서 GitHub Actions 실행 환경을 만들 수 있다(고 한다). GitHub Actions를 로컬에서 실행할 수도 있다. 해보지 않았으니 자세한 내용은 생략.

Adding self-hosted runners - GitHub Help

아직 베타 버전이고, 라이센스 플랜에 따라 사용하는 데에 제한이 있지만 프론트엔드 저장소를 빌드하는 목적으로 쓰기에는 충분해 보인다.

GitHub plan Total concurrent jobs Maximum concurrent macOS jobs
Free 20 5
Pro 40 5
Team 60 5
Enterprise 180 15

03. 사용 신청을 해보자!

아직 베타 서비스이기에 따로 신청을 해야만 사용할 수 있다. 신청을 하면 저장소 페이지의 상단 탭에 Actions 메뉴가 생긴다.

GitHub의 저장소 페이지 상단 탭에 Actions 메뉴가 보인다.

04. Action과 Workflow

Action은 사용자가 작성하는 실행 가능한 코드 덩어리로, 하나의 작업을 설명한다. Workflow는 Action을 실행하는 환경과 단계를 서술한다.

하나의 Workflow는 여러 개의 Action을 실행할 수 있다. 그래서 이름이 GitHub Actions다. Actions 페이지에 들어가 보면 저장소에서 사용하는 기술에 맞는 Workflow 템플릿을 추천 해준다.

GitHub가 제시하는 추천 Workflow가 보이는 이미지. Node.js, Node.js Package, Rust, Python package 등.

프론트엔드 애플리케이션의 CI 빌드 워크플로우를 작성하기 위해서 Node.js Workflow를 선택했다.

기본으로 제공하는 샘플 Node.js Workflow

Workflow 파일은 .github/workflows 디렉터리에 위치한다. 화면에 보이는 것처럼 GitHub의 Actions 탭에서 직접 작성할 수 있다. 로컬에서 소스 코드처럼 작성하고 GitHub 원격 저장소로 Push를 할 수도 있다.

Action을 npm 패키지처럼 Marketplace에 배포를 하면 우측의 GitHub Marketplace에 노출되어 다른 사용자와 공유할 수 있다. 물론 다른 사람이 작성한 Action을 이용하는 것도 가능. ​

05. Pull Request → Test 실행하기

Node.js Workflow를 선택하면 아래의 yml이 만들어진다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
name: Node CI

on: [push]

jobs:
  build:

    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [8.x, 10.x, 12.x]

    steps:
    - uses: actions/checkout@v1
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v1
      with:
        node-version: ${{ matrix.node-version }}
    - name: npm install, build, and test
      run: |
        npm ci
        npm run build --if-present
        npm test
      env:
        CI: true

YAML 문법으로 작성하기에, 몇 가지 패턴만 익히면 누구나 쉽게 작성할 수 있다. 내가 참여하고 있는 프로젝트는 GitHub Packages + GitHub Private Repository + Yarn을 이용하고 있기 때문에 이 Workflow를 그대로 사용할 수 없다. Wowrkflow를 수정하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
name: CI Build

on: [pull_request]

jobs:
  build:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [12.x]

    steps:
      - uses: actions/checkout@v1

      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v1
        with:
          node-version: ${{ matrix.node-version }}
          registry-url: "https://npm.pkg.github.com"

      - name: Set up SSH Agent
        uses: webfactory/ssh-agent@v0.1.1
        with:
          ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}

      - name: Install dependencies
        run: yarn install
        env:
          NODE_AUTH_TOKEN: ${{ secrets.GPR_AUTH_TOKEN }}
          CI: true

      - name: Run Build
        run: yarn build

      - name: Run Tests
        run: yarn run test:ci

이제 저장소로 PR을 보내면 아래와 같이 GitHub이 Workflow를 실행하는 모습을 볼 수 있다. 다른 설정을 할 필요가 없다.

PR 페이지에서 Status Check가 진행중이며, Some checks haven't completed yet이라는 문장이 보인다.

체크 섹션의 Details를 클릭하거나, PR 페이지 → Checks 탭을 클릭하면 실행 로그를 볼 수 있다.

Checks 탭에서 실행하고 있는 Workflow의 진행 로그를 보여주는 화면을 볼 수 있다.

성공하면 이렇게 바뀐다.

Status Check를 통과한 화면. All checks have passed라는 문장이 보인다.

06. Workflow 파일, 하나씩 뜯어 보기

문법이 매우 직관적이고 공식 문서도 잘 정리되어 있어서 누구나 30분만 공부하면 금방 Workflow를 만들 수 있다.

Automating your workflow with GitHub Actions

그것마저도 귀찮은 누군가를 위해서 내가 위에서 작성한 워크 플로우를 단계별로 간단히 살펴본다. ​

1) 워크 플로우 이름

1
name: CI Build

name은 이 워크 플로우의 이름을 의미한다.

2) 실행 시점

1
on: [pull_request]

on은 이 워크 플로우를 실행할 시점을 설정한다. pull_request는 GitHub로 PR을 보낼때, PR에 커밋을 추가할 때마다 실행하라는 뜻이다. Webhook 처럼 이벤트 시점을 등록할 수 있고, POSIX cron 문법으로 직접 스케줄을 정의할 수도 있다.

1
2
3
on:
  schedule:
    - cron:  '*/15 * * * *'

Events that trigger workflows

3) 머지 브랜치 체크아웃

1
- uses: actions/checkout@v1

uses는 사용할 액션의 패키지 이름을 지정한다. actions/checkout@v1은 저장소를 설정하고 PR의 머지 브랜치를 체크아웃한다.

사용할 수 있는 Action은 Marketplace에서 검색할 수 있다. npm package registry와 비슷한 친구다. 직접 로컬에 Action을 작성한 경우에는 로컬 디렉터리를 지정해줄 수도 있다.

1
- uses: ./.github/actions/coverage-commenter

사용자 정의 Action을 만들려면 GitHub이 안내하는 방식을 따라야 한다. 아래 문서에 잘 나와 있다.

Creating a JavaScript action

또는 직접 Marketplace에 올라온 Action의 코드(예, jest-reporter-action)를 살펴보는 것도 좋다. ​

4) Node.js 환경 설정

1
2
3
4
5
- name: Use Node.js ${{ matrix.node-version }}
  uses: actions/setup-node@v1
  with:
    node-version: ${{ matrix.node-version }}
    registry-url: "https://npm.pkg.github.com"

이후 Workflow를 실행할 Node.js 환경을 설정한다. 사설 npm 저장소를 이용하는 경우에 registry-url을 옵션으로 설정해야 한다. https://npm.pkg.github.com은 GitHub Packages Registry를 가리킨다.

${{ ... }}은 Runner를 실행하는 문맥(Contexts)에 대한 정보나 환경 변수(Environment Variables)를 참조할 때 이용하는 표현식이다. ​

5) SSH Agent 설정

1
2
3
4
- name: Set up SSH Agent
  uses: webfactory/ssh-agent@v0.1.1
  with:
    ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}

Private 저장소를 이용한다면 SSH 비밀키를 설정해야 한다. webfactory/ssh-agent@v0.1.1는 SSH Agent에 비밀키를 설정하는 걸 돕는다. 보안에 예민한 접근 제한키나 토큰과 같은 정보는 직접 워크 플로우 파일에 작성하기가 찜찜하다. 그래서 GitHub는 Secrets라는 기능을 제공한다.

​이 기능은 저장소 > Settings > Secrets에서 이용할 수 있다.

Secrets에 환경 변수를 등록하면 GitHub는 이를 암호화해서 들고 있다가, 런타임에 secrets의 프로퍼티로 제공한다. 참고로 Secrets에 입력한 값은 자동으로 암호화(Encrypted)가 된다. 토큰을 Secrets에 등록할 때는 암호화하지 않은 버전을 등록해야 한다는 걸 주의하자. ​

6) 패키지 설치

1
2
3
4
5
- name: Install dependencies
  run: yarn install
  env:
    NODE_AUTH_TOKEN: ${{ secrets.GPR_AUTH_TOKEN }}
    CI: true

run은 CLI 명령어를 실행할 때 사용한다. env를 이용해 Runner에 환경 변수를 설정할 수 있다. 사설 npm 저장소를 이용하기에 인증 토큰을 NODE_AUTH_TOKEN 환경 변수에 설정했다. ​

7) 빌드, 테스트

1
2
3
4
5
- name: Run Build
  run: yarn build

- name: Run Tests
  run: yarn run test:ci

마지막으로 소스 코드를 빌드하고 테스트를 실행한다. 미리 작성해둔 npm scripts를 실행하는 일이라 여기에서는 특별히 볼 게 없다. run에 입력한 코드를 그대로 실행하는 게 전부다. 두 개의 스텝을 아래와 같이 하나로 합칠 수도 있다.

1
2
3
- name: Run build and tests
  run: yarn build && yarn run test:ci
​

07. 소감은…?

GitHub와 CI 서버를 연동하기 위해서 번거로운 작업을 하지 않아도 되는 점이 가장 좋았다. Actions를 만들어서 오픈 소스로 쉽게 공유할 수 있는 것도 장점이다. 지금은 실행 속도가 조금 느리지만 앞으로 고사양의 가상 서버를 추가하면서 더 나아질거라 기대한다. 물론 공짜는 아니겠지.

Repository, GitHub Actions, GitHub Packages 까지. GitHub이 착실하게 자신의 영역을 넓히고 있다. GitHub DevOps를 볼 날이 얼마 남지 않았다.