하나의 클라우드 Mac에서 여러 CI 작업을 연속으로 실행할 때 가장 재현하기 어려운 장애는 코드 오류가 아니라 이전 작업이 남긴 임시 파일에서 비롯되는 경우가 많습니다. 오래된 소켓, 해제되지 않은 잠금, 쓰기가 덜 끝난 캐시, 이름이 겹치는 내보내기 디렉터리 때문에 동일한 커밋을 빌드해도 실행할 때마다 결과가 달라질 수 있습니다. 전용 물리 노드는 다른 계정의 간섭은 차단하지만 동일 계정에서 동시에 실행되는 작업까지 자동으로 격리하지는 않습니다. 따라서 파일 시스템 경계는 파이프라인에서 명시적으로 설정해야 합니다.
작업 간에 누출될 수 있는 디렉터리부터 식별하기
“작업 공간이 비어 있다”는 사실을 환경이 초기 상태로 복구되었다는 의미로 받아들여서는 안 됩니다. macOS 도구는 작업 공간 외부의 여러 위치에 상태를 저장하므로, 조사할 때는 최소한 다음 네 가지 위치를 확인해야 합니다.
TMPDIR아래의 임시 파일, 소켓, 다운로드 조각- Xcode의 DerivedData, 인덱스, 빌드 데이터베이스
- Clang 모듈 캐시와 SwiftPM의 작업별 캐시
- 스크립트가 직접 생성한 내보내기, 로그, 테스트 첨부 파일 디렉터리
먼저 정상 빌드 전후에 각각 env | sort를 실행하여 실제로 사용된 HOME, TMPDIR, 작업 공간 경로를 기록합니다. 그런 다음 처음부터 재귀적으로 삭제하지 말고 du -sh로 관련 디렉터리의 크기를 비교합니다. 장애가 동시 실행 시에만 발생한다면 고정된 파일 이름, 포트, 출력 경로부터 우선 확인해야 합니다.
격리의 목적은 매번 모든 캐시를 삭제하는 것이 아니라, 각 작업이 자체 쓰기 가능 디렉터리를 명확히 소유하고 자신이 생성한 내용만 정리하도록 만드는 것입니다.
작업마다 고유한 루트 디렉터리 만들기
작업 식별자는 CI 시스템이 제공하는 고유 번호를 사용해야 합니다. 신뢰할 수 있는 번호가 없다면 mktemp로 생성할 수 있습니다. 브랜치 이름만 사용해서는 안 됩니다. 같은 브랜치에서 여러 빌드가 동시에 실행될 수 있기 때문입니다.
#!/bin/bash
set -euo pipefail
job_id="${CI_JOB_ID:-manual}"
job_root="$(mktemp -d "${TMPDIR:-/tmp}/surevm-ci.${job_id}.XXXXXX")"
export TMPDIR="${job_root}/tmp"
export DERIVED_DATA="${job_root}/DerivedData"
export MODULE_CACHE="${job_root}/ModuleCache"
export RESULT_BUNDLE="${job_root}/Results/Test.xcresult"
mkdir -p "$TMPDIR" "$DERIVED_DATA" "$MODULE_CACHE" "$(dirname "$RESULT_BUNDLE")"
cleanup() {
local status=$?
rm -rf "$job_root"
exit "$status"
}
trap cleanup EXIT INT TERM
mktemp가 생성한 디렉터리는 기본적으로 다른 작업의 디렉터리와 이름이 겹치지 않습니다. 정리 함수는 먼저 종료 코드를 저장한 다음 디렉터리를 삭제하고 원래 상태 코드로 종료하므로, 정리 명령 때문에 테스트 실패가 가려지는 일을 방지할 수 있습니다. 작업 공간 이름에 공백이 있을 때 경로가 여러 인수로 분리되지 않도록 경로 변수는 따옴표로 감싸야 합니다.
HOME을 무작정 변경하지 않기
HOME을 작업 디렉터리로 지정하면 완벽하게 격리되는 것처럼 보이지만, 현재 사용자의 Keychain, 도구 환경설정, 이미 완료된 시스템 초기화와의 연결이 끊길 수 있습니다. 일반적인 컴파일 작업에서는 특정 패키지 관리 도구에 별도 캐시를 지정할 수 있습니다. 코드 서명이 포함된 작업에서는 일반적으로 HOME을 유지하고 빌드 산출물과 명확하게 이전할 수 있는 캐시만 격리합니다.
Xcode 쓰기 경로를 명시적으로 연결하기
TMPDIR만 설정해도 DerivedData가 자동으로 이전되지는 않습니다. 빌드 명령에 주요 경로를 인수로 전달하여 이번 작업에서 사용한 디렉터리가 로그에 직접 표시되도록 해야 합니다.
xcodebuild \
-workspace App.xcworkspace \
-scheme App \
-configuration Debug \
-derivedDataPath "$DERIVED_DATA" \
-clonedSourcePackagesDirPath "${job_root}/SourcePackages" \
COMPILER_INDEX_STORE_ENABLE=NO \
CLANG_MODULE_CACHE_PATH="$MODULE_CACHE" \
test \
-resultBundlePath "$RESULT_BUNDLE"
CI에서 코드 인덱싱이 필요하지 않다면 인덱스 저장소를 비활성화하여 불필요한 쓰기를 줄일 수 있습니다. 테스트 결과는 작업 루트 디렉터리 안에 저장해야 하지만, 실패 후 첨부 파일을 업로드해야 한다면 정리를 시작하기 전에 아카이브를 완료해야 합니다. 더 안전한 순서는 “테스트 실행—진단 파일을 파이프라인 산출물 영역에 복사—복사 결과 검증—종료”입니다.
공유 캐시는 읽기 전용으로, 작업 캐시는 쓰기 가능하게
의존성을 반드시 재사용해야 한다면 검증된 공유 캐시를 시드로 취급할 수 있습니다. 작업을 시작할 때 공유 캐시를 자체 디렉터리로 복사하거나 복원하고, 빌드 중에는 작업별 복사본에만 씁니다. 빌드가 성공한 뒤 별도 단계에서 공유 버전을 업데이트합니다. 두 xcodebuild 프로세스가 동일한 모듈 캐시나 빌드 데이터베이스에 동시에 쓰도록 해서는 안 됩니다.
비정상 종료 시에도 증거 남기기
trap 'rm -rf ...' EXIT를 그대로 사용하면 실패하는 순간 현장 정보가 삭제됩니다. 실제 운영에서는 먼저 종료 코드, 남은 디스크 공간, 디렉터리 크기, 아직 종료되지 않은 관련 프로세스를 포함하는 간단한 진단 목록을 만든 다음 보존할 파일을 결정해야 합니다. 토큰이나 서명 관련 값이 포함될 수 있으므로 환경 변수 전체를 로그에 출력해서는 안 됩니다.
정리 전에 다음 명령을 실행할 수 있습니다.
{
echo "exit_status=$status"
df -h "$job_root"
du -sh "$job_root"/* 2>/dev/null || true
find "$job_root" -type s -print
} > "${ARTIFACT_DIR}/job-cleanup.txt"
ARTIFACT_DIR는 작업 루트 디렉터리 외부에 있어야 하며, 업로드와 정기 정리는 파이프라인에서 담당해야 합니다. 실패 당시의 상태를 보존해야 한다면 진단 디렉터리를 압축한 뒤 원본 디렉터리를 삭제할 수 있으며, 로그와 경로에 포함된 민감한 정보도 제거해야 합니다.
잔여 파일 감사로 격리 상태 검증하기
구성을 변경한 뒤 동일한 커밋을 연속으로 두 번 실행하고, 두 번째 실행 전에 이전 작업 식별자에 해당하는 디렉터리가 남아 있지 않은지 확인합니다. 작업을 동시에 실행할 때는 두 작업의 TMPDIR, DerivedData, 결과 번들 경로가 완전히 다른지도 점검해야 합니다.
검증 목록은 다음 다섯 항목으로 고정할 수 있습니다. 각 작업에 고유한 루트 디렉터리가 있는지, 모든 Xcode 경로가 명시적으로 지정되었는지, 공유 캐시에 동시 쓰기가 발생하지 않는지, 성공과 실패 모두에서 종료 훅이 실행되는지, 진단 산출물을 먼저 복사한 후 정리하는지 확인합니다. 디스크 사용량이 계속 증가한다면 삭제 범위를 확대하지 말고 작업 식별자를 기준으로 경계에 포함되지 않은 도구 디렉터리를 추적해야 합니다.
SureVM의 전용 물리 Mac에서는 이 방식이 장기간 운영되는 빌드 노드에 특히 적합합니다. 시스템 단위의 도구 체인은 그대로 유지하면서 각 작업의 가변 상태를 추적하고 삭제할 수 있는 디렉터리 안으로 제한할 수 있기 때문입니다. 최종 판단 기준은 간단합니다. 이전 작업이 성공하거나 실패하거나 중단되더라도 다음 작업의 입력 조건에 영향을 주어서는 안 됩니다.
자주 묻는 질문
모든 CI 작업이 하나의 고정 임시 디렉터리를 공유하면 어떤 문제가 생기나요?
병렬 작업이 같은 이름의 파일을 덮어쓸 수 있고, 실패한 작업이 남긴 잠금 파일과 소켓이 다음 빌드에 영향을 줄 수 있습니다. 승인된 상위 경로 아래에 작업별 고유 디렉터리를 만들어야 합니다.
TMPDIR만 바꾸면 DerivedData도 격리되나요?
아닙니다. Xcode의 DerivedData와 모듈 캐시는 다른 경로에 저장될 수 있으므로 빌드 인자와 환경 변수로 각각 작업 전용 경로를 지정해야 합니다.
서명 작업에서도 HOME을 별도로 설정해야 하나요?
일반적으로 권장하지 않습니다. 서명 과정은 현재 사용자의 Keychain 설정에 의존할 수 있으므로 임시 파일과 빌드 캐시부터 분리하는 편이 안전합니다.
빌드, 개발 및 실험을 위한 전용 물리 Mac 노드를 선택하세요
두 가지 Mac mini M4 구성, 네 가지 대여 기간, 주문 가능한 다섯 개 노드를 비교한 뒤 실제 워크플로에 맞는 구성을 선택하세요.