Managing Systems and Applications with pyinfra
Scripted Setup
© Lead Image © Carlos Castilla Jimenez, 123RF.com
Keeping Linux machines in a known state requires a configuration management system. Discover how pyinfra simplifies this task with Python's full programming power.
When managing a few Linux machines, it's easy to handle them with ad-hoc commands, but this approach becomes inefficient as the number of machines increases. A configuration management tool ensures that all your machines have a consistent setup. Popular tools in this domain are Ansible, Puppet, Chef, SaltStack, and CFEngine. A lesser-known alternative, pyinfra [1], is the subject of this article. Unlike many other configuration management tools, pyinfra doesn't rely on YAML [2] or a domain-specific language. Instead, you configure your systems using Python code.
If you're familiar with Python, you'll find pyinfra easily accessible. Moreover, you can leverage the full Python language, including loops, conditions, functions, and classes to streamline your configuration tasks. You can even import any Python library to meet specific requirements, allowing you to harness the extensive Python ecosystem.
Under the hood, pyinfra translates your Python code into shell commands and executes them on your machines. This means the target machines don't need to have Python or any special dependencies; all that's required is a POSIX-compatible shell. There's no agent running on the target machines. Instead, pyinfra simply logs into machines via SSH to execute its shell commands.
As a downside, pyinfra isn't as widely used as other tools. Ansible, for example, offers numerous built-in functionalities and boasts a vast ecosystem of reusable packages. Pyinfra has more limited built-in functions, and its ecosystem isn't that big, which means there might be times when you need to implement solutions on your own. Fortunately, it's less daunting when you're dealing with Python.
Installing pyinfra
Because pyinfra is a Python package, you can install it using Python's package manager, pip, or using its more modern counterpart uv [3]. With uv, you can install pyinfra globally as a tool:
uv tool install pyinfra
Then confirm that the pyinfra command is recognized:
$ pyinfra --version pyinfra: v3.7
Inventory, Operations, and Facts
Three key concepts when working with pyinfra are an inventory, operations, and facts. An inventory is a collection of hosts, groups, and data. Hosts are the targets where pyinfra runs operations, and they can be assigned to groups. For example, you might define a group for all web servers to configure them uniformly. You can also assign data to individual hosts or to groups, such as a list of packages to install.
Operations come in two types. One type is a straightforward shell command to execute on a host. The other type is a state definition, which is an idempotent operation. This ensures that running it twice doesn't change the desired state if it has already been achieved. A state definition operation first compares the desired state against the current state and only makes changes when they differ. For instance, a state change might involve ensuring a specific package is installed.
Most operations in pyinfra are state definitions. You should consider using these over shell commands when possible, because this approach simplifies reasoning about your systems and enables optimizations during pyinfra execution. For instance, instead of explicitly running an apt get install command on your systems with pyinfra, opt for an apt.packages operation with the desired packages, and pyinfra will only perform installation if necessary.
To determine the current state of a system, pyinfra uses facts. These are read-only during pyinfra's run on your systems. Operations use these facts to generate shell commands that alter the system state. For instance, the apt.packages operation to install packages uses the deb.DebPackage fact to determine whether the packages are already installed.
Running Commands with the pyinfra CLI
Before writing Python code to configure your systems with pyinfra, you can explore the concepts of an inventory, operations, and facts through the command line. This will give you an idea about the tool's capabilities. All pyinfra commands on the command line follow the format pyinfra <inventory> <operations>.
In its simplest form, the inventory is just a hostname, and the operation is an exec command. For instance, if you want to check the kernel version a specific machine is running, just execute
pyinfra podpile.home exec -- uname -r
This command connects to the specified host over SSH, requesting your password or your key's passphrase if necessary, and runs a server.shell operation executing the uname -r command on the host. It shows the command's output, considers it successful, presents a table with the results, and then disconnects. If you add more hosts on the command line, separated by a comma, it will perform the same action on all hosts concurrently (see Figure 1).
You can also use this approach to keep an eye on the systemd journals of multiple systems in parallel:
pyinfra podpile.home,nov.home exec -- journalctl --follow
Because this is an operation that never finishes, pyinfra keeps showing you the command's output until you terminate it with Ctrl+C.
You can also use this to reboot one or more specific servers:
pyinfra podpile.home -- sudo server.reboot
Note the option --sudo, which directs pyinfra to execute the command of the server.reboot operation with sudo. Pyinfra prompts you for the sudo password before rebooting the machine. Don't be alarmed when you see a Python exception with a large stack trace: This occurs because the SSH connection is lost during the reboot.
After this, you can verify whether the server has really rebooted by checking its uptime:
pyinfra podpile.home exec -- uptime
Another use for pyinfra's command line is installing specific packages on your hosts:
pyinfra nov.home --sudo server.packages tmux vim
The server.packages operation is a state definition: It first checks whether the packages are installed and only installs them if they're absent. It uses your system's package manager, such as APT or DNF. Have a look at the pyinfra Operations Index [4] for an exhaustive list of provided operations.
Buy this article as PDF
(incl. VAT)