언리얼 엔진 프로젝트 빌드배포 시스템 개발후기

 

불과 5년 전만 해도 앱 빌드와 배포를 모두 수동으로 한 기억이 있다. 앱 뽑아서 메일로 팀원 전달하고, 리소스패치 만들어서 CDN 서버에 업로드하고, 운영툴 수정하고, 협력사에 앱 전달하고, 앱스토어에 앱 올리고 … 그 당시엔 이걸 어떻게 손으로 다 했나 싶다. 바쁜 시기와 겹치면 실수하기 쉽고 시간도 많이 쓰게 된다.

이제 게임 개발에 빌드배포 자동화는 필수이다. 언리얼 엔진 프로젝트 개발팀에 참여하면서 구축한 빌드배포 시스템 개발 경험과 느낀 바를 이 글로 풀어보고자 한다.

젠킨스 소개와 예시

젠킨스는 개발프로세스(빌드->테스트->배포…)를 자동화하고 관리할 수 있는 CI/CD 도구이다.

설치 시 젠킨스 데몬 서버가 실행되며 팀원들은 웹브라우저로 접속해 사전 등록된 자동화 작업(Job)을 실행할 수 있다. 무료이고 플러그인 확장, 분산 처리를 지원하기에 많은 팀들이 젠킨스를 사용한다. 이 글도 젠킨스 위주로 설명하고자 한다.

파이프라인 스크립트

빌드배포 시스템은 그저 스크립트의 모음집이다. 젠킨스는 복잡한 프로세스를 스크립트로 실행하고 비주얼 좋은 UI 편의를 제공한다고 볼 수 있다.

이를 위해 젠킨스는 파이프라인 스크립트를 지원한다. 파이프라인 스크립트에 등록된 스테이지(작업단위)를 순서대로 실행하고 그 과정을 모니터링할 수 있다. 실행중인 스테이지가 실패되면 파이프라인은 중단된다.

아래는 각 스테이지가 수행될 때 스테이지의 이름을 출력하는 예시이다. 단계를 구분하는 stage를 등록하고 그 안에서 수행할 작업은 script에 작성한다.

pipeline {
    agent any
    stages {
        stage('first stgae') { 
            steps {
                script {
					echo "run first stage"
                }
            }
        }
        stage('second stage') {
             steps {
                script {
					echo "run second stage"
                }
            }
        }
    }
}

(젠킨스 잡) 빌드가 실행되면 실행 결과를 출력한다. 스테이지는 컬럼으로 표시되어 진행-완료시간과 성공-실패 여부를 조회할 수 있다.

배시스크립트로 대리

젠킨스 파이프라인을 4년정도 사용함에 다음의 불편점이 있었는데

  • 등록된 젠킨스 잡의 설정파일은 xml 포맷으로 저장되기에 텍스트에디터로 수정이 불편
  • 파이프라인 스크립트 그루비 문법의 어려움
  • 전용 에디터가 없어 디버깅이 곤란

이를 해결하기 위해, 작업을 대리할 배시스크립트과 이 스크립트를 실행할 파이프라인스크립트 두 파트로 나누었다. 이는 파이프라인 스크립트 유지보수 비용을 낮추고, 배시스크립트 파일은 형상관리가 용이하게 외부 파일로 뺄 수 있어서 일거양득이라 할 수 있다.

이해를 돕기 위해, 다시 위 예제를 수정해보면 파이프라인 스크립트에서 echo_stage 스크립트를 실행하고

pipeline {
    agent any
    stages {
        stage('first stgae') { 
            steps {
                script {
					sh "bash echo_stage.sh"
                }
            }
        }
        stage('second stage') {
             steps {
                script {
					sh "bash echo_stage.sh"
                }
            }
        }
    }
}

echo_stage 스크립트에서는 STAGE_NAME 변수로 분기처리하여 스테이지 별 작업을 수행하도록 할 수 있다.

if [[ $STAGE_NAME == "first stage" ]]; then
	echo "run first stage"
elfi [[ $STAGE_NAME == "second stage" ]]; then
	echo "run second stage"
fi

STAGE_NAME 변수는 빌드가 실행될 때 사용할 수 있는 젠킨스 글로벌 환경변수로 파이프라인 스크립트에서 실행하는 배시스크립트도 환경변수로 접근 가능하니, 배시스크립트 실행 시 이런 변수들을 인자로 넘겨줄 필요는 없다.

