Ai/automazione

Ansible vs Scripts: Managing 200 Servers

Ansible vs Scripts: Managing 200 Servers

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@host and Ansible logs for specific messages (unreachable).
  • Idempotence not respected: If a module is not idempotent (or if a shell/command command is used improperly), side effects can occur. Ensure you use specific modules when available.
  • Slow performance: gather_facts: no is essential for large inventories. Use appropriate forks in the ansible.cfg file 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

Share this article:

Written by

Rosario Giordano

Rosario Giordano is a system administrator and IT consultant specializing in cybersecurity and cloud, with over 20 years of experience managing enterprise Linux infrastructures. His areas of expertise include SSH hardening, Kubernetes platforms, PostgreSQL databases, VMware/ Proxmox virtualization, and compliance with NIS2 and ISO 27001 security frameworks