When managing complex IT infrastructures, automation is key to maintaining efficiency, reducing human error, and ensuring consistency across systems. In an environment with hundreds of servers, such as one I manage for an enterprise client with 2,000 workstations and over 300 VMs, the choice of automation tool is never trivial. The decision between adopting a mature framework like Ansible or relying on custom Bash/Python scripts can significantly impact team productivity, infrastructure stability, and ultimately, operational costs. My experience has led me to test both approaches at scale, evaluating not only execution speed but also maintainability, scalability, and resilience.
Tested on: Ubuntu 22.04 LTS · Ansible 2.15 · Python 3.10 · August 2026
Test Context
The test was conducted on an environment of 200 virtual servers, replicating a typical setup for a medium-to-large infrastructure. All servers were based on Ubuntu Server 22.04 LTS. The primary objectives were: updating system packages, distributing an Nginx configuration file and restarting the service, and finally, verifying the status of a critical service. For each task, I measured the average execution time, success rate, and ease of debugging in case of failure. The results, as we will see, highlighted the strengths and weaknesses of both approaches, providing valuable insights for defining an effective automation strategy.
Prerequisites / Test Environment
To replicate this test, you need access to an environment with at least 200 Linux hosts. In my case, I used Proxmox to quickly create the VMs, but any hypervisor or cloud provider would be suitable. It is essential that all hosts are reachable via SSH and that passwordless access is configured using SSH keys. Read also: Passwordless SSH Linux: Configure Keys in 5 Minutes (2026)
For Ansible, you need to install the ansible package on the controller and configure an inventory file. For scripts, Python 3 and Bash just need to be present on the hosts (generally pre-installed on Linux).
# Install Ansible on the controller
sudo apt update
sudo apt install ansible -y
# Example Ansible inventory (hosts.ini)
[webservers]
web[001:200] ansible_host=192.168.1.{{ (ansible_loop.index | int) + 100 }}
1. Ansible: The Declarative and Idempotent Approach
Ansible relies on playbooks written in YAML, which describe the desired state of systems. Its strength lies in idempotence: you can run a playbook multiple times, and the system will reach the same final state without undesirable side effects. This greatly simplifies configuration management and troubleshooting.
Here’s an example of the playbook used for the tests:
---
- name: Manage servers with Ansible
hosts: webservers
become: yes
gather_facts: no
tasks:
- name: Update system packages
ansible.builtin.apt:
update_cache: yes
upgrade: dist
register: apt_update_result
- name: Debug apt update result
ansible.builtin.debug:
var: apt_update_result
when: apt_update_result.changed or apt_update_result.failed
- name: Distribute Nginx configuration
ansible.builtin.copy:
src: files/nginx.conf
dest: /etc/nginx/nginx.conf
mode: '0644'
notify: Restart Nginx
- name: Verify Nginx service status
ansible.builtin.service_facts:
- name: Ensure Nginx is running and enabled
ansible.builtin.service:
name: nginx
state: started
enabled: yes
handlers:
- name: Restart Nginx
ansible.builtin.service:
name: nginx
state: restarted
Running this playbook is straightforward:
ansible-playbook -i hosts.ini playbook.yml
During testing, Ansible managed the update of 200 servers in about 15-20 minutes, depending on network load and the number of packages to update. Configuration file distribution and Nginx restart were almost instantaneous. The main advantage was the clarity of the output log and the ease of identifying any problematic hosts. Read also: Kubernetes Production: First Deploy, Errors, and Fixes
2. Custom Scripts: Flexibility and Granular Control
Bash or Python scripts offer granular control over every single operation. They are ideal for very specific tasks or complex logic that doesn’t easily fit into existing Ansible modules. However, creating robust scripts that handle errors, idempotence, and logging requires significantly more development and testing effort. For our test, I created a Python script that connects via SSH to each server and executes the necessary commands.
# script_custom.py
import subprocess
import paramiko
import concurrent.futures
import time
hosts = [f'192.168.1.{i}' for i in range(101, 301)] # Example of 200 IPs
SSH_USER = 'your_ssh_user'
def run_command_on_host(host, command):
try:
client = paramiko.SSHClient()
client.load_system_host_keys()
client.connect(host, username=SSH_USER, timeout=10)
stdin, stdout, stderr = client.exec_command(command, get_pty=True)
output = stdout.read().decode().strip()
error = stderr.read().decode().strip()
client.close()
return host, 'SUCCESS', output, error
except Exception as e:
return host, 'FAILED', '', str(e)
def main():
print("Starting custom script for 200 servers...")
start_time = time.time()
commands_to_run = [
"sudo apt update && sudo apt upgrade -y",
"echo 'server { listen 80; root /var/www/html; index index.html; }' | sudo tee /etc/nginx/nginx.conf > /dev/null",
"sudo systemctl restart nginx",
"sudo systemctl is-active nginx"
]
with concurrent.futures.ThreadPoolExecutor(max_workers=50) as executor:
future_to_host = {
executor.submit(run_command_on_host, host, cmd): (host, cmd)
for host in hosts for cmd in commands_to_run
}
for future in concurrent.futures.as_completed(future_to_host):
host, cmd = future_to_host[future]
try:
result_host, status, output, error = future.result()
if status == 'SUCCESS':
print(f"[{result_host}] {cmd} -> {status}")
else:
print(f"[{result_host}] {cmd} -> {status}: {error}")
except Exception as exc:
print(f"[{host}] An exception occurred: {exc}")
end_time = time.time()
print(f"Script completed in {end_time - start_time:.2f} seconds.")
if __name__ == '__main__':
main()
Running this script, even with ThreadPoolExecutor for parallelization, took a similar or slightly longer time than Ansible for package updates (about 20-25 minutes). Configuration management and Nginx restarts were quick, but logging and error handling were much more rudimentary compared to Ansible. Any error on a single host required manual analysis of the script log, without the rich context provided by Ansible.
Common Errors and Troubleshooting
Ansible:
- SSH connection issues: Often due to incorrectly configured SSH keys or firewalls blocking port 22. Verify manual connectivity
ssh user@hostand Ansible logs for specific messages (unreachable). - Idempotence not respected: If a module is not idempotent (or if a
shell/commandcommand is used improperly), side effects can occur. Ensure you use specific modules when available. - Slow performance:
gather_facts: nois essential for large inventories. Use appropriateforksin theansible.cfgfile to parallelize operations.
Custom Scripts:
- Error handling: Ignoring command exit codes (
$?in Bash) can lead to undetected errors. Every command must be checked. - Idempotence: Must be implemented manually, for example, by checking for a file’s existence before copying it, or checking a service’s status before restarting it.
- Parallelization: Without proper thread/process management, scripts can be very slow on many hosts. Paramiko, if not managed with a connection pool, can be inefficient. Read also: Ansible Windows: Managing Servers with WinRM and Core Modules
FAQ — Frequently Asked Questions
What is the impact of network performance?
Network performance is a critical factor for both approaches. A network bottleneck can significantly slow down execution, especially during package updates or large file distribution. It is essential that the Ansible controller or the server running the script has adequate connectivity to all 200 hosts.
Is Ansible always slower than Python scripts?
Not necessarily. While there is initial overhead for playbook interpretation and inventory management, Ansible is highly optimized for parallelization and efficient module usage. For complex, well-defined tasks, Ansible can be faster because its modules are often written in optimized Python or call efficient binaries. Custom scripts, if not written with attention to parallelization and efficiency, can be slower.
Can I combine the two approaches?
Absolutely. A hybrid approach is often the most effective. You can use Ansible as the primary framework for most management and deployment operations, and call custom scripts (Bash or Python) via the ansible.builtin.script or ansible.builtin.command module for very specific or complex tasks that require particular logic not easily expressed in YAML. This allows you to leverage the strengths of both tools. Read also: Ansible: Run Remote Scripts with the Command Module
How do I securely manage credentials with both?
For Ansible, it is highly recommended to use Ansible Vault to encrypt sensitive data like passwords or API keys. For Python scripts, it is good practice to use environment variables, a secret manager like HashiCorp Vault, or a configuration management system that securely injects credentials at script startup, avoiding hardcoding them in the code.
Conclusions with Operational Takeaways
The comparison between Ansible and custom scripts on 200 servers clarified that both have their place in an automation strategy. Ansible excels in maintainability, idempotence, and managing complex configurations at scale, offering a superior debugging experience. Custom scripts, on the other hand, provide maximum flexibility and are indispensable for highly specific logic or integration with non-standardized systems. My operational recommendation is to adopt a hybrid approach: use Ansible as the primary framework for most management and deployment operations, and reserve Python scripts for exceptional cases that require more granular control or complex business logic. The important thing is to invest time in optimization and testing, regardless of the tool chosen, to ensure that automation is an asset and not a source of new problems.
Sources
Updated: August 2026