이 외에 참고하면 좋은 기본 환경 변수 BUILD_ID BUILD_NUMBER JOB_NAME

또한 노드(빌드가 실행되는 머신)에 종속적인 환경 변수를 등록할 수 있다.

피시에서 배시스크립트 사용

피시에서는 git bash 또는 msys를 설치해 사용할 수 있다. 경로분리자만 좀 신경쓰면 도스보다 사용이 용이하다.

언리얼 엔진 파이프라인 가이드

다음은 언리얼 엔진 프로젝트 실무레벨에서 사용한 파이프라인과 스테이지에서 실행한 핵심 동작과 커맨드를 짧게 기술하고자 한다. 회사마다 팀마다 환경이 다를터이니 그냥 참고정도로 봐주면 된다.

[1] standby 스테이지

실행 환경을 준비하는 스테이지이다. 빌드에 필요한 파일들이 존재하는지 검사, 환경 설정 파일 편집, 엔진 업데이트, 불필요한 빌드 캐시파일을 삭제 등 사전에 유효성을 검사하고 빌드 환경을 준비한다.

언리얼 엔진 모바일 프로젝트인 경우, 캐시를 비워주지 않으면 간혈적으로 이슈가 생기는 편이라 아래 디렉토리들은 standby단계에서 삭제하는 편이다.

rm -rf $프로젝트경로/Intermediate/Android
rm -rf $프로젝트경로/Binaries/Android
rm -rf ~/Library/Developer/Xcode/Archives/Payload/${프로젝트이름}.app
rm -rf $프로젝트경로/Intermediate/IOS
rm -rf $프로젝트경로/Intermediate/ProjectFilesIOS
rm -rf $언리얼엔진경로/Engine/Intermediate/UnzippedFrameworks
rm -rf $프로젝트경로/Build/IOS/Resources/Localizations

[2] svn-update 스테이지

빌드에 사용하는 내부저장소를 최신화한다. 특정 리비전으로 업데이트하거나 다른 브랜치로 스위칭하는 동작은 젠킨스 잡 설정에 파라미터를 등록해 분기처리 할 수 있도록 한다.

# 저장소 락 걸려 있으면, 해제
svn cleanup --username someone --password somekey $내부저장소_경로

# 저장소에 충돌/수정 내역 원복
svn status | grep "^[CD]" | cut -c 9- | xargs -r -I {} svn revert {}
svn status | grep "^......[C]" | cut -c 9- | xargs -r -I {} svn revert {}
svn status | grep "^[M]" | cut -c 9- | xargs -r -I {} svn revert {}

if [ 브랜치 스위칭 할 시 ]; then
	svn switch --accept theires-full $스위칭_브랜치_경로 $내부저장소_경로
fi

if [ 저장소 최신으로 업데이트할려면 ]; then
	svn update --accept theires-full $내부저장소_경로
elif [ 특정 리비전으로 업데이트할려면 ]; then
	url=$(svn info --show-item url $내부저장소 경로)
	svn checkout -r $리비전 $url $내부저장소_경로
fi

[3] project-cook 스테이지

언리얼 엔진에서 쿡은 텍스쳐나 오디오 같은 에셋들을 배포할 플랫폼에 적합한 포맷으로 변환하는 과정이다. 프로젝트의 규모가 어느정도 된다면 첫 실행 시 시간이 오래 걸리는 편이다.

${언리얼엔진경로}/Engine/Binaries/${플랫폼}/UE4Editor-Cmd ${프로젝트경로}/${프로젝트이름}.uproject -run=Cook -TargetPlatform=${플랫폼이름} -COOKALL -fileopenlog -unversioned -skipeditorcontent -stdout -CrashForUAT -unattended -NoLogTimes -UTF8Output

쿠킹된 데이터는 $프로젝트경로/Saved/$프로젝트이름/Cooked 경로에 저장된다.

iOS 쿠킹을 피시 환경에서도 쓰고 싶으면, 애플 개발자 사이트에서 Metal Developer Tools for Windows 도구를 설치한다.

[4] project-pak 스테이지

쿠킹된 에셋으로 리소스패치 파일(pak)을 제작한다.

