엔지니어링 실무

클라우드 Mac CI에서 Ruby와 fastlane 버전 격리하기

클라우드 Mac CI에서 Ruby와 fastlane 버전 격리하기

동일한 클라우드 Mac에서도 개발자가 터미널에서 fastlane을 실행하면 성공하지만 CI 작업에서는 gem이 없다는 오류가 발생할 수 있습니다. runner를 수정한 뒤에는 로그인 터미널이 또 다른 Ruby를 참조하기도 합니다. 이런 문제는 대개 fastlane 자체가 아니라 시스템 Ruby, 버전 관리자, Bundler 설정, 작업 환경이 함께 일으키는 경로 불일치에서 비롯됩니다. 해결책은 gem을 반복해서 설치하는 것이 아니라 Ruby 도구 체인 전체를 프로젝트의 일부로 관리하는 것입니다.

실제로 실행되는 Ruby 확인하기

먼저 설치 명령부터 실행하지 마세요. 로그인 터미널과 CI 작업에서 각각 다음 결과를 기록합니다.

command -v ruby
ruby -v
command -v bundle
bundle -v
gem env home
bundle config list
printf '%s
' "$PATH"

특히 rubybundle이 동일한 경로 접두사에서 제공되는지 비교해야 합니다. Ruby는 버전 관리자 디렉터리에 있지만 Bundler는 시스템 디렉터리에서 가져온다면 이후 설치한 항목이 잘못된 gem 경로에 기록될 가능성이 큽니다. CI에 GEM_HOME, GEM_PATH, BUNDLE_PATH가 설정되어 있는지도 확인하세요. 남아 있는 기존 환경 변수는 프로젝트 설정을 덮어쓸 수 있습니다.

대화형 터미널에서 실행된다고 해서 자동화 작업에도 같은 환경이 제공되는 것은 아닙니다. CI는 셸 시작 스크립트가 우연히 PATH를 변경한 결과에 의존하지 말고, 명시적인 버전 파일과 프로젝트 설정을 기준으로 시작해야 합니다.

진단 결과는 하나의 산출물로 만들되 로그에 토큰, 인증서 내용, 전체 환경 변수를 출력하지 마세요. 대부분의 경로 해석 문제는 경로와 버전 정보만으로도 판단할 수 있습니다.

Ruby, Bundler, fastlane 버전 고정하기

저장소 루트에는 .ruby-version, Gemfile, Gemfile.lock을 커밋해야 합니다. Ruby 버전은 모호한 3.3이 아니라 실제로 설치할 수 있는 전체 버전으로 지정해야 합니다. 팀에서 버전을 먼저 정한 뒤 다음 명령을 실행합니다.

printf '%s
' '3.3.6' > .ruby-version
bundle init
bundle add fastlane --version '~> 2.0'
bundle lock

Gemfile은 허용되는 버전 범위를 나타내고, Gemfile.lock은 이번에 해석된 정확한 버전을 기록합니다. CI에서 빌드할 때마다 bundle update를 실행하면 안 됩니다. 그렇게 하면 잠금 파일의 제약이 무의미해집니다. 설치가 끝난 뒤에는 모든 명령을 Bundler 진입점을 통해 실행합니다.

bundle exec fastlane --version
bundle exec fastlane ios ci

전역 fastlane을 편의용 명령으로 사용하지 마세요. 다른 Ruby에서 설치된 명령일 수 있으며, 시스템 업데이트나 수동 디버깅 후에 변경될 수도 있습니다. 프로젝트에 특정 Bundler 버전이 필요하다면 Gemfile.lockBUNDLED WITH 섹션에서 버전을 확인해 설치한 다음 bundle install을 실행합니다.

의존성 설치를 프로젝트 디렉터리로 제한하기

프로젝트 수준 설정은 저장소 옆의 .bundle/config에 기록하고, 호스트 전체에 적용되는 Bundler 전역 설정은 변경하지 마세요.

bundle config set --local path vendor/bundle
bundle config set --local clean true
bundle install --jobs 4 --retry 2

일반적으로 vendor/bundle은 Git에 커밋하지 않고 CI 캐시로 관리합니다. 이렇게 하면 시스템 gem을 오염시키지 않으면서 여러 프로젝트가 서로 다른 버전을 사용할 수 있습니다. 설치 후에는 다음 검사를 통해 전체 해석 체인이 올바른지 확인할 수 있습니다.

test -f Gemfile.lock
test "$(ruby -e 'print RUBY_VERSION')" = "$(cat .ruby-version)"
bundle check
bundle exec ruby -e 'require "fastlane"; puts Fastlane::VERSION'

마지막 단계가 실패하더라도 모든 캐시를 바로 삭제하지 마세요. 먼저 bundle config listbundle env를 확인해 작업이 현재 저장소의 Gemfile을 읽는지, 상위 디렉터리의 파일이나 BUNDLE_GEMFILE이 가리키는 다른 파일을 읽는 것은 아닌지 점검합니다.

깨끗한 비대화형 환경 시뮬레이션하기

runner에 정식으로 연결하기 전에 최소 환경에서 한 번 검증할 수 있습니다.

RUBY_BIN="$(dirname "$(command -v ruby)")"
env -i \
  HOME="$HOME" \
  PATH="$RUBY_BIN:/usr/bin:/bin:/usr/sbin:/sbin" \
  BUNDLE_GEMFILE="$PWD/Gemfile" \
  bundle exec fastlane --version

