동일한 Xcode 빌드 작업을 며칠 동안 연속으로 실행하다 보면 DerivedData에 쓰거나, 의존성 캐시를 갱신하거나, 이전 산출물을 삭제하는 과정에서 갑자기 Permission denied가 발생할 수 있습니다. 저장소를 다시 체크아웃하면 잠시 정상화되지만 다음 병렬 작업에서 문제가 재발하기도 합니다. 이때 Xcode부터 의심하거나 디렉터리 권한을 무턱대고 확대해서는 안 됩니다. 더 흔한 원인은 특정 스크립트가 실행 사용자를 바꾸었거나, 서로 다른 umask를 상속했거나, 작업공간에 추가 ACL을 남긴 경우입니다.
먼저 실패 당시 상태 보존하기
권한 문제는 “정리 후 재실행” 과정에서 흔적이 사라지기 쉽습니다. 먼저 작업의 실행 사용자, 홈 디렉터리, 현재 디렉터리, 기본 권한 마스크를 기록한 다음 최초로 실패한 경로를 확인합니다. 처음부터 디스크 전체를 검사할 필요는 없습니다.
printf 'user=%s
' "$(id -un)"
printf 'groups=%s
' "$(id -Gn)"
printf 'home=%s
' "$HOME"
printf 'cwd=%s
' "$PWD"
printf 'umask=%s
' "$(umask)"
target="${FAILED_PATH:?set FAILED_PATH first}"
stat -f 'owner=%Su group=%Sg mode=%Sp path=%N' "$target"
ls -led "$target"
stat은 “누가 소유하고 있으며 기본 모드가 무엇인지”를 보여주고, ls -le는 ACL을 표시합니다. 디렉터리 모드상 쓰기가 허용되어 보여도 실제 접근이 반드시 성공하는 것은 아닙니다. 상위 디렉터리에 실행 권한이 없거나, ACL에 제한 항목이 있거나, 파일이 다른 작업 사용자의 소유라면 작업이 차단될 수 있습니다.
최초로 실패한 경로와 그 상위 디렉터리만 단계별로 확인합니다. 작업공간 전체의 권한을 재귀적으로 변경하면 실패 당시의 흔적이 훼손될 뿐 아니라 자격 증명, 캐시, 빌드 산출물이 무관한 프로세스에 노출될 수 있습니다.
세 가지 권한 변동 구분하기
소유자가 변경된 경우
가장 흔한 사례는 특정 단계가 권한 상승 명령으로 의존성을 설치하거나 파일을 복사한 뒤, 산출물을 다른 사용자 소유로 남기는 경우입니다. 먼저 현재 작업 사용자의 소유가 아닌 항목을 나열합니다.
workspace="${WORKSPACE:?set WORKSPACE first}"
find "$workspace" -x ! -user "$(id -un)" -print
출력이 특정 작업 디렉터리에 집중되어 있다면 재귀적 chown으로 원인을 가리지 말고 해당 디렉터리를 만든 스크립트를 추적해야 합니다. CI 작업공간은 생성 시점부터 작업 사용자가 소유하는 것이 가장 좋습니다. 높은 권한으로 작업해야 하는 경우에도 출력이 저장소, 캐시 또는 DerivedData에 다시 기록되지 않도록 해야 합니다.
예상하지 못한 ACL이 있는 경우
Finder 작업, 마이그레이션 스크립트 또는 복사 도구는 ACL을 그대로 보존할 수 있습니다. ls -le의 기본 권한 행 아래에 번호가 붙은 항목이 나타나면 해당 ACL의 출처를 확인해야 합니다. 재생성 가능한 임시 작업공간임이 확실한 디렉터리에 한해서만 ACL을 선택적으로 제거합니다.
job_dir="${JOB_DIR:?set JOB_DIR first}"
chmod -RN "$job_dir"
사용자 홈 디렉터리나 자격 증명 디렉터리에는 이 명령을 실행하지 마십시오. 수정 후 ls -led를 다시 실행하여 ACL이 사라졌고 기본 모드가 여전히 요구 사항을 충족하는지 확인합니다.
umask가 일치하지 않는 경우
SSH 대화형 세션, CI 데몬, 독립 실행형 스크립트가 항상 동일한 셸 설정을 읽는 것은 아닙니다. 한 작업이 077로 캐시를 생성하면 후속 작업이 같은 그룹에 속해 있어도 해당 캐시를 재사용하지 못할 수 있습니다. 시작 파일에 의존하는 대신 작업 진입점에서 명시적으로 선언하는 편이 좋습니다.
umask 022
install -d -m 0755 "$JOB_DIR"
install -d -m 0755 "$JOB_DIR/DerivedData"
install -d -m 0755 "$JOB_DIR/Artifacts"
민감한 자료는 별도 디렉터리에 보관하고 더 엄격한 모드를 적용해야 합니다. 빌드 캐시를 공유하려는 목적으로 모든 권한을 일괄 완화해서는 안 됩니다.
작업마다 독립된 작업공간 사용하기
병렬 작업이 하나의 DerivedData 또는 아카이브 디렉터리를 공유하면 한 작업의 정리 동작이 다른 작업에서 쓰고 있는 파일과 충돌할 수 있습니다. 저장소 식별자, 커밋 식별자, 작업 번호를 조합해 디렉터리를 만들고 해당 경로를 빌드 명령에 명시적으로 전달하는 것이 좋습니다.
run_id="${CI_RUN_ID:?set CI_RUN_ID first}"
root="$HOME/ci-runs/$run_id"
src="$root/source"
derived="$root/DerivedData"
artifacts="$root/Artifacts"
umask 022
install -d -m 0755 "$src" "$derived" "$artifacts"
xcodebuild \
-workspace App.xcworkspace \
-scheme App \
-configuration Release \
-derivedDataPath "$derived" \
archive \
-archivePath "$artifacts/App.xcarchive"
공유 캐시와 작업 출력은 분리해야 합니다. 의존성 다운로드 캐시는 읽기 전용으로 재사용할 수 있지만 DerivedData, 아카이브, 로그는 작업별로 격리해야 합니다. 이렇게 하면 작업 간 권한 오염을 줄일 수 있고 작업이 끝날 때까지 실패 당시 상태도 온전히 보존할 수 있습니다.
| 경로 유형 | 권장 소유 주체 | 수명 주기 | 병렬 쓰기 허용 여부 |
|---|---|---|---|
| 소스 코드 체크아웃 | 개별 작업 | 단일 실행 | 아니요 |
| DerivedData | 개별 작업 | 단일 실행 | 아니요 |
| 아카이브 및 로그 | 개별 작업 | 검증 후 정리 | 아니요 |
| 다운로드 캐시 | 고정 작업 사용자 | 여러 작업에 걸쳐 유지 | 캐시 처리 절차만 갱신 |
빌드 전후에 검증 기준 적용하기
권한 검사는 오류가 발생한 뒤 수동으로 수행하는 작업이 아니라 작업 진입점의 일부여야 합니다. 빌드 전에는 디렉터리에 쓸 수 있는지, 소유자가 올바른지, 예상하지 못한 ACL이 없는지 확인합니다. 빌드 후에는 다른 소유자의 파일이 생성되었는지 검사합니다. 아래 검사는 권한 변동을 발견하면 작업을 즉시 종료합니다.
test -d "$JOB_DIR"
test -w "$JOB_DIR"
unexpected_owner="$(
find "$JOB_DIR" -x ! -user "$(id -un)" -print -quit
)"
if [ -n "$unexpected_owner" ]; then
printf 'unexpected owner: %s
' "$unexpected_owner" >&2
exit 1
fi
if ls -led "$JOB_DIR" | tail -n +2 | grep -q '^[[:space:]]*[0-9]:'; then
printf 'unexpected ACL on job directory
' >&2
exit 1
fi
팀에서 공유 캐시가 필요하다면 캐시 쓰기 주체, 갱신 시점, 원자적 교체 방식을 별도로 정의해야 합니다. 모든 빌드 작업이 동시에 같은 디렉터리를 수정하도록 두어서는 안 됩니다. 작업 완료 후 정리할 때도 현재 run_id에 해당하는 경로만 삭제해야 하며, 먼저 해당 경로가 예상한 루트 디렉터리 아래에 있는지 검증해야 합니다.
문제를 만든 스크립트에서 수정하기
신뢰할 수 있는 수정은 네 가지 질문에 답할 수 있어야 합니다. 어떤 단계가 비정상 파일을 생성했는지, 생성 당시 어떤 사용자를 사용했는지, 어떤 umask를 상속했는지, 왜 공유 디렉터리에 기록했는지를 알아야 합니다. 답을 찾았다면 대화형 로그인 후의 환경에 의존하지 말고 디렉터리 생성 방식, 실행 사용자, 출력 위치를 스크립트에 명시해야 합니다.
RunnerVM의 클라우드 Mac에서 CI를 실행할 때도 각 작업 시작 시 최소한의 환경 스냅샷을 출력하고, 콘솔에서 현재 선택 가능한 구성을 확인해야 합니다. 머신이 재시작되거나 작업이 마이그레이션된 뒤에도 진입 스크립트가 디렉터리와 권한 기준선을 다시 설정할 수 있다면 빌드는 이전 실행에서 남은 상태에 의존하지 않습니다. 최종 목표는 모든 권한 제한을 없애는 것이 아니라 각 파일의 생성자, 읽기·쓰기 범위, 정리 책임을 예측 가능하게 만드는 것입니다.
자주 묻는 질문
Permission denied가 발생하면 작업공간 전체에 chmod 777을 적용해도 되나요?
권장하지 않습니다. 소유권과 ACL 문제를 숨기고 다른 프로세스가 빌드 파일을 수정할 수 있게 됩니다. stat과 ls -le로 실패 경로를 확인한 뒤 해당 작업 디렉터리만 수정해야 합니다.
같은 스크립트가 SSH에서는 성공하지만 CI 작업에서는 실패하는 이유는 무엇인가요?
실행 사용자, HOME, umask, PATH와 시작 환경이 다를 수 있습니다. 작업 시작 시 해당 값을 기록하고 스크립트에서 필요한 권한 기준을 명시적으로 설정해야 합니다.
다음 빌드를 독점 물리 Mac에서 실행하세요
작업 주기에 맞는 클라우드 Mac 노드를 선택하고, 콘솔에서 주문, 연결 정보 및 지원 요청을 관리하세요.