클라우드 Mac CI에서 SMB 공유 저장소로 빌드 오염 막기

클라우드 Mac CI에서 SMB 공유 저장소로 빌드 오염 막기

CI 노드에서 아카이브 작업이 끝나면 산출물을 테스트, 배포 또는 장기 보관 작업으로 전달해야 합니다. 가장 먼저 떠올리기 쉬운 방법은 SMB 공유 디렉터리를 작업 공간으로 직접 지정해 여러 클라우드 Mac이 동일한 프로젝트를 읽고 쓰게 하는 것입니다. 소규모 저장소에서는 당분간 별문제 없이 동작할 수 있지만, 네트워크가 불안정하거나 파일 잠금이 지연되거나 마운트가 끊기면 Xcode 중간 파일이 손상될 수 있습니다. 심한 경우 결과가 같은 이름의 비어 있는 로컬 디렉터리에 기록될 수도 있습니다.

더 안전한 경계는 명확합니다. SMB는 입력 스냅샷을 받고 최종 산출물을 전달하는 용도로만 사용하고, 소스 코드 전개, 의존성 해석, DerivedData 및 임시 파일은 모두 로컬 디스크에 두어야 합니다.

먼저 디렉터리를 세 가지 영역으로 분리하기

각 작업에는 서로 용도를 섞지 않는 경로가 최소 세 개 필요합니다.

  • 읽기 전용 입력 영역: 소스 코드 스냅샷이나 버전이 고정된 의존성 패키지를 보관합니다.
  • 로컬 작업 영역: 체크아웃한 소스 코드, DerivedData, 테스트 결과 및 임시 파일을 저장합니다.
  • 원격 전달 영역: 검증을 마친 아카이브, 로그 요약 및 매니페스트만 보관합니다.

두 개의 동시 실행 작업이 같은 DerivedData를 공유하게 해서는 안 됩니다. 동일한 커밋을 빌드하더라도 인덱스, 모듈 캐시 및 빌드 데이터베이스가 동시에 변경될 수 있습니다. ~/ci-work/$RUN_ID처럼 작업 식별자를 경로에 포함하고, 작업이 끝난 뒤 보존 정책에 따라 정리하는 방식을 권장합니다.

네트워크 공유는 로컬 디스크를 투명하게 대체하는 저장소가 아니라 산출물 전달 경계로 취급해야 합니다. 빌드 성공과 업로드 성공도 반드시 서로 독립된 상태여야 합니다.

상주 프로세스로 SMB 마운트 관리하기

무인 작업에서는 명령줄 인자에 사용자 이름과 비밀번호를 조합해 넣어서는 안 됩니다. 통제되는 상주 프로세스가 SMB를 미리 마운트하고, 별도의 키체인이나 접근이 제한된 설정에서 자격 증명을 읽도록 하는 편이 적절합니다. 일반 빌드 작업에는 대상 디렉터리에 필요한 최소한의 쓰기 권한만 부여합니다.

작업을 시작할 때 디렉터리가 존재하는지만 확인해서는 안 됩니다. SMB 연결이 끊긴 뒤에도 마운트 지점이 일반 로컬 디렉터리로 바뀐 채 남아 있을 수 있습니다. 최소한 파일 시스템 유형과 쓰기 가능 여부를 확인하고, 작은 파일을 한 번 쓰고 다시 읽어야 합니다.

set -euo pipefail

SHARE_ROOT="/Volumes/ci-artifacts"
PROBE="$SHARE_ROOT/.probe-${RUN_ID}"

fs_type="$(stat -f '%T' "$SHARE_ROOT")"
test "$fs_type" = "smbfs"
test -w "$SHARE_ROOT"

printf '%s\n' "$RUN_ID" > "$PROBE"
test "$(cat "$PROBE")" = "$RUN_ID"
rm -f "$PROBE"

어느 단계에서든 실패하면 작업을 중단해야 합니다. 마운트 지점을 자동으로 생성한 뒤 작업을 계속해서는 안 됩니다. 그렇게 하면 공유 연결이 한 번 끊긴 것만으로도 “빌드는 성공했지만 산출물은 로컬에만 남은” 상황이 발생할 수 있습니다.

빌드 작업은 로컬 디스크로 제한하기

먼저 고정된 커밋을 로컬 임시 디렉터리에 동기화한 다음 의존성 해석과 빌드를 실행합니다. 작업 디렉터리, DerivedData 및 결과 번들에는 모두 고유한 작업 식별자를 포함해 병렬 작업이 서로의 데이터를 덮어쓰지 않도록 해야 합니다.

LOCAL_ROOT="$(mktemp -d "$TMPDIR/vmcache-ci.XXXXXX")"
trap 'rm -rf "$LOCAL_ROOT"' EXIT

rsync -a --delete "$SOURCE_SNAPSHOT/" "$LOCAL_ROOT/repo/"

xcodebuild \
  -workspace "$LOCAL_ROOT/repo/App.xcworkspace" \
  -scheme App \
  -configuration Release \
  -derivedDataPath "$LOCAL_ROOT/DerivedData" \
  -resultBundlePath "$LOCAL_ROOT/TestResults.xcresult" \
  build

여기서 SOURCE_SNAPSHOT은 다른 작업이 계속 수정할 수 있는 활성 디렉터리가 아니라 변경 불가능한 커밋을 가리켜야 합니다. 입력이 공유 볼륨에서 제공된다면 동기화를 시작하기 전에 커밋 번호나 매니페스트를 확인할 수 있습니다.

DerivedData 전체를 캐시하지 않기