이 단계에서는 개인 셸 설정에만 정의된 경로, 별칭, 환경 변수를 미리 발견할 수 있습니다. 명령이 실패하면 CI가 개인 설정 전체를 불러오게 하지 말고 runner의 명시적 초기화 절차를 수정해야 합니다.

서로 섞이지 않는 캐시 키 설계하기

네이티브 확장이 포함된 gem은 Gemfile.lock만 기준으로 캐시해서는 안 됩니다. Ruby 버전, CPU 아키텍처, macOS 주·부 버전이 다르면 이미 컴파일된 파일을 불러오지 못할 수 있습니다. 캐시 키에는 최소한 다음 필드가 포함되어야 합니다.

필드 확인 방법 목적
Ruby 버전 ruby -e 'print RUBY_VERSION' ABI 차이 격리
CPU 아키텍처 uname -m 서로 다른 네이티브 코드 구분
macOS 버전 sw_vers -productVersion 시스템 라이브러리 차이 방지
잠금 파일 해시 shasum -a 256 Gemfile.lock 의존성 변경 시 무효화

다음과 같이 안정적인 지문을 생성할 수 있습니다.

RUBY_VERSION_KEY="$(ruby -e 'print RUBY_VERSION')"
ARCH_KEY="$(uname -m)"
OS_KEY="$(sw_vers -productVersion | cut -d. -f1,2)"
LOCK_KEY="$(shasum -a 256 Gemfile.lock | cut -d' ' -f1)"
printf '%s-%s-%s-%s
' "$RUBY_VERSION_KEY" "$ARCH_KEY" "$OS_KEY" "$LOCK_KEY"

캐시를 복원한 뒤에도 bundle check를 실행해야 합니다. 검사에 실패한 경우에만 bundle install을 실행해 누락된 항목을 보완하세요. 캐시 압축 해제에 성공했다는 사실만으로 의존성을 사용할 수 있다고 판단해서는 안 됩니다.

실패 원인을 추적할 수 있는 검증 게이트 만들기

최종 작업은 “환경 검증, 의존성 검증, 비즈니스 lane”의 세 단계로 실행해야 합니다. 첫 단계에서는 버전과 경로를 검사하고, 두 번째 단계에서는 bundle check를 실행한 뒤, 세 번째 단계에서만 fastlane을 호출합니다. 어느 단계에서든 실패하면 즉시 중단해야 합니다. 그래야 이후 단계에서 오류가 이해하기 어려운 플러그인 누락이나 상수 로드 예외로 나타나는 일을 막을 수 있습니다.

병합 검사에는 다음 항목을 포함하는 것이 좋습니다.

  • .ruby-version과 runner에서 실제로 사용하는 Ruby 버전이 완전히 일치합니다.
  • Gemfile.lock이 커밋되어 있으며 작업 실행 전후에 변경되지 않습니다.
  • fastlane은 반드시 bundle exec를 통해서만 호출합니다.
  • .bundle/config가 프로젝트 수준의 의존성 디렉터리를 사용합니다.
  • 캐시 키에 Ruby, 아키텍처, 시스템 버전, 잠금 파일 해시가 포함됩니다.
  • 로그에는 버전, 경로, 검증 결과만 남기고 민감한 환경 변수 값은 출력하지 않습니다.
  • runner와 로그인 터미널이 동일한 명시적 초기화 로직을 사용합니다.

이러한 제약을 적용하면 Ruby 도구 체인은 “호스트에 설치된 명령 모음”이 아니라 감사 가능한 빌드 입력으로 바뀝니다. 이후 Ruby나 fastlane을 업그레이드할 때는 별도의 커밋으로 버전 파일과 잠금 파일을 갱신하고 동일한 검증 절차로 변경 범위를 확인할 수 있습니다. 실패가 발생한 뒤 작업이 어떤 환경을 호출했는지 추측할 필요가 없습니다.

자주 묻는 질문

Gemfile.lock을 커밋했는데도 fastlane 버전이 달라지는 이유는 무엇인가요?

전역 fastlane을 직접 호출하거나 실행 전에 잠금 파일을 다시 만들었을 가능성이 큽니다. 항상 bundle exec fastlane을 사용하고 잠금 파일을 변경 불가능한 입력으로 취급해야 합니다.

vendor/bundle을 여러 클라우드 Mac에서 공유해도 되나요?

조건 없이 공유하면 안 됩니다. Ruby 버전, CPU 아키텍처, macOS 주·부 버전, Gemfile.lock 해시를 캐시 키에 포함해야 네이티브 확장 충돌을 피할 수 있습니다.

CI는 성공하지만 로그인 셸에서는 실패할 때 무엇을 먼저 확인하나요?

command -v ruby, ruby -v, bundle config list와 환경 변수를 비교하세요. 두 세션이 서로 다른 PATH나 Ruby 관리 도구 초기화를 사용하는 경우가 많습니다.

전용 물리 노드

클라우드에서 Mac mini 워크플로를 계속 실행하세요

RunAMac M4와 대여 기간, 배포 노드를 선택하고 개발, 빌드 또는 실험 작업을 전용 물리 머신에서 실행하세요.

대여 플랜 선택