Home-Assistant Distributed: Deployment with Ansible
Why Ansible
Ansible is open-source state management and application deployment tool; we use Ansible for all of our server configuration management in our home lab. It uses human-readable YAML configuration files that define tasks, variables and files to be deployed to a machine, making it fairly easy to pick up and work with and, when written correctly can be idempotent (running the same playbook again should have no side-effects) – perfect for the deployment of our Home-Assistant Distributed Cluster.
Ansible uses inventory files that define groups that hosts are members of. For this project, I created a group under our home lab parent group named “automation-cluster” with child groups for each slave instance, e.g. tracker, hubs, settings, etc. Using this pattern facilitates scalability: if deploying the cluster to a single host, or deploying to multiple hosts makes no difference to the playbook.
Role: automation-cluster
Ansible operates using roles. A role defines a set of tasks to run, variables, files/file templates in order to configure the target host.
Tasks
The automation-cluster role is fairly simple:
- Clone the git repository for the given slave instances being deployed
- Generate a secrets.yaml file specific to each instance, providing only the necessary secrets for the instance
- Modify the file mods for the configuration directory for the service user account
- Run instance-specific tasks, i.e. create the plex.conf file for the media instance
- Generate the Docker container configurations for the instances being deployed
- Pull the Home-Assistant Docker image and run a Docker container for each slave instance
Variables (vars)
To make the automation-cluster role scalable to add new instances in the future, a list variable named instance_arguments was created. Each element of the list represents one instance with the following properties:
- Name – The name of the instance which matches the automation-cluster child group name mentioned above
- Description – The description to be applied to the Docker container
- Port – The port to be exposed by the Docker container
- Host – The host name or IP used for connecting the cluster instances together
- Secrets – The list of Ansible variables to be inserted into the secrets.yaml file
- Configuration Repo – The URL to the git repository for the instance configuration
- Devices – Any devices to be mounted in the Docker container
The other variables defined are used for populating template files and the secrets.yaml files.
Defaults
In Ansible, the defaults are variables that are meant to be overridden, either by group variables, host variables, or variables entered in the command line.
Templates
Templates are files that are rendered via the Jinja2 templating engine. These are perfect for files that differ for different groups or hosts using Ansible variables. For the secrets.yaml, my template looks like this:
#Core Config Variables {% for secret in core_secrets %} {{ secret }}: {{ hostvars[inventory_hostname][secret] }} {% endfor %} #Instance Config Variables {% for secret in item.secrets %} {{ secret }}: {{ hostvars[inventory_hostname][secret] }} {% endfor %}
There is an Ansible list variable named core_secrets that contains the names of variables used in all slave instances, i.e. home latitude, home longitude, time zone, etc. The secrets variable is defined in each automation-cluster child group and is a list variable that contains the names of variables that the instance requires. This allows you to keep the secrets.yaml file out of the configuration repository, in case an API key or password may be accidentally committed and become publically available.
In the template, you’ll see hostvars[inventory_name][secret]. Since the loop variable, secret, is simply the name of the Ansible variable, we need to retrieve the actual value of the variable. The hostvars dictionary, where the keys are the hostname currently being ran against, contains all of the variables and facts that are currently known. So hostvars[inventory_hostname] is a dictionary, with keys of the variable names, of all of the variables that are applicable to the current host. In the end, hostvars[inventory_name][secret] returns the value of that variable.
Handlers (WIP)
Handlers are tasks that are run after each block of tasks in a play, perfect for restarting services, or if a task should only be run if something changed. For the automation-cluster, if the instance configuration or secrets.yaml change from a previous run will trigger the Docker container to be restarted and thus loading the changes into the state machine
Role: Docker
Our Docker role simplifies the deployment of Docker containers on the target host by reducing boilerplate code and is a dependency of the automation-cluster role.
Gotchas Encountered
Home-Assistant deprecated the API password authentication back in version 0.77 and now uses a more secure authentication framework that relies on refresh tokens for each user. In order to connect to the API via services or, in our case, separate instances, you must create a long-lived bearer token. Because of this, the main (aggregation instance) must be deployed after all other instances are deployed and long-lived bearer tokens are created for each instance. It is possible to deploy all instances on the first play, but the tokens will need to be added to the variables and the play re-ran to regenerate the secrets.yaml file for the main instance.
If you are deploying multiple instances on the same host, you will have to deal with intercommunication between each instance. There are a few different solutions here:
- Set the host for each instance in the instance-arguments variable to the Docker bridge gateway address
- Add an entry to the /etc/hosts file in the necessary containers that aliases the Docker bridge gateway address to a resolvable hostname
- Set the homeassistant.http.port property in the instance configurations to discrete ports and run the Docker container with network mode host
- Create a Docker network and deploy the instances on that network with a static IP set for each instance
Next Steps
- Unlink all Z-Wave and Zigbee devices from my current setup and link them on the host housing the hubs instance
- Remove the late-night /etc/hosts file hack and deploy the containers to docker network or host network mode
- Implement handlers to restart or recreate the containers based on task results