클라우드 Mac에서 iOS 의존성 공급망 감사하기

클라우드 Mac에서 iOS 의존성 공급망 감사하기

같은 커밋이 어제는 통과했지만 오늘은 다른 전이 의존성을 받아 오는 상황은 클라우드 Mac 빌드에서 원인을 추적하기 특히 어려운 문제다. 겉으로는 단순한 컴파일 오류처럼 보여도 실제로 달라진 것은 패키지 리비전, 다운로드된 콘텐츠 또는 새로 추가된 스크립트 단계일 수 있다. 이런 문제는 단순히 “다시 실행”하는 방식으로 해결해서는 안 된다. 모든 빌드가 무엇을 해석했는지, 콘텐츠가 달라졌는지, 그 변경이 검토를 거쳤는지라는 세 가지 질문에 답할 수 있어야 한다.

보존해야 할 증거부터 정의하기

의존성 감사는 최소한 SwiftPM과 CocoaPods의 직접 및 전이 의존성을 모두 다뤄야 한다. 증거는 다음 세 계층으로 나누는 것이 좋다.

계층 증거 확인할 사항
선언 Package.swift, Podfile, Xcode 프로젝트 참조 팀이 도입하려는 항목
해석 Package.resolved, Podfile.lock 실제로 고정된 버전 또는 리비전
빌드 자재 명세서, 파일 해시, 빌드 로그 이번 빌드에서 실제로 사용된 항목

Package.resolvedPodfile.lock은 반드시 버전 관리 저장소에 포함해야 한다. CI에서 이 파일들을 무시해서는 안 되며, 파이프라인이 파일을 자동으로 업데이트한 뒤 그대로 배포를 계속하도록 두어서도 안 된다.

잠금 파일 자체가 보안성을 보장하는 것은 아니다. 잠금 파일은 안정적인 입력일 뿐이며, 이 입력을 빌드에 사용해도 되는 이유는 검토 기록, 해시, 차이 이력이 뒷받침한다.

ZoomMini 전용 노드에서는 작업 디렉터리를 지속적으로 유지할 수 있다. 그러나 감사의 기준은 여전히 저장소에 포함된 파일이어야 하며, 특정 머신의 캐시 상태를 유일한 증거로 삼아서는 안 된다.

해석 디렉터리를 고정하고 깨끗한 상태에서 해석하기

먼저 SwiftPM 다운로드 디렉터리를 작업 공간 안으로 고정해 서로 다른 runner가 비교할 수 없는 전역 캐시를 사용하지 않도록 한다. workspace가 포함된 프로젝트에서는 다음을 실행한다.

set -euo pipefail
ROOT="$(pwd)"
SPM_DIR="$ROOT/.audit/spm"
DERIVED_DIR="$ROOT/.audit/DerivedData"

rm -rf "$SPM_DIR" "$DERIVED_DIR"
mkdir -p "$SPM_DIR" "$DERIVED_DIR"

xcodebuild \
  -resolvePackageDependencies \
  -workspace App.xcworkspace \
  -scheme App \
  -clonedSourcePackagesDirPath "$SPM_DIR"

xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Release \
  -derivedDataPath "$DERIVED_DIR" \
  -clonedSourcePackagesDirPath "$SPM_DIR" \
  build

프로젝트에 .xcodeproj만 있다면 -workspace-project로 바꾼다. 해석이 끝나면 저장소가 수정되었는지 즉시 확인한다.

git diff --exit-code -- '**/Package.resolved' Podfile.lock

이 명령이 실패하면 빌드를 중단해야 한다. 개발자가 의존성을 업데이트한 뒤 잠금 파일을 커밋하지 않았거나, 서로 다른 도구 버전이 파일 형식을 다시 작성했거나, 의존성 제약 조건이 새로운 전이 버전을 허용한 경우가 대표적인 원인이다.

도구 체인 경계 기록하기

감사 기록에는 xcodebuild -version, swift --version, ruby --version의 출력과 의존성 관리 도구 버전도 포함해야 한다. 이 정보가 반드시 자재 명세서에 들어갈 필요는 없지만 빌드 로그와 함께 보관해야 한다. 그렇지 않으면 잠금 파일은 같은데 해석 결과가 다를 때 그 차이가 도구 체인에서 발생했는지, 소스 코드에서 발생했는지 판단하기 어렵다.

잠금 파일에서 자재 명세서 생성하기

아래 스크립트는 저장소 안의 Package.resolved를 순회한다. 일반적으로 사용되는 최상위 pins 구조와 object.pins 구조를 모두 지원하며, 버전, 브랜치, 리비전 및 잠금 파일 해시를 기록한다.

import glob
import hashlib
import json
import os

items = []

for path in glob.glob("**/Package.resolved", recursive=True):
    if "/.audit/" in f"/{path}":
        continue
    with open(path, "rb") as handle:
        raw = handle.read()
    data = json.loads(raw)
    pins = data.get("pins") or data.get("object", {}).get("pins", [])
    packages = []
    for pin in pins:
        state = pin.get("state", {})
        packages.append({
            "identity": pin.get("identity") or pin.get("package"),
            "location": pin.get("location") or pin.get("repositoryURL"),
            "version": state.get("version"),
            "branch": state.get("branch"),
            "revision": state.get("revision")
        })
    items.append({
        "file": path,
        "sha256": hashlib.sha256(raw).hexdigest(),
        "packages": sorted(packages, key=lambda item: item["identity"] or "")
    })

for path in glob.glob("**/Podfile.lock", recursive=True):
    with open(path, "rb") as handle:
        raw = handle.read()
    items.append({
        "file": path,
        "sha256": hashlib.sha256(raw).hexdigest()
    })

