Tech
Unity 커스텀 패키지를 위한 CI/CD 워크플로우의 도입기 - 1부
2023. 11. 27


기술지원팀 DevOps 엔지니어 오평석입니다. 기술지원팀에서는 각 스튜디오가 사용하는 공통 모듈을 개발하고, 이를 Unity 패키지 형식으로 관리하고 있습니다. Unity 커스텀 패키지를 제작하여 사내 레지스트리에 배포하면 각 스튜디오에서는 Unity Package Manager을 열어 설치를 하는 방식입니다. 이번 아티클에서는 Unity 커스텀 패키지를 배포하기 위한 CI/CD 워크플로우의 도입과 이를 어떻게 구성했는지 소개하려 합니다. 

 


 

어떤 CI/CD 도구를 선택해야 할까?

CI/CD 워크플로우를 도입하려면 어떤 도구를 선택해야 하는지부터 정해야 합니다. DevOps가 주목받으면서 다양한 CI/CD 도구가 등장했습니다. 선택의 자유가 생긴 만큼 상황에 맞는 도구를 선택하는 것이 중요합니다. 저희는 유니티 프로젝트 빌드 시 Jenkins라는 도구를 사용했습니다.

CI/CD 도구 중 하나인 Jenkins (이미지 출처: Jenkins.io)

 

쿡앱스에는 Jenkins에 대한 경험이 풍부한 동료들도 많고, 사내 문서를 살펴보면 빌드나 각 스토어에 업로드하는 작업 등이 잘 정리돼 있습니다. 이러한 경험을 잘 살려 Jenkins 머신을 세팅해 워크플로우를 구성하면 되지만, 결국 Jenkins를 사용하지 않았습니다. 그 이유를 간단하게 살펴보면 다음과 같습니다.

  • 인프라 관리의 시점에서 바라보면 Jenkins는 플러그인의 버전 관리가 어렵고, 머신에 필요한 도구가 있다면 Jenkins 외부에서 따로 설치를 진행해야 합니다. 사내에 있는 Jenkins 머신을 살펴보면 설정이 다 제각각 달라서 관리 코스트가 큽니다.

  • GitHub과의 연동이 매끄럽지 않습니다. 처리하지 못하는 GitHub 이벤트들이 많이 있고, 필요한 경우 Webhook을 이용하여 직접 커스텀 하여 이를 처리해야 합니다. 이는 복잡한 CI/CD 워크플로우를 붙일 때 많은 부담이 됩니다.

 

논의 끝에 GitHub에서 제공하는 GitHub Actions를 사용하기로 했습니다. GitHub Actions가 무엇인지, 이를 어떻게 사용하고 CI/CD 워크플로우를 구성하는지 알아보도록 하겠습니다.

 


 

GitHub Actions 소개

GitHub Actions는 GitHub에서 제공하는 CI/CD 플랫폼입니다. GitHub 리포지토리에서 어떤 이벤트가 발생할 때 동작하는 워크플로우를 정의하는 방식이며, 이러한 워크플로우를 YAML 문법을 이용해 구성할 수 있습니다. 

name: learn-github-actions

on:
  push:
    branches: [ main ]

jobs:
  hello-world:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Run a one-line script
        run: echo "Hello, world!"
        
      - name: Run a multi-line script
        run: |
          echo "Add other actions to build,"
          echo "test, and deploy your project."
리포지토리 Actions 탭의 워크플로우 템플릿 일부

 

위에서 정의한 워크플로우에서 눈여겨볼 점은 다음과 같습니다.

  • on 에는 이 워크플로우를 트리거 할 이벤트를 정의합니다. 여기서는 main 브랜치에 커밋이 푸시되었을 때 워크플로우가 동작합니다.

  • job 에는 워크플로우에 들어갈 작업들을 정의합니다.

  • runs-on 에는 워크플로우를 실행할 러너를 정의합니다. 여기서는 ubuntu-latest 머신을 사용하고 있습니다.

  • step 에는 하나의 작업에 들어가는 스텝들을 정의합니다.

  • uses 에는 사용할 액션의 패키지 이름이 들어갑니다. 여기서는 러너에서 리포지토리에 접근할 수 있도록 세팅 및 체크아웃을 하는 action/checkout 액션을 사용하고 있습니다. 이처럼 GitHub Actions는 복잡하거나 자주 사용하는 작업을 재사용이 가능하도록 Actions 라는 모듈로 묶을 수 있으며, 이러한 액션은 GitHub에서 공식적으로 제공하는 것을 사용하거나 다른 사람이 작성한 것을 가져올 수 있습니다.

  • run 에는 러너에서 명령어를 실행할 때 사용합니다.

 

이제 이 워크플로우를 리포지토리에서 .github/workflows 디렉토리 안에 파일로 저장해두면, 이벤트가 발생할 때 자동으로 워크플로우가 트리거 되어 Actions 탭에서 확인할 수 있습니다.

리포지토리의 Actions 탭에서 확인되는 로그

 


 

어떻게 워크플로우를 구성해야 할까?

