<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Infrastructure-as-Code on mhemeryck</title><link>https://mhemeryck.xyz/tags/infrastructure-as-code/</link><description>Recent content in Infrastructure-as-Code on mhemeryck</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Thu, 16 Dec 2021 00:00:00 +0000</lastBuildDate><atom:link href="https://mhemeryck.xyz/tags/infrastructure-as-code/index.xml" rel="self" type="application/rss+xml"/><item><title>unifi terraform</title><link>https://mhemeryck.xyz/posts/2021-12-16-unifi_terraform/</link><pubDate>Thu, 16 Dec 2021 00:00:00 +0000</pubDate><guid>https://mhemeryck.xyz/posts/2021-12-16-unifi_terraform/</guid><description>&lt;p&gt;Last week, I did write about &lt;a href="https://mhemeryck.xyz/posts/2021-12-08-cloudkey_reset/"&gt;resetting my unifi cloud key&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The main reason I did gain a renewed interest in the cloud key was because I wanted to add some more fixed IPs to my home network.
One of the nice things about the unifi controller software is that it provides a single dashboard interface to manage everything network-related.&lt;/p&gt;
&lt;p&gt;On the downside, the interface is overall quite limited.
While it does offer DHCP (the fixed IPs) in this fashion, it does not offer built-in DNS (e.g. for naming dedicated hosts on the home network).&lt;/p&gt;</description><content:encoded>&lt;p&gt;Last week, I did write about &lt;a href="https://mhemeryck.xyz/posts/2021-12-08-cloudkey_reset/"&gt;resetting my unifi cloud key&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The main reason I did gain a renewed interest in the cloud key was because I wanted to add some more fixed IPs to my home network.
One of the nice things about the unifi controller software is that it provides a single dashboard interface to manage everything network-related.&lt;/p&gt;
&lt;p&gt;On the downside, the interface is overall quite limited.
While it does offer DHCP (the fixed IPs) in this fashion, it does not offer built-in DNS (e.g. for naming dedicated hosts on the home network).&lt;/p&gt;
&lt;p&gt;Additionally, I also don&amp;rsquo;t really like these point-and-click interfaces which are hard to put under version control except for a full snapshot.
Coming from a software development background and with the current infrastructure-as-code movement, I wanted to look into an alternative.&lt;/p&gt;
&lt;h2 id="infrastructure-as-code-and-hashicorp-terraform"&gt;infrastructure-as-code and HashiCorp terraform&lt;/h2&gt;
&lt;p&gt;Nowadays, if you want to run some kind of software service, all the major cloud providers do provide web APIs to manage the (virtual) infrastructure for you.
Managing infrastructure at this point becomes another &lt;strong&gt;software problem&lt;/strong&gt; for interacting with these APIs, thus the term infrastructure-as-code.&lt;/p&gt;
&lt;p&gt;One platform that has become increasingly popular in that regard has been HashiCorp&amp;rsquo;s &lt;strong&gt;&lt;a href="https://www.terraform.io/"&gt;terraform&lt;/a&gt;&lt;/strong&gt;.
It provides an abstraction layer for all of these different cloud provider and offers an ecosystem for easily adding more modules.&lt;/p&gt;
&lt;p&gt;Additionally, the format is &lt;strong&gt;declarative&lt;/strong&gt;, meaning that the tool will manage the life cycle of the components you describe.
As an example, adding a resource means just declaring it in the configuration file and running it through the &lt;code&gt;terraform&lt;/code&gt; CLI tool.
At that point, the resource becomes managed by terraform.
Destroying the resource is done by removing the entry from the configuration file and once again applying.
Essentially, the configuration file will always reflect the state of the resources being managed and vice versa.&lt;/p&gt;
&lt;h2 id="unifi-terraform"&gt;unifi-terraform&lt;/h2&gt;
&lt;p&gt;What does any of that have to do with my tiny homelab setup?
Enter &lt;strong&gt;&lt;a href="https://registry.terraform.io/providers/paultyng/unifi/latest/docs"&gt;unifi-terraform&lt;/a&gt;&lt;/strong&gt;, which is a community provided terraform module that interacts with the API of the unifi cloud key.
This way, I could manage all the required fixed IPs.&lt;/p&gt;
&lt;p&gt;As I mentioned in my previous post, I did have some trouble getting started though, so let&amp;rsquo;s walk a bit through the steps I had taken.&lt;/p&gt;
&lt;h3 id="terraform-configuration-file"&gt;terraform configuration file&lt;/h3&gt;
&lt;p&gt;The first step is creating a &lt;code&gt;main.tf&lt;/code&gt; file containing the actual configuration.
I&amp;rsquo;m not going to elaborate too much on this here, see the &lt;a href="https://www.terraform.io/docs"&gt;terraform docs&lt;/a&gt; for that.&lt;/p&gt;
&lt;p&gt;Generally, my configuration file looked like this (redacted).&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;terraform {
required_providers {
unifi = {
source = &amp;#34;paultyng/unifi&amp;#34;
version = &amp;#34;0.34.0&amp;#34;
}
}
}
provider &amp;#34;unifi&amp;#34; {
username = var.username # These are in a variables file
password = var.password
api_url = var.api_url
}
resource &amp;#34;unifi_site&amp;#34; &amp;#34;sitename&amp;#34; {
description = &amp;#34;Default&amp;#34;
}
resource &amp;#34;unifi_user_group&amp;#34; &amp;#34;default&amp;#34; {
name = &amp;#34;Default&amp;#34;
}
# the network / DHCP config
resource &amp;#34;unifi_network&amp;#34; &amp;#34;lan&amp;#34; {
name = &amp;#34;LAN&amp;#34;
purpose = &amp;#34;corporate&amp;#34;
dhcp_dns = [
&amp;#34;192.168.1.21&amp;#34;,
&amp;#34;1.1.1.1&amp;#34;,
&amp;#34;8.8.8.8&amp;#34;,
&amp;#34;9.9.9.9&amp;#34;,
]
dhcp_enabled = true
dhcp_start = &amp;#34;192.168.1.20&amp;#34;
dhcp_stop = &amp;#34;192.168.1.254&amp;#34;
domain_name = &amp;#34;lan&amp;#34;
igmp_snooping = true
subnet = &amp;#34;192.168.1.0/24&amp;#34;
}
# WLAN
resource &amp;#34;unifi_wlan&amp;#34; &amp;#34;wlan&amp;#34; {
name = &amp;#34;wlan&amp;#34;
security = &amp;#34;wpapsk&amp;#34;
passphrase = var.passphrase_wifi_wlan
network_id = unifi_network.lan.id
user_group_id = unifi_user_group.default.id
wlan_band = &amp;#34;5g&amp;#34;
multicast_enhance = true # chromecast
# default group ID
ap_group_ids = [
var.ap_group_id
]
# enable WPA2/WPA3 support
wpa3_support = true
wpa3_transition = true
pmf_mode = &amp;#34;optional&amp;#34;
}
...
# Devices
resource &amp;#34;unifi_device&amp;#34; &amp;#34;my-router&amp;#34; {
name = &amp;#34;my-router&amp;#34;
}
...
# Users: network clients
resource &amp;#34;unifi_user&amp;#34; &amp;#34;my_client&amp;#34; {
name = &amp;#34;my-client&amp;#34;
mac = &amp;#34;d9:6e:3d:12:8f:53&amp;#34;
fixed_ip = &amp;#34;192.168.1.10&amp;#34;
network_id = unifi_network.lan.id
}
...
resource &amp;#34;unifi_user&amp;#34; &amp;#34;floating_client&amp;#34; {
name = &amp;#34;floating&amp;#34;
mac = &amp;#34;bc:e4:26:db:df:e4&amp;#34;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;At the top of this file the &lt;code&gt;required_provider&lt;/code&gt; is mentioned, i.e. the module that contains the client code to talk to the unifi cloud key API.
The &lt;code&gt;provider&lt;/code&gt; block contains the data used for the provider to do the actual connection (essentially username / password and a URL to connect to).&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a couple of interesting resources in there:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;unifi_site&lt;/code&gt;: just the physical location &amp;ndash; I guess this makes more sense if you have multiple locations to manage&lt;/li&gt;
&lt;li&gt;&lt;code&gt;unifi_user_group&lt;/code&gt;: this is relevant if you want to separate out specific access for e.g. WiFi clients.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;unifi_network&lt;/code&gt;: main (wired) network parameters, including e.g. the DHCP settings&lt;/li&gt;
&lt;li&gt;&lt;code&gt;unifi_wlan&lt;/code&gt;: wireless network configuration. Helpful if you want push in passwords&lt;/li&gt;
&lt;li&gt;&lt;code&gt;unifi_device&lt;/code&gt;: the unifi hardware such as switches, routers and wireless access points. I actually don&amp;rsquo;t have anything in particular to manage here, but I guess if you want to have specific port profiles for a switch, this would be the way to link to it&lt;/li&gt;
&lt;li&gt;&lt;code&gt;unifi_user&lt;/code&gt;: the clients on the network. It&amp;rsquo;s at this point that I can actually fix the IPs.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Have a look at &lt;a href="https://registry.terraform.io/providers/paultyng/unifi/latest/docs"&gt;unifi-terraform&lt;/a&gt; for more resources that can be managed.&lt;/p&gt;
&lt;h3 id="terraform-import"&gt;terraform import&lt;/h3&gt;
&lt;p&gt;At this point, you could in theory start creating all of the resources by applying it (with &lt;code&gt;terraform apply&lt;/code&gt;).
In reality, these resources are likely already recognized and managed by the cloud key itself.
The way around that is by &lt;strong&gt;importing&lt;/strong&gt; the resources via &lt;code&gt;terraform import {resource_name} {resource_id}&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;{resource_name}&lt;/code&gt; is just the name as you did provide it in the main configuration file, e.g. &lt;code&gt;unifi_user.my_client&lt;/code&gt;
The &lt;code&gt;{resource_id}&lt;/code&gt; is an ID that&amp;rsquo;s used in the API of the cloud key.
This was for me the point for which it all broke down since I couldn&amp;rsquo;t find any good documentation of this API.&lt;/p&gt;
&lt;h3 id="cloud-key-api"&gt;cloud key API&lt;/h3&gt;
&lt;p&gt;Fortunately, I did find some pointer into the cloud key API in a &lt;a href="https://thenewstack.io/how-to-manage-a-home-network-with-infrastructure-as-code/"&gt;blog post from the original module author&lt;/a&gt;.
Another real help to me has been this &lt;a href="https://github.com/Art-of-WiFi/UniFi-API-client/blob/fbfd6a824628d2d45f7b5dadb211cb1191335156/src/Client.php"&gt;unifi PHP client code&lt;/a&gt; which also lists most of the endpoints.&lt;/p&gt;
&lt;p&gt;I did distill all my findings into this &lt;a href="https://mhemeryck.xyz/2021-12-16/cloudkey.postman_collection.json"&gt;cloud key postman collection&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s walk through some of the API calls, built using &lt;a href="https://httpie.io/"&gt;&lt;code&gt;httpie&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="login-post-hostapilogin"&gt;login: &lt;code&gt;POST {host}/api/login&lt;/code&gt;&lt;/h4&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;printf &lt;span style="color:#e6db74"&gt;&amp;#39;{
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; &amp;#34;username&amp;#34;: &amp;#34;foo&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; &amp;#34;password&amp;#34;: &amp;#34;bar&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;}&amp;#39;&lt;/span&gt;| http --follow --timeout &lt;span style="color:#ae81ff"&gt;3600&lt;/span&gt; POST &lt;span style="color:#e6db74"&gt;&amp;#39;https://cloudkey.lan:8443/api/login&amp;#39;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Content-Type:&lt;span style="color:#e6db74"&gt;&amp;#39;application/json&amp;#39;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Cookie:&lt;span style="color:#e6db74"&gt;&amp;#39;csrf_token=abc; unifises=dfcwZvHcpBwc5LTlOzgxYSlsutpC6F2b&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;HTTP/1.1 &lt;span style="color:#ae81ff"&gt;200&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Access-Control-Allow-Credentials: true
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Access-Control-Expose-Headers: Access-Control-Allow-Origin,Access-Control-Allow-Credentials
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Connection: keep-alive
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Content-Length: &lt;span style="color:#ae81ff"&gt;30&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Content-Type: application/json;charset&lt;span style="color:#f92672"&gt;=&lt;/span&gt;UTF-8
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Date: Thu, &lt;span style="color:#ae81ff"&gt;16&lt;/span&gt; Dec &lt;span style="color:#ae81ff"&gt;2021&lt;/span&gt; 20:55:24 GMT
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Keep-Alive: timeout&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;60&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Set-Cookie: unifises&lt;span style="color:#f92672"&gt;=&lt;/span&gt;19uFvBLta4optBuzwV4QyVip5TbdR0t6; Path&lt;span style="color:#f92672"&gt;=&lt;/span&gt;/; Secure; HttpOnly
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Set-Cookie: csrf_token&lt;span style="color:#f92672"&gt;=&lt;/span&gt;Z1dK47sp8NsjfpdbgHNyVNgYewNYIodd; Path&lt;span style="color:#f92672"&gt;=&lt;/span&gt;/; Secure
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;X-Frame-Options: DENY
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;vary: Origin
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;data&amp;#34;&lt;/span&gt;: &lt;span style="color:#f92672"&gt;[]&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;meta&amp;#34;&lt;/span&gt;: &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;rc&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;ok&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This call will perform the login on the login endpoint and set a cookie for subsequent calls.
Note that all other calls will need this cookie and that it will expire.&lt;/p&gt;
&lt;h4 id="self-get-hostapissiteself"&gt;self: &lt;code&gt;GET {host}/api/s/{site}/self&lt;/code&gt;&lt;/h4&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;http --follow --timeout &lt;span style="color:#ae81ff"&gt;3600&lt;/span&gt; GET &lt;span style="color:#e6db74"&gt;&amp;#39;https://cloudkey.lan:8443/api/s/default/self&amp;#39;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Cookie:&lt;span style="color:#e6db74"&gt;&amp;#39;csrf_token=cNHfNcA1zQoMW9wzFvCmGwEEo22irJ6D; unifises=dfcwZvHcpBwc5LTlOzgxYSlsutpC6F2b&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;HTTP/1.1 &lt;span style="color:#ae81ff"&gt;200&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Access-Control-Allow-Credentials: true
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Access-Control-Expose-Headers: Access-Control-Allow-Origin,Access-Control-Allow-Credentials
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Connection: keep-alive
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Content-Encoding: gzip
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Content-Type: application/json;charset&lt;span style="color:#f92672"&gt;=&lt;/span&gt;UTF-8
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Date: Thu, &lt;span style="color:#ae81ff"&gt;16&lt;/span&gt; Dec &lt;span style="color:#ae81ff"&gt;2021&lt;/span&gt; 20:58:28 GMT
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Keep-Alive: timeout&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;60&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Transfer-Encoding: chunked
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;X-Frame-Options: DENY
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;vary: accept-encoding,origin,accept-encoding
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;data&amp;#34;&lt;/span&gt;: &lt;span style="color:#f92672"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ...
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;site_id&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;{site_id}&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;site_name&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;default&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ...
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;]&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;meta&amp;#34;&lt;/span&gt;: &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;rc&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;ok&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This just lists some generic information about the site (location), e.g. the &lt;code&gt;site_id&lt;/code&gt; to be imported.&lt;/p&gt;
&lt;h4 id="devices-get-hostapissitestatdevice"&gt;devices: &lt;code&gt;GET {host}/api/s/{site}/stat/device&lt;/code&gt;&lt;/h4&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;http --follow --timeout &lt;span style="color:#ae81ff"&gt;3600&lt;/span&gt; GET &lt;span style="color:#e6db74"&gt;&amp;#39;https://cloudkey.lan:8443/api/s/default/stat/device&amp;#39;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Cookie:&lt;span style="color:#e6db74"&gt;&amp;#39;csrf_token=cNHfNcA1zQoMW9wzFvCmGwEEo22irJ6D; unifises=dfcwZvHcpBwc5LTlOzgxYSlsutpC6F2b&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;...
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;meta&amp;#34;&lt;/span&gt;: &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;rc&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;ok&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;data&amp;#34;&lt;/span&gt;: &lt;span style="color:#f92672"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;_id&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;5ea4395234309a00041ffa6e&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;adopted&amp;#34;&lt;/span&gt;: true,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;anon_id&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;d5416fde-c2ad-4558-9aa0-4b4024c441de&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;antenna_table&amp;#34;&lt;/span&gt;: &lt;span style="color:#f92672"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;default&amp;#34;&lt;/span&gt;: true,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;id&amp;#34;&lt;/span&gt;: 4,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;Combined&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;wifi0_gain&amp;#34;&lt;/span&gt;: 3,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;wifi1_gain&amp;#34;&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;]&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ...
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This endpoint is interesting to get the IDs of the devices to import.&lt;/p&gt;
&lt;h4 id="users-get-hostapissiterestuser"&gt;users: &lt;code&gt;GET {host}/api/s/{site}/rest/user&lt;/code&gt;&lt;/h4&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;http --follow --timeout &lt;span style="color:#ae81ff"&gt;3600&lt;/span&gt; GET &lt;span style="color:#e6db74"&gt;&amp;#39;https://cloudkey.lan:8443/api/s/default/rest/user&amp;#39;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Cookie:&lt;span style="color:#e6db74"&gt;&amp;#39;csrf_token=cNHfNcA1zQoMW9wzFvCmGwEEo22irJ6D; unifises=dfcwZvHcpBwc5LTlOzgxYSlsutpC6F2b&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;...
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;meta&amp;#34;&lt;/span&gt;: &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;rc&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;ok&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;data&amp;#34;&lt;/span&gt;: &lt;span style="color:#f92672"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;_id&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;9dfeee213c227000f47aade2&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;mac&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;48:98:e3:f6:21:a5&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;site_id&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;5dffdda64c2370010eb91fa0&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ...
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;}&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This was the most important endpoint for my purposes: it lists the resource ID under &lt;code&gt;_id&lt;/code&gt; and makes it possible to import it.&lt;/p&gt;
&lt;p&gt;Practically, this means&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;terraform import unifi_user.my_client 9dfeee213c227000f47aade2
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;At this point, the client should be managed by terraform.&lt;/p&gt;
&lt;p&gt;Refer to the &lt;a href="https://mhemeryck.xyz/2021-12-16/cloudkey.postman_collection.json"&gt;cloud key postman collection&lt;/a&gt; for more resources.
Note that I only really looked into the read-only endpoints (the &lt;code&gt;GET&lt;/code&gt; operations) since I would have terraform do the actual operations (&lt;code&gt;POST&lt;/code&gt;, &lt;code&gt;PATCH&lt;/code&gt;).&lt;/p&gt;
&lt;h2 id="closing-thoughts"&gt;Closing thoughts&lt;/h2&gt;
&lt;p&gt;I was really pleased to see that after some crawling of the cloud key API I was able to get the unifi resources managed with terraform.
At this point, I can &lt;strong&gt;manage my unifi configuration under source control&lt;/strong&gt;, most notably the DHCP fixed IPs and some firewall rules.
This only scratches the surface of what is now possible at this point though.&lt;/p&gt;
&lt;p&gt;Since terraform also has modules for various &lt;strong&gt;DNS providers&lt;/strong&gt; such as &lt;a href="https://registry.terraform.io/providers/pan-net/powerdns/latest/docs"&gt;powerdns&lt;/a&gt;, &lt;a href="https://github.com/shelmangroup/terraform-provider-coredns"&gt;coredns&lt;/a&gt; and even &lt;a href="https://registry.terraform.io/providers/ryanwholey/pihole/latest/docs"&gt;pihole&lt;/a&gt; I could start thinking of managing the fixed IPs and the host names from a single spot.
The same local network DNS could then be used by my local kubernetes to register service names into.&lt;/p&gt;
&lt;p&gt;Since it&amp;rsquo;s supposedly infrastructure-as-code, I could also start thinking of integrating everything in &lt;strong&gt;CI/CD&lt;/strong&gt; flow, where the terraform code is first validated and then automatically applied.
This in turn would mean I&amp;rsquo;d need to find a way to either self-host a source control / CI/CD platform (thinking of &lt;a href="https://about.gitlab.com/"&gt;gitlab&lt;/a&gt;) or find some way to safely tunnel out to a cloud-based platform with (thinking github actions + &lt;a href="https://www.wireguard.com/"&gt;wireguard&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Another change I&amp;rsquo;d like to make, is to use a &lt;strong&gt;&lt;a href="https://www.terraform.io/language/state/remote-state-data"&gt;remote state back-end&lt;/a&gt;&lt;/strong&gt; instead of the local file storage.&lt;/p&gt;
&lt;p&gt;&amp;hellip; but that&amp;rsquo;s for another time!&lt;/p&gt;</content:encoded></item></channel></rss>