Mengapa Mac Khusus untuk Jenkins?
Jenkins adalah platform CI/CD yang paling banyak digunakan, menjalankan lebih dari 50% pipeline build enterprise. Namun, membangun aplikasi iOS dengan Jenkins memerlukan macOS, dan macOS secara legal hanya dapat berjalan di perangkat keras Apple. Mac Mini M4 khusus memberi Anda:
-
Lingkungan Build yang Konsisten
Perangkat keras sama, OS sama, alat sama setiap build. Tidak ada build yang tidak stabil akibat pergeseran lingkungan.
-
Akses Root Penuh
Instal alat apa pun, konfigurasikan pengaturan sistem, kelola keychain untuk penandatanganan kode.
-
Performa Apple Silicon
Chip M4 memberikan build Xcode 2-3x lebih cepat dibandingkan Mac Mini Intel.
-
Cache Persisten
Cache DerivedData, CocoaPods, dan SPM tetap ada di antara setiap build.
Prasyarat
- Sebuah server build Mac mini M4 khusus dari My Remote Mac (mulai $85/bln, USD, belum termasuk pajak)
- Sebuah Jenkins controller (dapat berada di Linux, Docker, atau VM cloud mana pun)
- Akses SSH ke Mac Mini Anda
- Xcode terinstal di Mac Mini (lihat panduan GitHub Actions kami untuk langkah-langkah instalasi Xcode)
Langkah 1: Instal Java JDK di macOS
Agen Jenkins memerlukan Java Runtime Environment. Rilis Jenkins saat ini mendukung Java 21 dan Java 25 (Java 17 tidak lagi didukung), jadi kami menginstal JDK 21 (LTS) melalui Homebrew untuk kemudahan pengelolaan.
# 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
Langkah 2: Konfigurasikan Agen Jenkins
Ada dua metode utama untuk menghubungkan agen Jenkins: agen inbound (agen terhubung ke controller; dulu disebut JNLP) dan SSH (controller terhubung ke agen). Kami membahas keduanya.
Metode A: Agen SSH (Direkomendasikan)
Ini adalah pendekatan yang paling andal. Jenkins terhubung ke Mac melalui SSH dan mengelola proses agen dari jarak jauh.
# 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
Sekarang konfigurasikan agen di Jenkins:
- Buka Manage Jenkins > Nodes > New Node
- Atur namanya menjadi
mac-mini-m4 - Atur Remote root directory menjadi
/Users/jenkins/workspace - Label:
mac macos apple-silicon m4 ios - Metode peluncuran: Launch agents via SSH
- Host: Alamat IP Mac Mini Anda
- Credentials: Tambahkan kunci privat SSH yang cocok dengan authorized_keys di Mac
- Host Key Verification Strategy: Known hosts file Verification Strategy. Sebelum peluncuran pertama, sambungkan sekali dari controller sebagai pengguna yang menjalankan Jenkins (
ssh jenkins@YOUR_MAC_IP) untuk menambahkan Mac ke~/.ssh/known_hostsmiliknya
Metode B: Agen Inbound (WebSocket)
Gunakan metode ini ketika Jenkins controller tidak dapat menjangkau Mac secara langsung (mis., Mac berada di belakang firewall). Agen yang memulai koneksi. Dengan opsi -webSocket, agen hanya memakai port web controller (HTTP atau HTTPS), tanpa port TCP agen terpisah.
# 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"
Jalankan Agen Inbound sebagai Layanan launchd
Agen berjalan sebagai LaunchAgent milik pengguna jenkins, jadi agen mulai saat pengguna itu login, bukan saat boot. Di Mac headless, aktifkan Automatic login untuk pengguna jenkins (System Settings > Users & Groups; tidak tersedia selama FileVault aktif) agar agen kembali online setelah reboot. Sesi yang sudah login ini juga memberi simulator, UI test, dan login keychain lingkungan pengguna yang mereka butuhkan.
# 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
Langkah 3: Buat Pipeline Build iOS
Buat sebuah Jenkinsfile di root repositori Anda. Pipeline deklaratif ini membangun, menguji, dan secara opsional mengarsipkan aplikasi iOS Anda.
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'
}
}
}
Langkah 4: Integrasikan Fastlane untuk Deployment
Fastlane menyederhanakan penandatanganan kode dan deployment TestFlight. Instal sekali di Mac:
# Install Fastlane on the Mac mini (one-time setup)
brew install fastlane
Tambahkan stage deploy ke Jenkinsfile Anda:
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
'''
}
}
Dan buat fastlane/Fastfile yang sesuai:
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
Praktik Keamanan Terbaik
-
Gunakan pengguna Jenkins khusus
Jangan pernah menjalankan agen Jenkins sebagai root. Buat pengguna khusus dengan izin minimal.
-
Simpan secret di Jenkins Credentials
Jangan pernah menuliskan sertifikat, kunci API, atau kata sandi secara hardcode di Jenkinsfile. Gunakan plugin Jenkins Credentials.
-
Aktifkan hanya autentikasi berbasis kunci SSH
Nonaktifkan autentikasi kata sandi untuk SSH. Gunakan kunci Ed25519 atau RSA 4096-bit.
-
Gunakan Firewall macOS
Batasi koneksi masuk hanya ke IP Jenkins controller. My Remote Mac menyediakan firewall terkelola dengan akses API.
-
Selalu perbarui macOS dan Xcode
Terapkan patch keamanan segera. Gunakan
softwareupdate -luntuk memeriksa pembaruan.
FAQ
Bisakah saya menjalankan Jenkins controller di Mac yang sama?
Berapa banyak build bersamaan yang dapat ditangani Mac Mini M4?
Apakah saya memerlukan sesi GUI untuk simulator iOS?
Bagaimana cara menangani penandatanganan kode di server headless?
security unlock-keychain.