1. なぜJenkinsに専用のMacが必要なのか?
Jenkinsは最も広く使われているCI/CDプラットフォームであり、エンタープライズのビルドパイプラインの50%以上で稼働しています。しかし、JenkinsでiOSアプリをビルドするにはmacOSが必要であり、macOSはApple製ハードウェア上でのみ合法的に動作します。専用のMac Mini M4なら、次のメリットが得られます。
毎回のビルドで、同じハードウェア、同じOS、同じツールを使えます。環境の差異による不安定なビルド(flaky build)はありません。
任意のツールをインストールし、システム設定を構成し、コード署名用のキーチェーンを管理できます。
M4チップは、Intel製Mac Miniと比較して2〜3倍高速なXcodeビルドを実現します。
DerivedData、CocoaPods、SPMのキャッシュがビルド間で保持されます。
2. 前提条件
- MyRemoteMacのMac Mini M4(月額$85から)
- Jenkinsコントローラー(Linux、Docker、または任意のクラウドVM上に配置可能)
- Mac MiniへのSSHアクセス
- Mac MiniにインストールされたXcode(Xcodeのインストール手順については GitHub Actionsガイド を参照してください)
3. ステップ1:macOSにJava JDKをインストールする
Jenkinsエージェントには、Java Runtime Environmentが必要です。管理を容易にするため、Homebrew経由でインストールするJDK 17(LTS)または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 17 (recommended for Jenkins)
brew install openjdk@17
# Create the symlink for system Java wrappers
sudo ln -sfn /opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk \
/Library/Java/JavaVirtualMachines/openjdk-17.jdk
# Add to PATH
echo 'export PATH="/opt/homebrew/opt/openjdk@17/bin:$PATH"' >> ~/.zprofile
source ~/.zprofile
# Verify Java installation
java -version
# openjdk version "17.0.13" 2024-10-15
# OpenJDK Runtime Environment Homebrew (build 17.0.13+0)
# OpenJDK 64-Bit Server VM Homebrew (build 17.0.13+0, mixed mode, sharing)
4. ステップ2:Jenkinsエージェントを構成する
Jenkinsエージェントを接続する主な方法は2つあります。JNLP(エージェントからコントローラーへのインバウンド接続)とSSH(コントローラーからエージェントへのアウトバウンド接続)です。ここでは両方を取り上げます。
方法A:SSHエージェント(推奨)
これは最も信頼性の高い方法です。JenkinsはSSH経由でMacに接続し、エージェントプロセスをリモートで管理します。
# On the Mac Mini: Create a dedicated Jenkins user
sudo dscl . -create /Users/jenkins
sudo dscl . -create /Users/jenkins UserShell /bin/zsh
sudo dscl . -create /Users/jenkins RealName "Jenkins Agent"
sudo dscl . -create /Users/jenkins UniqueID 550
sudo dscl . -create /Users/jenkins PrimaryGroupID 20
sudo dscl . -create /Users/jenkins NFSHomeDirectory /Users/jenkins
sudo mkdir -p /Users/jenkins
sudo chown jenkins:staff /Users/jenkins
# Set a password for the jenkins user
sudo dscl . -passwd /Users/jenkins "STRONG_PASSWORD_HERE"
# 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秘密鍵を追加します
方法B:JNLPエージェント(インバウンド)
Jenkinsコントローラーが直接Macに到達できない場合(例:Macがファイアウォールの内側にある場合)は、この方法を使用します。エージェント側から接続を開始します。
# 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:
mkdir -p ~/jenkins-agent && cd ~/jenkins-agent
curl -sO https://your-jenkins-url/jnlpJars/agent.jar
# Test the agent connection
java -jar agent.jar \
-url https://your-jenkins-url \
-secret YOUR_AGENT_SECRET \
-name "mac-mini-m4" \
-workDir "/Users/jenkins/workspace"
JNLPエージェントをlaunchdサービスとして実行する
# Create the launchd plist file
cat > ~/Library/LaunchAgents/com.jenkins.agent.plist <<'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>com.jenkins.agent</string>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/opt/openjdk@17/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>-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
# Load the service
launchctl load ~/Library/LaunchAgents/com.jenkins.agent.plist
# Verify it's running
launchctl list | grep jenkins
5. ステップ3:iOSビルドパイプラインを作成する
リポジトリのルートに Jenkinsfile を作成します。この宣言的パイプラインは、iOSアプリのビルド、テスト、そして必要に応じてアーカイブを行います。
pipeline {
agent { label 'mac && m4' }
environment {
SCHEME = 'MyApp'
WORKSPACE = 'MyApp.xcworkspace'
DESTINATION = 'platform=iOS Simulator,name=iPhone 16 Pro,OS=18.2'
DERIVED_DATA = "${WORKSPACE_DIR}/DerivedData"
}
options {
timeout(time: 30, unit: 'MINUTES')
buildDiscarder(logRotator(numToKeepStr: '20'))
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Install Dependencies') {
steps {
sh '''
# Select Xcode version
sudo xcode-select -s /Applications/Xcode-16.2.app/Contents/Developer
xcodebuild -version
# Install CocoaPods dependencies
if [ -f "Podfile" ]; then
pod install --repo-update
fi
'''
}
}
stage('Build') {
steps {
sh '''
xcodebuild build \
-workspace "${WORKSPACE}" \
-scheme "${SCHEME}" \
-destination "${DESTINATION}" \
-derivedDataPath "${DERIVED_DATA}" \
CODE_SIGNING_ALLOWED=NO \
| xcbeautify
'''
}
}
stage('Test') {
steps {
sh '''
xcodebuild test \
-workspace "${WORKSPACE}" \
-scheme "${SCHEME}" \
-destination "${DESTINATION}" \
-derivedDataPath "${DERIVED_DATA}" \
-resultBundlePath "TestResults.xcresult" \
-parallel-testing-enabled YES \
| xcbeautify
'''
}
post {
always {
archiveArtifacts artifacts: 'TestResults.xcresult/**', allowEmptyArchive: true
}
}
}
stage('Archive') {
when {
branch 'main'
}
steps {
sh '''
xcodebuild archive \
-workspace "${WORKSPACE}" \
-scheme "${SCHEME}" \
-archivePath "${DERIVED_DATA}/MyApp.xcarchive" \
-destination "generic/platform=iOS" \
| xcbeautify
'''
}
}
}
post {
success {
echo 'Build and tests passed!'
}
failure {
echo 'Build or tests failed.'
}
cleanup {
sh 'xcrun simctl shutdown all 2>/dev/null || true'
}
}
}
6. ステップ4:デプロイのためにFastlaneを統合する
Fastlaneは、コード署名とTestFlightへのデプロイを簡素化します。Jenkinsfileにデプロイステージを追加します。
# Install Fastlane on the Mac Mini (one-time setup)
brew install fastlane
# Or via Ruby:
gem install fastlane -NV
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 '''
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
7. セキュリティのベストプラクティス
Jenkinsエージェントを決してrootで実行しないでください。最小限の権限を持つ専用ユーザーを作成します。
証明書、APIキー、パスワードをJenkinsfileにハードコードしないでください。Jenkins Credentialsプラグインを使用します。
SSHのパスワード認証を無効にします。Ed25519またはRSA 4096ビットの鍵を使用します。
受信接続をJenkinsコントローラーのIPのみに制限します。MyRemoteMacは、APIアクセスに対応したマネージドファイアウォールを提供します。
セキュリティパッチを速やかに適用します。softwareupdate -l を使用して更新を確認します。
8. FAQ
Jenkinsコントローラーを同じMacで実行できますか?
可能ですが、分離することをお勧めします。コントローラーはLinux VMまたはDockerコンテナで実行し、Macはビルドエージェント専用として使用してください。これにより、負荷の高いビルド中のリソース競合を回避できます。
Mac Mini M4は同時に何件のビルドを処理できますか?
16GB RAMのMac Mini M4は、2件の同時Xcodeビルドを余裕を持って処理できます。24GBなら3件まで増やせます。48GBのM4 Proなら、4件以上の同時ビルドも簡単に処理できます。Jenkinsのノード設定で「Number of executors」をそれに応じて構成してください。
iOSシミュレーターにGUIセッションは必要ですか?
いいえ。iOSシミュレーターはSSH経由のヘッドレスモードで動作します。ただし、GUIが必要な場合(例:UIテストのスクリーンショット取得)は、VNCでMacにログインし、GUIセッションがアクティブであることを確認してください。
ヘッドレスサーバーでコード署名を扱うにはどうすればよいですか?
Fastlane Matchを使用して、Gitリポジトリまたはクラウドストレージから証明書を同期します。あるいは、証明書をmacOSのキーチェーンにインポートし、パイプライン内で security unlock-keychain を使用してロックを解除します。