${언리얼엔진경로}/Engine/Binaries/${플랫폼}/UE4Editor-Cmd ${프로젝트경로}/${프로젝트이름}.uproject -run=${커스텀 커맨드렛} -TargetPlatform=${플랫폼이름} -distribution -NoShaderCompile

리소스패치는 pak파일을 생성하는 규칙이나 다운로드 환경을 고려한 pak파일의 크기 등 프로젝트 별로 전략이 다를 것이다. 따라서, 이를 처리하는 별도의 커맨드렛을 만들고 이를 실행하는 것을 권장한다.

[5] project-build 스테이지

클라이언트 앱을 생성한다.

${언리얼엔진경로}/Engine/Build/BatchFiles/RunUAT.sh(bat) BuildCookRun -project=${프로젝트경로}/${프로젝트이름}.uproject -nop4 -utf8output -nocompile -configuration=${빌드 환경} -cook -unversionedcookedcontent -SkipCookingEditorContent -compressed -stage -pak -package -prereqs -archive -archivedirectory="${빌드파일 저장 경로}" -build -platform=${플랫폼}

클라이언트 빌드인 경우, 다음 옵션을 추가한다. iOS에서 development 인증서를 사용 시 -distribution 옵션 제거

 -client -ubtargs=${프로젝트에 넘기고 싶은 커스텀 인자} -map={빌드에 포함할 맵} -distribution

안드로이드의 경우 앱에 포함되는 텍스쳐에셋의 포맷을 지정할 수 있다.

 -cookflavor=ASTC

데디케이티드 서버 빌드인 경우, 다음 옵션을 추가한다.

-noclient -server

리눅스 빌드를 피시에서 크로스컴파일하고자 한다면 다음 문서를 참고 https://docs.unrealengine.com/4.27/en-US/SharingAndReleasing/Linux/GettingStarted/

[6] archive 스테이지

이전 스테이지에서 생성된 파일들을 백업서버로 백업한다.

[7] deploy-app 스테이지

project-build 스테이지에서 만든 앱을 배포한다. 사내 HTTP 서버에 모두 업로드하고, 완료되면 앱을 다운로드할 수 있는 링크와 빌드 정보를 담아 사내 메신저와 메일로 전파했다. iOS IPA 빌드는 adhoc-distribution 시스템을 사용해 배포할 수 있다. 데디케이티드서버 앱은 서버로 전송하고 서버패치를 진행한다.

원격 서버 파일 전송툴은 scp 또는 rsync를 권장한다.

rsync -avhrP --delete 소스디렉토리경로/ 유저이름@192.168.100.x:카피디렉토리경로

피시에서 rsync 설치는 다음 링크를 참고 https://www.ubackup.com/windows-10/rsync-windows-10-1021.html

[8] deploy-pak 스테이지

project-pak 스테이지에서 만든 리소스패치 파일을 CDN서버로 업로드한다. 실무에선 주로 AWS S3, CloudFront를 사용하였다.

aws s3 sync 소스디렉토리경로 카피디렉토리경로 --profile 프로필
aws cloudfront create-invalidation --distribution-id 배포아이디 --pahts "무효화경로" --profile 프로필

여기 단계까지 진행이 완료되면 앱의 빌드와 테스트를 위한 배포까지 모두 완료된다.

데디케이티드서버 파이프라인

위의 스테이지를 나열하면 다음과 같다.

client(win64, ios, android) pipeline
|---------|------------|--------------|-------------|---------------|---------|------------|------------|
| standby | svn-update | project-cook | project-pak | project-build | archive | deploy-app | deploy-pak |
|---------|------------|--------------|-------------|---------------|---------|------------|------------|

데디케이티드서버 빌드는 project-build 스테이지에서 쿠킹과 앱을 한 세트로 처리하기에 리소스패치와 관련된 스테이지는 필요없다. 따라서, 파이프라인은 다음과 같이 진행된다.

dedicated server pipeline
|---------|------------|--------------|-------------|---------------|---------|------------|------------|
| standby | svn-update |              |             | project-build | archive | deploy-app |            |
|---------|------------|--------------|-------------|---------------|---------|------------|------------|

파이프라인 스크립트에서 특정 스테이지를 스킵할려면, when 노드를 사용해 분기처리 할 수 있다.