작업 간에 DerivedData 전체를 복사하는 것은 대개 이득보다 비용이 큽니다. 캐시 크기가 크고 내부 경로나 빌드 설정도 달라질 수 있기 때문입니다. 명확하게 재사용할 수 있는 의존성 다운로드 디렉터리만 캐시하고, Xcode 버전, 아키텍처 및 잠금 파일의 해시를 캐시 키에 포함하는 편이 관리하기 쉽습니다. 캐시 키가 일치하지 않으면 새로 생성하고, 비슷하다는 이유로 재사용하지 않습니다.

임시 이름으로 원자적 전달 완료하기

처음부터 정식 파일 이름으로 복사하면 아직 완성되지 않은 파일이 노출됩니다. 하위 작업은 파일이 나타나는 즉시 읽기 시작할 수 있지만, 그 시점에도 데이터가 전송 중일 수 있습니다. 올바른 순서는 로컬에서 패키징하고, 로컬에서 해시를 계산하고, 공유 볼륨에 임시 이름으로 복사한 뒤, 공유 볼륨에서 검증하고, 마지막으로 같은 디렉터리 안에서 이름을 변경하는 것입니다.

ARTIFACT="$LOCAL_ROOT/App-release.zip"
ditto -c -k --norsrc "$LOCAL_ROOT/DerivedData/Build/Products/Release" "$ARTIFACT"

REMOTE_DIR="$SHARE_ROOT/releases/$GIT_COMMIT"
REMOTE_TMP="$REMOTE_DIR/.App-release.zip.${RUN_ID}.partial"
REMOTE_FINAL="$REMOTE_DIR/App-release.zip"

mkdir -p "$REMOTE_DIR"
cp "$ARTIFACT" "$REMOTE_TMP"

local_hash="$(shasum -a 256 "$ARTIFACT" | awk '{print $1}')"
remote_hash="$(shasum -a 256 "$REMOTE_TMP" | awk '{print $1}')"
test "$local_hash" = "$remote_hash"

mv "$REMOTE_TMP" "$REMOTE_FINAL"
printf '%s  %s\n' "$remote_hash" "App-release.zip" \
  > "$REMOTE_DIR/SHA256SUMS.${RUN_ID}"

원자적 이름 변경이 보장되려면 임시 파일과 정식 파일이 동일한 공유 볼륨에 있어야 하며, 가능하면 같은 디렉터리에 두는 것이 좋습니다. 로컬에 먼저 기록한 다음 볼륨을 가로질러 mv하지 마십시오. 서로 다른 볼륨 사이의 작업은 복사 후 삭제하는 방식으로 바뀝니다.

실패를 판별하고 정리할 수 있게 만들기

업로드에 실패하면 재전송할 수 있도록 로컬 빌드 결과를 통제된 기간 동안 보관해야 합니다. 그러나 해당 작업을 전달 완료 상태로 표시해서는 안 됩니다. build_completed, artifact_verified, publish_completed를 각각 기록하고, 하위 작업에서는 마지막 상태만 허용하는 것이 좋습니다.

보존 기간이 지난 .partial 파일도 정기적으로 정리해야 합니다. 정리 작업에서는 파일 이름 형식과 수정 시간을 확인해야 하며, 임시 접미사가 붙은 파일만 처리하고 정식 산출물을 검색하거나 삭제해서는 안 됩니다. 공유 볼륨이 읽기 전용으로 바뀌거나 체크섬이 일치하지 않거나 연결이 반복해서 끊기면 게시 대기열을 중단하고, 먼저 로컬 결과와 마운트 진단 정보를 보존해야 합니다.

VMCache의 클라우드 Mac에서 이 절차를 구현할 때 중요한 것은 특정 마운트 명령이 아니라 경계를 명확히 하는 것입니다. 네트워크 볼륨은 교환을 담당하고 로컬 디스크는 빌드를 담당하며, 정식 파일 이름은 검증이 완료된 산출물에만 부여해야 합니다. 이렇게 하면 SMB에 일시적인 문제가 생겨도 빌드 작업 현장과 전달 상태가 뒤섞이지 않습니다.

자주 묻는 질문

DerivedData를 SMB 공유 폴더에 두어도 되나요?

권장하지 않습니다. DerivedData는 작은 파일과 잠금, 메타데이터 변경을 빈번하게 만들므로 네트워크 볼륨의 지연에 민감합니다. 작업별 로컬 디렉터리를 사용하는 편이 안전합니다.

복사 중인 산출물을 다음 작업이 읽지 못하게 하려면 어떻게 하나요?

공유 폴더에 임시 이름으로 먼저 복사하고 체크섬과 완료 매니페스트를 기록합니다. 검증이 끝난 뒤 같은 디렉터리에서 최종 이름으로 변경하고, 소비자는 최종 이름만 읽게 합니다.

마운트 디렉터리가 보이면 바로 빌드를 시작해도 되나요?

안 됩니다. 파일 시스템 유형과 쓰기 권한을 확인하고 작은 파일을 쓴 뒤 다시 읽는 검사를 해야 합니다. 실패하면 같은 경로의 로컬 빈 폴더에 결과가 기록되지 않도록 즉시 종료합니다.

독점 물리 노드

빌드, 테스트와 MLX 추론을 위한 클라우드 Mac 선택

실제 작업에 맞춰 M4, 메모리, 스토리지, 노드와 사용 기간을 선택하세요. 모든 대여에는 가상 머신이 아닌 독점 물리 장비가 제공됩니다.

대여 플랜 선택