All posts
Automation AWS Azure Linux Networking Terraform

Bootstrapping UniFi Controller on AWS

· Mike Hosker

As part of a recent project to upgrade some legacy infrastructure and split services across both Azure and AWS I found myself needing to migrate a UniFi instance. The old setup was a Windows 2019 VM in Azure. The new target: AWS, Ubuntu, fully bootstrapped via Infrastructure as Code.

What is Bootstrapping?

Bootstrapping is the first piece of code that runs when a machine starts — used to trigger software installation and configuration. The goal here was a fully repeatable deployment: running terraform apply should result in either a setup page or login page appearing in the browser at the provisioned public IP, with no manual steps.

High-Level Steps

  1. Provision S3 bucket
  2. Provision EC2 instance
  3. Update/upgrade Ubuntu and set hostname
  4. Install prerequisites (MongoDB, AWS CLI, jq)
  5. Install UniFi
  6. Restore from latest S3 backup (if available)
  7. Configure UniFi (guest portal ports)
  8. Set up a sync service to push backups to S3 every 5 minutes
  9. Start UniFi

Terraform Integration

The bootstrap script is a shell template rendered by Terraform's templatefile function:

user_data = templatefile("../_bootstrap/bootstrap.sh.tpl", {
  server_name       = var.server_name
  env_name          = var.env_name
  unifi_version     = var.unifi_version
  mongo_version     = var.mongo_version
  portal_https_port = local.unifi_portal_https_port
  portal_http_port  = local.unifi_portal_http_port
  bucket_name       = aws_s3_bucket.unifi.id
})

Step 3: Ubuntu Updates and Hostname

sudo apt-get update
sudo apt-get upgrade -y
hostname "${server_name}-${env_name}"
echo "${server_name}-${env_name}" > /etc/hostname

Step 4: Prerequisites — MongoDB

The script loops through Ubuntu distros from newest to oldest to find the right MongoDB repository, ensuring it works across Ubuntu versions:

for distro in jammy focal bionic xenial trusty precise; do
  result=$(curl -s "https://repo.mongodb.org/apt/ubuntu/dists/$distro/")
  if echo "$result" | grep -q "mongodb-org-${mongo_version}"; then
    # Add the repo for this distro
    break
  fi
done

Step 6: Backup Restoration (The Tricky Bit)

This is the most involved step. The script:

  1. Copies all backups from S3 to a temp directory
  2. Parses backup metadata JSON with jq to find the most recent backup by timestamp
  3. Uploads the backup file to UniFi's local HTTPS endpoint
  4. Issues a restore command using the returned backup ID
  5. Copies all backups to the UniFi backup directory with correct ownership

A critical detail: the upload must include the X-Requested-With: XMLHttpRequest header, and since UniFi uses a self-signed cert on localhost, --insecure is required:

backup_id=$(curl --insecure -s \
  -H "X-Requested-With: XMLHttpRequest" \
  -F "backupFile=@${backup_file}" \
  "https://localhost:8443/api/cmd/backup" \
  | jq -r '.data.backup_id')

curl --insecure -s \
  -H "X-Requested-With: XMLHttpRequest" \
  -d "{\"cmd\":\"restore\",\"backup\":\"${backup_id}\"}" \
  "https://localhost:8443/api/cmd/system"

Step 8: S3 Backup Sync Service

A simple systemd service keeps backups continuously synced to S3:

cat <<EOF > /etc/systemd/system/unifi-backup-sync.service
[Unit]
Description=Sync UniFi backups to S3

[Service]
Type=simple
ExecStart=/usr/local/bin/unifi-backup-sync.sh
Restart=always

[Install]
WantedBy=multi-user.target
EOF

cat <<EOF > /usr/local/bin/unifi-backup-sync.sh
#!/bin/sh
while true; do
  sudo aws s3 sync /var/lib/unifi/backup s3://${bucket_name}/backups \
    --only-show-errors --delete
  sleep 300
done
EOF

Key Considerations

Port restrictions — UniFi runs as a non-root user and cannot bind to privileged ports below 1024. The module configures alternative ports (2083/HTTPS, 8880/HTTP) via system.properties.

File ownership — After copying backups into the UniFi backup directory, ownership must be set to the unifi user or UniFi won't write new backup metadata.

Immutability — The only persistent element across rebuilds is the S3 bucket. Everything else is recreated on each terraform apply.

Full Code

Available on GitHub: mhosker/terraform-unifi-bootstrap