pipeline {
    agent any
    stages {
	    ...
        stage('project-cook') {
	        when {
		        expression { 데디케이티드서버 빌드면 스킵되도록 조건문 작성 }
	        }
            steps {
                script {
					sh "bash some_pipeline_script.sh"
                }
            }
        }
		...
    }
}

젠킨스 분산 빌드

사실, 이 글은 분산 빌드 설명할려고 쓴거다. 위는 이를 위한 징검다리일 뿐.

내가 참여한 프로젝트는 기본적으로 피시(Win64)와 모바일 플랫폼(Android, iOS)을 지원하고 리눅스 데디케이티드서버를 사용하기 때문에 매 빌드마다 4개의 빌드가 나와야 했다. 하나의 머신에 여러 빌드를 동시에 실행할 수 없고, 빌드 시간이 오래걸리기에, 젠킨스의 마스터-슬레이브 기능을 사용해 다수의 피시를 슬레이브 노드로 작업을 분산해 빌드 속도를 올리고자 하였다.

마스터-슬레이브 설계

젠킨스 서버가 설치된 노드는 기본적으로 단일 마스터 노드이다. 여기에 슬레이브 노드는 마스터 노드와 연결(ssh)된 물리적인 별도 머신이고 잡을 대리해 수행할 수 있다. 이를 젠킨스에서 마스터-슬레이브 구조라 칭하며 이를 통해 분산 빌드 아키텍쳐를 구현할 수 있다.

실 프로젝트에서는 매 빌드마다 3개의 클라이언트와 1개의 데디케이티드 서버가 필요하기에 라이젠 5900 + RTX 3060 조합의 피시 3대와 맥스튜디오 1대를 사용해 마스터-슬레이브를 구축할 수 있었다.

젠킨스 노드 4개
|--------|----------------|---------|
| Node   | Build Platform | Node OS |
|--------|----------------|---------|
| Master | Win64          | PC      |
|--------|----------------|---------|
| Slave  | iOS            | Mac     |
|--------|----------------|---------|
| Slave  | Android        | PC      |
|--------|----------------|---------|
| Slave  | Server         | PC      |
|--------|----------------|---------|

이 경우, 매 빌드 시 대략 50분정도 소요되었다.

파이프라인 50분 소요
|---------|------------|--------------|-------------|---------------|---------|------------|------------|
| standby | svn-update | project-cook | project-pak | project-build | archive | deploy-app | deploy-pak |
|---------|------------|--------------|-------------|---------------|---------|------------|------------|
| 1m      | 1m         | 12m          | 8m          | 15m           | 2m      | 5m         | 5m         |
|---------|------------|--------------|-------------|---------------|---------|------------|------------|

파이프라인 스테이지 분산 처리 전략

50분은 너무 길다. 개발 - 빌드 - 테스트 - 버그 수정 - 빌드 - 테스트 의 굴레속에서 빌드시간을 줄이는 건 우리 모두의 퇴근 시간을 앞당기는 중대 요소이기에 여기서 더 줄일 수 있어야 한다.

여기에 피시 3대를 더 추가해, 파이프라인의 project-build(클라이언트 앱 제작)과 project-cook/pak(리소스패치 제작) 스테이지를 나눠(앱과 리소스패치 작업간 의존성이 없도록 잘 설계해야함) 처리했다. 결과적으로 빌드 시간을 20분대로 줄일 수 있었다.

젠킨스 노드 7개
|--------|---------------------|---------|
| Node   | Build Platform      | Node OS |
|--------|---------------------|---------|
| Master | Win64(client app)   | PC      |
|--------|---------------------|---------|
| Slave  | iOS(client app)     | Mac     |
|--------|---------------------|---------|
| Slave  | Android(client app) | PC      |
|--------|---------------------|---------|
| Slave  | Server              | PC      |
|--------|---------------------|---------|
| Slave  | Win64(res patch)    | PC      |
|--------|---------------------|---------|
| Slave  | iOS(res patch)      | PC      |
|--------|---------------------|---------|
| Slave  | Android(res patch)  | PC      |
|--------|---------------------|---------|

클라이언트 앱 제작 파이프라인

클라이언트 앱 뽑고 배포 24분
|---------|------------|--------------|-------------|---------------|---------|------------|------------|
| standby | svn-update | project-cook | project-pak | project-build | archive | deploy-app | deploy-pak |
|---------|------------|--------------|-------------|---------------|---------|------------|------------|
| 1m      | 1m         | -            | -           | 15m           | 2m      | 5m         | -          |
|---------|------------|--------------|-------------|---------------|---------|------------|------------|

