Deploying K3s with Ansible on DigitalOcean, Part 2
Using DigitalOcean Servers as Ansible Dynamic Inventory
Ansible Host Groups, Host Variables, and Dynamic Inventory Plugins
Franco Posa
Published 2022-07-30 · Updated 2024-11-17
Goals
We will:
- Learn the basic concepts of Ansible’s “inventory”, organizing hosts into named groups
- Declare our DigitalOcean server as static Ansible inventory
- Convert the static declaration to dynamic inventory, synced from our DigitalOcean account
- Run our first Ansible playbook against the dynamic inventory
0. Prerequisites
0.1 Export the DigitalOcean API Token
Make the API token created in Part 1 available to our shell environment, with the variable name expected by the DigitalOcean Ansible inventory plugin:
% export DO_API_TOKEN=dop_v1_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
1. Ansible Inventory Basics
If you are familiar with Ansible inventory, jump to Using Dynamic Inventory Plugins
Hosts, Host Groups, and Host Variables
Hosts
Ansible works by running playbook tasks against an “inventory” of hosts.
To Ansible, a host is pretty much anything with an IP address that runs a standard operating system: a cloud or local VM, a bare metal server, a Docker container, etc.
Hosts are assigned to host groups and hosts have host variables.
Host Groups
Every host in an inventory belongs to at least two groups.
- Every host belongs to the
all
group, except the implicitlocalhost
used when no host is specified. - Every host also belongs to either:
- one or more groups assigned to it using static or dynamic inventory, or
- the
ungrouped
group for hosts not explicitly assigned any other group
Host Variables
Host variables are attributes assigned to a host. While host variables can be any arbitrarily nested key-value structure, there are specific variables which define how Ansible connects to and behaves when operating on hosts; how to set up the connection, which user to run as on the host, which shell to use, etc.
The full list of these “behavioral inventory parameters” is here. For now, we will only need two:
ansible_host
- the resolvable DNS name or IP address of a host to connect toansible_user
- the username to use when connecting to the host
2. Declaring Static Inventory
The simplest way to build Ansible inventory is by hardcoding the hosts, groups, and variables into static files. While we will ultimately move on to using dynamic inventory, we can first take a look at how we would represent our inventory in a static configuration.
Declaring Static Host Groups
When we created our server with the DigitalOcean Ansible module in Part 1, we assigned three tags with the intention of using them later as the Ansible host groups.
Our single host is in all three groups we created: k3s-demo-master
for the master node, k3s demo
for all nodes in the cluster, and demo
for all resources in the “demo” project of the DigitalOcean account.
Assigning Static Host Variables
ansible_host
- set this to the public IPv4 address assigned to the server on creation.ansible_user
- set this to theUSERNAME
from the Cloud Init script from Part 1.- use DigitalOcean’s default
root
user if you skipped the user setup via cloud-init step
- use DigitalOcean’s default
DigitalOcean Static Inventory
A bare-bones static inventory definition to assign our host to three host groups would look like below:
# github.com/francoposa/learn-infra-ops/blob/main/infrastructure/ansible/inventory/sources/digitalocean-static-example.yaml
---
demo: # host group
hosts:
# host alias or name, the same as the Droplet name in DigitalOcean
debian-s-1vcpu-2gb-sfo3-01:
# key-value pairs nested below the host are host variables
ansible_host: 143.198.76.8
ansible_user: infra_ops
k3s-demo: # another host group
hosts:
# repeat host name with empty map to inherit existing host variables
debian-s-1vcpu-2gb-sfo3-01: {}
k3s-demo-master: # another host group
hosts:
# repeat host name with empty map to inherit existing host variables
debian-s-1vcpu-2gb-sfo3-01: {}
We can see that even with just a single host, the static configuration can be tedious. Hosts must be re-declared in each group they belong to, and any changes to the host alias or host variables may have to be duplicated across several sections.
Further, as we are dynamically provisioning infrastructure with a cloud provider, we generally do not have a way to know the host IPs ahead of time.
3. Using Dynamic Inventory Plugins
Ansible supports dynamic inventory plugins, which interface with a cloud provider, local hypervisor stack, or other host-management solutions in order to map the current state of your hosts into Ansible’s inventory format.
The DigitalOcean inventory plugin comes bundled with a full Ansible installation, or can be installed with ansible-galaxy
.
We can use a simplified version of the plugin config example in the DigitalOcean inventory plugin docs:
# github.com/francoposa/learn-infra-ops/blob/main/infrastructure/ansible/inventory/sources/digitalocean.yaml
---
plugin: community.digitalocean.digitalocean
api_token: "{{ lookup('ansible.builtin.env', 'DO_API_TOKEN') }}"
attributes:
# which fields provided by the inventory plugin do we want to use
- name
- tags
- networks
keyed_groups:
# which attributes do we want to use to map hosts into groups
# the default var_prefix for this plugin is `do_`
# so the `tags` attribute becomes `do_tags`
- key: do_tags
leading_separator: no # no leading underscore in front of host group name
compose:
# compose uses Jinja expressions to process attributes into Ansible variables
# we need to parse the `networks` attribute, which is prefixed to be `do_networks`
# into a resolvable `ansible_host` variable.
ansible_host: do_networks.v4 | selectattr('type','eq','public')
| map(attribute='ip_address') | first
We can check the dynamic inventory output with a graph view of just the hosts:
% ansible-inventory -i ./infrastructure/ansible/inventory/sources/digitalocean.yaml --graph # add --vars to see all host variables
@all:
|--@demo:
| |--debian-s-1vcpu-2gb-sfo3-01
|--@k3s-demo:
| |--debian-s-1vcpu-2gb-sfo3-01
|--@k3s-demo-master:
| |--debian-s-1vcpu-2gb-sfo3-01
|--@ungrouped:
or a full view in the same format as a static inventory file:
% ansible-inventory -i ./infrastructure/ansible/inventory/sources/digitalocean.yaml --list --yaml
all:
children:
demo:
hosts:
debian-s-1vcpu-2gb-sfo3-01:
ansible_host: 143.198.76.8
# ...
k3s-demo:
hosts:
debian-s-1vcpu-2gb-sfo3-01: {}
k3s-demo-master:
hosts:
debian-s-1vcpu-2gb-sfo3-01: {}
ungrouped: {}
We can ignore the [WARNING]: Invalid characters were found in group names
.
Ansible prefers the host groups to be valid Python identifiers (no hyphens), but it will not affect anything.
The Ansible team received significant pushback on this change, and have stated that this warning will never turn into an error.
4. Assigning Host Group Variables to Dynamic Inventory
Ansible provides plenty of guidance on how to manage multiple inventory sources. For further details on more complex configurations follow the links in that section titled Variable precedence: Where should I put a variable? and How variables are merged.
For now, we just want to do something simple: apply the USERNAME
we put in the Cloud Init script from Part 1 as the ansible_user
across all hosts.
This will prevent us from having to re-declare the ansible_user
or any other common host variables in every playbook.
We can add the following to [inventory directory]/group_vars/all.yaml
, or demo.yaml
, or whichever host group we want to target.
# github.com/francoposa/learn-infra-ops/blob/main/infrastructure/ansible/inventory/sources/group_vars/demo.yaml
---
ansible_user: infra_ops
Now if we run our Ansible inventory list commands again, we can see the ansible_user
variable applied to the host.
% ansible-inventory -i ./infrastructure/ansible/inventory/sources/digitalocean.yaml --list --yaml
all:
children:
demo:
hosts:
debian-s-1vcpu-2gb-sfo3-01:
ansible_host: 147.182.252.75
ansible_user: infra_ops
# ...
5. Running an Ansible Playbook with Dynamic Inventory
With the dynamic inventory plugin hooked up to our DigitalOcean account and the host variables in place, we can test the usage of the inventory in an Ansible playbook.
We will keep it simple, just using some shell output to verify that the playbook is running on the intended host:
# github.com/francoposa/learn-infra-ops/blob/main/infrastructure/ansible/inventory/mgmt/digitalocean-demo-shell-example.yaml
---
- hosts: k3s-demo-master
tasks:
- name: cat hostname
ansible.builtin.shell: |
cat /etc/hostname
register: cat_hostname
- name: show cat hostname output
ansible.builtin.debug:
msg: |
{{ cat_hostname.stdout_lines }}
Run the Ansible playbook:
% ansible-playbook \
--inventory ./infrastructure/ansible/inventory/sources/digitalocean.yaml \
./infrastructure/ansible/inventory/mgmt/digitalocean-demo-shell-example.yaml
The output from cat /etc/hostname
on the DigitalOcean server should match the ansible_host
we get from the inventory plugin - in this example, debian-s-1vcpu-2gb-sfo3-01
.
Conclusion
We now have some basic familiarity with the structure of Ansible’s inventory concepts, used to organize hosts into host groups and assign variables to them.
We have the ability to declare static inventory in flat file, which can be an easy way to get started but a pain to maintain in the dynamic, ephemeral environments of modern infrastructure.
Finally, we address the pain points of static inventory with the use of an Ansible dynamic inventory plugin, allowing the DigitalOcean API to provide a live view of the host inventory. With the DigitalOcean servers mapped into Ansible host groups based on tags and labels, our Ansible playbooks can now be run against these dynamic host groups without the need to continuously juggle DNS hostnames or IP addresses.
Though the dynamic inventory setup has more initial complexity for our current example use case of a single host, it will pay off down the line as our infrastructure components are rotated, destroyed, and recreated.