本文へスキップ

Jenkins Macビルドエージェント:完全セットアップガイド

専用のMac Mini M4にJenkinsビルドエージェントを構成します。ビルドインフラを完全にコントロールしながら、Xcodeビルドの自動化、iOSテストの実行、TestFlightへのデプロイを行えます。

CI/CD 読了35分 2026年10月更新

なぜ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でエージェントを構成します。

  1. Manage Jenkins > Nodes > New Node に移動します
  2. 名前を mac-mini-m4 に設定します
  3. リモートのルートディレクトリを /Users/jenkins/workspace に設定します
  4. ラベル:mac macos apple-silicon m4 ios
  5. 起動方法:Launch agents via SSH
  6. ホスト:Mac MiniのIPアドレス
  7. 認証情報:Mac上の authorized_keys に対応するSSH秘密鍵を追加します
  8. 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で実行できますか?
可能ですが、分離することをお勧めします。コントローラーは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 を使用してロックを解除します。

Jenkinsのビルドを加速する準備はできましたか?

Jenkins CI/CDパイプライン向けに専用のMac Mini M4を導入しましょう。月額$85から利用できます。

My Remote Mac の料金は USD・税抜です。

さらに詳しい情報をお探しですか?

ステップバイステップのセットアップ手順、設定リファレンス、トラブルシューティングについては、完全なドキュメントをご覧ください。

ドキュメントを開く →