<?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>Evok2mqtt on mhemeryck</title><link>https://mhemeryck.xyz/tags/evok2mqtt/</link><description>Recent content in Evok2mqtt on mhemeryck</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Mon, 09 Aug 2021 00:00:00 +0000</lastBuildDate><atom:link href="https://mhemeryck.xyz/tags/evok2mqtt/index.xml" rel="self" type="application/rss+xml"/><item><title>evok2mqtt ansible</title><link>https://mhemeryck.xyz/posts/2021-08-09-evok2mqtt_ansible/</link><pubDate>Mon, 09 Aug 2021 00:00:00 +0000</pubDate><guid>https://mhemeryck.xyz/posts/2021-08-09-evok2mqtt_ansible/</guid><description>&lt;p&gt;As part of my &lt;a href="https://mhemeryck.xyz/posts/2021-06-15-home_automation_why/"&gt;home automation series&lt;/a&gt;, I did write about a small interfacing library that I did write, called &lt;a href="https://github.com/mhemeryck/evok2mqtt"&gt;evok2mqtt&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The purpose of this service is to translate between two different protocols: websockets, as exposed by &lt;a href="https://github.com/UniPiTechnology/evok"&gt;evok&lt;/a&gt; and &lt;a href="https://mqtt.org/"&gt;mqtt&lt;/a&gt;, which is the primary protocol I did use in communicating with &lt;a href="https://www.home-assistant.io/integrations/mqtt/"&gt;home assistant&lt;/a&gt;.
If you&amp;rsquo;d like to get more details about the setup itself, check the blog post series first.&lt;/p&gt;
&lt;p&gt;The focus of this post is the &lt;strong&gt;automated provisioning of a unipi unit using &lt;a href="https://www.ansible.com/"&gt;ansible&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;</description><content:encoded>&lt;p&gt;As part of my &lt;a href="https://mhemeryck.xyz/posts/2021-06-15-home_automation_why/"&gt;home automation series&lt;/a&gt;, I did write about a small interfacing library that I did write, called &lt;a href="https://github.com/mhemeryck/evok2mqtt"&gt;evok2mqtt&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The purpose of this service is to translate between two different protocols: websockets, as exposed by &lt;a href="https://github.com/UniPiTechnology/evok"&gt;evok&lt;/a&gt; and &lt;a href="https://mqtt.org/"&gt;mqtt&lt;/a&gt;, which is the primary protocol I did use in communicating with &lt;a href="https://www.home-assistant.io/integrations/mqtt/"&gt;home assistant&lt;/a&gt;.
If you&amp;rsquo;d like to get more details about the setup itself, check the blog post series first.&lt;/p&gt;
&lt;p&gt;The focus of this post is the &lt;strong&gt;automated provisioning of a unipi unit using &lt;a href="https://www.ansible.com/"&gt;ansible&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id="rationale"&gt;Rationale&lt;/h2&gt;
&lt;p&gt;Earlier, I did discuss all of the software running on each of my &lt;a href="https://www.unipi.technology/"&gt;unipi&lt;/a&gt; units, where each part is responsible for a layer in the overall process:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the linux-based OS: for capturing and sending I/O events&lt;/li&gt;
&lt;li&gt;&lt;code&gt;evok&lt;/code&gt;: for exposing this I/O information in various formats and protocols (of which I did use websockets)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;evok2mqtt&lt;/code&gt;: for translating between websockets and MQTT&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Arguably, it would be better to have fewer of these layers, but the current situation is one of &lt;strong&gt;many different moving parts&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Next to that, I do have &lt;strong&gt;multiple of these unipi nodes&lt;/strong&gt;, each interfacing different types of I/O, yet fundamentally they share a common setup.&lt;/p&gt;
&lt;p&gt;Occasionally, there is a need to &lt;strong&gt;update or reprovision&lt;/strong&gt; the unipi nodes, e.g. in the case of regular software or in the unlucky event that and SD-card crashed.&lt;/p&gt;
&lt;p&gt;Considering all of these factors, having a way to automatically provision the unipi units in a predefined state is very helpful to have.&lt;/p&gt;
&lt;h2 id="automated-provisioning"&gt;Automated provisioning&lt;/h2&gt;
&lt;p&gt;Automated provisioning refers to tooling that can handle the tedious tasks related to setting up a client or server automatically.
Many different kinds of tooling can be used, e.g.:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;scripting like &lt;a href="https://www.gnu.org/software/bash/"&gt;bash&lt;/a&gt;:&lt;/li&gt;
&lt;li&gt;declarative-style recipes like &lt;a href="https://www.ansible.com/"&gt;ansible&lt;/a&gt; and &lt;a href="https://puppet.com/"&gt;puppet&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;immutable builds like &lt;a href="https://www.packer.io/"&gt;packer&lt;/a&gt; or &lt;a href="https://www.docker.com/"&gt;docker&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;infrastructure-as-code like &lt;a href="https://www.terraform.io/"&gt;terraform&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Scripting&lt;/strong&gt; is one of the oldest and most widespread approaches to automation.
In scripting, you define a list of commands that each run in sequence after each other, generally from the top of the script to the bottom.
Scripting is &lt;em&gt;procedural&lt;/em&gt;, meaning you define &lt;em&gt;how&lt;/em&gt; a specific action is to be carried out, like a recipe.
The advantage of scripting is that it is simple and ubiquitous.
On the other side of the equation are all issues related to state manipulation.
As a script runs through its sequence of steps, it changes the state to end up in a desired target state.
In theory, you can write your scripts in such a way that they can adapt for any kind of initial or in-between state.
Also, you could attempt to write some parts of your scripts in an &lt;em&gt;idempotent&lt;/em&gt; way, meaning that they perform the same action regardless of their input.
At larger scale however, scripting quickly becomes unwieldy of this state manipulation it needs to take into account and idempotency is simply not always possible.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Declarative&lt;/strong&gt; tools take a different approach, not by defining &lt;em&gt;how&lt;/em&gt; the provisioning should take place, but rather &lt;em&gt;what&lt;/em&gt; the end-result needs to be.
Particularly in the case of provisioning, this makes for an interesting programming paradigm since ideally after running the provisioning tool, it will always result in the same end state, regardless of any initial state the node was in.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Immutable build&lt;/strong&gt; tools also operate in this fashion, the difference being that the provisioning process itself consists of picking a pre-built image (virtual machine image or container image) and deploying that entirely.
Doing updates in this fashion just means killing the entire image and replacing it with another one.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Infrastructure as code&lt;/strong&gt; augments this approach by extending it to any kind of resource.
Cloud providers make all of their virtualised infrastructure components available via APIs.
Infrastructure as code provides a common code-based framework to declaratively define your desired cloud architecture.&lt;/p&gt;
&lt;p&gt;All of these have their place, but for now, I did focus on using &lt;a href="https://www.ansible.com/"&gt;ansible&lt;/a&gt; as a declarative provisioning tool:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;scripting is too much of a pain, considering the state manipulation issues mentioned before&lt;/li&gt;
&lt;li&gt;immutable build tools seem really interesting, but might not fit my use case where I would manually need to swap out built images&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
&lt;li&gt;infrastructure as code: this does not apply here, since I want to run the tooling on the unipi unit itself&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
&lt;li&gt;I recently did learn about ansible and also a good use case to apply it&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Some basic &lt;em&gt;ansible-lingo&lt;/em&gt; before we dive into the details:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ansible&lt;/code&gt; is the tool you run on your local machine to perform actions on a remote system&lt;/li&gt;
&lt;li&gt;an action on a remote machine is a &lt;em&gt;task&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;a set of tasks can be combined into a &lt;em&gt;role&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;a set of roles can be further combined into a &lt;em&gt;playbook&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&amp;hellip; further higher levels of grouping also still exist (collections?), but I didn&amp;rsquo;t give that much extra thought&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ansible-playbook&lt;/code&gt; is the tool to orchestrate the execution of a playbook&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For my purposes, I did write a &lt;em&gt;playbook&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id="initial-setup"&gt;Initial setup&lt;/h2&gt;
&lt;p&gt;Ansible has some basic initial conditions for it to be run on the target host:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the target host needs to be reachable via SSH (ideally key-based)&lt;/li&gt;
&lt;li&gt;the target host should run some version of python (as ansible itself is based on python)&lt;/li&gt;
&lt;li&gt;depending on the permissions you need to execute, the connecting user needs to have root access&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Fortunately, unipi does provides such an image that already satisfies these constraints (most modern linux-based OS images would do, actually): the &lt;a href="https://kb.unipi.technology/en:files:software:os-images:00-start#neuron_opensource_os"&gt;Neuron OpenSource OS&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Practical next steps include:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;flash the SD card for the unipi with this image&lt;/li&gt;
&lt;li&gt;ensure an internet connection, preferably wired, although a &lt;a href="https://www.raspberrypi.com/documentation/computers/configuration.html#connect-to-a-wireless-network"&gt;headless raspberry pi wifi install&lt;/a&gt; would also work&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;From here on, you can start running the playbook!&lt;/p&gt;
&lt;h2 id="running-evok2mqtt-ansible"&gt;Running &lt;code&gt;evok2mqtt-ansible&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;The playbook can be found on github &lt;a href="https://github.com/mhemeryck/evok2mqtt-ansible"&gt;evok2mqtt-ansible&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;From the README file:&lt;/p&gt;
&lt;p&gt;The first step is to clone the repository:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone https://github.com/mhemeryck/evok2mqtt-ansible.git
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, we need some extra requirements which can be installed using &lt;code&gt;ansible-galaxy&lt;/code&gt; (a tool to pull in other people&amp;rsquo;s playbooks).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ansible-galaxy install -r requirements.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Adapt the &lt;code&gt;hosts.yaml&lt;/code&gt; file for your own purposes.&lt;/p&gt;
&lt;p&gt;Run the playbook with&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ansible-playbook site.yaml -i hosts.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="a-closer-look"&gt;A closer look&lt;/h2&gt;
&lt;p&gt;Taking a step back; what does this playbook actually do?
Let&amp;rsquo;s check the folder structure:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.
├── hosts.yaml
├── LICENSE.txt
├── README.md
├── requirements.yaml
├── roles
│   ├── account
│   │   └── tasks
│   │   └── main.yaml
│   ├── evok2mqtt
│   │   ├── handlers
│   │   │   └── main.yaml
│   │   ├── tasks
│   │   │   └── main.yaml
│   │   └── templates
│   │   └── evok2mqtt.service.j2
│   ├── hostname
│   │   ├── tasks
│   │   │   └── main.yaml
│   │   └── vars
│   │   └── hostnames.yaml
│   ├── security
│   │   ├── handlers
│   │   │   └── main.yaml
│   │   └── tasks
│   │   └── main.yaml
│   └── wifi
│   ├── handlers
│   │   └── main.yaml
│   ├── tasks
│   │   └── main.yaml
│   └── templates
│   └── wpa_supplicant.conf.j2
└── site.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The playbook consists of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a &lt;code&gt;site.yaml&lt;/code&gt; file which is the main entry point which invokes the different roles&lt;/li&gt;
&lt;li&gt;a &lt;code&gt;hosts.yaml&lt;/code&gt; file which lists the groups of target hosts to run the commands against (you would definitely need to adapt this to your needs)&lt;/li&gt;
&lt;li&gt;a &lt;code&gt;requirements.yaml&lt;/code&gt; file which defines external roles and plugins to be used&lt;/li&gt;
&lt;li&gt;5 different folders, each related to a specific role; a set of tasks that are available for execution&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Discussing each of the roles themselves might be a bit too much (you can just was well read the source for that), but I want to discuss the main &lt;code&gt;site.yaml&lt;/code&gt; file which brings all of the roles together:&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-yaml" data-lang="yaml"&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;hosts&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;testpi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;vars&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;ansible_python_interpreter&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;/usr/bin/python3&lt;/span&gt; &lt;span style="color:#75715e"&gt;# Force py3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;ansible_ssh_password&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;unipi.technology&lt;/span&gt; &lt;span style="color:#75715e"&gt;# not really a secret&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;ansible_ssh_user&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;unipi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;mqtt_host&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;shuttle.lan&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;username&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;mhemeryck&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;wifi_ssd&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;luctor_IoT&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;wifi_passphrase&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;{{ lookup(&amp;#39;community.general.passwordstore&amp;#39;, &amp;#39;home/luctor&amp;#39;) }}&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;roles&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;account&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;hostname&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;wifi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;evok2mqtt&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;- &lt;span style="color:#f92672"&gt;hosts&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;testpi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;vars&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;ansible_python_interpreter&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;/usr/bin/python3&lt;/span&gt; &lt;span style="color:#75715e"&gt;# Force py3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;username&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;mhemeryck&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;roles&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ae81ff"&gt;security&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The main file defines &lt;strong&gt;two sets of roles&lt;/strong&gt;, referring to the different roles that are made available.
The first set is the biggest one, the second one can only run after the first has completed.
The reason for this split is that when the initial image is made available, it does include a default &lt;code&gt;unipi&lt;/code&gt; user with a fixed password.
The first set of roles will run as this user (as this is the only user available at that time).
Afterwards, we can run a second security-related role as a newly available hardened user and then remove that &lt;code&gt;unipi&lt;/code&gt; user &amp;ndash; you obviously can&amp;rsquo;t remove the &lt;code&gt;unipi&lt;/code&gt; user while you are executing tasks with it.
You can see that the first set of roles does include the &lt;code&gt;ansible_ssh_user&lt;/code&gt; and &lt;code&gt;ansible_ssh_password&lt;/code&gt;: these refer to those standard credentials.&lt;/p&gt;
&lt;p&gt;In terms of &lt;strong&gt;credentials&lt;/strong&gt;, I did include those ssh user / password as variables in plain text, as this publicly available knowledge.
For the &lt;code&gt;wifi_passphrase&lt;/code&gt; however, I did use my password manager &lt;a href="https://www.passwordstore.org/"&gt;&lt;code&gt;pass&lt;/code&gt;&lt;/a&gt; and the related ansible &lt;a href="https://docs.ansible.com/ansible/latest/collections/community/general/passwordstore_lookup.html"&gt;&lt;code&gt;pass&lt;/code&gt; plugin&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;A quick look at the &lt;strong&gt;different roles&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;account&lt;/code&gt;: this provisions my own user with proper root access and copies in my public SSH keys from github&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hostname&lt;/code&gt;: this role changes the host name to a sensible one. The mapping for the host name is determined from a variables file&lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wifi&lt;/code&gt;: this sets up &lt;code&gt;wpa_supplicant&lt;/code&gt; with proper WIFI credentials for my home network.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;evok2mqtt&lt;/code&gt;: the main job; it will do a full system upgrade, install &lt;code&gt;evok&lt;/code&gt;, install &lt;code&gt;evok2mqt&lt;/code&gt;, create a system unit file and make sure it runs as a daemon&lt;/li&gt;
&lt;li&gt;&lt;code&gt;security&lt;/code&gt;: some cleanup tasks, like disabling password-based SSH logins (key-based only) and removing the default &lt;code&gt;unipi&lt;/code&gt; user.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After running these tasks, the unipi unit should in theory be in my ideal end state!&lt;/p&gt;
&lt;h2 id="closing-thoughts"&gt;Closing thoughts&lt;/h2&gt;
&lt;p&gt;Given the central place my unipi units play in my home automations setup, &lt;strong&gt;having any failures on them is, however rare, not fun&lt;/strong&gt;.
With this in mind and also a general interest in learning infrastructure automation, I did set out to automate those tasks with ansible.&lt;/p&gt;
&lt;p&gt;The current &lt;strong&gt;downside&lt;/strong&gt; of this approach is that the full system upgrade will mean that the unipi is occupied for a large portion of time.
Consequently, I haven&amp;rsquo;t yet set out to use it on my live environment.
However, any breaking failures on my unipi units could now be quickly mitigated by flashing an SD-card and running this playbook against, which is a huge improvement over the previous situation.&lt;/p&gt;
&lt;p&gt;Alternative &lt;strong&gt;future solutions&lt;/strong&gt; could consist of changing the software required to run on the unipi units (would simplify the install process) and / or fully immutable (packer) builds, which completely prepare the image upfront.&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;in the cloud server world, this is of course an entirely different situation.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;a platform that could mimic infrastructure-as-code for local development would be really cool, though!&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;I did not include my own file, since this could contain sensitive information &amp;hellip;&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content:encoded></item></channel></rss>