1–5분 배포

전용 M4 Runner, macOS 대기열과 작별

$21.5 /일부터 · 물리 머신 단독 사용
클라우드 Mac 구성
M4 · 16 GB Xcode 버전 고정 가능 launchd 상시 실행

FIELD NOTE · iOS 빌드

전용 M4 노드 GitHub Actions Runner 등록: iOS CI/CD 파이프라인 구축기

상주 Mac 없이 GitHub에서 iOS Archive를 안정적으로 실행해야 하나요? 이 글은 JexMac 전용 Mac mini M4 물리 노드에서의 전체 구축 과정을 기록합니다: SSH 첫 연결, Runner 등록 및 launchd 상시 실행, Workflow 라벨 라우팅, Xcode 버전 고정, 무 UI 환경에서 Distribution 인증서 가져오기까지 — 각 단계마다 재현 가능한 명령어와 실측 소요 시간을 첨부합니다.

구축 목표: 검증 가능한 iOS 빌드 파이프라인 완성

Runner 설치 전에 「구축 완료」의 검수 기준을 먼저 정의해, Runner는 설치했지만 Archive가 서명 단계에서 멈추는 상황을 방지합니다. 이번 실록의 목표는: 지정 브랜치에 push → GitHub Actions가 전용 M4 노드에서 트리거 → 코드 checkout → xcodebuild archive 실행 및 .xcarchive 성공적 생성입니다. TestFlight 업로드는 이 글 범위 밖이지만, Archive가 안정화되면 이후 fastlane 또는 altool은 추가 단계일 뿐입니다.

테스트 저장소는 중간 규모 SwiftUI 앱: 약 60개 Swift 소스 파일, CocoaPods로 서드파티 의존성 관리, Release 구성은 Manual 서명. 기준 환경은 JexMac 싱가포르 노드의 Mac mini M4(16 GB 통합 메모리, 256 GB NVMe), Xcode 16.2는 xcodes로 설치 및 고정.

4m 08s
Clean Archive 실측(pod install 포함)
< 25s
셀프 호스트 job 대기부터 실행 시작까지
0 swap
16 GB 메모리 전 구간 스왑 없음
1대
전용 물리 머신, 가상화 오버셀 없음

대조군은 동일 저장소를 runs-on: macos-14 호스트 Runner에서 실행한 결과: UTC 13:00–18:00(아시아·태평양 오후) job 대기 중앙값 11분, 최장 19분; macos-14 사전 설치 Xcode 버전이 로컬 개발 머신과 불일치해 Swift 6 구문 호환 문제 발생. 셀프 호스트의 가치가 여기서 명확해집니다 — 대기 시간이 분 단위에서 초 단위로, Xcode 버전을 노드에 완전히 고정.

호스트 macOS Runner의 세 가지 숨은 비용

많은 팀이 처음 GitHub 호스트 Runner를 선택하는 이유는 「제로 운영」이지만, iOS 시나리오에서는 숨은 비용이 청구서보다 더 까다로운 경우가 많습니다.

첫 번째는 시간 비용입니다.GitHub macOS 풀 용량은 제한적이며, 무료 계정 월 2000분 할당량은 macOS 10배 가중치로 환산되어 실제 약 200분 사용 가능. 하루 8회 트리거, 빌드 1회 6분인 프로젝트는 월 약 1440 가중치 분 소비 — 무료 할당량 상한에 근접, nightly 빌드 하나만 추가해도 초과.

두 번째는 환경 드리프트입니다.macos-latest 태그는 GitHub 인프라 업그레이드에 따라 기본 Xcode가 바뀌며, 「어제는 녹색, 오늘은 빨간색」 상황이 여러 번 발생했습니다. 임시 해결책은 workflow에 sudo xcode-select 단계를 추가하는 것이지만, 버전 전환마다 1–2분 추가 소요되며 로컬 개발 머신과 완전히 일치할 수 없습니다.

