셀프 호스팅 Runner가 GitHub 호스팅 Runner보다 iOS 빌드에 더 적합한 이유는?
GitHub Actions의 macOS 호스팅 Runner는 서버 관리 부담을 줄여 주지만, iOS CI/CD를 실제로 돌리면 세 가지 병목이 반복됩니다. 팀마다 겪는 패턴이 비슷해서, 셀프 호스팅 Runner로 전환하는 경우가 많습니다.
첫 번째는 큐 대기입니다. GitHub macOS Runner 풀은 Linux 대비 훨씬 작습니다. 피크 시간대(UTC 12:00–20:00, 한국·일본 업무 시간)에는 runs-on: macos-latest job이 Runner에 잡히기까지 중앙값 8–15분, 간헐적으로 20분 이상 걸립니다. Clean Build가 6분이면 되는 중형 SwiftUI 앱도, 대기 시간이 컴파일보다 길어지는 날이 흔합니다.
두 번째는 Xcode 버전 고정 불가입니다. macos-latest는 GitHub 인프라 업그레이드와 함께 Xcode가 조용히 바뀝니다. Xcode 15에서 16으로 넘어갈 때 사전 공지 없이 빌드가 깨진 사례가 많고, workflow에 xcode-select 단계를 추가하거나 메이저 버전 호환 작업을 기다려야 했습니다.
세 번째는 무료 분 소진 속도입니다. macOS Runner는 Linux 대비 10배 과금(GitHub 공식 문서)이라, 무료 플랜 월 2,000분은 macOS 기준 약 200분입니다. 스프린트마다 PR 빌드가 몇 번만 돌아도 초과 요금 구간에 들어갑니다.
셀프 호스팅 Runner를 베어메탈 Mac에 붙이면 위 세 가지를 한 번에 해소할 수 있습니다. Runner는 상시 온라인, Xcode 버전은 직접 고정, 실행 시간은 GitHub 과금 대상이 아닙니다. 남은 과제는 24시간 켜 둘 macOS 호스트를 확보하는 일—이 글은 그 부분을 다룹니다.
준비 작업: 클라우드 Mac 노드 개통 및 기본 환경
iOS CI/CD 파이프라인을 연결하려면 SSH로 접속 가능하고 24시간 가동되는 macOS 호스트가 필요합니다. 이 가이드는 VPSRox Mac mini M4 베어메탈 노드를 기준으로 설명합니다. 사무실 Mac mini나 Mac Studio에도 동일하게 적용되며, VPSRox 콘솔에서 배포 후 1–5분 안에 아래 단계를 진행할 수 있습니다.
SSH 접속 및 Xcode Command Line Tools
-
01
SSH 키 인증 구성
콘솔에서 공인 IP를 확인한 뒤 로컬 터미널에서 실행합니다.
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@<노드IP>설정이 끝나면 비밀번호 로그인을 끄고(
PasswordAuthentication no) 무차별 대입 공격을 막으세요. -
02
Xcode Command Line Tools 설치
xcode-select --installArchive 서명까지 자동화하려면 전체 Xcode가 필요합니다. developer.apple.com/downloads에서
.xip를 받아 압축 해제한 뒤/Applications/로 옮기고, 활성 경로를 전환합니다.sudo xcode-select -s /Applications/Xcode.app/Contents/Developer -
03
Homebrew 설치
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"설치 스크립트가 안내하는 대로
/opt/homebrew/bin을 PATH에 넣고~/.zprofile에 저장하세요. -
04
Xcode 빌드 체인 검증
xcodebuild -version출력된 Xcode 버전과 Build 번호를 기록해 두세요. 이후 workflow YAML에서
XCODE_VERSION환경 변수로 동일 버전을 고정할 수 있습니다.
brew install xcodesorg/made/xcodes로 xcodes를 설치하면 Xcode 전환이 빠르고, xcode-select 경로를 매번 손으로 바꿀 필요가 없습니다. CI workflow에서 xcodes select 16.2처럼 버전을 지정하면 Runner 업그레이드로 인한 드리프트를 막을 수 있습니다.
GitHub Actions 셀프 호스팅 Runner 설치 및 등록
GitHub가 배포하는 셀프 호스팅 Runner 패키지는 Mac에서 폴링 데몬을 띄웁니다. workflow가 트리거되면 job을 내려받아 로컬에서 실행하므로, Mac 쪽에 포트를 열 필요가 없습니다.
GitHub 저장소에서 등록 토큰 발급
대상 저장소 → Settings → Actions → Runners → New self-hosted runner에서 macOS를 선택합니다. 페이지에 나온 다운로드 명령과 등록 토큰을 복사하세요. 토큰은 발급 후 1시간만 유효합니다.
-
01
Runner 다운로드 및 압축 해제
mkdir actions-runner && cd actions-runnercurl -o actions-runner-osx-arm64-2.319.1.tar.gz -L https://github.com/actions/runner/releases/download/v2.319.1/actions-runner-osx-arm64-2.319.1.tar.gztar xzf ./actions-runner-osx-arm64-2.319.1.tar.gzosx-arm64패키지(Apple Silicon)를 선택하세요. 버전 번호는 GitHub 페이지에 표시된 최신 릴리스를 따릅니다. -
02
Runner 등록
./config.sh --url https://github.com/YOUR_ORG/YOUR_REPO --token <TOKEN> --name mac-m4-vpsrox --labels mac,xcode16,ios--labels로 붙인 사용자 정의 라벨은 YAML의runs-on에서 사용해, 이 Mac으로 job을 정확히 라우팅할 수 있습니다. -
03
launchd 서비스로 설치(부팅 시 자동 시작)
./svc.sh install./svc.sh startlaunchd가 로그인 후 Runner 프로세스를 자동으로 기동합니다. 상태 확인:
./svc.sh status
Workflow YAML에서 셀프 호스팅 Runner 지정
등록이 끝나면 .github/workflows/ios-ci.yml의 runs-on을 사용자 정의 라벨로 바꿉니다.
jobs:
build:
runs-on: [self-hosted, mac, xcode16]
steps:
- uses: actions/checkout@v4
- name: Show Xcode version
run: xcodebuild -version
공개 저장소의 셀프 호스팅 Runner는 fork PR workflow까지 실행될 수 있어, 악성 코드가 Mac에서 돌아갈 위험이 있습니다. 권장 사항: ① 비공개 저장소 또는 Organization 범위에서만 사용; ② Runner를 비관리자 시스템 계정으로 실행하고 권한을 제한; ③ ~/.bash_history, ~/actions-runner/_diag/ 로그를 정기적으로 검토.
Xcode 빌드 환경 및 의존성 관리 구성
안정적인 iOS CI/CD 파이프라인을 만들려면 보통 세 가지를 정리해야 합니다. 의존성 가져오기, 빌드 캐시, DerivedData 경로 격리입니다. 셀프 호스팅 Runner는 매 job마다 깨끗한 VM이 아니므로, 증분 빌드와 캐시 전략이 특히 중요합니다.
CocoaPods vs Swift Package Manager
프로젝트가 CocoaPods를 쓰면 Runner Mac에 한 번만 설치하면 됩니다.
sudo gem install cocoapods
CI 단계 예시:
- name: Install CocoaPods
run: pod install --repo-update
working-directory: ./ios
Swift Package Manager는 Xcode가 첫 빌드 때 의존성을 해석합니다. 이후 job 속도를 올리려면 SPM 해석 결과를 캐시하세요.
- uses: actions/cache@v4
with:
path: ~/Library/Developer/Xcode/DerivedData
key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }}
DerivedData 경로 격리
한 Runner에서 여러 브랜치나 프로젝트가 동시에 빌드되면 기본 DerivedData 경로가 충돌합니다. xcodebuild 호출마다 경로를 명시하는 편이 안전합니다.
xcodebuild \
-project MyApp.xcodeproj \
-scheme MyApp \
-sdk iphoneos \
-configuration Release \
-derivedDataPath ./DerivedData \
clean build
병렬 빌드 파라미터 조정
Mac mini M4는 10코어 CPU(4 성능 코어 + 6 효율 코어)를 탑재했습니다. Xcode 기본 병렬 컴파일 수는 보통 코어 수와 같습니다. 대형 모듈이 많은 프로젝트는 메모리 부담을 줄이기 위해 값을 낮출 수 있습니다.
defaults write com.apple.dt.Xcode IDEBuildOperationMaxNumberOfConcurrentCompileTasks 6
Swift 파일 약 80개 규모의 SwiftUI 앱을 16 GB RAM M4 베어메탈 노드에서 측정했을 때, Clean Build는 약 4분 12초, swap 없이 완료되었습니다. 같은 Runner에서 증분 빌드는 1분 내외로 떨어지는 경우가 많습니다.
코드 서명 및 키체인: Archive 패키징 전체 프로세스
코드 서명은 iOS CI/CD에서 가장 자주 막히는 구간입니다. GUI 없는 CI 환경에서는 macOS Keychain 동작이 로컬 개발 Mac과 달라지므로, 키체인을 workflow 안에서 명시적으로 만들고 해제해야 합니다.
두 가지 주요 서명 방식 비교
| 방안 | 적용 시나리오 | 장점 | 단점 |
|---|---|---|---|
| p12 인증서 수동 가져오기 | 소규모 팀 단일 머신 CI | 구성 간단, 외부 의존성 불필요 | 인증서 순환 시 수동 재가져오기 필요 |
| fastlane match | 팀 협업 / 다중 CI 노드 | 인증서 통합 암호화 저장, 자동 순환 | Git 저장소 또는 S3 스토리지 백엔드 필요 |
수동 p12 방식: job마다 임시 키체인 생성
CI에서 가장 안정적인 패턴은 job 시작 시 임시 키체인을 만들고, Archive가 끝나면 삭제하는 것입니다. 이전 job의 인증서가 남지 않습니다.
# Create a temporary keychain
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security set-keychain-settings -t 3600 -u build.keychain
# Import signing certificate
security import "$CERTIFICATE_P12_PATH" \
-k build.keychain \
-P "$P12_PASSWORD" \
-T /usr/bin/codesign \
-T /usr/bin/xcodebuild
# Allow xcodebuild to access the key without UI prompt
security set-key-partition-list \
-S apple-tool:,apple: \
-s -k "$KEYCHAIN_PASSWORD" build.keychain
KEYCHAIN_PASSWORD, P12_PASSWORD 같은 민감 값은 GitHub Repository Secrets에 넣고 ${{ secrets.KEYCHAIN_PASSWORD }}로 참조하세요. YAML에 직접 적으면 안 됩니다.
Archive 실행 및 IPA 내보내기
Archive → Export는 두 단계로 나뉩니다. 먼저 xcodebuild archive로 .xcarchive를 만들고, 이어서 -exportArchive로 IPA를 뽑습니다.
# Step 1: Archive
xcodebuild archive \
-workspace MyApp.xcworkspace \
-scheme MyApp \
-sdk iphoneos \
-archivePath ./build/MyApp.xcarchive \
CODE_SIGN_IDENTITY="Apple Distribution: Your Name (XXXXXXXX)" \
DEVELOPMENT_TEAM="XXXXXXXX"
# Step 2: Export IPA
xcodebuild -exportArchive \
-archivePath ./build/MyApp.xcarchive \
-exportOptionsPlist ExportOptions.plist \
-exportPath ./build/
ExportOptions.plist는 Export 단계의 핵심 설정 파일입니다. method(app-store / ad-hoc), teamID, signingStyle 필드가 필요합니다. Xcode GUI에서 한 번 Export한 뒤 임시 폴더에 생성된 파일을 템플릿으로 쓰면 편합니다.
fastlane으로 TestFlight 자동 업로드 연동
IPA가 준비되면 마지막 단계는 App Store Connect TestFlight 채널로 올리는 일입니다. fastlane pilot(upload_to_testflight)은 App Store Connect API Key 인증을 지원해, Apple ID 비밀번호 없이도 안정적으로 업로드할 수 있습니다.
fastlane 설치
버전 고정을 위해 Bundler 사용을 권장합니다. 전역 gem 충돌을 피할 수 있습니다.
# In project root, create Gemfile
source "https://rubygems.org"
gem "fastlane"
이후 bundle install을 실행하고, fastlane 명령은 모두 bundle exec fastlane ... 형태로 호출하세요.
App Store Connect API Key 구성
App Store Connect → Users and Access → Integrations → App Store Connect API에서 API Key를 만들고 .p8 파일을 내려받습니다. Key ID와 Issuer ID도 함께 기록하세요.
GitHub Secrets에 아래 변수를 등록합니다.
ASC_API_KEY_ID: API Key IDASC_API_ISSUER_ID: Issuer IDASC_API_KEY_CONTENT:.p8파일 내용의 Base64 인코딩
Fastfile 구성 예시
lane :beta do
api_key = app_store_connect_api_key(
key_id: ENV["ASC_API_KEY_ID"],
issuer_id: ENV["ASC_API_ISSUER_ID"],
key_content: Base64.decode64(ENV["ASC_API_KEY_CONTENT"])
)
upload_to_testflight(
api_key: api_key,
ipa: "./build/MyApp.ipa",
skip_waiting_for_build_processing: true
)
end
GitHub Actions YAML에서 호출 예시:
- name: Upload to TestFlight
env:
ASC_API_KEY_ID: ${{ secrets.ASC_API_KEY_ID }}
ASC_API_ISSUER_ID: ${{ secrets.ASC_API_ISSUER_ID }}
ASC_API_KEY_CONTENT: ${{ secrets.ASC_API_KEY_CONTENT }}
run: bundle exec fastlane beta
skip_waiting_for_build_processing: true를 쓰면 fastlane이 업로드 직후 종료하고, App Store Connect의 IPA 처리(보통 10–30분)를 기다리지 않습니다. 후속 단계가 처리 완료에 의존하지 않는다면(예: Slack 빌드 번호 알림) iOS CI/CD 전체 소요 시간을 크게 줄일 수 있습니다.
키체인 일반 오류 집중 해결
코드 서명 오류 메시지는 종종 모호합니다. 헤드리스 CI에서 자주 보는 증상, 원인, 해결책을 표로 정리했습니다.
| 오류 메시지 | 근본 원인 | 해결 방법 |
|---|---|---|
errSecItemNotFound |
인증서가 활성 키체인에 없거나, 임시 키체인이 GC됨 | security default-keychain -s build.keychain 실행 여부 확인; archive 전에 security unlock-keychain 추가 |
| could not find signing certificate for "Apple Distribution" | 인증서 Identity와 Xcode CODE_SIGN_IDENTITY 불일치 |
security find-identity -v -p codesigning build.keychain으로 전체 Identity 문자열을 확인해 그대로 복사 |
| User interaction is not allowed | 키체인 잠금 상태 — codesign이 UI 승인 대화상자를 요구(헤드리스 환경 불가) | security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" build.keychain 추가로 UI 없이 개인키 접근 허용 |
| Provisioning profile doesn't include the entitlement | 프로비저닝 프로파일과 Entitlements 불일치(Push, App Groups 등) | Apple Developer에서 프로파일 재생성, Capability 선택 확인 후 재다운로드·설치 |
| No signing certificate "iOS Distribution" found | 프로비저닝 프로파일과 인증서 유형 불일치 (Distribution vs Development) | ExportOptions.plist에서 method 필드를 확인하세요 (app-store는 Distribution 인증서 필요) |
디버깅 팁: 빌드 로그에서 CodeSign 명령 확인
xcodebuild 뒤에 | xcpretty 파이프를 붙이면 로그가 정리되지만, 서명 디버깅에는 원본 로그가 더 유용합니다. 파이프를 제거하거나 CodeSign 키워드로 검색하면 실제 서명 인자를 확인할 수 있습니다. Xcode GUI 오류 패널보다 정보가 훨씬 많습니다.
추가로 자주 쓰는 검증 명령:
codesign -dv --verbose=4 ./build/MyApp.xcarchive/Products/Applications/MyApp.app
.app 번들 안의 서명 정보를 직접 읽어 인증서 체인이 완전한지 확인합니다.
Jenkins Agent 비교: 마이그레이션 및 트레이드오프 분석
이미 Jenkins를 운영 중이거나, 사내 정책상 코드를 GitHub에 올릴 수 없다면 macOS Jenkins Agent도 흔한 선택입니다. GitHub Actions 셀프 호스팅 Runner와 비교할 때 실무에서 체감되는 차이는 아래 표와 같습니다.
| 비교 항목 | GitHub Actions 셀프 호스팅 Runner | Jenkins Agent (macOS) |
|---|---|---|
| 구성 난이도 | 낮음, 5분 이내 등록 완료 | 중간, Jenkins master 구축 필요 (보통 Linux/Docker) |
| 코드 저장소 연동 | GitHub 네이티브, PR 검사 상태 자동 반영 | GitHub Branch Source 플러그인 구성 필요 |
| 멀티 플랫폼 혼합 | 동일 workflow에서 macOS / Linux / Windows 혼합 사용 가능 | Label 라우팅 필요, 구성이 상대적으로 번거로움 |
| 파이프라인 시각화 | GitHub Actions 네이티브 UI, 직관적 | Blue Ocean 플러그인 효과 좋으나 자체 유지보수 필요 |
| 시크릿 관리 | GitHub Secrets + OIDC | Jenkins Credentials, Vault 연동 가능 |
| 오프라인/내부망 빌드 | 미지원 (GitHub 접근 필요) | 지원, 강력한 컴플라이언스 시나리오에 적합 |
| 하드웨어 요구사항 | 온라인 macOS 머신 1대 필요 (Runner 호스트) | 온라인 macOS 머신 1대 필요 (Jenkins Agent 호스트) |
Jenkins에서 넘어올 때 sh 'xcodebuild ...' 단계는 GitHub Actions YAML의 run: 필드로 거의 그대로 옮길 수 있습니다. 차이가 큰 부분은 트리거 문법과 동시 실행 제어입니다. Jenkins when { branch ... } ↔ GitHub Actions on: push: branches:; Jenkins lockable-resources ↔ GitHub Actions concurrency: 그룹.
두 방식 모두 공통 전제는 상시 가동·고정 macOS 버전의 베어메탈 Mac 1대가 Agent/Runner 호스트로 필요하다는 점입니다. 이 Mac이 파이프라인 안정성의 기반이며, 발열·버전 드리프트가 생기면 CI 실패가 예측하기 어려워집니다.
많은 중소 팀이 막히는 지점은 도구 선택이 아니라 장기간 CI를 돌릴 Mac이 없다는 사실입니다. 사무용 MacBook은 8 GB RAM인 경우가 많고, 대형 Xcode 빌드에서 발열·클럭 다운이 심합니다. Mac mini를 새로 사면 초기 비용에 전력·공간·유지보수까지 더해집니다.
클라우드 전용 Mac: 전용 물리 서버가 없을 때의 고정 빌드 솔루션
전용 Mac이 없을 때 Mac mini 클라우드 렌탈은 현실적인 대안입니다. 초기 하드웨어 투자 없이도 베어메탈 Mac mini M4 위에 Xcode 버전을 고정한 iOS CI/CD 빌드 노드를 바로 쓸 수 있습니다.
이 가이드에서 사용한 VPSRox Mac mini M4 베어메탈 노드 기준: 16 GB 통합 메모리, 256 GB NVMe SSD, 1 Gbps 전용 대역폭, 일 $21.8부터, 결제 후 1–5분 내 자동 배포, 장기 계약 없음, SLA 99.9%.
CI/CD 관점에서는 상시 온라인인 전용 Mac mini를 렌탈하는 것과 같습니다. 로컬 물리 서버와 동일하게 아래 작업이 가능합니다.
xcodes로 원하는 Xcode 버전 설치·빌드 환경 완전 고정- GitHub Actions 셀프 호스팅 Runner 또는 Jenkins Agent로 등록 — 이 글의 단계를 그대로 적용
- M4 10코어 CPU·38 TOPS Neural Engine으로 Core ML 모델 컴파일 프로젝트에 유리
- Thunderbolt 5 병렬 확장으로 여러 Mac mini를 사설 빌드 클러스터로 구성(대형 팀)
GitHub 호스팅 Runner와 비교하면, 하루 30회 이상 빌드하는 팀은 일 단위 렌탈 비용이 macOS Runner 초과 과금보다 낮은 경우가 많습니다. Mac mini 구매와 비교하면, 스프린트 기간만 노드를 늘리고 출시 후 줄이는 식으로 렌탈 기간을 조절할 수 있어 초기 CAPEX 부담이 없습니다.
싱가포르, 일본(도쿄), 한국(서울), 홍콩, 미국 동부 — 전 세계 5개 노드 중 팀과 가까운 위치를 고르면 git fetch와 의존성 다운로드 지연이 줄어 전체 빌드 시간을 더 단축할 수 있습니다. 한국(서울) 노드는 국내 팀의 iOS CI/CD latency에 특히 유리합니다.
클라우드 Mac 노드는 Runner 전용일 필요가 없습니다. 브라우저 VNC나 SSH로 원격 Mac 데스크톱처럼 쓰면서, 같은 머신에서 Xcode 디버깅·Instruments 분석·시뮬레이터 테스트까지 할 수 있습니다. 노드 활용률이 올라가면 일 단위 렌탈 단가 체감 비용도 낮아집니다.
이 CI/CD 파이프라인을 가동하기
이 글의 모든 단계는 VPSRox Mac mini M4 베어메탈 노드에서 검증했습니다. 노드 개통 → SSH 접속 → 가이드대로 설정하면, 당일 안에 TestFlight 자동 업로드까지 iOS CI/CD 파이프라인을 완성할 수 있습니다. 일 단위 렌탈, 장기 계약 불필요.