Github Actions 과 함께 Continuous Delivery 구축하기

Yuwon Oh
29CM TEAM
Published in
14 min readOct 28, 2022

안녕하세요, 29CM Android 개발자 오유원입니다.

이번 글이 저희 Android 팀의 첫 글이 될 예정인데요, 이번 글에서는 Github Actions 를 도입하여 저희 Android 팀의 기존 배포 파이프라인을 개선해 Continuous Delivery 를 구축한 내용을 소개해 드리고자 합니다.

전환 배경

19년도 초까지는 Android 팀에 배포 파이프라인 자체가 존재하지 않았습니다. 그래서 매 릴리즈 마다 각 팀원들의 머신에서 앱을 직접 아카이브 해 팀에 공유 하고 있었는데요, 그러다 보니 바이너리 배포를 위해 빌드를 돌리는 동안 다른 작업을 하지 못하고 기다리는 시간이 주기적으로 발생했습니다.

위 상황을 개선하기 위해 2020년 초 팀에 익숙했던 Jenkins 를 사용해 사내 빌드를 팀에 배포할 수 있도록 하는 체제를 구축했었는데요, 다만 Jenkins 를 사용하면서도 팀에서는 여러 불편함을 느끼게 되었고 지속적 배포를 하는 환경까지는 완성하지 못했던 상태였기에 팀에서는 추가적인 시스템 개선을 고민하는 상태였습니다. 이후 여러 도구를 조사하면서 Github Actions 를 알게 되었고 결과적으로 이 도구를 도입해 생산성을 많이 끌어올릴 수 있었습니다.

그래서 이번 글에서는 GitHub Actions 에 대한 소개를 시작으로 팀이 기존에 겪고 있던 문제들이 무엇인지 살펴보고, 이를 어떤 식으로 개선해 나갔는지 말씀드리겠습니다.

기존의 앱 배포 파이프라인 — Jenkins

개선 작업을 하기 전 저희 팀은 많이 사용되는 도구 중 하나인 Jenkins 를 기반으로 파이프라인을 구성하고 있었습니다.

사내 내부 서버에 아래와 같이 구성을 해두었는데 이를 팀에서 사용하는데 몇 가지 불편함이 있었습니다.

Jenkins 시스템 구성도

Jenkins 인스턴스가 내부 서버에 떠있다 보니 VPN 을 통해야만 접속해야 했는데요, 사내 배포는 일상적으로 많이 일어나는 활동이나 보니 재택이 일상화된 환경이 되고 나서는 생각보다 불편함을 주는 요소였습니다. 그래서 VPN을 사용하지 않고도 파이프라인을 구성할 수 있는 방향을 먼저 고려하게 되었습니다.

또한 저희가 기존에 구축한 환경에서는 앱을 배포할 때마다 Jenkins 콘솔에 접속하여 정보를 입력해야만 하는 형태로 구성되어 있었고, 아래처럼 정보 입력 후 버튼을 눌러야지만 빌드를 시작할 수 있었습니다. 즉 Continuous Delivery 가 구축된 상태는 아니었습니다. 사소한 부분이지만 이런 요소들이 모여 팀의 생산성을 조금씩 낮추고 있다는 생각이 들었습니다.

Jenkins 콘솔 화면

저희 팀 구성원들은 프로덕트팀 내 여러 스쿼드에 각각 소속되어 일을 하고 있는데요, 그러다 보니 각 스쿼드의 피쳐 중간 개발물이나 개발 후 QA 반영사항을 적용한 빌드를 사내에 지속적으로 배포하고 있어서 매번 Jenkins 에서 위 작업을 반복해야만 하는 번거로움이 발생할 수밖에 없었습니다.

또한, 저희가 구축한 환경에선 Jenkins 에서 아카이브를 시작할 때 레포의 브랜치 정보가 동기화되어있지 않아 아래와 같이 오타가 있을 때는 프로세스 시작 후 시간이 지나서야 오류를 발견할 수 있는 불편함도 있었습니다.

