なぜJenkinsに専用のMacが必要なのか?
Jenkinsは最も広く使われているCI/CDプラットフォームであり、エンタープライズのビルドパイプラインの50%以上で稼働しています。しかし、JenkinsでiOSアプリをビルドするにはmacOSが必要であり、macOSはApple製ハードウェア上でのみ合法的に動作します。専用のMac Mini M4なら、次のメリットが得られます。
-
一貫したビルド環境
毎回のビルドで、同じハードウェア、同じOS、同じツールを使えます。環境の差異による不安定なビルド(flaky build)はありません。
-
完全なルートアクセス
任意のツールをインストールし、システム設定を構成し、コード署名用のキーチェーンを管理できます。
-
Apple Siliconのパフォーマンス
M4チップは、Intel製Mac Miniと比較して2〜3倍高速なXcodeビルドを実現します。
-
永続的なキャッシュ
DerivedData、CocoaPods、SPMのキャッシュがビルド間で保持されます。
前提条件
- My Remote Macの専用 Mac mini M4 ビルドサーバー(月額$85から、USD・税抜)
- Jenkinsコントローラー(Linux、Docker、または任意のクラウドVM上に配置可能)
- Mac MiniへのSSHアクセス
- Mac MiniにインストールされたXcode(Xcodeのインストール手順については GitHub Actionsガイド を参照してください)
ステップ1:macOSにJava JDKをインストールする
Jenkinsエージェントには、Java Runtime Environmentが必要です。現在のJenkinsはJava 21とJava 25に対応し、Java 17はサポート対象外になったため、管理しやすいHomebrew経由でJDK 21(LTS)をインストールします。
# Install Homebrew if not already present
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile
eval "$(/opt/homebrew/bin/brew shellenv)"
# Install Java JDK 21 (LTS): current Jenkins releases support Java 21 and 25
brew install openjdk@21
# Create the symlink for system Java wrappers
sudo ln -sfn /opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk \
/Library/Java/JavaVirtualMachines/openjdk-21.jdk
# Add to PATH
echo 'export PATH="/opt/homebrew/opt/openjdk@21/bin:$PATH"' >> ~/.zprofile
source ~/.zprofile
# Verify Java installation (should report version 21)
java -version
# Tools used by the pipeline in Step 3 (CocoaPods only if you use a Podfile)
brew install xcbeautify cocoapods
ステップ2:Jenkinsエージェントを構成する
Jenkinsエージェントを接続する主な方法は2つあります。インバウンドエージェント(エージェントからコントローラーに接続。旧称JNLP)とSSH(コントローラーからエージェントに接続)です。ここでは両方を取り上げます。
方法A:SSHエージェント(推奨)
これは最も信頼性の高い方法です。JenkinsはSSH経由でMacに接続し、エージェントプロセスをリモートで管理します。
# On the Mac mini: create a dedicated, non-admin Jenkins user
sudo sysadminctl -addUser jenkins -fullName "Jenkins Agent" -password "STRONG_PASSWORD_HERE"
# Create its home folder from the macOS template (includes ~/Library)
sudo createhomedir -c -u jenkins
# If Remote Login is limited to some users (System Settings > General >
# Sharing > Remote Login), allow the jenkins user to connect over SSH
sudo dseditgroup -o edit -a jenkins -t user com.apple.access_ssh
# Create SSH directory and add the Jenkins controller's public key
sudo mkdir -p /Users/jenkins/.ssh
sudo sh -c 'echo "ssh-rsa YOUR_JENKINS_CONTROLLER_PUBLIC_KEY" > /Users/jenkins/.ssh/authorized_keys'
sudo chmod 700 /Users/jenkins/.ssh
sudo chmod 600 /Users/jenkins/.ssh/authorized_keys
sudo chown -R jenkins:staff /Users/jenkins/.ssh
# Create a workspace directory
sudo mkdir -p /Users/jenkins/workspace
sudo chown jenkins:staff /Users/jenkins/workspace
次に、Jenkinsでエージェントを構成します。
- Manage Jenkins > Nodes > New Node に移動します
- 名前を
mac-mini-m4に設定します - リモートのルートディレクトリを
/Users/jenkins/workspaceに設定します - ラベル:
mac macos apple-silicon m4 ios - 起動方法:Launch agents via SSH
- ホスト:Mac MiniのIPアドレス
- 認証情報:Mac上の authorized_keys に対応するSSH秘密鍵を追加します
- Host Key Verification Strategy:Known hosts file Verification Strategy。初回起動の前に、Jenkinsを実行しているユーザーでコントローラーから一度接続し(
ssh jenkins@YOUR_MAC_IP)、そのユーザーの~/.ssh/known_hostsにMacを追加します
方法B:インバウンドエージェント(WebSocket)
Jenkinsコントローラーが直接Macに到達できない場合(例:Macがファイアウォールの内側にある場合)は、この方法を使用します。エージェント側から接続を開始します。-webSocketオプションを使えば、別のTCPエージェントポートは不要で、コントローラーのWebポート(HTTPまたはHTTPS)だけを使います。
# First, create the node in Jenkins UI:
# Manage Jenkins > Nodes > New Node
# Launch method: "Launch agent by connecting it to the controller"
# Note the secret token from the node configuration page
# On the Mac mini, download the agent JAR as the jenkins user
# (download it again after each Jenkins controller upgrade)
sudo -u jenkins mkdir -p /Users/jenkins/jenkins-agent
sudo -u jenkins curl -fsSL -o /Users/jenkins/jenkins-agent/agent.jar \
https://your-jenkins-url/jnlpJars/agent.jar
# Test the agent connection. -webSocket goes through the controller's
# HTTP(S) port: no separate TCP agent port to open
sudo -u jenkins /opt/homebrew/opt/openjdk@21/bin/java \
-jar /Users/jenkins/jenkins-agent/agent.jar \
-url https://your-jenkins-url \
-secret YOUR_AGENT_SECRET \
-name "mac-mini-m4" \
-webSocket \
-workDir "/Users/jenkins/workspace"
インバウンドエージェントをlaunchdサービスとして実行する
エージェントはjenkinsユーザーのLaunchAgentとして動作するため、Macの起動時ではなく、そのユーザーのログイン時に起動します。ヘッドレスのMacでは、jenkinsユーザーの自動ログインを有効にしてください(システム設定 > ユーザとグループ。FileVaultがオンの間は利用できません)。これで再起動後もエージェントがオンラインに戻ります。このログイン済みセッションは、シミュレーター、UIテスト、ログインキーチェーンが必要とするユーザー環境にもなります。
# Create the LaunchAgent in the jenkins user's own Library
sudo -u jenkins mkdir -p /Users/jenkins/Library/LaunchAgents
sudo -u jenkins tee /Users/jenkins/Library/LaunchAgents/io.jenkins.agent.plist > /dev/null <<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>io.jenkins.agent</string>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/opt/openjdk@21/bin/java</string>
<string>-jar</string>
<string>/Users/jenkins/jenkins-agent/agent.jar</string>
<string>-url</string>
<string>https://your-jenkins-url</string>
<string>-secret</string>
<string>YOUR_AGENT_SECRET</string>
<string>-name</string>
<string>mac-mini-m4</string>
<string>-webSocket</string>
<string>-workDir</string>
<string>/Users/jenkins/workspace</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/Users/jenkins/jenkins-agent/stdout.log</string>
<key>StandardErrorPath</key>
<string>/Users/jenkins/jenkins-agent/stderr.log</string>
</dict>
</plist>
EOF
# The agent loads at every login of the jenkins user. On a headless Mac,
# connect over VNC as an admin, open System Settings > Users & Groups and set
# "Automatically log in as" to jenkins (unavailable while FileVault is on),
# then restart:
sudo shutdown -r now
# After the restart, verify it's running
sudo launchctl print gui/$(id -u jenkins)/io.jenkins.agent | grep state
ステップ3:iOSビルドパイプラインを作成する
リポジトリのルートに Jenkinsfile を作成します。この宣言的パイプラインは、iOSアプリのビルド、テスト、そして必要に応じてアーカイブを行います。
pipeline {
agent { label 'mac && m4' }
environment {
// Pick the Xcode for every stage without sudo (path from: xcodes installed)
DEVELOPER_DIR = '/Applications/Xcode-27.0.0.app/Contents/Developer'
SCHEME = 'MyApp'
// Not WORKSPACE: Jenkins already uses it for the job's workspace path
XC_WORKSPACE = 'MyApp.xcworkspace'
// Any device listed by: xcrun simctl list devices available
DESTINATION = 'platform=iOS Simulator,name=iPhone 17,OS=latest'
// Relative to the job workspace, kept between builds
DERIVED_DATA = 'DerivedData'
}
options {
timeout(time: 30, unit: 'MINUTES')
buildDiscarder(logRotator(numToKeepStr: '20'))
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Install Dependencies') {
steps {
sh '''
# Homebrew tools are not on the PATH of a non-interactive SSH session
export PATH="/opt/homebrew/bin:$PATH"
# Xcode comes from DEVELOPER_DIR: no sudo xcode-select in a pipeline
xcodebuild -version
# Install CocoaPods dependencies
if [ -f "Podfile" ]; then
pod install --repo-update
fi
'''
}
}
stage('Build') {
steps {
sh '''
# Jenkins runs sh -xe: without pipefail, xcbeautify's exit status would hide a failed build
set -o pipefail
export PATH="/opt/homebrew/bin:$PATH"
xcodebuild build \
-workspace "${XC_WORKSPACE}" \
-scheme "${SCHEME}" \
-destination "${DESTINATION}" \
-derivedDataPath "${DERIVED_DATA}" \
CODE_SIGNING_ALLOWED=NO \
| xcbeautify
'''
}
}
stage('Test') {
steps {
sh '''
set -o pipefail
export PATH="/opt/homebrew/bin:$PATH"
rm -rf TestResults.xcresult
xcodebuild test \
-workspace "${XC_WORKSPACE}" \
-scheme "${SCHEME}" \
-destination "${DESTINATION}" \
-derivedDataPath "${DERIVED_DATA}" \
-resultBundlePath "TestResults.xcresult" \
-parallel-testing-enabled YES \
| xcbeautify --report junit --report-path build/reports
'''
}
post {
always {
junit allowEmptyResults: true, testResults: 'build/reports/junit.xml'
archiveArtifacts artifacts: 'TestResults.xcresult/**', allowEmptyArchive: true
}
}
}
stage('Archive') {
when {
branch 'main'
}
steps {
sh '''
set -o pipefail
export PATH="/opt/homebrew/bin:$PATH"
# Unsigned archive: checks that the Release build compiles.
# Fastlane signs and uploads the real build in Step 4.
xcodebuild archive \
-workspace "${XC_WORKSPACE}" \
-scheme "${SCHEME}" \
-archivePath "${DERIVED_DATA}/MyApp.xcarchive" \
-destination "generic/platform=iOS" \
CODE_SIGNING_ALLOWED=NO \
| xcbeautify
'''
}
}
}
post {
success {
echo 'Build and tests passed!'
}
failure {
echo 'Build or tests failed.'
}
cleanup {
sh 'xcrun simctl shutdown all 2>/dev/null || true'
}
}
}
ステップ4:デプロイのためにFastlaneを統合する
Fastlaneは、コード署名とTestFlightへのデプロイを簡素化します。まず、Macに一度だけインストールします。
# Install Fastlane on the Mac mini (one-time setup)
brew install fastlane
Jenkinsfileにデプロイステージを追加します。
stage('Deploy to TestFlight') {
when {
branch 'main'
}
environment {
APP_STORE_CONNECT_API_KEY_ID = credentials('app-store-key-id')
APP_STORE_CONNECT_API_ISSUER_ID = credentials('app-store-issuer-id')
APP_STORE_CONNECT_API_KEY_CONTENT = credentials('app-store-key-content')
MATCH_PASSWORD = credentials('match-password')
}
steps {
sh '''
export PATH="/opt/homebrew/bin:$PATH"
fastlane beta
'''
}
}
そして、対応する fastlane/Fastfile を作成します。
default_platform(:ios)
platform :ios do
desc "Push a new beta build to TestFlight"
lane :beta do
setup_ci
# Fetch code signing certificates via match
match(type: "appstore", readonly: true)
# Increment build number
increment_build_number(
build_number: ENV["BUILD_NUMBER"]
)
# Build the app
build_app(
workspace: "MyApp.xcworkspace",
scheme: "MyApp",
export_method: "app-store",
derived_data_path: "DerivedData"
)
# Upload to TestFlight
upload_to_testflight(
skip_waiting_for_build_processing: true,
api_key: app_store_connect_api_key(
key_id: ENV["APP_STORE_CONNECT_API_KEY_ID"],
issuer_id: ENV["APP_STORE_CONNECT_API_ISSUER_ID"],
key_content: ENV["APP_STORE_CONNECT_API_KEY_CONTENT"]
)
)
end
end
セキュリティのベストプラクティス
-
専用のJenkinsユーザーを使用する
Jenkinsエージェントを決してrootで実行しないでください。最小限の権限を持つ専用ユーザーを作成します。
-
シークレットをJenkins Credentialsに保存する
証明書、APIキー、パスワードをJenkinsfileにハードコードしないでください。Jenkins Credentialsプラグインを使用します。
-
SSHの鍵ベース認証のみを有効にする
SSHのパスワード認証を無効にします。Ed25519またはRSA 4096ビットの鍵を使用します。
-
macOSファイアウォールを使用する
受信接続をJenkinsコントローラーのIPのみに制限します。My Remote Macは、APIアクセスに対応したマネージドファイアウォールを提供します。
-
macOSとXcodeを最新の状態に保つ
セキュリティパッチを速やかに適用します。
softwareupdate -lを使用して更新を確認します。
FAQ
Jenkinsコントローラーを同じMacで実行できますか?
Mac Mini M4は同時に何件のビルドを処理できますか?
iOSシミュレーターにGUIセッションは必要ですか?
ヘッドレスサーバーでコード署名を扱うにはどうすればよいですか?
security unlock-keychain を使用してロックを解除します。