Unifi Controller on Core OS with Terraform (Part 1: Basic CoreOS host)

tl;dr The code for the complete Unifi setup is available in the niels-s/unifi-terraform-example repo

I’ve been running Unifi AP’s for the last couple of years, but I never took the time to set up a dedicated Unifi Controller to keep track of my AP’s. You are getting some benefits like Automatic Rolling upgrades and new features like Wifi AI, which should configure your AP’s in the most optimal way. Since I wanted to learn some new things, I’ve opted to set up a CoreOS host and utilize systemd to configure all the needed services.

In this first post of a “series”, we get started setting up a primary Droplet on Digital Ocean with SSH configured.

We use Terraform to configure the infrastructure, so we need the Digital Ocean , and the Ignition terraform providers. The Ignition provider generates a provisioning configuration, which the Droplet uses at the first boot. Ignition is a new provisioning utility designed specifically for Container Linux. You can find more information at the CoreOS site.

The basics

First of all let’s configure the Terraform providers we are going to use

provider "digitalocean" {
  token   = var.digitalocean_token
  version = "~> 1.9"
}

provider "ignition" {
  version = "~> 1.2"
}

To make the configuration a little easier to configure I’ve abstracted some hardcoded configuration options to variables:

variable "digitalocean_token" {
  type        = string
  description = "Token used to query the DigitalOcean API"
}

variable "ssh_public_key_name" {
  type        = string
  description = "Name of the Public key resource in Digital Ocean"
}

variable "ssh_public_key" {
  type        = string
  description = "Public key used to allow SSH access to the VM"
}

variable "hostname" {
  type        = string
  description = "Fully Qualified host name of the server, this will be used to request certificat with Let's Encrypt"
}

The Digital Ocean Project

Next, we configure the Digital Ocean project. If this is your first Digital Ocean project, you already created your default project via the Digital Ocean UI. In that case, you need to import the Digital Ocean project first, so the Terraform resource is connected to it. Using the following command does the command terraform import digitalocean_project.unifi your-project-id. You can find your project ID from the URL in the browser 😉

resource "digitalocean_project" "unifi" {
  name        = "unifi"
  description = "all resources for Unifi Ubiquiti controller"
  purpose     = "Unifi Controller"
  environment = "Production"
  resources = [
    "do:droplet:${digitalocean_droplet.unifi_controller.id}"
  ]
}

resource "digitalocean_ssh_key" "ssh_key" {
  name       = "Your Name"
  public_key = var.ssh_public_key
}

You also provide a public ssh key so we can use a private ssh key for authentication instead of using passwords.

The CoreOS Droplet

Finally we can setup the initial Droplet itself,

resource "digitalocean_droplet" "unifi_controller" {
  image       = "coreos-stable"
  name        = var.hostname
  region      = "ams3"
  size        = "s-1vcpu-2gb"
  ipv6        = true
  resize_disk = false
  ssh_keys    = [digitalocean_ssh_key.ssh_key.fingerprint]
  user_data   = data.ignition_config.unifi_controller.rendered

  lifecycle {
    create_before_destroy = true
  }
}

data "ignition_config" "unifi_controller" {
  files = [
    data.ignition_file.profile_variables.rendered,
    data.ignition_file.sshd_config.rendered
  ]
  systemd = [
    data.ignition_systemd_unit.sshd_port.rendered
  ]
}

data "ignition_file" "profile_variables" {
  filesystem = "root"
  path       = "/etc/profile.d/variables.sh"
  mode       = 420 # 644

  content {
    content = <<-EOT
      export TERM=xterm
    EOT
  }
}

# Configure SSH Service
data "ignition_file" "sshd_config" {
  filesystem = "root"
  path       = "/etc/ssh/sshd_config"
  mode       = 384 # 600

  content {
    content = <<-CONFIG
      # Use most defaults for sshd configuration.
      UsePrivilegeSeparation sandbox
      Subsystem sftp internal-sftp
      ClientAliveInterval 180
      UseDNS no
      UsePAM yes
      PrintLastLog no # handled by PAM
      PrintMotd no # handled by PAM

      PermitRootLogin no
      AllowUsers core
      AuthenticationMethods publickey
    CONFIG
  }
}

data "ignition_systemd_unit" "sshd_port" {
  name = "sshd.socket"

  dropin {
    name    = "10-sshd-port.conf"
    content = <<-CONFIG
      [Socket]
      ListenStream=
      ListenStream=2222
    CONFIG
  }
}

A little explanation with this piece of code. With the digitalocean_droplet resource, we tell DO to create a droplet using these properties. An important property is user_data, this property supplies the generated Ignition configuration, so the CoreOS host knows what it needs to configure (files, mounts, services, …).

When you look at the Ignition resources, you notice we configure ignition_file and ignition_systemd_unit resource. The first, ignition_file, creates a file on the host using the content you provide. I’ve chosen to keep the content inline, but you could also load the content from external files like file("${path.cwd}/example.conf").

The later, ignition_systemd_unit, configures the sshd service to our liking since we already have an sshd service configured we use a drop-in approach here to only adjust the settings we want. But in a later post, you will see how we configure a non-preexisting service.

NOTE: I switched to a droplet with 2GB of memory because the MongoDB data was getting corrupted after some time. I guess some compression is happening periodically, which because of the memory pressure switched to using swap, but that didn’t work out that great. I didn’t have any issues anymore since I upgrade the memory. However, I have a minimal setup (4 sites, 8 AP’s) so you need to take into account your own needs 😉

The code for the complete Unifi setup is available in the niels-s/unifi-terraform-example repo , the changes of this post can be found in this commit

This post is part of a small series, go and read the next post to setup a volume mount