이후 아카이브가 완료되면 앱 바이너리가 특정 페이지로 올라가도록 구축되어 있었는데, 그 페이지의 링크를 내부 구성원들에게 슬랙을 통해 매번 공유해야만 하는 불편함도 있었습니다.

마지막으로는 Jenkins 라는 도구에 대한 팀의 유지보수 비용이 팀 규모 대비 크다는 생각이 있었습니다.(모든 구성원이 Jenkins 에 대한 이해도가 있지 않기도 했습니다) 그래서 팀의 리소스를 최대한 회사의 비즈니스를 위해서 쓸 수 있도록 팀 차원의 관리비용을 최소한으로 만들고 싶은 생각이 있었습니다.

정리하자면

  • 매번 VPN 을 통해 접근하는 번거로움
  • 빌드 정보 입력 시 휴먼 에러의 여지가 있고, 발생 시 팀의 인지 시점이 늦음
  • 빌드 완료 후 개발자가 매번 내부 구성원에게 슬랙을 통해 직접 전달
  • 적지 않은 유지보수 비용

이런 이슈들을 해소하고자 파이프라인 개선 작업을 시작하게 되었습니다.

GitHub Actions 을 선택한 이유

위에서 말씀드린 이슈들을 해결하기 위해 Circle CI, Bitrise 등을 살펴봤었는데요, 저희 팀에서는 최종적으로 Github Actions 을 선택하게 되었습니다. 우선 Github Actions 이란 무엇인지 간략히 소개해 드리고, 이를 선택한 이유를 말씀드리겠습니다.

GitHub Actions 은 GitHub 에서 제공하는 워크플로우 자동화 도구로 GitHub 에서 바로 코드를 빌드, 테스트 및 배포를 자동화하도록 돕습니다. 그리고 GitHub Actions 는 이미 생태계가 매우 잘 구축되어 있어 GitHub Marketplace 에서 다른 개발자들이 이미 잘 만들어둔 Action 들을 간단히 가져와 사용할 수 있기도 하고 각 Action 의 버전 관리도 쉽게 할 수 있다는 점이 매우 편리합니다.

GitHub Actions Marketplace

예를 들어 Workflow 가 끝난 후 슬랙을 보내는 작업을 하고 싶은 경우 아래의 Action 을 추가하여 WebHook URL을 추가하면 됩니다.

위와 같은 장점들이 있었고, 29CM 의 다른 플랫폼 개발팀에서 이미 Github Actions 을 클라우드로 도입하여 안정적으로 사용하고 있어 Android 팀에서도 Best Practice 를 참고하여 도입할 수 있다는 점도 있었습니다.

결정적으로는 이러한 장점을 가진 도구가 Self-Hosted 를 사용한다면 무료라는 점이 컸습니다.

VPN Free

GitHub Action을 PoC 하는 과정에서 고민이 되었던 부분은 클라우드로 구축하지 않는 경우엔 내부망에 접근해야 하지 않을까 하는 점이었는데요, 다행히 Self-Hosted Runner 의 경우는 러너에서 GitHub 쪽으로 Polling 을 해오는 방식이라 특별한 네트워크 작업 없이도 쉽게 내부에서 구축이 가능했습니다.

Self-hosted 비용

사실 내부 구축을 위해서는 인프라팀과 협의할 것들이 있었기에 팀의 빠른 검증을 위해 초기에는 클라우드를 사용해 PoC 를 진행했었습니다. 다만 모바일 빌드의 경우 비교적 무겁고 오래 걸려서 많은 성능을 필요로 하는데 클라우드로 운영하면 비용적인 측면을 무시할 수 없어서, 빠르게 시도만 먼저 해 보고 어느 정도 검증이 완료된 시점부터는 기존 Jenkins 가 돌고 이었던 서버 머신에 Self-hosted 로 구축해 별도의 비용 없이 사용하는 것을 최종 목표로 두고 진행했습니다.

Github Actions Per-minute rates

이벤트 트리거를 통한 손쉬운 자동화

저희 팀은 QA용 바이너리를 배포하기 위해 Jenkins 접속 후 직접 배포할 브랜치 정보를 입력해야만 Job 을 시작할 수 있었는데요, Github Actions 의 이벤트 트리거 기능을 통해 canary 브랜치에 push 를 할 때마다 배포되도록 자동화를 하였습니다.

