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 0Furthermore, 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 -enableAutomating 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=NOHandling 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:
- Configure
ec2-userfor automatic GUI login into the macOS WindowServer on boot. - Pre-populate or grant Accessibility permissions to
Xcodeandxctestin/Library/Application Support/com.apple.TCC/TCC.dbwhile System Integrity Protection (SIP) permits management, or approve them once via Apple Remote Desktop (ARD) / VNC before creating your golden AMI. - Archive the
.xcresultbundle to Amazon S3 after test completion so developers can inspect failure screenshots, crash logs, and code coverage reports locally in Xcode.