How to Build macOS and iOS Apps on Amazon EC2 Mac?

Apple developers can build and test macOS and iOS applications in the cloud by provisioning bare metal Amazon EC2 Mac instances powered by Apple silicon or Intel processors. These instances run on physical Mac mini computers connected to the AWS Nitro System, enabling engineering teams to offload Xcode compilation, automated UI testing, and continuous integration workflows to scalable cloud infrastructure while integrating directly with native AWS services.

Architecture of Amazon EC2 Mac Instances

Amazon EC2 Mac instances provide on-demand macOS environments in the cloud by combining physical Apple Mac mini hardware with the AWS Nitro System. Because Apple macOS licensing requires macOS to run on Apple-branded hardware, every EC2 Mac instance is backed by a dedicated physical Mac mini host rather than a shared hypervisor. The AWS Nitro controller attaches directly to the Mac mini via a high-speed Thunderbolt connection, exposing the host as a standard EC2 bare metal instance (mac1.metal, mac2.metal, mac2-m2.metal, or mac2-m2pro.metal).

Through the Nitro System, the physical Mac mini gains native access to Amazon Virtual Private Cloud (VPC) networking, Amazon Elastic Block Store (EBS) volumes, and IAM instance roles without requiring custom drivers inside the guest operating system. Boot volumes and secondary storage drives are served over the network via EBS, allowing developers to snapshot, clone, and resize macOS disks just like standard Linux or Windows EC2 instances.

Instance Family Processor CPU Cores Memory Network Bandwidth EBS Bandwidth
mac1.metal Intel Core i7 (8th Gen) 6 physical (12 vCPU) 32 GiB Up to 10 Gbps Up to 8 Gbps
mac2.metal Apple M1 8 cores (4P + 4E) 16 GiB Up to 10 Gbps Up to 8 Gbps
mac2-m2.metal Apple M2 8 cores (4P + 4E) 24 GiB Up to 10 Gbps Up to 8 Gbps
mac2-m2pro.metal Apple M2 Pro 12 cores (8P + 4E) 32 GiB Up to 10 Gbps Up to 8 Gbps

Allocating Dedicated Hosts and Compliance Requirements

Before launching an EC2 Mac instance, you must allocate an EC2 Dedicated Host. In compliance with the Apple macOS Software License Agreement, AWS enforces a minimum host allocation period of 24 consecutive hours (86400 seconds). You cannot release a Mac Dedicated Host until a full 24-hour window has elapsed from the moment of allocation.

When sizing a build fleet for continuous integration, you can determine the required number of Dedicated Hosts (\(H\)) based on your daily build volume (\(N\)), average build duration in minutes (\(t_{\text{build}}\)), and target utilization factor (\(\rho\)) during peak working hours (\(T_{\text{peak}}\) in minutes):

\[H = \left\lceil \frac{N \times t_{\text{build}}}{\rho \times T_{\text{peak}}} \right\rceil\]

Because scrubbing and re-initializing physical Apple silicon firmware takes approximately 60 to 110 minutes when an instance is terminated on a Dedicated Host, you should avoid terminating and relaunching instances after every single CI job unless strict single-tenant isolation is required. Instead, either reuse persistent build agents or maintain a rolling pool of pre-warmed Dedicated Hosts.

To allocate an Apple M2 Pro Dedicated Host and launch a macOS instance using the AWS CLI, run the following commands:

# 1. Allocate a Dedicated Host for mac2-m2pro.metal in us-east-1a
HOST_ID=$(aws ec2 allocate-hosts \
  --instance-type mac2-m2pro.metal \
  --availability-zone us-east-1a \
  --auto-placement on \
  --quantity 1 \
  --query 'HostIds[0]' \
  --output text)

# 2. Launch an EC2 Mac instance onto the allocated Dedicated Host
aws ec2 run-instances \
  --image-id ami-0123456789abcdef0 \
  --instance-type mac2-m2pro.metal \
  --placement Tenancy=host,HostId=$HOST_ID \
  --key-name mac-ci-keypair \
  --security-group-ids sg-0123456789abcdef0 \
  --subnet-id subnet-0123456789abcdef0 \
  --block-device-mappings '[{"DeviceName":"/dev/sda1","Ebs":{"VolumeSize":200,"VolumeType":"gp3","Iops":6000,"Throughput":500}}]'

Storage Optimization and EBS Pre-Warming

AWS provides official macOS Amazon Machine Images (AMIs) pre-installed with specific versions of macOS (such as macOS Sonoma or macOS Sequoia) along with the AWS CLI, AWS Systems Manager SSM Agent, and Homebrew. However, Xcode installations, iOS Simulator runtimes, and Swift Package Manager caches require substantial disk space and high I/O throughput.

When creating an instance or building a custom AMI, configure the root EBS volume as a gp3 volume with at least 200 GiB to 500 GiB of storage, 6000 IOPS, and 500 MiB/s throughput. By default, macOS does not automatically expand the APFS container when you increase the underlying EBS volume size. After launching an instance with a larger root volume, expand the APFS partition over SSH:

# Connect to the EC2 Mac instance
ssh -i ~/.ssh/mac-ci-keypair.pem ec2-user@<instance-ip>

# Repair the partition map and resize the APFS container to fill the EBS volume
PDISK=$(diskutil list physical external | head -n1 | cut -d" " -f1)
APFSCONT=$(diskutil list physical external | grep Apple_APFS | tr -s " " | cut -d" " -f8)
yes | sudo diskutil repairDisk $PDISK
sudo diskutil apfs resizeContainer $APFSCONT 0

