1. ¿Por qué un Mac dedicado para Jenkins?
Jenkins es la plataforma CI/CD más utilizada, ejecutando más del 50% de los pipelines de compilación empresariales. Sin embargo, compilar apps iOS con Jenkins requiere macOS, y macOS solo puede ejecutarse legalmente en hardware Apple. Un Mac Mini M4 dedicado le ofrece:
Mismo hardware, mismo SO, mismas herramientas en cada compilación. Sin compilaciones inestables por deriva del entorno.
Instale cualquier herramienta, configure los ajustes del sistema, gestione keychains para la firma de código.
El chip M4 ofrece compilaciones Xcode 2-3x más rápidas comparado con Mac Minis Intel.
Las cachés de DerivedData, CocoaPods y SPM sobreviven entre compilaciones.
2. Requisitos previos
- Un Mac Mini M4 de MyRemoteMac (desde $75/mes)
- Un controlador Jenkins (puede estar en Linux, Docker o cualquier VM en la nube)
- Acceso SSH a su Mac Mini
- Xcode instalado en el Mac Mini (consulte nuestra guía de GitHub Actions para los pasos de instalación de Xcode)
3. Paso 1: Instalar Java JDK en macOS
Los agentes Jenkins requieren un Java Runtime Environment. Recomendamos usar JDK 17 (LTS) o JDK 21 (LTS) instalado vía Homebrew para una gestión fácil.
# 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. Paso 2: Configurar el agente Jenkins
Hay dos métodos principales para conectar un agente Jenkins: JNLP (conexión entrante del agente al controlador) y SSH (conexión saliente del controlador al agente). Cubrimos ambos.
Método A: Agente SSH (Recomendado)
Este es el enfoque más fiable. Jenkins se conecta al Mac por SSH y gestiona el proceso del agente de forma remota.
# 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
Ahora configure el agente en Jenkins:
- Vaya a Manage Jenkins > Nodes > New Node
- Establezca el nombre como
mac-mini-m4 - Establezca Remote root directory como
/Users/jenkins/workspace - Labels:
mac macos apple-silicon m4 ios - Método de lanzamiento: Launch agents via SSH
- Host: La dirección IP de su Mac Mini
- Credenciales: Añada la clave privada SSH que coincida con las authorized_keys del Mac
Método B: Agente JNLP (Entrante)
Use este método cuando el controlador Jenkins no pueda alcanzar el Mac directamente (p. ej., el Mac está detrás de un firewall). El agente inicia la conexión.
# 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"
Ejecutar el agente JNLP como un servicio 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. Paso 3: Crear un pipeline de compilación iOS
Cree un Jenkinsfile en la raíz de su repositorio. Este pipeline declarativo compila, prueba y opcionalmente archiva su app 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. Paso 4: Integrar Fastlane para el despliegue
Fastlane simplifica la firma de código y el despliegue en TestFlight. Añada una etapa de despliegue a su Jenkinsfile:
# Install Fastlane on the Mac Mini (one-time setup)
brew install fastlane
# Or via Ruby:
gem install fastlane -NV
Añada una etapa de despliegue a su 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
'''
}
}
Y cree un fastlane/Fastfile correspondiente:
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. Mejores prácticas de seguridad
Nunca ejecute el agente Jenkins como root. Cree un usuario dedicado con permisos mínimos.
Nunca codifique certificados, claves API o contraseñas directamente en los Jenkinsfiles. Use el plugin Jenkins Credentials.
Deshabilite la autenticación por contraseña para SSH. Use claves Ed25519 o RSA de 4096 bits.
Restrinja las conexiones entrantes solo a la IP del controlador Jenkins. MyRemoteMac proporciona un firewall gestionado con acceso API.
Aplique los parches de seguridad de forma oportuna. Use softwareupdate -l para verificar las actualizaciones.
8. Preguntas frecuentes
¿Puedo ejecutar el controlador Jenkins en el mismo Mac?
Sí, pero recomendamos separarlos. Ejecute el controlador en una VM Linux o un contenedor Docker, y use el Mac exclusivamente como agente de compilación. Esto evita la contención de recursos durante compilaciones pesadas.
¿Cuántas compilaciones concurrentes puede manejar un Mac Mini M4?
Un Mac Mini M4 con 16GB de RAM puede manejar cómodamente 2 compilaciones Xcode concurrentes. Con 24GB, puede llegar a 3. El M4 Pro con 48GB maneja 4+ compilaciones concurrentes fácilmente. Configure el "Number of executors" en los ajustes del nodo Jenkins según corresponda.
¿Necesito una sesión GUI para simuladores iOS?
No. Los simuladores iOS funcionan en modo headless por SSH. Sin embargo, si necesita una GUI (p. ej., para capturas de pruebas de interfaz), use VNC para iniciar sesión en el Mac y asegúrese de que haya una sesión GUI activa.
¿Cómo manejo la firma de código en un servidor headless?
Use Fastlane Match para sincronizar certificados desde un repositorio Git o almacenamiento en la nube. Alternativamente, importe certificados en el keychain de macOS y desbloquéelo en su pipeline usando security unlock-keychain.