세 번째는 디버깅 비용입니다.호스트 Runner는 job 종료 후 환경이 파기되어 SSH로 접속해 문제를 재현할 수 없습니다. 키체인 팝업, 프로비저닝 프로파일 만료처럼 「직접 확인해야 하는」 장애는 호스트 환경에서 로그만 보고 반복 push해야 합니다 — User interaction is not allowed 오류 하나를 partition list 누락으로 특정하기까지 7번 push했습니다.

실측 환경 설명

이 글의 모든 명령어와 소요 시간 데이터는 JexMac 전용 Mac mini M4 물리 노드에서 실행했으며, 노드는 싱가포르 데이터센터에 위치, 테스트 기간은 2026년 7월 하순. 하드웨어 사양: Apple M4 · 10코어 CPU · 16 GB 통합 메모리 · 1 Gbps 전용 대역폭.

노드 배포 후: SSH 첫 연결 및 환경 기준선

JexMac 콘솔은 결제 확인 후 보통 1–5분 내 SSH 자격 증명을 전달합니다. 공인 IP를 받은 후 Ed25519 키로 먼저 접속하고 비밀번호 로그인을 비활성화한 뒤 Runner 설치를 시작하세요 — Runner 프로세스는 현재 macOS 사용자 권한으로 실행되므로 SSH 보안 기준선을 먼저 확립해야 합니다.

  1. 01
    SSH 공개키 등록

    ssh-copy-id -i ~/.ssh/id_ed25519.pub jexmac@<노드IP>

    비밀번호 없는 로그인 확인 후 /etc/ssh/sshd_config를 편집해 PasswordAuthentication no 설정, sshd 재시작.

  2. 02
    Homebrew 및 xcodes 설치

    /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

    brew install xcodesorg/made/xcodes

    xcodes install 16.2 --experimental-fast-pass로 Xcode 16.2 설치 및 고정(버전 번호는 프로젝트 요구에 맞게 조정).

  3. 03
    빌드 체인 검증

    xcodebuild -versionXcode 16.2 및 빌드 번호를 출력해야 합니다.

    xcodebuild -showsdks | grep iphoneos로 iOS SDK 사용 가능 여부 확인.

  4. 04
    CocoaPods 설치(프로젝트 사용 시)

    sudo gem install cocoapods -n /usr/local/bin

    노드에서 미리 pod install을 한 번 실행해 Specs 저장소 캐시 준비, CI 첫 빌드가 repo update에서 멈추는 것 방지.

기준선 구성 완료 후 노드에 전용 디렉터리(이 글에서는 ~/ci-runner)가 있어 Runner와 빌드 산출물을 일상 개발 파일과 격리해야 합니다. 해당 디렉터리에 DerivedData 하위 디렉터리를 예약하고, 이후 workflow에서 -derivedDataPath를 명시적으로 지정해 다중 프로젝트 동시 실행 시 경로 충돌 방지.

GitHub 측: Runner 토큰 및 라벨 라우팅 설계

대상 저장소 → Settings → Actions → Runners → New self-hosted runner, 플랫폼은 macOS ARM64 선택. 페이지에서 일회용 등록 토큰(유효 1시간)과 다운로드 링크가 생성됩니다.

라벨 설계는 workflow가 올바른 머신으로 라우팅되는지 직접 좌우합니다. 저희 명명 규칙:

  • mac: 모든 macOS 셀프 호스트 노드 공통 라벨
  • m4: Apple Silicon M4 칩 식별, 구형 Intel 노드와 구분
  • xcode-16-2: Xcode 마이너 버전 고정, 업그레이드 시 workflow 로직 변경 없이 라벨만 수정
  • sg: 데이터센터 지역(싱가포르), 다중 지역 배포 시 근접 라우팅

등록 명령의 --labels 매개변수에 위 라벨을 한 번에 기록. workflow에서는 배열 형식으로 매칭:

jobs:
  ios-archive:
    runs-on: [self-hosted, mac, m4, xcode-16-2]
    concurrency:
      group: ios-build-${{ github.ref }}
      cancel-in-progress: true