Furthermore, EBS volumes created from snapshots load storage blocks lazily from Amazon S3 on first read. This initial read penalty can double the time of the first Xcode compilation on a freshly booted instance. To eliminate first-build latency, either enable Amazon EBS Fast Snapshot Restore (FSR) on your custom AMI snapshot or pre-warm the Xcode toolchain and simulator directories using dd or fio during instance initialization.

Building Custom Golden AMIs with Packer and Xcode

Downloading and extracting a 12 GB Xcode .xip archive and installing multiple iOS Simulator runtimes takes 30 to 45 minutes. Doing this dynamically at instance boot time wastes build time. Instead, use HashiCorp Packer with the amazon-ebs builder and the xcodes CLI tool to bake a golden macOS AMI containing your exact Xcode toolchain, CocoaPods, Fastlane, and iOS runtimes.

Below is a production shell provisioning script to run inside a Packer build on an EC2 Mac instance:

#!/bin/zsh
set -euo pipefail

# Ensure Homebrew is in PATH on Apple silicon
eval "$(/opt/homebrew/bin/brew shellenv)"

# Install aria2 for multi-connection downloads and xcodes CLI
brew install aria2 xcodesorg/made/xcodes fastlane cocoapods

# Install Xcode 16.2 using Apple credentials stored in AWS Secrets Manager
export XCODES_USERNAME="ci-apple-id@example.com"
export XCODES_PASSWORD=$(aws secretsmanager get-secret-value --secret-id apple-id-password --query SecretString --output text)

xcodes install 16.2 --experimental-unxip
sudo xcode-select -s /Applications/Xcode-16.2.0.app/Contents/Developer

# Accept Xcode license and install mandatory system components
sudo xcodebuild -license accept
sudo xcodebuild -runFirstLaunch

# Download and install the iOS 18.2 Simulator runtime
xcodes runtimes install "iOS 18.2"

# Enable Developer Mode for automated testing without GUI prompts
sudo DevToolsSecurity -enable

Automating Code Signing and Keychain Management

Building iOS and macOS applications for TestFlight or App Store distribution in a headless SSH session requires non-interactive access to Apple signing certificates and provisioning profiles. By default, the macOS system login keychain locks automatically or prompts for GUI authorization via a modal dialog when codesign attempts to read a private key—causing headless CI builds to hang indefinitely.

To prevent headless signing failures, create a dedicated ephemeral keychain during each build job, import the PKCS12 (.p12) certificate fetched from AWS Secrets Manager or SSM Parameter Store, unlock the keychain, and explicitly grant codesign access using security set-key-partition-list:

#!/bin/zsh
set -euo pipefail

KEYCHAIN_NAME="ci-build.keychain-db"
KEYCHAIN_PASSWORD=$(openssl rand -base64 24)
CERT_PATH="/tmp/dist_cert.p12"

# 1. Fetch base64-encoded certificate and provisioning profile from AWS Secrets Manager
aws secretsmanager get-secret-value \
  --secret-id ios/distribution-p12-base64 \
  --query SecretString --output text | base64 --decode > "$CERT_PATH"

CERT_PASSWORD=$(aws secretsmanager get-secret-value \
  --secret-id ios/distribution-p12-password \
  --query SecretString --output text)

mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
aws secretsmanager get-secret-value \
  --secret-id ios/provisioning-profile-base64 \
  --query SecretString --output text | base64 --decode > ~/Library/MobileDevice/Provisioning\ Profiles/build.mobileprovision

# 2. Create, configure, and unlock the ephemeral keychain
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME"
security set-keychain-settings -lut 21600 "$KEYCHAIN_NAME"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME"

# 3. Add the ephemeral keychain to the user search list
security list-keychains -d user -s "$KEYCHAIN_NAME" $(security list-keychains -d user | sed s/\"//g)
security default-keychain -s "$KEYCHAIN_NAME"

# 4. Import certificate and allow codesign to read the private key without GUI prompts
security import "$CERT_PATH" -k "$KEYCHAIN_NAME" -P "$CERT_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME"

# Clean up temporary certificate file on disk
rm -f "$CERT_PATH"

Executing Parallel Unit and UI Tests with xcodebuild

Once the environment and signing keychain are ready, you can compile the application and run unit and UI test suites using xcodebuild. On Apple silicon instances such as mac2-m2pro.metal (12 cores and 32 GiB RAM), running multiple iOS Simulator clones in parallel significantly reduces test execution time.

#!/bin/zsh
set -euo pipefail

# Boot a clean simulator check or let xcodebuild clone dynamically
xcodebuild test \
  -workspace MyApp.xcworkspace \
  -scheme MyApp \
  -destination 'platform=iOS Simulator,name=iPhone 16 Pro,OS=18.2' \
  -derivedDataPath ./DerivedData \
  -resultBundlePath ./TestResults.xcresult \
  -parallel-testing-enabled YES \
  -parallel-testing-worker-count 4 \
  -maximum-concurrent-test-simulator-destinations 4 \
  CODE_SIGNING_ALLOWED=NO

Handling GUI Permissions for macOS UI Tests

While iOS Simulator tests run cleanly in headless SSH sessions, native macOS UI tests (XCTest driving a macOS app window) require an active WindowServer session and Transparency, Consent, and Control (TCC) accessibility permissions. To run macOS UI tests on an EC2 Mac instance:

  1. Configure ec2-user for automatic GUI login into the macOS WindowServer on boot.
  2. Pre-populate or grant Accessibility permissions to Xcode and xctest in /Library/Application Support/com.apple.TCC/TCC.db while System Integrity Protection (SIP) permits management, or approve them once via Apple Remote Desktop (ARD) / VNC before creating your golden AMI.
  3. Archive the .xcresult bundle to Amazon S3 after test completion so developers can inspect failure screenshots, crash logs, and code coverage reports locally in Xcode.