os.makedirs(".audit", exist_ok=True)
with open(".audit/dependency-manifest.json", "w") as handle:
    json.dump({"artifacts": items}, handle, indent=2, sort_keys=True)

실행이 끝나면 dependency-manifest.json을 빌드 산출물로 보관한다. 팀이 이 파일을 저장소에 커밋하기로 했다면 의존성 업데이트 커밋에서 함께 갱신해야 하며, 배포 작업이 이를 조용히 덮어쓰도록 해서는 안 된다.

차이 검사를 배포 게이트로 만들기

게이트는 모든 변경을 무조건 금지하는 방식이 아니라 변경 이유를 설명할 수 있도록 요구해야 한다. 다음 순서로 구현할 수 있다.

  1. 선언 파일과 잠금 파일이 함께 변경되었는지 확인한다.
  2. 격리된 의존성 디렉터리를 비우고 다시 해석한다.
  3. 해석 과정에서 잠금 파일이 다시 작성되지 않았는지 확인한다.
  4. 새 자재 명세서를 생성해 기준선과 비교한다.
  5. 새로 추가된 의존성의 라이선스, 유지 관리 출처 및 스크립트 실행 가능 여부를 확인한다.
  6. 컴파일과 테스트를 마친 뒤 명세서, 잠금 파일 해시 및 도구 체인 버전을 보관한다.

먼저 표준 차이 비교 도구를 사용해 자동화된 게이트를 구성할 수 있다.

python3 tools/dependency_manifest.py
diff -u audit-baseline/dependency-manifest.json .audit/dependency-manifest.json

업데이트를 허용할 때도 게이트를 그대로 비활성화해서는 안 된다. 새 기준선은 별도 브랜치에서 커밋하고, 검토자가 새로 추가된 저장소 주소, 버전에서 브랜치로의 전환, 리비전 해시 변경 및 버전 번호가 없는 의존성을 중점적으로 확인하도록 해야 한다.

스크립트 단계 집중 점검하기

의존성은 빌드 단계를 통해 shell 스크립트를 실행할 수 있다. Xcode 프로젝트와 생성된 의존성 프로젝트를 검토할 때는 스크립트가 네트워크에 접근하는지, 작업 공간 외부의 자격 증명을 읽는지, 소스 디렉터리를 수정하는지 또는 환경 변수를 로그에 기록하는지 확인해야 한다. CI 계정에는 체크아웃, 해석 및 빌드에 필요한 최소 권한만 부여해야 한다.

이상 상황 대응 및 배포 점검 항목

잠금 파일이 바뀌지 않았는데 자재 명세서 해시가 달라졌다면 먼저 배포를 중단하고 작업 공간, 해석 로그 및 다운로드 디렉터리를 보존한다. 그런 다음 파일이 다시 생성되었는지, 의존성이 변경 가능한 브랜치를 사용하는지, 내부 미러의 콘텐츠가 교체되었는지, 해석 도구 버전이 달라졌는지 확인한다. 가장 가치 있는 현장 증거를 잃을 수 있으므로 캐시부터 삭제해서는 안 된다.

일상적인 배포 전에는 다음 항목을 고정적으로 점검할 수 있다.

  • 선언 파일, 잠금 파일 및 자재 명세서가 동일한 커밋에 속한다.
  • 모든 SwiftPM 의존성에 명시적인 버전 또는 변경 불가능한 리비전이 지정되어 있다.
  • CocoaPods 해석 결과가 Podfile.lock과 일치한다.
  • 새로 추가된 의존성의 라이선스와 스크립트 단계가 검토되었다.
  • 깨끗한 상태에서 해석해도 Git 차이가 발생하지 않는다.
  • 빌드 산출물이 명세서 해시 및 도구 체인 버전과 연결되어 있다.
  • CI에서 의존성 스크립트에 불필요한 토큰이나 개인 키가 노출되지 않는다.

의존성 공급망 감사의 목적은 더 긴 목록을 만드는 것이 아니라 반복 가능한 판단 체계를 세우는 데 있다. 입력이 바뀌지 않았다면 결과를 검증할 수 있어야 하고, 입력이 바뀌었다면 배포가 중단되어야 하며, 검토자는 변경이 발생한 위치를 정확히 확인할 수 있어야 한다.

자주 묻는 질문

Package.resolved와 Podfile.lock을 커밋하면 별도 명세가 필요 없나요?

필요합니다. 잠금 파일은 해석 결과를 고정하고, 명세는 이름·버전·리비전·파일 해시를 같은 형식으로 정리해 빌드 간 비교와 보관을 가능하게 합니다.

의존성 검사는 전체 Xcode 빌드 전과 후 중 언제 실행해야 하나요?

깨끗한 의존성 해석 직후, 전체 빌드 전에 실행해야 합니다. 그래야 검토되지 않은 상태에서 프로젝트의 스크립트 단계가 실행되는 일을 줄일 수 있습니다.

간접 의존성의 리비전이 바뀌면 어떻게 대응해야 하나요?

배포를 멈추고 상위 의존성 경로, 변경 내용과 라이선스를 확인합니다. 별도 브랜치에서 다시 해석하고 테스트한 뒤 잠금 파일과 감사 기록을 함께 반영합니다.

ZoomMini 클라우드 Mac

빌드, 테스트 및 실험에 사용할 독점 물리 노드 선택

두 가지 M4 구성이 싱가포르, 도쿄, 서울 및 홍콩 노드를 지원합니다. 실제 이용 가능 여부는 주문 시 반환되는 결과를 기준으로 합니다.

모델 선택 및 주문