CI/CD 워크플로우를 구성하려면 우선 수동으로 하는 프로세스를 정확하게 파악한 후, 이를 어떻게 자동화하고 어떤 정책을 가져갈 것인지 정해야 합니다. 이를 위해 우선 Unity 커스텀 패키지를 배포할 때 하는 일련의 과정은 다음과 같습니다.

  • package.json 파일을 열어서 version 을 원하는 값으로 수정합니다.

  • 커밋을 한 후 태그를 매깁니다.

  • npm login 명령어를 이용해 사내 패키지 레지스트리에 로그인합니다.

  • npm publish 명령어로 패키지를 배포합니다.

 


 

어떻게 워크플로우를 실행해야 할까?

GitHub Actions에서는 워크플로우를 트리거 하는 다양한 이벤트들을 지원합니다. 특정 라벨의 PR이 병합되거나 특정 패턴의 태그가 푸시 되었을 때, 심지어는 특정 파일을 수정하는 커밋이 푸시 되었을 때 워크플로우를 실행하도록 설정할 수도 있습니다. 패키지 버전은 시맨틱 버전을 최대한 따르도록 작성하고 있기에, 어떤 버전을 매길지 정하는 것은 자동화가 어렵고 사람의 판단을 필요로 합니다. 중요한 것은 어떤 이벤트에 버전 정보를 담아서 워크플로우를 실행할지 정하는 것이었습니다.

저희는 직접 배포 워크플로우를 실행하는 방법을 선택하였습니다. GitHub Actions에서 지원하는 이벤트 중에서는 workflow_dispatch 가 있는데, 이를 이용하면 액션 탭에서 직접 워크플로우를 실행할 수 있도록 버튼을 노출합니다. 더 중요한 점은 이렇게 워크플로우를 트리거 할 때 원하는 입력을 넘겨줄 수 있고, 이 값을 워크플로우 내에서 읽고 사용할 수도 있습니다. 

on:
  workflow_dispatch:
    inputs:
      tag:
        description: "Version you want to publish (ex: 1.0.0)"
        required: true
workflow_dispatch 이벤트 정의

 

workflow_dispatch 이벤트를 정의하면, 입력 필드가 나오게 됩니다. 이제 배포를 하고 싶으면 액션 탭에 들어간 다음, 배포 워크플로우를 선택한 후 입력 필드에 원하는 버전을 적고 Run workflow 버튼을 누르면 배포 워크플로우를 실행할 수 있게 됩니다.

배포 워크플로우를 실행할 수 있는 Run workflow 버튼

 


 

어떻게 버전 정보를 수정해야 할까?

기본적으로 UPM은 package.json 파일의 version 값으로부터 버전 정보를 가져옵니다. 새로운 버전을 배포하기 위해서는 이 값을 바꿔야 하므로 적절한 방법을 이용하여 이 파일을 파싱하고 수정하는 작업이 필요합니다. package.json 파일은 확장자에서도 알 수 있듯이 JSON 형식을 따르므로 이를 파싱 하는 데 jq 와 같은 프로그램을 사용해도 좋지만, 어차피 특정 값만 수정하는 것이라면 sed 명령어를 이용하여 특정 패턴을 수정하는 방식으로도 작성할 수 있습니다.

여기서는 =e "s/regexp/replacement/" 옵션을 이용하여 version 뒤에 있는 값을 워크플로우 트리거 시 입력받은 값으로 바꾸도록 작성하였습니다.

- name: Update package.json to version ${{ inputs.tag }}
  run: sed -i -e "s/\(\"version\":\) \"\(.*\)\",/\1 \"${{ inputs.tag }}\",/" package.json
-e "s/regexp/replacement/” 옵션을 이용한 표본


package.json 파일이 변경되었으므로, 이를 커밋하고 원격 리포지토리에 반영하여야 합니다. 그리고 태그 역시 생성하고 푸시 해야 합니다.

- name: Commit and push
  run: |
    git config --local user.name ${GITHUB_ACTOR}
    git config --local user.email ${GITHUB_ACTOR}@users.noreply.github.com
    git commit -a -m "Prepare for version ${{ inputs.tag }}"
    git push ${REPO_URL}
- name: Create tag ${{ inputs.tag }} and push
  run: |
    git tag ${{ inputs.tag }}
    git push ${REPO_URL} ${{ inputs.tag }}
변경된 package.json을 커밋 후 원격 리포지토리에 반영한 표본

 


 

어떻게 인증에 필요한 민감한 정보를 전달해야 할까?

GitHub 리포지토리에 푸시 할 때 인증을 진행해야 합니다. 일반적으로는 평범하게 GitHub 아이디와 토큰을 입력하면 되지만, CI/CD에서는 이러한 상호작용 과정이 워크플로우를 실패하도록 하는 원인이 됩니다. 이를 막으려면 리포지토리 주소를 명시할 때 사용자 이름과 토큰을 같이 전달하여야 합니다.

REPO_URL: https://<your_github_id>:<your_personal_access_token>@github.com/${{ github.repository }}.git
사용자 이름과 토큰 전달 표본

 