이벤트 트리거 설정 역시 매우 간단하게 설정할 수 있는데 예를 들어 develop 브랜치에 push 가 일어날 때마다 특정 Workflow 를 실행하고 싶다면 아래처럼 설정할 수 있습니다.

# 트리거를 설정하지 않을 경우 아래 코드를 제거하면 됩니다.
# [pull_request, issues] 처럼 배열로도 선언이 가능합니다.
on:
push:
branches:
- canary/*

이렇게 GitHub Actions을 선택한 여러 이유들에 대해 알아보았는데요, 이어서 적용 과정에 대해 설명드리겠습니다.

GitHub Actions 적용하기

먼저 설치부터 시작하겠습니다. Github Actions 초기 설정은 매우 간단한 편입니다. 아래처럼 Repository 의 Settings 메뉴에서 Action → Runners → New self-hosted runner 를 통해 이동하시면 설치 가이드가 제공되는데 이를 그대로 따라 하시면 Runner 셋업이 끝납니다.

Create self-hosted runner

다음으로는 Workflow 를 만드는 작업인데요, 먼저 Workflow 의 이름을 작성하고 해당 Workflow 가 발생할 시점을 지정합니다. 그 후에 Workflow 를 수동으로 실행할 때 입력받고 싶은 값이 있을 경우 input 을 통해 선언할 수 있습니다.

# 워크플로우 이름을 지정합니다.
name: CD (Canary)
on:
# 워크플로우를 실행할 트리거를 지정합니다.
# 아래의 경우 canary/* 경로의 브랜치에서 push가 일어날 때 워크플로우가 실행됩니다.
push:
branches:
- canary/*

# 워크플로우 입력값을 선언합니다.
workflow_dispatch:
inputs:
BuildTitle:
description: Build Title
required: false # 필수값 여부
type: string # 입력값 데이터 타입

배포 Workflow 소개

저희 팀에서는 사내 빌드 배포와 Google Play 앱 제출을 위해 Workflow 를 만들어 사용하고 있는데 그중에서 Google Play 제출용 Workflow 를 소개해 드리겠습니다.

해당 Workflow 는 작업에 필요한 의존성을 먼저 설치하고 앱을 아카이브한 뒤, Google Play 에 심사등록 후 슬랙으로 결과를 알려주도록 구성했습니다.

Workflow 의 앞부분에 약간의 전처리가 있는데요, Ruby 버전이나 Runner 의 머신 정보 등 이후의 Action 에서 필요한 정보들을 GITHUB_ENV 환경변수에 추가로 지정해 Workflow 내에서 활용될 수 있도록 합니다.

다음으로는 앱 아카이브를 위해 Java 와 Ruby, Android SDK 를 설정하는 Action 을 사용해 의존성을 설치합니다. 매번 새로 설치하지 않도록 적절한 키 값을 조합해 캐시가 유효하지 않은 경우에만 새로 설치하도록 구성했습니다.

- name: Setup JDK 11
uses: actions/setup-java@v3
with:
distribution: "zulu"
java-version: 11
- name: Cache Ruby Dependencies
uses: actions/cache@v3
id: ruby-bundle-cache-step
with:
path: ruby-bundle-cache
key: ${{ env.machine_name }}-${{ env.ruby_version }}-ruby-bundle-cache-${{ hashFiles('Gemfile.lock') }}
restore-keys: ${{ env.machine_name }}-${{ env.ruby_version }}-ruby-bundle-cache-
- name: Setup Ruby
if: steps.ruby-bundle-cache-step.cache-hit != 'true'
run: bundle config set path 'ruby-bundle-cache' && bundle install
- name: Setup Android SDK
uses: android-actions/setup-android@v2

환경 구성이 끝나면 Fastlane 을 통해 앱을 아카이브하고 Fastlane 내 Google Play 플러그인을 사용하여 Fastfile 에 사전 정의한 배포율에 따라 점진적 배포를 진행하게 됩니다. 배포가 완료되면 배포 결과를 저희 팀 채널에 슬랙으로 알려주도록 해두었습니다. 또한, 배포를 취소하거나 실패할 경우에도 마찬가지로 적절한 메시지로 알려주도록 설정을 해두었습니다.

- name: Upload Play Store
run: |
bundle exec fastlane android submit_play_store rollout:"${{ github.event.inputs.rollout }}" jenkins_build_user_id:"${{ github.actor }}" jenkins_project_name:"Android" jenkins_build_number:"${{ github.run_id }}"
- name: Notify to Slack
if: always()
uses: innocarpe/actions-slack@v1
with:
status: ${{ job.status }}
success_text: "<!subteam^> `29CM AOS - ${{ github.ref }}` 플레이스토어 제출 완료 ✅"
failure_text: "<!subteam^> `29CM AOS - ${{ github.ref }}` 플레이스토어 제출 실패 😱"
cancelled_text: "<!subteam^> @${{ github.actor }} 님이 `29CM AOS - ${{ github.ref }}` 플레이스토어 제출을 취소했습니다 ⚠️"
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

GitHub Actions 도입 전후의 프로세스 비교

저희 팀이 Github Actions 을 도입해나가는 과정에 대해 소개를 드렸는데요, Actions 를 도입한 덕분에 QA 를 위한 바이너리 전달, Google Play 심사 등록까지 손수 진행했던 과정을 push 한 번으로 완료할 수 있도록 Workflow 를 만들어 개선하였습니다.

바뀐 프로세스에서는 보통 1분 이내에 바이너리 빌드, 릴리즈 노트 생성, 심사등록까지 완료하게 되었습니다.

그리고 이렇게 구축한 환경을 더욱 잘 활용하기 위해 저희 팀은 피쳐플래그를 사용해 아래와 같이 바이너리 타입을 구분해 사용하고 있습니다.

  • Debug
  • Canary
  • Release Candidate
  • Release

각 피쳐에 대해 담당 개발자가 상태를 설정해 두면, 아카이브 과정에서 바이너리 타입을 체크하고 각 피쳐 상태와 조합해 해당 피쳐를 노출할지를 결정하게 됩니다.

Future Actions

저희 Android 팀의 Continuous Delivery 파이프라인 개선이 아직 완전히 마무리된 것은 아닙니다. 이번 개선 작업에서는 배포 전까지의 프로세스를 개선하는 데 중점을 두었기에 바이너리가 배포되는 시스템까지는 개선이 이어지지 않았습니다.

자체 구축된 사내 앱 배포 환경 역시 팀의 유지보수가 필요하기에 바로 이어지는 다음 개선 과정에서는 앱 아카이빙과 바이너리 배포 부분을 개선해나갈 예정이고, Microsoft App Center 라는 도구를 사용해 팀의 유지보수 영역도 최소화하려고 합니다.

마치며

이번 글에서는 저희 29CM Android 팀에서 Github Actions 를 도입하며 배포 파이프라인을 개선해 나간 과정을 소개해 드렸는데요, 이러한 과정을 통해 저희 팀은 자동화된 사내 빌드 공유, 심사등록 프로세스를 최소한의 시간으로 수행할 수 있게 되었습니다. 그리고 그렇게 아낀 시간을 다른 업무에 더 집중할 수 있게 되었습니다 💪

이번 글이 배포 파이프라인과 관련된 고민을 하고 계신 팀이나 개발자분들께 도움이 되셨으면 좋겠습니다!

감사합니다.

함께 성장할 동료를 찾습니다

29CM (무신사) 는 3년 연속 거래액 2배의 성장을 이루었습니다.

앞으로도 더 큰 성장을 만들기 위해 여러 스쿼드에서 OKR을 기반으로 여러 피쳐들을 만들어 가고 있으며, 이번 글과 같이 인프라 혹은 플랫폼 업무를 하기도 합니다.

함께 성장하고 유저 가치를 만들어낼 동료 개발자분들을 찾습니다
많은 지원 부탁합니다!

🚀 29CM 채용 페이지 : https://www.29cmcareers.co.kr/

--

--