Automated, repeatable, production-ready macOS VM images for CI/CD workflows.
Built with Packer + Tart + Puppet, orchestrated through GitHub Actions, and deployed to an OCI registry. This system provisions fully-configured macOS VMs that mirror bare-metal hardware workers, complete with dynamic hostnames, TaskCluster integration, and automatic configuration management.
This repository automates the entire lifecycle of macOS CI virtual machines:
- Builds golden images from Apple IPSW files using Packer
- Configures them with Puppet (including secrets management via vault files)
- Stores them in a local OCI registry
- Deploys them automatically on Tart worker hosts
- Manages them through Puppet for ongoing configuration drift prevention
Key Features:
- π CI/CD Integration: Automatic builds on PR (fake vault) and main branch (real vault)
- π·οΈ Dynamic Hostnames: VMs self-configure unique identities based on MAC addresses
- π Secrets Management: Vault-based credential injection during build
- ποΈ Four-Phase Build: Base β SIP Disable β Puppet Phase 1 β Puppet Phase 2
- π¦ OCI Distribution: Images stored in registry with prod/PR tagging
- π€ Worker Automation: Puppet-managed Tart workers that auto-pull and deploy VMs
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β GitHub Actions (Builder) β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ β
β β PR Build β β Main Build β β Push to OCI β β
β β (Fake Vault) βββββΆβ (Real Vault) βββββΆβ Registry β β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββ
β OCI Registry β
β 10.49.56.161:5000 β
β β
β sequoia-tester: β
β - prod-latest β
β - prod-{sha} β
β - pr-{n}-latest β
βββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββ
β Tart Worker Hosts β
β (Puppet-managed) β
β β
β Runs 2 VMs each: β
β - sequoia-tester-1 β
β - sequoia-tester-2 β
βββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββ
β TaskCluster β
β (CI Orchestration) β
βββββββββββββββββββββββ
macos-vms/
βββ .github/workflows/build.yml # CI/CD pipeline
βββ mac/tester15/
βββ builder.sh # Main build orchestrator
βββ create-base.pkr.hcl # Phase 1: Base macOS installation
βββ disable-sip.pkr.hcl # Phase 2: Disable SIP
βββ puppet-setup-phase1.pkr.hcl # Phase 3: Initial Puppet run
βββ puppet-setup-phase2.pkr.hcl # Phase 4: Final Puppet run
βββ set_hostname.sh # Dynamic hostname configuration
βββ com.mozilla.sethostname.plist # LaunchDaemon for hostname
βββ vault-fake.yaml # Test credentials (safe to commit)
# Install required tools
brew tap hashicorp/tap cirruslabs/cli
brew install hashicorp/tap/packer cirruslabs/cli/tart ansiblecd mac/tester15
# Use the fake vault for testing (no secrets required)
./builder.sh
# Or specify a custom vault
export VAULT_FILE="/path/to/your/vault.yaml"
./builder.shPull Requests β Builds with vault-fake.yaml, pushes to pr-{number}-latest
Main Branch β Builds with real vault from /etc/ronin/vault-real.yaml, pushes to prod-latest and prod-{sha}
Fully automated macOS installation from IPSW:
- Downloads and installs macOS 15.3 (Sequoia)
- Creates admin user with password
admin - Enables SSH and Screen Sharing
- Automated keyboard navigation through setup wizard
- Boots into macOS Recovery
- Disables System Integrity Protection via
csrutil disable - Required for certain CI tools and Puppet configurations
Initial system configuration:
- Vault injection: Copies vault file to
/var/root/vault.yaml - Installs Rosetta 2, Xcode Command Line Tools, Puppet Agent
- Sets puppet role:
gecko_t_osx_1500_m_vms - Temporarily disables TCC permissions, SafariDriver, and pipconf (requires reboot)
- Runs initial Puppet apply
Why two phases? Some Puppet modules (TCC, SafariDriver) require a reboot to apply correctly.
Final configuration:
- Sets up dynamic hostname script and LaunchDaemon
- Re-enables TCC permissions, SafariDriver, and pipconf
- Runs final Puppet apply with full configuration
- Removes vault file for security
Total Build Time: ~27 minutes
VMs automatically configure unique, stable hostnames on first boot:
# Example: mac-eb3740 (derived from MAC address 12:5d:17:eb:37:40)How it works:
set_hostname.shruns at boot via LaunchDaemon- Extracts last 3 octets of primary interface MAC address
- Sets hostname to
mac-{MAC} - Updates TaskCluster worker configuration files with new hostname
- Worker registers with TaskCluster using unique identity
Benefits:
- No hostname collisions between VMs
- Stable identity across reboots
- Automatic TaskCluster integration
- Easy identification in logs and monitoring
Tart worker hosts are configured via Puppet (roles_profiles::roles::tart_worker):
# data/roles/tart_worker.yaml
tart:
version: '2.30.0'
registry_host: '10.49.56.161'
registry_port: 5000
oci_image: 'sequoia-tester:prod-latest'
worker_count: 2
insecure: truePuppet automatically:
- Installs Tart 2.30.0
- Pulls
sequoia-tester:prod-latestfrom OCI registry - Clones 2 VMs per host (Apple licensing limit)
- Configures LaunchDaemons to keep VMs running
- Creates manual update script at
/usr/local/bin/tart-update-vms.sh
When you want to deploy a new image to running workers:
# On the Tart worker host
sudo /usr/local/bin/tart-update-vms.shThis script:
- Stops all worker VMs gracefully (30s wait)
- Deletes old VMs
- Pulls latest image from OCI registry
- Clones fresh VMs
- Restarts LaunchDaemons
Downtime: ~2-3 minutes per worker host
vault-fake.yaml contains non-sensitive placeholder data for testing. Safe to commit to the repository.
Located at /etc/ronin/vault-real.yaml on GitHub Actions runners. Contains:
- Puppet Hiera secrets
- API keys
- Certificates
- Service credentials
Security:
- Never committed to repository
- Injected during build via GitHub Actions
- Copied to
/var/root/vault.yamlin VM during Phase 3 - Deleted in Phase 4 after Puppet consumes it
- Not present in final OCI image
prod-latestβ Most recent main branch buildprod-{sha}β Specific commit from main branchpr-{number}-latestβ Most recent build from PR #{number}pr-{number}-{sha}β Specific commit from PR #{number}
VM_NAME="sequoia-tester" # Base VM name
VAULT_FILE="vault-fake.yaml" # Path to vault file
REGISTRY_HOST="10.49.56.161" # OCI registry host
REGISTRY_PORT="5000" # OCI registry port
REGISTRY_IMAGE="sequoia-tester" # Image name in registryCheck: Vault file exists and is readable
ls -la /var/root/vault.yaml # Inside VMCheck LaunchDaemons:
sudo launchctl list | grep mozilla
tail -f /var/log/tartworker-1.outManually run hostname script:
sudo /usr/local/bin/set_hostname.shVerify connectivity:
curl http://10.49.56.161:5000/v2/
tart pull --insecure 10.49.56.161:5000/sequoia-tester:prod-latestBuild Times:
- Phase 1 (Base): ~12-15 min
- Phase 2 (SIP): ~2 min
- Phase 3 (Puppet 1): ~7 min
- Phase 4 (Puppet 2): ~2-3 min
- Total: ~27 minutes
Image Sizes:
- Base macOS: ~14 GB
- After Puppet: ~16 GB
- Compressed in OCI: ~22 GB
- Uncompressed on disk: ~100 GB
Fun Fact: Due to copy-on-write magic in Apple File System (APFS), a cloned VM won't actually claim all 100 GB right away. Only changes to a cloned disk will be written and claim new space. This also speeds up clones enormouslyβcreating a new worker VM takes seconds instead of minutes.
Resource Requirements:
- CPU: 4 cores per VM
- Memory: 8 GB per VM
- Disk: 100 GB per VM
- Maximum: 2 VMs per physical host (Apple license)
- Create a feature branch
- Test locally with
./builder.sh - Open a PR (triggers PR build with fake vault)
- Merge to main (triggers prod build with real vault)
- VMs automatically deployed via Puppet
Mozilla Public License 2.0
Built with β€οΈ by Mozilla RelOps