TerraformPilot

DevOps

Terraform for macOS CI Runners on EC2 Mac Instances

Provision macOS CI build infrastructure with Terraform: EC2 Mac instances (mac1, mac2-m2pro), dedicated hosts, and self-hosted GitHub Actions runners.

LLuca Berton1 min read

macOS CI infrastructure is one of the few cases where Terraform meets Apple silicon. EC2 Mac instances (mac1.metal, mac2.metal, mac2-m2.metal, mac2-m2pro.metal) run real Apple hardware in AWS data centers, on a 24-hour minimum dedicated host allocation. iOS/macOS apps need them to build and codesign. Terraform provisions the dedicated hosts and instances; an autoscaling group of Linux runners can't replace them.

Quick Pattern (TL;DR)

#
resource "aws_ec2_host" "mac" {
  instance_type     = "mac2-m2pro.metal"
  availability_zone = "us-east-1a"
  auto_placement    = "on"
}
 
resource "aws_instance" "mac_runner" {
  ami           = data.aws_ami.macos_sonoma.id
  instance_type = "mac2-m2pro.metal"
  host_id       = aws_ec2_host.mac.id
  tenancy       = "host"
 
  root_block_device {
    volume_size = 500
    volume_type = "gp3"
    iops        = 6000
    throughput  = 500
  }
 
  tags = { Name = "mac-runner-1" }
}

Find a macOS AMI

#
data "aws_ami" "macos_sonoma" {
  most_recent = true
  owners      = ["amazon"]
 
  filter {
    name   = "name"
    values = ["amzn-ec2-macos-14.*"]
  }
 
  filter {
    name   = "architecture"
    values = ["arm64_mac"]
  }
}

Bootstrap a GitHub Actions Self-Hosted Runner

#
resource "aws_instance" "mac_runner" {
  ami           = data.aws_ami.macos_sonoma.id
  instance_type = "mac2-m2pro.metal"
  host_id       = aws_ec2_host.mac.id
  tenancy       = "host"
  key_name      = aws_key_pair.mac.key_name
 
  user_data = base64encode(<<-EOT
    #!/bin/bash
    set -euxo pipefail
    sudo -u ec2-user -i <<'EOF'
    set -euxo pipefail
    cd ~
    mkdir -p actions-runner && cd actions-runner
    curl -o runner.tar.gz -L https://github.com/actions/runner/releases/download/v2.319.1/actions-runner-osx-arm64-2.319.1.tar.gz
    tar xzf runner.tar.gz
    ./config.sh \
      --url https://github.com/${var.gh_org}/${var.gh_repo} \
      --token ${var.gh_runner_token} \
      --labels self-hosted,macos,arm64 \
      --unattended
    ./svc.sh install
    ./svc.sh start
    EOF
  EOT
  )
 
  tags = { Name = "gha-mac-runner-1", role = "ci" }
}

Important Constraints

#
  • 24-hour minimum on dedicated hosts. Releasing earlier still bills 24 h.
  • Bring-your-own AMI is allowed but Amazon's macOS AMIs are recommended.
  • EBS root volumes only — no instance store on Mac instances.
  • Stop/start required to reset between builds; use aws_ssm_document to schedule.

Reset Between Builds

#
resource "aws_ssm_document" "mac_reset" {
  name          = "mac-reset"
  document_type = "Command"
 
  content = jsonencode({
    schemaVersion = "2.2"
    mainSteps = [{
      action = "aws:runShellScript"
      name   = "reset"
      inputs = {
        runCommand = [
          "rm -rf /Users/ec2-user/work/*",
          "rm -rf /Users/ec2-user/Library/Developer/Xcode/DerivedData/*",
          "xcrun simctl erase all || true"
        ]
      }
    }]
  })
}

Best Practices

#
  • Provision a small pool (2–4) of mac instances; they're expensive (~$1.50/hr for mac2-m2pro) and the 24 h floor punishes elasticity.
  • Stop instances overnight if the runner pool is idle — billing for the host continues, but stopping clears state cheaply.
  • Bake AMIs with Packer (Xcode + simulators + brew packages preinstalled) — first-build cold start otherwise drags 30+ min.
  • Restrict SSH behind SSM Session Manager — never expose port 22 publicly.
  • Tag for cost attribution: team, repo, xcode_version — this fleet is the most expensive line item in many CI bills.
#
#Terraform#macOS#AWS#EC2 Mac#CI/CD

Share this article