Ansible in Nix
I bought a new VPS amid the deluge of LowendTalk’s Black Friday promotions, and spent some time to hone my DevOps skills. My favorite IaC is terraform, unfortunately there exist no mature provider for the bare metal linux boxes. My second choice was SaltStack thanks to its declarative states, though the foreplay was a little tedious. I would like something lightweight, so I tried Ansible.
Ansible is agentless, which means the minimum requirement is simply a ssh connection, with superuser privilege.I could bootstrap the machine to my desired state:
- A service account, for example, ansible, is provisioned and added to the
sudoerwithout password. - The RSA key pair is generated and copied to the remote machine.
- The
sshdis hardened with best practice, such as password login disabled, etc.
with the following command:
ansible-playbook -i inventory.ini bootstrap.yml -u root -k
The -u root instructed ansible to use root account to connect, and prompt the password
challenge thanks to the -k argument. Once the service account is setup, they are no longer
required.
The rabbit hole
The real challenge emerged from the wireguard setup. Due to the complexity of wireguard setup,
I used the role provided by
lablabs.wireguard. First,
install the collection via ansbile-galaxy:
ansible-galaxy collection install lablabs.wireguard
Then craft a new playbook, wireguard.yml like this:
- name: Deploy WireGuard VPN using the lablabs.wireguard collection
hosts: wireguard_servers
become: true
roles:
- lablabs.wireguard.wireguard
Additionally, define the wireguard_server in the inventory.yml, and our configuration
in group_vars/wireguard_servers.yml. Then we could run:
ansible-playbook -i inventory.yml wireguard.yml
... ...
TASK [lablabs.wireguard.wireguard : Create clients configs] ********************
fatal: [example.com]: FAILED! => {"msg": "You need to install \"jmespath\" prior to running json_query filter"}
Checking out the code here,
it seemed that the jinja filter json_query depended on an absent package jmespath.
Install jmespath via python3Packages
My first attempt is to add jmespath to the python packages like:
{
home.packages = with pkgs.python313Packages; [
python
ipython
... ...
jmespath
uv
];
}
Same error, also confirmed that the package was NOT discoverable at all!
❯ python -c "import jmespath"
Traceback (most recent call last):
File "<string>", line 1, in <module>
import jmespath
ModuleNotFoundError: No module named 'jmespath'
I understood that the nix packages were installed in an isolated subdirectory
of /nix/store, but I would expect the packages installed via this approach
would be integrated into a single environment. Clearly I am wrong.
But how do the python apps, for example ansible-playbook, look for their dependencies then?
Just out of curiosity, I peeked the implementation of ansible-playbook:
❯ head -n 7 /nix/store/gnls4kcmy9fr5a3x7kqrsqwq2m96rrsg-python3.12-ansible-core-2.18.6/bin/ansible-playbook
#! /nix/store/8ivrpmp3arvxbr6imdwm2d28q9cjsqvi-bash-5.2p37/bin/bash -e
PATH=${PATH:+':'$PATH':'}
PATH=${PATH/':''/nix/store/ljpcaw5flk6pp75dr0jnb9pyfc2pydmq-python3.12-yangson-1.6.2/bin'':'/':'}
PATH='/nix/store/ljpcaw5flk6pp75dr0jnb9pyfc2pydmq-python3.12-yangson-1.6.2/bin'$PATH
PATH=${PATH#':'}
PATH=${PATH%':'}
export PATH
The dependencies were resolved and injected via the PATH environment variables in
the nix-specific wrapper! That explained why the newly installed jmespath was never
discovered.
We have to build a development environment to resolve the dependency issue.
Install via devenv
I created a devenv configuration
with the prompt, Python with ansible and jmespath lib; copied the following snippet to devenv.nix:
{
pkgs,
lib,
config,
...
}:
{
# https://devenv.sh/packages/
packages = [ pkgs.ansible ];
# https://devenv.sh/languages/
languages = {
python = {
enable = true;
venv.enable = true;
venv.requirements = "jmespath";
};
};
# See full reference at https://devenv.sh/reference/options/
}
and ran devenv shell to install the dependencies. Then I encountered a different error:
TASK [lablabs.wireguard.wireguard : Check if Public Key is valid base64 string] *******************************************************************************************************
[ERROR]: Task failed: Conditional result (True) was derived from value of type 'str' at '/Users/kunxi/.ansible/collections/ansible_collections/lablabs/wireguard/roles/wireguard/tasks/pre_check.yml:6:9'. Conditionals must have a boolean result.
The error was self-explainable:
the precheck asserts the publicKey is base64 encoded,
but the str value would NOT infer the boolean
result. But why we did not see this error before? It turned out the devenv installed
a cutting edge ansbile-2.19.3 with python-3.13.8, the latest version was more strict
on the type checking.
With pinned version, it took a long time for devenv to build the package from the source. I bailed out and tried the nix-shell.
Install via nix-shell
The shell.nix is pretty straightforward:
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
# Add buildInputs for packages available directly in Nixpkgs
buildInputs = [
(pkgs.python312.withPackages (ps: [
ps.ansible
ps.ansible-core
ps.jmespath
# Add other Python packages here using ps.<package_name>
]))
# Add other system-level tools if needed, e.g., pkgs.git, pkgs.jq
];
}
Viola, everything just works!
Conclusion
The python libraries in the nix are installed just like others, — in their own sandbox.
Technically, we should never install them via nixpkgs unless they expose cli entry points, such as
uv, ruff, etc. If the library has optional dependencies, it is determined by the
nix package maintainer whether to include them; — that is why some
packages have foo, fooFull variants.
nix-shell and devenv are designed exactly to address this issue by bundling isolated packages into a cohesive working environment.