Operations are the core building blocks of pyinfra. They define the desired state of your infrastructure and pyinfra ensures that state is achieved. This guide covers everything you need to know about using operations effectively.
What Are Operations?
Operations are Python functions that:
Define the desired state of a system (e.g., “package X should be installed”)
Generate shell commands to achieve that state
Are idempotent - running them multiple times produces the same result
Can detect changes before applying them (dry-run mode)
Basic Operation Usage
Importing Operations
Operations are organized by category in the pyinfra.operations module:
from pyinfra.operations import (
apt, # Debian/Ubuntu package management
yum, # RedHat/CentOS package management
files, # File and directory operations
server, # System operations
git, # Git operations
docker, # Docker operations
)
Calling Operations
Every operation call should include a descriptive name parameter:
apt.packages(
name = "Install web server packages" ,
packages = [ "nginx" , "certbot" ],
update = True ,
_sudo = True ,
)
The name parameter is required and helps you identify operations in pyinfra’s output.
Common Operations
Package Management
APT (Debian/Ubuntu)
YUM/DNF (RHEL/CentOS)
Multiple Package Managers
from pyinfra.operations import apt
# Install packages
apt.packages(
name = "Install packages" ,
packages = [ "nginx" , "postgresql" , "redis-server" ],
update = True ,
_sudo = True ,
)
# Remove packages
apt.packages(
name = "Remove old packages" ,
packages = [ "apache2" ],
present = False ,
_sudo = True ,
)
# Upgrade all packages
apt.upgrade(
name = "Upgrade all packages" ,
_sudo = True ,
)
File Operations
Upload Files
File Management
Directory Operations
from pyinfra.operations import files
# Upload a file
files.put(
name = "Upload application config" ,
src = "config/app.conf" ,
dest = "/etc/myapp/app.conf" ,
user = "www-data" ,
group = "www-data" ,
mode = "644" ,
_sudo = True ,
)
# Upload with templating
files.template(
name = "Upload nginx config from template" ,
src = "templates/nginx.conf.j2" ,
dest = "/etc/nginx/sites-available/myapp" ,
user = "root" ,
mode = "644" ,
_sudo = True ,
)
System Operations
Users and Groups
Service Management
Shell Commands
from pyinfra.operations import server
# Create user
server.user(
name = "Create application user" ,
user = "appuser" ,
home = "/home/appuser" ,
shell = "/bin/bash" ,
system = True ,
_sudo = True ,
)
# Create group
server.group(
name = "Create application group" ,
group = "appgroup" ,
_sudo = True ,
)
Git Operations
from pyinfra.operations import git
# Clone repository
git.repo(
name = "Clone application repository" ,
src = "https://github.com/example/myapp.git" ,
dest = "/opt/myapp" ,
branch = "main" ,
pull = True , # Pull latest changes if already cloned
user = "deploy" ,
_sudo = True ,
)
Global Arguments
Global arguments (prefixed with _) control how operations are executed:
Common Global Arguments
from pyinfra.operations import apt
apt.packages(
name = "Install packages" ,
packages = [ "nginx" ],
# Execute with sudo
_sudo = True ,
# Execute as specific user
_sudo_user = "postgres" ,
# Set environment variables
_env = { "DATABASE_URL" : "postgres://localhost/mydb" },
# Set timeout (seconds)
_timeout = 300 ,
# Continue on error
_continue_on_error = True ,
# Only run if condition is true
_if = lambda : host.get_fact( "LinuxDistribution" ) == "Ubuntu" ,
)
Global arguments are prefixed with underscore (_) to distinguish them from operation-specific parameters.
Operation Return Values
Operations return metadata that you can use for conditional execution:
from pyinfra.operations import files, server
# Capture operation result
config_changed = files.template(
name = "Upload nginx config" ,
src = "templates/nginx.conf.j2" ,
dest = "/etc/nginx/nginx.conf" ,
_sudo = True ,
)
# Conditionally restart if config changed
server.service(
name = "Restart nginx if config changed" ,
service = "nginx" ,
restarted = True ,
_sudo = True ,
_if = lambda : config_changed.did_change(),
)
Use did_change() to check if an operation made changes, enabling smart service restarts and conditional operations.
Working with Templates
pyinfra uses Jinja2 for templating:
Create a template file
Create templates/nginx.conf.j2: server {
listen {{ port }} ;
server_name {{ server_name }} ;
location / {
proxy_pass http:// {{ backend_host }} : {{ backend_port }} ;
}
}
Use template in deploy
from pyinfra import host
from pyinfra.operations import files
files.template(
name = "Upload nginx config" ,
src = "templates/nginx.conf.j2" ,
dest = "/etc/nginx/sites-available/myapp" ,
port = host.data.get( "nginx_port" , 80 ),
server_name = host.data.get( "server_name" , "example.com" ),
backend_host = "localhost" ,
backend_port = 8000 ,
_sudo = True ,
)
Combining Operations
Build complex deployments by combining multiple operations:
from pyinfra.operations import apt, files, git, server
# Install dependencies
apt.packages(
name = "Install system dependencies" ,
packages = [ "python3" , "python3-pip" , "git" , "nginx" ],
update = True ,
_sudo = True ,
)
# Create application user
server.user(
name = "Create app user" ,
user = "myapp" ,
system = True ,
home = "/opt/myapp" ,
_sudo = True ,
)
# Clone application
git.repo(
name = "Clone application" ,
src = "https://github.com/example/myapp.git" ,
dest = "/opt/myapp/code" ,
branch = "main" ,
pull = True ,
user = "myapp" ,
_sudo = True ,
)
# Install Python dependencies
server.shell(
name = "Install Python dependencies" ,
commands = [
"cd /opt/myapp/code" ,
"pip3 install -r requirements.txt" ,
],
_sudo = True ,
_sudo_user = "myapp" ,
)
# Configure nginx
files.template(
name = "Configure nginx" ,
src = "templates/nginx.conf.j2" ,
dest = "/etc/nginx/sites-available/myapp" ,
_sudo = True ,
)
files.link(
name = "Enable nginx site" ,
path = "/etc/nginx/sites-enabled/myapp" ,
target = "/etc/nginx/sites-available/myapp" ,
_sudo = True ,
)
# Restart services
server.service(
name = "Restart nginx" ,
service = "nginx" ,
restarted = True ,
_sudo = True ,
)
Operation Categories
pyinfra includes operations for many common tasks:
Category Module Purpose Package Management apt, yum, dnf, brew, apkInstall/manage packages Files filesFile/directory operations, uploads, templates System serverUsers, groups, services, shell commands Version Control gitGit repository management Containers dockerDocker container/image management Databases mysql, postgresqlDatabase operations Init Systems systemd, upstart, sysvinitService management Web Servers Operations use files for config Configure nginx, apache
Best Practices
Always name your operations - Use descriptive names that explain what the operation does:# Good
apt.packages(
name = "Install PostgreSQL database server" ,
packages = [ "postgresql" ],
)
# Bad
apt.packages(
name = "Install pkg" ,
packages = [ "postgresql" ],
)
Use idempotent operations - Prefer state-defining operations over raw shell commands:# Good - idempotent
files.directory(
name = "Create directory" ,
path = "/opt/myapp" ,
_sudo = True ,
)
# Bad - will fail on second run
server.shell(
name = "Create directory" ,
commands = [ "mkdir /opt/myapp" ],
_sudo = True ,
)
Check return values for conditional execution - Use operation return values to create smart workflows:config_changed = files.template( name = "Upload config" , ... )
server.service(
name = "Restart only if config changed" ,
service = "nginx" ,
restarted = True ,
_if = lambda : config_changed.did_change(),
)
Next Steps