하지만 이와 같은 방식은 보안에 취약할 수 있습니다. PAT(Personal Access Token)는 사실상 패스워드에 해당하는 개념이며, 이 토큰이 있으면 주어진 권한 내에서 자유롭게 계정을 사용할 수 있게 됩니다. 따라서 실제 리포지토리에 이러한 코드가 있으면 매우 큰 보안 사고로 이어질 수 있습니다.

다행히 GitHub Actions는 이러한 민감한 값들을 저장하는 공간을 제공해 주며, 토큰의 경우 아예 워크플로우 내에서 인증 시 사용할 수 있도록 GITHUB_TOKEN 키를 제공합니다. 이 키는 secrets 컨텍스트에서 가져올 수 있는데, 이름에서 알 수 있듯이 비밀스럽게 보관되어야 하는 값들이 저장되는 곳입니다. 이러한 값들은 워크플로우 내에서 암호화된 상태로 유지되며, 로그 상에서는 마스킹 되어 보이지 않습니다.

REPO_URL: https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git
안전하게 리포지토리 주소를 명시한 표본

 


 

어떻게 레지스트리에 배포해야 할까?

이제 배포할 패키지가 준비됐으니 레지스트리에 로그인한 후 배포를 하면 끝입니다. 이때 npm 명령어를 이용하므로 node 환경이 필요한데, GitHub에서는 이러한 node 환경을 구축하기 위한 actions/setup-node 액션을 공식적으로 제공합니다. 그리고 레지스트리 로그인 시 필요한 인증 정보는 앞에서와 마찬가지로 secrets 컨텍스트에 추가한 후 이를 읽어들이면 됩니다.

- name: Setup node
  uses: actions/setup-node@v3
  with:
    node-version: 16
- name: Login to registry and publish
  env:
    NPM_USER: ${{ secrets.NPM_USER }}
    NPM_PASS: ${{ secrets.NPM_PASS }}
    NPM_EMAIL: ${{ secrets.NPM_EMAIL }}
    NPM_REGISTRY: ${{ secrets.NPM_REGISTRY }}
  run: |
    npm install -g npm-cli-login
    npm-cli-login
    npm publish --registry $NPM_REGISTRY
배포가 준비된 커스텀 패키지 표본

 


 

종합

위에서 작성한 워크플로우를 종합하면, 버튼 하나만 누르면 배포 과정이 자동으로 진행되어 바로 UPM을 통해 배포한 패키지를 설치할 수 있습니다. 약 50여 줄의 코드 만으로 제작한 마법의 버튼. 놀랍지 않나요?

name: Publish


on:
  workflow_dispatch:
    inputs:
      tag:
        description: "Version you want to publish (ex: 1.0.0)"
        required: true


defaults:
  run:
    working-directory: ./Assets/Package


jobs:
  update-package-json:
    runs-on: ubuntu-latest
    env:
      REPO_URL: https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git
    steps:
      - name: Checkout
        uses: actions/checkout@v3
        with:
          ref: ${{ github.ref }}
          fetch-depth: 0
      - name: Update package.json to version ${{ inputs.tag }}
        run: sed -i -e "s/\(\"version\":\) \"\(.*\)\",/\1 \"${{ inputs.tag }}\",/" package.json
      - name: Commit and push
        run: |
          git config --local user.name ${GITHUB_ACTOR}
          git config --local user.email ${GITHUB_ACTOR}@users.noreply.github.com
          git commit -a -m "Prepare for version ${{ inputs.tag }}"
          git push $REPO_URL
      - name: Create tag ${{ inputs.tag }} and push
        run: |
          git tag ${{ inputs.tag }}
          git push $REPO_URL ${{ inputs.tag }}
      - name: Setup node
        uses: actions/setup-node@v3
        with:
          node-version: 16
      - name: Login to registry and publish
        env:
          NPM_USER: ${{ secrets.NPM_USER }}
          NPM_PASS: ${{ secrets.NPM_PASS }}
          NPM_EMAIL: ${{ secrets.NPM_EMAIL }}
          NPM_REGISTRY: ${{ secrets.NPM_REGISTRY }}
        run: |
          npm install -g npm-cli-login
          npm-cli-login
          npm publish --registry $NPM_REGISTRY
실제 CI/CD 워크플로우의 간략화 표본

 


 

마치며

지금까지 GitHub Actions를 이용하여 버전을 입력하면 패키지가 배포되는 워크플로우를 구축하였습니다. 이처럼 GitHub Actions는 비교적 간단한 방법으로 워크플로우를 구축할 수 있으며, GitHub과의 연동도 매끄럽게 진행할 수 있습니다. 무엇보다 워크플로우에 대한 정의를 리포지토리 내에서 바로 확인할 수 있어, 어떠한 CI/CD가 있는지 쉽게 파악이 가능한 것 역시 GitHub Actions의 큰 장점이라고 생각합니다. 다음 아티클에서는 이러한 워크플로우를 구성한 후 이를 어떻게 더 발전시켰는지에 대한 이야기를 드리도록 하겠습니다.