리소스패치 제작 파이프라인

리소스패치 뽑고 배포 29분
|---------|------------|--------------|-------------|---------------|---------|------------|------------|
| standby | svn-update | project-cook | project-pak | project-build | archive | deploy-app | deploy-pak |
|---------|------------|--------------|-------------|---------------|---------|------------|------------|
| 1m      | 1m         | 12m          | 8m          | -             | 2m      | -          | 5m         |
|---------|------------|--------------|-------------|---------------|---------|------------|------------|

노드 별로 잡 등록하기

이제 머신 7대를 운용해 각 노드마다 수행해야할 잡을 등록해 사용하면 된다. 다음 스크린샷에서 잡의 이름의 -build로 끝나면 클라이언트 또는 데디케이티드서버 앱을, -cook은 리소스패치를 빌드 및 배포 작업을 의미한다.

위 잡들의 이름은 서로 다르지만 파이프라인 스크립트는 모두 동일한 파이프라인 코드를 사용한다. 이는 파이프라인 스크립트와 실제 작업이 수행될 배시 스크립트로 로직이 분할되었기 때문이며, 처음부터 파이프라인 스크립트를 단순하게 만들기 위해 이 구조를 고안한 바이다.

그럼 왜 동일한 파이프라인 스크립트 코드를 사용하고 단순하게 만들고자 하였는가? 다음으로 이어지는 원격으로 빌드 유발 기능을 쉽게 사용하기 위해서이다.

원격으로 빌드 유발

다시 위 스크린샷처럼 빌드를 돌리기 위해선 수동으로 7번 빌드를 실행을 해야한다. 이는 불편한 일이니 젠킨스의 원격으로 빌드 유발 기능을 응용해 한 큐에 다수의 잡들이 동시 실행되도록 만들 수 있다.

이를 위해선 젠킨스 계정을 통해 API 토큰을 생성하고, curl을 통해 젠킨스서버로 각 잡의 빌드 실행을 요청할 수 있음에

curl -v -X POST http://계정:토큰@젠킨스서버IP:8080/job/잡이름1/buildWithParameters?parameter1=${parameter_some_value} \
-F parameter2=${parameter_some_value}
curl -v -X POST http://계정:토큰@젠킨스서버IP:8080/job/잡이름2/buildWithParameters?parameter1=${parameter_some_value} \
-F parameter2=${parameter_some_value}
curl ...

다수의 잡을 일괄 실행하는 새로운 잡을 만들어 프로젝트의 빌드배포를 한 큐에 실행토록 할 수 있다.

슬레이브 노드 등록 방법

마지막으로 젠킨스에서 슬레이드 노드를 등록하는 방법을 간단히 소개하고자 한다.

  1. Dashboard > 젠킨스 관리 > Credentials에서 슬레이브 노드가 사용할 자격증명 정보를 기입한다. 슬레이브 노드간 통신은 SSH를 사용하고 Username에는 슬레이브 노드의 로그인된 피시 계정이름을, Private Key에는 마스터 노드의 피시의 SSH 비밀키를 입력 후 자격증명을 생성한다.

  1. Dashboard > Nodes에서 새로운 슬레이브 노드를 등록한다. 아래 Host는 해당 노드의 로컬 주소를 입력한다.

  1. 등록된 슬레이브 노드에서 실행할 잡의 파이프라인 스크립트 수정이 필요하다. agent에서 슬레이브 노드 이름을 기입해 빌드를 실행하면 해당 슬레이브 노드에서 수행되도록 한다.
pipeline {
    agent {
        label 'slave-14'
    }
    ...

마치며

게임 프로젝트의 젠킨스 분산 빌드배포 시스템 구축은 개인적으로 만족도가 높은 작업이었다. 글의 내용이 많아 초고에서 사담은 빼고 말하고자 하는 바를 간결하게 쓰기 위해 노력하였다. 이 글의 목적은 젠킨스 사용법을 알려주는 것이 아닌 문제를 해결하기 위해 어떤 고민을 하였고 어떻게 해결하였는지 풀이하는 것에 중점을 두었음에, 독자분들의 개발라이프에 소소한 도움이 되었으면 하는 바이다.