Руководство - CI/CD

Jenkins Mac Build Agent: полное руководство по настройке

Настройте Jenkins build agent на выделенном Mac Mini M4. Автоматизируйте сборки Xcode, запускайте тесты iOS и деплойте в TestFlight с полным контролем над инфраструктурой сборки.

35 мин чтения Обновлено в январе 2025

1. Зачем нужен выделенный Mac для Jenkins?

Jenkins — это наиболее широко используемая платформа CI/CD, обслуживающая более 50% корпоративных пайплайнов сборки. Однако сборка iOS-приложений с Jenkins требует macOS, а macOS может легально работать только на оборудовании Apple. Выделенный Mac Mini M4 даёт вам:

Стабильная среда сборки

Одинаковое оборудование, одна ОС, одни инструменты при каждой сборке. Никаких нестабильных сборок из-за различий в окружении.

Полный root-доступ

Устанавливайте любые инструменты, настраивайте системные параметры, управляйте связками ключей для подписи кода.

Производительность Apple Silicon

Чип M4 обеспечивает сборки в Xcode в 2-3 раза быстрее по сравнению с Intel Mac Mini.

Постоянные кэши

DerivedData, CocoaPods и кэши SPM сохраняются между сборками.

2. Предварительные требования

  • Mac Mini M4 от MyRemoteMac (от $75/мес.)
  • Контроллер Jenkins (может быть на Linux, Docker или любой облачной ВМ)
  • SSH-доступ к вашему Mac Mini
  • Xcode, установленный на Mac Mini (см. наше руководство по GitHub Actions для инструкций по установке Xcode)

3. Шаг 1: Установка Java JDK на macOS

Jenkins agent требует Java Runtime Environment. Мы рекомендуем использовать JDK 17 (LTS) или JDK 21 (LTS), установленные через Homebrew для удобного управления.

# 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 Agent

Существует два основных метода подключения Jenkins agent: JNLP (входящее подключение от agent к контроллеру) и SSH (исходящее подключение от контроллера к agent). Мы рассмотрим оба.

Метод A: SSH Agent (рекомендуется)

Это наиболее надёжный подход. Jenkins подключается к Mac через SSH и управляет процессом agent удалённо.

# 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

Теперь настройте agent в Jenkins:

  1. Перейдите в Manage Jenkins > Nodes > New Node
  2. Задайте имя mac-mini-m4
  3. Укажите Remote root directory: /Users/jenkins/workspace
  4. Labels: mac macos apple-silicon m4 ios
  5. Launch method: Launch agents via SSH
  6. Host: IP-адрес вашего Mac Mini
  7. Credentials: добавьте SSH-закрытый ключ, соответствующий authorized_keys на Mac

Метод B: JNLP Agent (входящий)

Используйте этот метод, когда контроллер Jenkins не может подключиться к Mac напрямую (например, Mac находится за файрволом). Agent инициирует подключение.

# 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 Agent как службы 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

Никогда не запускайте Jenkins agent от имени root. Создайте выделенного пользователя с минимальными правами.

Храните секреты в Jenkins Credentials

Никогда не вставляйте сертификаты, API-ключи или пароли напрямую в Jenkinsfile. Используйте плагин Jenkins Credentials.

Включите только аутентификацию по SSH-ключам

Отключите аутентификацию по паролю для SSH. Используйте ключи Ed25519 или RSA 4096 бит.

Используйте файрвол macOS

Ограничьте входящие подключения только IP-адресом контроллера Jenkins. My Remote Mac предоставляет управляемый файрвол с доступом через API.

Обновляйте macOS и Xcode

Своевременно устанавливайте патчи безопасности. Используйте softwareupdate -l для проверки обновлений.

8. FAQ

Можно ли запускать контроллер Jenkins на том же Mac?

Да, но мы рекомендуем разделять их. Запускайте контроллер на виртуальной машине Linux или в Docker-контейнере, а Mac используйте исключительно как build agent. Это позволит избежать конкуренции за ресурсы во время тяжёлых сборок.

Сколько параллельных сборок выдержит Mac Mini M4?

Mac Mini M4 с 16 ГБ RAM комфортно справляется с 2 параллельными сборками Xcode. С 24 ГБ можно увеличить до 3. M4 Pro с 48 ГБ легко выдерживает 4+ параллельных сборок. Настройте «Number of executors» в настройках узла Jenkins соответственно.

Нужна ли GUI-сессия для симуляторов iOS?

Нет. Симуляторы iOS работают в headless-режиме через SSH. Однако если вам нужен GUI (например, для скриншотов UI-тестов), подключитесь по VNC к Mac и убедитесь, что GUI-сессия активна.

Как управлять подписью кода на headless-сервере?

Используйте Fastlane Match для синхронизации сертификатов из Git-репозитория или облачного хранилища. Альтернативно, импортируйте сертификаты в связку ключей macOS и разблокируйте её в пайплайне с помощью security unlock-keychain.

Связанные руководства

Готовы ускорить ваши сборки Jenkins?

Получите выделенный Mac Mini M4 для вашего пайплайна Jenkins CI/CD. От $75/мес. с 7-дневным бесплатным пробным периодом.