concurrency 그룹은 동일 브랜치에서 두 Archive가 병렬 실행되지 않도록 보장 — 16 GB 메모리 M4 노드에서 두 프로젝트 동시 Clean Build는 가능하지만 DerivedData 경합으로 1회 소요 시간이 30% 이상 변동. 팀에 여러 제품 라인이 있으면 라벨로 분리(예: product-a, product-b)하고 노드를 추가하는 것이 한 머신에서 강제 병렬보다 낫습니다.

공개 저장소의 보안 경계

셀프 호스트 Runner는 공개 저장소에서 fork PR로 트리거될 수 있으며, 악성 workflow가 Mac에서 임의 코드를 실행합니다. 반드시 비공개 저장소 또는 Organization 수준에서만 활성화하고, Runner 프로세스는 비관리자 계정으로 실행하세요. 프로덕션 환경에서는 GitHub Environment 보호 규칙과 함께 secrets를 지정 브랜치에서만 사용하도록 제한하는 것을 권장합니다.

M4 노드에 Runner 설치 및 launchd 상시 실행 구성

다음 단계는 SSH 세션에서 실행하며, Runner 버전 번호는 GitHub 등록 페이지 표시 기준(이 글 예시 v2.321.0).

  1. 01
    다운로드 및 압축 해제

    mkdir -p ~/ci-runner/actions-runner && cd ~/ci-runner/actions-runner

    curl -o actions-runner-osx-arm64-2.321.0.tar.gz -L \ https://github.com/actions/runner/releases/download/v2.321.0/actions-runner-osx-arm64-2.321.0.tar.gz

    tar xzf ./actions-runner-osx-arm64-2.321.0.tar.gz

  2. 02
    대화형 등록

    ./config.sh --url https://github.com/YOUR_ORG/YOUR_REPO \ --token YOUR_ONE_TIME_TOKEN \ --name jexmac-m4-sg-01 \ --labels mac,m4,xcode-16-2,sg \ --unattended

    --unattended는 대화형 확인을 건너뛰어 스크립트 배포에 적합. Work folder는 기본 _work 유지.

  3. 03
    launchd 서비스 설치

    ./svc.sh install

    ./svc.sh start

    검증: ./svc.sh statusactive (running) 표시. GitHub 저장소 Runners 페이지에 녹색 Online 상태가 나타나면 등록 성공.

launchd는 시스템 재시작 후 Runner를 자동으로 기동하지만 한 가지 세부 사항: Runner는 설치 시 macOS 사용자 권한으로 실행되며, 해당 사용자는 최소 한 번 로그인했거나(또는 자동 로그인 활성화) 그렇지 않으면 launchd가 Keychain에 접근하지 못할 수 있습니다. 노드에 전용 계정 ci-bot을 만들고 SSH로 첫 로그인해 키체인 초기화 후 svc.sh 설치, 이후 재시작 시 수동 개입 불필요.

Runner 로그는 ~/ci-runner/actions-runner/_diag/에 있으며, job 종료마다 Worker_*.log 생성. 「job이 노드에 할당됐지만 단계 출력 없음」 유형 문제는 GitHub UI Annotations보다 RunnerListener 로그가 더 완전합니다.

최소 동작 Workflow: checkout부터 Archive까지

등록 완료 후 저장소에 .github/workflows/ios-archive.yml 생성. 아래는 검증된 최소 버전, TestFlight 업로드 없이 Archive 산출에 집중:

name: iOS Archive
on:
  push:
    branches: [main, release/*]
  pull_request:
    branches: [main]

jobs:
  archive:
    runs-on: [self-hosted, mac, m4, xcode-16-2]
    timeout-minutes: 30

    steps:
      - uses: actions/checkout@v4

      - name: Select Xcode
        run: xcodes select 16.2

      - name: Install pods
        run: pod install --deployment
        working-directory: ios

      - name: Build archive
        run: |
          xcodebuild archive \
            -workspace ios/MyApp.xcworkspace \
            -scheme MyApp \
            -sdk iphoneos \
            -configuration Release \
            -archivePath ./build/MyApp.xcarchive \
            -derivedDataPath ~/ci-runner/DerivedData \
            CODE_SIGN_STYLE=Manual \
            CODE_SIGN_IDENTITY="Apple Distribution: Your Team (TEAMID)" \
            PROVISIONING_PROFILE_SPECIFIER="MyApp AppStore"
        env:
          KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}

      - name: Upload xcarchive artifact
        uses: actions/upload-artifact@v4
        with:
          name: MyApp-xcarchive
          path: ./build/MyApp.xcarchive
          retention-days: 7

의도적으로 유지한 설계 선택:

pod install --deployment은 Podfile.lock으로 버전 고정, CI와 로컬 의존성 불일치 방지.-derivedDataPath는 노드의 고정 디렉터리를 가리켜 여러 빌드에서 컴파일 캐시 재사용, 증분 빌드 4분에서 약 1분 40초로 단축.upload-artifact는 xcarchive를 GitHub에 업로드해 SSH 권한 없는 동료가 다운로드해 검증 — artifact 7일 보관, QA 샘플 검사에 충분.

무 UI 환경: 키체인 및 Distribution 인증서 가져오기

Archive 단계 성공 여부는 CI 환경이 그래픽 UI 없이 Distribution 개인키에 접근할 수 있는지에 달립니다. 호스트 Runner는 시스템 키체인이 사전 구성되어 있지만 셀프 호스트 노드는 직접 관리해야 합니다 — 「Runner는 설치했지만 Archive 서명 오류」로 막히는 팀이 많은 이유입니다.

권장: job마다 임시 키체인 생성, p12 가져온 직후 서명에 사용, job 종료 시 자동 파기, 개인키 장기 디스크 상주 방지.

# "Build archive" 단계 이전에 추가
- name: Import signing certificate
  run: |
    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

    echo "${{ secrets.CERTIFICATE_P12_BASE64 }}" | base64 --decode > cert.p12
    security import cert.p12 \
      -k build.keychain \
      -P "${{ secrets.P12_PASSWORD }}" \
      -T /usr/bin/codesign \
      -T /usr/bin/xcodebuild

    security set-key-partition-list \
      -S apple-tool:,apple: \
      -s -k "$KEYCHAIN_PASSWORD" build.keychain
    rm -f cert.p12
  env:
    KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}

GitHub Repository Secrets에 세 변수를 미리 구성해야 합니다: KEYCHAIN_PASSWORD(임시 키체인 비밀번호, 임의 문자열), CERTIFICATE_P12_BASE64(Distribution 인증서 Base64 인코딩), P12_PASSWORD(p12 내보낼 때 설정한 비밀번호).

오류 키워드 일반적인 원인 해결 방법
User interaction is not allowed 개인키 partition list 미설정, codesign이 UI 팝업 시도 set-key-partition-list 명령 재실행(위 스크립트 참조)
errSecItemNotFound 인증서가 잘못된 키체인에 가져와졌거나 default-keychain 미전환 import 전 security default-keychain -s build.keychain 실행 확인
could not find signing certificate CODE_SIGN_IDENTITY 문자열과 키체인 Identity 불완전 일치 security find-identity -v -p codesigning build.keychain 실행 후 전체 이름 복사
Provisioning profile doesn't match 프로비저닝 프로파일 만료 또는 Bundle ID / Capability 불일치 Apple Developer에서 재생성 후 다운로드, secrets 또는 match로 동기화

가져오기 성공 후 job 로그에서 한 줄 명령으로 빠르게 검증:

security find-identity -v -p codesigning build.keychain | grep Distribution

1 valid identities found 확인 후 Archive 단계 진입하면 「서명 실패 원인 불명」 디버깅 시간을 크게 절약.

문제 해결 실록: push를 한 번 더 하게 만든 세 가지

위 단계를 따라도 첫 구축에서 다음 상황이 발생할 수 있습니다 — 모두 실제로 겪은 함정, 로그 특징과 해결책 포함.

라벨 불일치: job이 영원히 대기

현상: GitHub Actions UI job 상태 Queued, Runners 페이지 노드는 Online. 원인: workflow runs-on 라벨과 등록 시 불완전 일치 — workflow xcode-16.2(점), 등록 xcode-16-2(하이픈). GitHub 라벨 매칭은 정확한 문자열 비교, 한 글자 차이로 라우팅 안 됨.

해결: 저장소 Runners 페이지에서 노드 이름 클릭, 실제 라벨 목록 확인 후 YAML에 복사·붙여넣기, 직접 입력 금지.

launchd 재시작 후 Runner Offline

현상: 노드 reboot 또는 JexMac 유지보수 재시작 후 Runner Offline, SSH로 ./svc.sh start 수동 실행 필요. 원인: launchd plist UserName과 SSH 로그인 사용자 불일치, 또는 해당 사용자가 첫 그래픽/session 로그인 미완료.

해결: ./svc.sh install./svc.sh start를 동일 사용자에서 실행 확인; 재시작 후 ./svc.sh status 확인. 실패 시 /Library/Logs/GitHubActionsRunner/ stderr 로그 확인.

DerivedData 권한 충돌

현상: 두 번째 빌드에서 Unable to write to DerivedData 또는 일부 .o 파일 permission denied. 원인: 첫 job이 root 또는 다른 사용자로 DerivedData 생성, 이후 job 쓰기 권한 없음.

해결: DerivedData 경로 통일 및 workflow 시작에 정리 단계: rm -rf ~/ci-runner/DerivedData && mkdir -p ~/ci-runner/DerivedData. 또는 Archive 전 해당 프로젝트 hash 하위 디렉터리만 정리, 재사용 가능 컴파일 캐시 유지.

상주 Mac 없을 때: 빌드 리듬에 맞춰 전용 노드 임대

여기까지 오면 GitHub Actions 셀프 호스트 Runner 기술 장벽은 높지 않지만, 진짜 병목은 24시간 온라인·Xcode 버전 제어 가능한 macOS 물리 머신 보유 여부임을 알게 됩니다. 로컬 MacBook은 CI 호스트로 부적합 — 8 GB 메모리 기종 Archive 시 팬 최대·빈번한 swap; 회사 Mac mini 구매는 고정자산 승인, IDC 호스팅, 인증서 교체 시 현장 유지보수.

저희 방식: 반복 스프린트 기간(예: 출시 2주 전) JexMac 전용 Mac mini M4 노드를 Runner 호스트로 임대, 안정화 후 주 단위 연장 또는 해제. 표준 16 GB 통합 메모리, 256 GB NVMe, 1 Gbps 전용 대역폭, 일 $21.5부터, 결제 후 1–5분 SSH/VNC 배포, 계약 없음. 다섯 노드 — 싱가포르, 일본(도쿄), 한국(서울), 홍콩, 미국 동부 — 팀 위치에 맞춰 git fetch·CocoaPods Specs 동기화 지연 최소화.

GitHub 호스트 macOS Runner 대비: 피크 대기 10–20분 vs 셀프 호스트 25초 내 시작; macOS 분 10배 가중치 vs 고정 일 임대 예측 가능. 자체 구매 대비: upfront 없음, 버전 업그레이드 시 라벨 변경 또는 Xcode 재설치, 구매 주기 대기 불필요.

동일 노드를 원격 개발 데스크탑 겸용 — 브라우저 VNC로 UI 디버깅, Instruments 성능 캡처, CI 유휴 시간 활용. 2–3인 소규모 팀에 「M4 물리 머신 1대 = Runner + 원격 Mac 개발 환경」이 호스트 Runner 분+로컬 메모리 업그레이드 분리보다 유리한 경우 많음.

물리 머신 단독 · 1–5분 배포

M4 노드에 Runner 연결

글의 모든 명령은 JexMac 전용 Mac mini M4 물리 노드에서 검증. 개통 → SSH 접속 → Runner 등록, 최단 1시간 내 Archive 완료. 일 단위 임대, 스프린트 종료 후 해제, 연간 약정 없음.

표준 구성
Apple M4 · 38 TOPS
CPU10코어(4P + 6E)
메모리16 GB 통합 메모리
네트워크1 Gbps 전용 대역폭
SLA99.9% 가용성
배포1–5분 자동 개통