하나의 클라우드 Mac에서 빌드 작업을 연속으로 처리할 때 가장 발견하기 어려운 문제는 컴파일 실패가 아니라, 이전 작업이 남긴 상태 때문에 다음 작업이 ‘우연히 성공’하는 상황인 경우가 많습니다. 캐시에 의존성이 이미 들어 있거나, 백그라운드 서비스가 계속 포트를 점유하거나, 임시 자격 증명이 삭제되지 않았거나, 그래픽 세션에 사용자 수준 에이전트가 남아 있을 수 있습니다. 이런 파이프라인은 단기적으로 안정적으로 보이지만 다른 시스템으로 옮기거나 작업 순서를 바꾸면 문제가 한꺼번에 드러납니다. 해결책은 마지막에 무작정 삭제 명령 하나를 추가하는 것이 아닙니다. 먼저 실행 사용자, 디렉터리, 캐시와 세션 도메인의 경계를 명확히 나누고, 각 경계마다 검증 절차를 마련해야 합니다.
먼저 격리해야 할 경계 정의하기
최소한 실행 사용자, 작업 디렉터리, 임시 디렉터리, 캐시, 백그라운드 프로세스라는 다섯 가지 상태를 점검해야 합니다. CI 서비스 자체는 시스템 수준 데몬이 시작할 수 있지만, 실제 빌드를 관리자 권한으로 계속 실행해서는 안 됩니다. 더 안전한 방법은 ci_runner와 같은 관리자 권한이 없는 표준 사용자를 만들고 체크아웃, 컴파일과 테스트를 모두 해당 사용자로 실행하는 것입니다.
격리의 목적은 작업에서 ‘시스템 전체를 보지 못하게’ 하는 것이 아니라, 한 작업에서 생성된 가변 상태가 명시되지 않은 채 다음 작업에 영향을 주지 못하게 하는 것입니다.
이후 문제를 진단할 때 비교할 수 있도록 먼저 기준 상태를 기록합니다.
id
umask
printf 'HOME=%s\nTMPDIR=%s\n' "$HOME" "$TMPDIR"
launchctl print "gui/$(id -u)" >/tmp/launchctl-baseline.txt 2>&1 || true
ps -axo user,pid,ppid,command
runner가 일부 권한 작업을 수행해야 한다면 명령을 통제된 스크립트로 제한하고, 특정 동작에만 권한을 부여해야 합니다. 전체 빌드를 높은 권한으로 실행해서는 안 됩니다. 컴파일 스크립트도 시스템 수준 툴체인, 전역 네트워크 설정 또는 다른 사용자의 디렉터리를 변경하지 않아야 합니다.
작업마다 전용 작업 공간 만들기
작업 디렉터리는 예측하기 어렵지만 감사할 수 있는 작업 식별자로 생성하고, 슬래시와 공백, 제어 문자는 허용하지 않아야 합니다. 디렉터리 권한은 700으로 설정하고 임시 파일도 작업 디렉터리 내부에 배치해, 동시에 실행되는 여러 작업이 시스템 임시 경로를 공유하지 않도록 합니다.
set -eu
case "${RUN_ID:-}" in
""|*[!A-Za-z0-9._-]*)
echo "Invalid RUN_ID" >&2
exit 64
;;
esac
RUN_ROOT="/Users/ci_runner/Jobs/$RUN_ID"
install -d -m 700 "$RUN_ROOT"
install -d -m 700 "$RUN_ROOT/src" "$RUN_ROOT/tmp" "$RUN_ROOT/cache"
export HOME="/Users/ci_runner"
export TMPDIR="$RUN_ROOT/tmp/"
export XDG_CACHE_HOME="$RUN_ROOT/cache"
umask 077
모든 작업이 쓸 수 있는 공유 디렉터리에 작업 공간을 두지 마십시오. 읽기 전용 시드 캐시를 공유해야 한다면 작업 시작 시 로컬 캐시로 복사하고, 빌드 과정에서는 그 복사본만 수정하게 합니다. 이렇게 하면 미리 준비된 결과를 활용하면서도 실패한 작업이 공용 기준 상태를 오염시키는 일을 막을 수 있습니다.
이름 규칙이 아닌 권한으로 격리하기
디렉터리 이름에 작업 번호가 포함되어 있다고 해서 안전한 것은 아닙니다. stat -f '%Su %Sp %N' "$RUN_ROOT"로 소유자와 권한을 확인하고, ls -lde로 추가 ACL이 있는지 점검할 수 있습니다. 상속 규칙이 발견되면 먼저 그 출처를 확인한 뒤 통제된 초기화 절차를 통해 불필요한 권한을 제거해야 합니다. 빌드 스크립트에서 권한을 재귀적으로 완화해서는 안 됩니다.
캐시를 세 가지 수명으로 구분하기
모든 캐시를 삭제하면 파이프라인이 느려지고, 모든 캐시를 재사용하면 오염의 영향이 커집니다. 실제 운영에서는 수명에 따라 다음과 같이 나눌 수 있습니다.
캐시 키에는 최소한 툴체인 버전, 의존성 잠금 파일의 해시와 대상 아키텍처가 포함되어야 합니다. 브랜치 이름만 키로 사용하면 툴체인을 변경한 뒤에도 이전 산출물이 계속 적중할 수 있습니다. 캐시를 복원한 후에는 잠금 파일 해시, 주요 바이너리의 아키텍처와 디렉터리 소유자 등을 가볍게 검증해야 합니다. 조건에 맞지 않으면 해당 복사본을 버려야 하며, 그 자리에서 수정하려고 해서는 안 됩니다.
민감한 자료는 캐시에 포함하지 않습니다. 단기 자격 증명은 작업 환경을 통해 주입하고, 필요한 하위 프로세스 범위에서만 존재하게 해야 합니다. 로그에도 전체 환경을 출력해서는 안 됩니다. 작업 종료 시 관련 변수를 명시적으로 unset하고, 생성된 파일이 아카이브 디렉터리에 들어가지 않았는지 확인해야 합니다.
launchctl 세션 도메인 올바르게 이해하기
macOS의 백그라운드 에이전트는 시스템 도메인, 사용자 도메인 또는 그래픽 세션 도메인에 속할 수 있습니다. 빌드를 ci_runner로 실행한다고 해서 해당 사용자가 시작한 모든 에이전트가 자동으로 예상한 도메인에 배치되는 것은 아닙니다. 먼저 현재 사용자 식별자를 확인한 다음 대상 도메인을 점검합니다.
uid="$(id -u ci_runner)"
sudo -u ci_runner launchctl print "user/$uid" >/tmp/user-domain.txt
if launchctl print "gui/$uid" >/dev/null 2>&1; then
sudo -u ci_runner launchctl print "gui/$uid" >/tmp/gui-domain.txt
fi
명령줄 전용 빌드는 일반적으로 그래픽 세션에 의존하지 않습니다. 시뮬레이터나 그래픽 인터페이스가 필요한 테스트라면 먼저 유효한 세션이 있는지 확인해야 하며, 에이전트를 반복해서 시작하는 방식으로 세션 부재를 감춰서는 안 됩니다. 작업에서 임시로 시작한 서비스는 PID를 저장하고, 종료 단계에서 정상 종료 신호를 보내야 합니다. 프로세스 이름을 기준으로 일괄 종료하면 같은 시스템에서 실행 중인 다른 작업까지 잘못 중단할 수 있습니다.
작업 사이에 남은 프로세스 식별하기
작업 시작 전과 종료 후에 각각 ps 스냅샷을 수집하고 사용자, 부모 프로세스와 작업 디렉터리를 기준으로 연결해 확인합니다. 포트만 점검하는 것으로는 충분하지 않습니다. 수신 포트가 없는 파일 감시 프로세스나 테스트 데몬도 리소스를 소비하기 때문입니다. 종료 후에도 남아 있고 명령줄이 현재 RUN_ROOT를 가리키는 프로세스는 조용히 무시하지 말고 실패로 처리해야 합니다.
실패할 수 있는 종료 검증 구축하기
정리 단계에서는 원래 작업의 종료 코드를 보존하는 동시에, 정리 과정의 이상을 관찰할 수 있어야 합니다. 검사는 다음과 같은 고정된 순서로 나누는 것이 좋습니다.
- 작업이 시작한 하위 프로세스를 중지하고 종료될 때까지 기다립니다.
- 작업 공간에 소켓, 마운트 지점 또는 권한이 비정상적인 파일이 생성되었는지 확인합니다.
- 테스트 보고서를 작업 공간 외부의 통제된 아카이브 디렉터리로 복사합니다.
- 작업의 임시 자격 증명과 환경 변수를 제거합니다.
- 경로 접두사를 검증한 뒤 작업 디렉터리를 삭제합니다.
- 프로세스와 launchctl 상태를 다시 수집해 기준 상태와 비교합니다.
삭제하기 전에는 반드시 경로를 보호해야 합니다.
cleanup() {
case "$RUN_ROOT" in
/Users/ci_runner/Jobs/*)
rm -rf -- "$RUN_ROOT"
;;
*)
echo "Refusing unsafe cleanup path" >&2
return 1
;;
esac
}
trap cleanup EXIT HUP INT TERM
검증 스크립트는 성공 상태를 유지하기 위해 모든 오류를 무시해서는 안 됩니다. 아카이브 실패, 잔류 프로세스 또는 디렉터리 소유자 변경은 모두 명확한 0이 아닌 상태를 반환해야 합니다. SoarMac에서 CI를 계속 운영한다면 먼저 콘솔에서 현재 선택 가능한 구성을 확인한 다음, 동시 실행 작업이 메모리와 디스크에 주는 부담을 기준으로 runner를 분리할지 결정해야 합니다. 어떤 구성을 사용하든 사용자와 상태의 경계는 일관되게 유지해야 합니다.
운영 전 체크리스트
처음 연동할 때는 내용은 다르지만 단계는 동일한 두 작업을 연속으로 실행합니다. 첫 번째 작업에서 의도적으로 캐시 파일, 백그라운드 프로세스와 임시 변수를 생성합니다. 두 번째 작업은 명시되지 않은 파일을 읽을 수 없어야 하며, 이전 작업의 프로세스와 자격 증명도 상속해서는 안 됩니다. 마지막으로 다음 결과를 확인합니다.
- runner가 관리자 권한이 없는 전용 사용자를 사용합니다.
- 각 작업 디렉터리의 권한이
700이며 경로 형식이 검증됩니다. - 임시 디렉터리와 쓰기 가능한 캐시는 기본적으로 작업 간 공유되지 않습니다.
- 시스템 수준 시드 캐시는 빌드 사용자에게 읽기 전용입니다.
- 그래픽 테스트가 어떤 launchctl 세션 도메인에 의존하는지 명확히 지정합니다.
- 하위 프로세스는 이름으로 일괄 종료하지 않고 PID 또는 작업 관계를 기준으로 중지합니다.
- 아카이브를 완료한 뒤에만 삭제를 실행합니다.
- 정리에 실패하면 로그 한 줄만 남기는 것이 아니라 작업 자체가 실패합니다.
이러한 검사를 안정적으로 반복할 수 있게 되면 파이프라인의 성공은 시스템에 우연히 남은 상태가 아니라 명시된 입력에서 비롯됩니다. 이후 동시 실행 수를 조정하거나 작업을 이전하거나 실행 디렉터리를 변경하더라도 동일한 경계를 사용해 차이가 발생한 지점을 빠르게 판단할 수 있습니다.
자주 묻는 질문
CI 작업마다 별도의 macOS 사용자가 필요한가요?
대부분은 필요하지 않습니다. 먼저 runner 전용 표준 사용자를 만들고 작업마다 권한 700의 디렉터리를 할당합니다. 서로 신뢰하지 않는 팀이나 보안 등급이 다른 파이프라인만 실행 사용자를 분리하면 됩니다.
작업 디렉터리만 삭제하면 충분하지 않은 이유는 무엇인가요?
임시 디렉터리, 사용자 캐시, 자격 증명, 백그라운드 프로세스와 launchctl 세션에도 상태가 남을 수 있습니다. 종료 단계에서 각 경계를 검사하고 검증된 작업 경로만 삭제해야 합니다.
워크로드에 맞는 클라우드 Mac 선택
칩, 메모리, 저장 공간, 대여 기간 및 리전을 확인한 후 Apple Silicon 독점 물리 서버를 주문하세요.