퍼포스와 젠킨스
예산이 넉넉하지 않다면 Git이나 SVN을 사용하는 경우가 많지만,
일정 규모 이상의 게임 회사에서는 퍼포스(Perforce)를 버전 관리 솔루션으로 선호하는 편이다.
한편, 젠킨스(Jenkins)는 많은 기업에서 사용하는 오픈소스 CI/CD 도구다.
따라서 게임 개발 환경에서는 퍼포스와 젠킨스를 함께 사용하는 경우를 흔히 볼 수 있다.
퍼포스는 다양한 API를 지원하므로 젠킨스 빌드 파이프라인에서 직접 사용할 수 있지만,
젠킨스의 SCM(Software Configuration Management) 시스템과 파이프라인에 자연스럽게 통합할 수 있도록 공식 퍼포스 플러그인도 제공된다.
GitHub - jenkinsci/p4-plugin: Perforce plugin for Jenkins
이 퍼포스 플러그인을 사용하면 다음과 같은 기능을 사용할 수 있다.
- 퍼포스 변경 목록을 빌드 페이지에서 바로 확인
- polling으로 자동 빌드 트리거
- 빌드용 퍼포스 워크스페이스 자동 생성
문제
구성이 단순한 경우에는 큰 문제가 없지만,
파이프라인이 복잡해지고 빌드 노드의 슬롯 수가 늘어나면 충돌 문제가 발생할 수 있다.
이와 관련해 라이엇 게임즈에서 잘 정리한 글이 있으니 먼저 참고하면 좋다.
Using Perforce in a Complex Jenkins Pipeline | Riot Games Technology
이 글에서는 그 내용을 바탕으로 보충 설명과 대안을 제시하고자 한다.
퍼포스 워크스페이스
문제를 이해하려면 먼저 퍼포스의 워크스페이스 개념을 알아야 한다.
퍼포스는 Git과 달리 중앙 집중형 구조를 가진다.
퍼포스에서 파일을 내려받기 위해서는 워크스페이스를 등록해야 하며,
이 워크스페이스에는 고유한 이름과 함께 퍼포스 경로와 로컬 디렉토리 간의 매핑 정보가 포함된다.
예를 들어 kjs104901_depot이라는 이름의 워크스페이스가 다음과 같이 등록되었다면,
퍼포스 서버의 //depot 경로의 파일은 D:\p4work\kjs104901_depot 디렉토리에 다운로드된다.
젠킨스 워크스페이스 이해하기
다음으로 알아야 할 것은 젠킨스의 워크스페이스 구조다.
젠킨스는 각 빌드마다 고유한 디렉토리를 생성해 작업을 수행한다.
예를 들어 젠킨스의 루트 디렉토리가 C:\Jenkins\workspace일 경우,
blog라는 이름의 프로젝트 빌드 워크스페이스는 다음과 같다:
C:\Jenkins\workspace\blog
빌드 노드에서 하나의 빌드가 여러 인스턴스로 동시에 실행되는 경우, 워크스페이스 충돌을 피하기 위해
젠킨스는 자동으로 워크스페이스 이름 뒤에 @숫자를 붙인다:
C:\Jenkins\workspace\blog@2
C:\Jenkins\workspace\blog@3
퍼포스 플러그인 문제
퍼포스에서 파일을 내려받고 빌드 결과물을 저장하려면 퍼포스 워크스페이스가 필요하다.
이 워크스페이스는 젠킨스 퍼포스 플러그인을 통해 자동 생성할 수 있다.
이때 퍼포스 워크스페이스의 로컬 경로는 젠킨스의 워크스페이스 디렉토리를 그대로 사용하면 된다.
문제는 퍼포스 워크스페이스의 이름을 어떻게 정할 것인가이다.
퍼포스 플러그인 공식 문서는 다음과 같은 방식의 이름 생성을 제안한다.
jenkins-%{NODE_NAME}-%{JOB_NAME}-${EXECUTOR_NUMBER}
이 이름은 노드 이름, 작업 이름, 그리고 빌드 실행 번호를 조합한 형태로,
워크스페이스 이름의 고유성을 확보할 수 있다는 점에서 합리적으로 보인다.
하지만 문제는 젠킨스의 @2, @3과 같은 워크스페이스 이름과
EXECUTOR_NUMBER가 일치하지 않을 수 있다는 점이다.
JENKINS-48882 Build Executor number is not consistent with EXECUTOR_NUMBER
이 두 값은 독립적으로 관리되며, 그 결과
jenkins-node-blog-2라는 퍼포스 워크스페이스가 실제로는 @1 또는 @3 젠킨스 워크스페이스에서 실행될 수 있다.
이는 퍼포스의 구조상 심각한 문제가 될 수 있다.
퍼포스는 로컬 디렉토리에 어떤 파일이 내려받아졌는지 정보를 캐시해 두기 때문에,
예컨대 @2에서 파일을 모두 내려받은 상태에서 @3으로 넘어가면,
퍼포스는 “이미 최신 상태”라고 판단해 파일을 다시 받지 않게 된다.
즉, 서로 다른 워크스페이스 경로에서 같은 퍼포스 워크스페이스 이름을 공유하게 되면
빌드 결과물 누락, 캐시 오류 등 다양한 문제가 발생할 수 있다.
해결방법
1안 - 라이엇 게임즈
라이엇 게임즈는 EXECUTOR_NUMBER의 불일치 문제를 해결하기 위해,
젠킨스가 생성한 실제 워크스페이스 경로를 확인하고 @ 기호를 기준으로 파싱하여 슬롯 번호를 추출한 뒤, 이를 퍼포스 워크스페이스 이름에 사용했다.
그뿐만 아니라, 젠킨스 워크스페이스를 Stage별로 구분하고,
퍼포스 워크스페이스는 그 안에서 다시 한 번 슬롯 번호 기준으로 분리했다.
(Stage 이름은 너무 길어질 수 있으므로, 해시 값을 이용해 앞 4자리만 사용)
젠킨스 워크스페이스 경로 | 퍼포스 워크스페이스 이름 |
---|---|
JOB_NAME + STAGE을 활용한 커스텀 경로 | jenkins-%{JOB_NAME}-STAGE-슬롯번호 |
참고로 젠킨스 워크스페이스는 파이프라인에서 ws를 사용해 변경할 수 있다. |
def riotP4Sync(Map config = [:]) {
def humanReadableName = safePath("${JOB_NAME}-${STAGE_NAME}")
def jenkinsWorkspaceName = safePath("${JOB_NAME}-") + workspaceShortname(env.STAGE_NAME)
ws(jenkinsWorkspaceName) {
echo "[INFO] [riotP4Sync] Running in ${pwd()}"
def workspaceNumber = 1
if (pwd().contains('@')) {
workspaceNumber = pwd().split('@').last()
if (!workspaceNumber.isInteger()) {
error("${workspaceNumber} is not an integer! Something went wrong in riotP4Sync. PWD is ${pwd()}")
}
}
def p4WorkspaceName = safePath("${humanReadableName}-${workspaceNumber}")
echo "[INFO] [riotP4Sync] Using P4 Workspace ${p4WorkspaceName}"
}
2안 - 단순화 버전
라이엇의 예시 코드에는 NODE_NAME이 포함되지 않으며,
많은 경우 Stage별로 워크스페이스를 분리할 필요가 없기도 하다.
따라서 아래처럼 슬롯 번호만 안전하게 추출하는 간단한 함수를 만든 뒤,
기존 EXECUTOR_NUMBER 대신 사용해도 충분하다.
젠킨스 워크스페이스 경로 | 퍼포스 워크스페이스 이름 |
---|---|
기본 경로 | jenkins-%{NODE_NAME}-%{JOB_NAME}-슬롯번호 |
private int getSlotNumber() {
if (pwd().contains('@')) {
workspaceNumber = pwd().split('@').last()
if (!workspaceNumber.isInteger()) {
error("${workspaceNumber} is not an integer. PWD is ${pwd()}")
}
return workspaceNumber as int
}
return 0
}
3안 - 동시 빌드 제한
만약 동일한 빌드가 동시에 여러 곳에서 실행될 필요가 없다면,
슬롯 번호를 추출하지 않고도 퍼포스 워크스페이스의 고유성을 유지할 수 있다.
각 빌드가 하나의 노드에서 단독으로 실행된다면, 아래와 같은 구성이 가능하다.
젠킨스 워크스페이스 경로 | 퍼포스 워크스페이스 이름 |
---|---|
JOB_NAME을 사용한 커스텀 경로 | jenkins-%{NODE_NAME}-%{JOB_NAME} |
하나의 노드에서 각 빌드의 고유성을 어떻게 보장할까?
젠킨스의 Lockable Resources 플러그인을 활용하면 노드+Job 단위로 락을 걸 수 있다.
Lockable Resources | Jenkins plugin
예를 들면 agent1 노드에서 build-A Job은 한 번에 하나만 실행되도록 할 수 있다.
빌드를 노드로 분산시키지 않고, 빌드가 실행될 노드를 고정해서 사용하고 있다면 좀 더 간단하게 할 수 있다.
아래와 같은 빌드 속성으로 동시 빌드를 막으면 젠킨스 전체 노드에서 고유성이 보장된다.
properties properties: [ disableConcurrentBuilds() ]
변경점 구하기 문제
원래라면 젠킨스 퍼포스 플러그인만 사용해도 아래와 같이 자동으로 변경점을 확인할 수 있다.
그러나 앞서 설명한 것처럼, Stage나 슬롯 번호에 따라 워크스페이스를 분리하게 되면
퍼포스 플러그인 입장에서는 워크스페이스가 매번 새로 생성되거나 변경되는 것으로 인식되어
정상적으로 빌드가 수행되더라도 변경점이 누락되는 문제가 발생할 수 있다.
즉, 같은 Job이더라도 워크스페이스 이름이 달라지면 퍼포스 플러그인은 "이전 빌드와 연속성"이 없다고 판단한다.
해결책: 변경점을 직접 추적
만약 빌드 변경점을 기반으로 슬랙 알림, 릴리즈 노트, 배포 기록 시스템 등을 연동하고 있다면, 변경점을 직접 구해야한다.
변경점 구하는 방법
- 빌드 성공 시, 현재의 changelist 번호를 Jenkins Artifacts로 저장해 둔다. archiveArtifacts
- 다음 성공한 빌드에서, 이전에 저장해 둔 changelist 번호와 현재 번호 사이의 변경 목록을 퍼포스 명령어 또는 API를 통해 직접 구한다.