How to Install Ansible on Ubuntu 24.04 — Configuration Management
If you manage more than two or three servers, manual SSH sessions stop scaling. You forget which box got the latest patch, config drift creeps in, and onboarding a new team member becomes a ritual of tribal knowledge. Ansible solves this by codifying every change into a version-controlled playbook that runs the same way, every time, across your whole fleet.
This guide walks you through installing Ansible on Ubuntu 24.04 LTS, writing your first inventory and playbook, and growing into production patterns like roles, Vault, and dynamic inventories.
What Is Ansible?
Ansible is an open-source configuration management, application deployment, and orchestration tool originally created by Michael DeHaan in 2012 and now maintained by Red Hat. It lets you describe the desired state of your infrastructure in simple YAML files called playbooks, and then applies that state across any number of machines.
Three things make Ansible stand out:
Why agentless matters
Competing tools like Puppet and Chef require a persistent agent on every managed node, plus a central server (Puppet Master, Chef Server) that agents check into. That means:
- Extra daemons consuming RAM and CPU on every VPS.
- Firewall rules to allow agent-to-master traffic.
- Certificates to rotate, agent versions to upgrade.
- A chicken-and-egg problem: how do you install the agent in the first place?
Prerequisites
Before you start, make sure you have:
- An Ubuntu 24.04 LTS machine to act as the control node (your laptop works too).
- SSH access to one or more target nodes. A CloudCore Starter VPS is a great candidate.
- A non-root user with
sudoprivileges on both control and target nodes. - Python 3.10+ on the control node (Ubuntu 24.04 ships with 3.12).
sudo apt update && sudo apt upgrade -yInstallation Method 1: Ansible PPA (Recommended for Latest)
The official Ansible PPA ships the newest stable release, which is usually a few versions ahead of the Ubuntu archive. This is the path we recommend for most users.
sudo apt install -y software-properties-common
sudo add-apt-repository --yes --update ppa:ansible/ansible
sudo apt install -y ansibleVerify the install:
ansible --versionYou should see output similar to:
ansible [core 2.17.x]
config file = /etc/ansible/ansible.cfg
configured module search path = ['/home/user/.ansible/plugins/modules', ...]
ansible python module location = /usr/lib/python3/dist-packages/ansible
python version = 3.12.xInstallation Method 2: pip3 in a Virtual Environment
If you need a specific Ansible version, want to isolate it per project, or are working on a shared host where you don't have sudo, install Ansible into a Python virtual environment.
sudo apt install -y python3-venv python3-pip
python3 -m venv ~/.venvs/ansible
source ~/.venvs/ansible/bin/activate
pip install --upgrade pip
pip install ansibleTo activate the environment in future sessions:
source ~/.venvs/ansible/bin/activateThis gives you full control over the version:
pip install 'ansible==9.5.1'Installation Method 3: apt (Ubuntu Archive)
The version in the default Ubuntu 24.04 archive is older but perfectly usable for most needs:
sudo apt install -y ansibleCheck the version — you'll typically get a release that is 6–12 months behind upstream. Fine for learning, but we recommend the PPA for production.
Verifying the Installation
Regardless of install method, confirm Ansible is functional:
ansible --version
ansible localhost -m pingThe ping module (which doesn't actually send ICMP — it just verifies Python works on the target) should return:
localhost | SUCCESS => {
"changed": false,
"ping": "pong"
}Your First Inventory File
An inventory tells Ansible which hosts to manage and how to group them. The default location is /etc/ansible/hosts, but project-local inventories (./inventory.ini) are considered best practice.
Create /etc/ansible/hosts:
# /etc/ansible/hosts[webservers] web1.example.com web2.example.com ansible_host=203.0.113.45
[dbservers] db1.example.com ansible_port=2222
[production:children] webservers dbservers
[webservers:vars] ansible_user=deploy ansible_python_interpreter=/usr/bin/python3
Key concepts:
- Groups (
[webservers]) let you target subsets of hosts. - Host variables (
ansible_host,ansible_port) override connection defaults. - Group variables (
[webservers:vars]) apply to every host in the group. - Group of groups (
[production:children]) lets you nest.
ansible-inventory --list -ySSH Key Setup to Targets
Ansible uses your SSH configuration. Set up key-based auth once and every subsequent run is passwordless.
On the control node:
ssh-keygen -t ed25519 -C "ansible@control"
ssh-copy-id [email protected]
ssh-copy-id [email protected]Test the connection:
ssh [email protected] 'echo connected'For privilege escalation (running tasks as root), ensure your deploy user has passwordless sudo:
echo "deploy ALL=(ALL) NOPASSWD:ALL" | sudo tee /etc/sudoers.d/deployAd-hoc Commands
Before writing playbooks, practice with one-off commands. The pattern is ansible <pattern> -m <module> -a "<args>".
# Ping every host
ansible all -m pingCheck uptime on webservers
ansible webservers -m command -a "uptime"Install nginx on all webservers (requires sudo: -b)
ansible webservers -b -m apt -a "name=nginx state=present update_cache=yes"Restart a service
ansible webservers -b -m service -a "name=nginx state=restarted"Copy a file
ansible webservers -m copy -a "src=./index.html dest=/var/www/html/index.html"Gather facts (system info)
ansible web1.example.com -m setupAd-hoc commands are perfect for quick sanity checks and exploratory work. For anything you'll run more than twice, write a playbook.
Your First Playbook: Install and Configure Nginx
Playbooks are YAML files that describe a sequence of tasks. Create nginx.yml:
---hosts: webservers become: yes vars: server_name: example.com document_root: /var/www/html
- name: Install and configure Nginx on webservers
tasks: - name: Update apt cache apt: update_cache: yes cache_valid_time: 3600
- name: Install Nginx apt: name: nginx state: present
- name: Ensure document root exists file: path: "{{ document_root }}" state: directory owner: www-data group: www-data mode: '0755'
- name: Deploy index page copy: content: "<h1>Hello from {{ inventory_hostname }}</h1>" dest: "{{ document_root }}/index.html" owner: www-data group: www-data mode: '0644'
- name: Deploy Nginx site config template: src: templates/nginx-site.conf.j2 dest: /etc/nginx/sites-available/{{ server_name }} mode: '0644' notify: Reload Nginx
- name: Enable site file: src: /etc/nginx/sites-available/{{ server_name }} dest: /etc/nginx/sites-enabled/{{ server_name }} state: link notify: Reload Nginx
- name: Ensure Nginx is started and enabled service: name: nginx state: started enabled: yes
handlers: - name: Reload Nginx service: name: nginx state: reloaded
And templates/nginx-site.conf.j2:
server { listen 80; server_name {{ server_name }}; root {{ document_root }}; index index.html;access_log /var/log/nginx/{{ server_name }}.access.log; error_log /var/log/nginx/{{ server_name }}.error.log;
location / { try_files $uri $uri/ =404; } }
Run it:
ansible-playbook -i /etc/ansible/hosts nginx.ymlUse --check for a dry run (Ansible reports what would change, without changing anything):
ansible-playbook nginx.yml --check --diffRoles and Ansible Galaxy
Once your playbooks grow beyond a handful of tasks, split them into roles. A role is a self-contained bundle of tasks, handlers, templates, files, and variables with a standard directory layout.
Create one with:
ansible-galaxy role init roles/webserverThe generated structure:
roles/webserver/
├── defaults/main.yml # default variables (lowest precedence)
├── files/ # static files to copy
├── handlers/main.yml # handlers
├── meta/main.yml # role metadata and dependencies
├── tasks/main.yml # the actual tasks
├── templates/ # Jinja2 templates
├── tests/
└── vars/main.yml # role variables (higher precedence)Use the role from a playbook:
---
- hosts: webservers
become: yes
roles:
- webserver
- { role: firewall, firewall_allow_ports: [80, 443] }Installing community roles
Ansible Galaxy (galaxy.ansible.com) hosts thousands of community roles and collections. Install one:
ansible-galaxy role install geerlingguy.nginx
ansible-galaxy collection install community.generalPin versions in requirements.yml:
--- roles: - name: geerlingguy.nginx version: 3.1.4 - name: geerlingguy.postgresql version: 3.5.2
collections: - name: community.general version: 8.6.0 - name: ansible.posix version: 1.5.4
Install everything with:
ansible-galaxy install -r requirements.ymlVariables and Templating
Ansible uses Jinja2 for templating and offers a rich variable precedence system.
Where variables live
group_vars/all.yml— applies to every host.group_vars/webservers.yml— applies to hosts in thewebserversgroup.host_vars/web1.example.com.yml— applies to a single host.vars:block in a playbook.defaults/main.ymlandvars/main.ymlin a role.- Extra vars on the command line:
-e "key=value"(highest precedence).
Example
group_vars/webservers.yml:
---
nginx_worker_processes: auto
nginx_worker_connections: 1024
app_port: 8080
environment: productionReference in a template:
worker_processes {{ nginx_worker_processes }};
events {
worker_connections {{ nginx_worker_connections }};
}Facts
Ansible automatically gathers host facts (CPU count, memory, IP addresses, distro version, etc.). Use them in templates and conditions:
- name: Configure workers based on CPU count
template:
src: workers.conf.j2
dest: /etc/myapp/workers.conf
vars:
worker_count: "{{ ansible_processor_vcpus * 2 }}"Ansible Vault for Secrets
Never commit plaintext passwords, API tokens, or private keys to Git. Ansible Vault encrypts secrets at rest with a password you control.
Encrypt a file
ansible-vault create group_vars/webservers/vault.ymlYou'll be prompted for a password, then dropped into $EDITOR. Put secrets inside:
---
vault_db_password: "s3cr3t-p@ssw0rd"
vault_api_token: "tok_1a2b3c4d"Reference encrypted vars
Convention: the vault file holds vault_* variables. A regular (unencrypted) file aliases them:
group_vars/webservers/vars.yml:
---
db_password: "{{ vault_db_password }}"
api_token: "{{ vault_api_token }}"Playbooks reference db_password normally; the Vault password is only required at run time.
Run a playbook that uses Vault
# Prompt for password
ansible-playbook site.yml --ask-vault-passRead from a password file (add to .gitignore!)
ansible-playbook site.yml --vault-password-file ~/.vault_passPer-vault-id passwords for multi-environment setups
ansible-playbook site.yml --vault-id prod@~/.vault_prod --vault-id dev@~/.vault_devOther vault operations
ansible-vault edit group_vars/webservers/vault.yml
ansible-vault view group_vars/webservers/vault.yml
ansible-vault rekey group_vars/webservers/vault.yml # change password
ansible-vault encrypt_string 's3cr3t' --name 'db_password'Writing Custom Modules (Overview)
Most of the time, existing modules cover your needs. But when they don't, you can write your own. Modules are simply executables that:
{"changed": true/false}.A minimal Python module at library/hello.py:
#!/usr/bin/python from ansible.module_utils.basic import AnsibleModuledef main(): module = AnsibleModule( argument_spec=dict( name=dict(type='str', required=True), ), supports_check_mode=True, ) name = module.params['name'] module.exit_json(changed=False, msg=f"Hello, {name}!")
if __name__ == '__main__': main()
Use it in a task:
- name: Say hello
hello:
name: "Ansible"
register: resultdebug: var=result.msg
library/ next to your playbook. For production, bundle them in a collection.Dynamic Inventories (AWS, DigitalOcean, Others)
Static inventory files don't scale when your servers spin up and down with autoscaling. Dynamic inventory plugins query your cloud provider's API and build the inventory on the fly.
AWS EC2
Install the AWS collection:
ansible-galaxy collection install amazon.aws
pip install boto3 botocoreCreate inventory.aws_ec2.yml:
---
plugin: amazon.aws.aws_ec2
regions:
- us-east-1
- eu-west-1
keyed_groups:
- key: tags.Role
prefix: role
- key: placement.region
prefix: region
filters:
tag:Environment: production
instance-state-name: running
hostnames:
- tag:Name
- dns-name
compose:
ansible_host: public_ip_addressRun against it:
ansible-inventory -i inventory.aws_ec2.yml --graph
ansible-playbook -i inventory.aws_ec2.yml site.ymlDigitalOcean
ansible-galaxy collection install community.digitalocean
export DO_API_TOKEN="dop_v1_..."inventory.digitalocean.yml:
---
plugin: community.digitalocean.digitalocean
api_token: "{{ lookup('env', 'DO_API_TOKEN') }}"
attributes:
- id
- name
- networks
- region
- tags
keyed_groups:
- key: do_region.slug
prefix: region
- key: do_tags | default(['untagged'], true)
prefix: tagOther providers
Similar plugins exist for Azure, GCP, Hetzner, Linode, Vultr, Proxmox, VMware, and more. Search the Ansible collection index.
Ansible AWX and Semaphore
Running playbooks from a laptop is fine for one person. Teams need a UI, RBAC, scheduled runs, and audit logs. Two popular options:
- Ansible AWX (github.com/ansible/awx) — the open-source upstream of Red Hat Ansible Automation Platform. Kubernetes-native, full-featured, heavier to operate.
- Semaphore (semaphoreui.com) — lightweight Go-based alternative. Runs as a single binary or Docker container. Much easier to deploy on a VPS.
docker compose up -d, and you have a team-friendly Ansible UI in about ten minutes.Best Practices
After a few years of running Ansible at scale, these are the habits that pay off:
ansible.cfg, inventory/, roles/, group_vars/, host_vars/, and requirements.yml in the repo. Avoid depending on /etc/ansible/.requirements.yml. No surprise upgrades.- name: Install Nginx reads better in logs than the bare module call.command/shell. Modules are idempotent; shell commands often aren't.--check and --diff before prod runs. Ansible's dry run is genuinely useful.ansible-playbook site.yml --tags "nginx,ssl" is faster than re-running everything.vault.yml for encrypted, vars.yml for plaintext aliases.forks higher than the default 5. In ansible.cfg: forks = 50 for fleets of dozens of servers.Example project-local ansible.cfg:
[defaults] inventory = ./inventory roles_path = ./roles collections_paths = ./collections host_key_checking = False retry_files_enabled = False forks = 50 stdout_callback = yaml callbacks_enabled = timer, profile_tasks
[ssh_connection] pipelining = True control_path = ~/.ssh/ansible-%%r@%%h:%%p
Troubleshooting
SSH connection errors
UNREACHABLE! => {"changed": false, "msg": "Failed to connect to the host via ssh: ..."}- Confirm you can SSH manually:
ssh deploy@host. - Check
ansible_user,ansible_port,ansible_hostin inventory. - Disable host key checking temporarily:
export ANSIBLE_HOST_KEY_CHECKING=False. - Increase verbosity:
ansible-playbook site.yml -vvvvshows the raw SSH command.
Python interpreter errors
/usr/bin/python: not foundUbuntu 24.04 only ships Python 3. Tell Ansible explicitly:
[all:vars]
ansible_python_interpreter=/usr/bin/python3Permission denied (sudo)
sudo: a password is requiredEither set up passwordless sudo (see SSH Key Setup) or run with --ask-become-pass / -K.
Slow runs
- Enable SSH pipelining (see
ansible.cfgabove) — often a 3-5x speedup. - Use
asyncandpoll: 0for long-running tasks that don't block the next step. - Tune
forksto match your fleet size. - Use
strategy: freeto let hosts progress independently instead of waiting on the slowest.
YAML syntax errors
YAML is whitespace-sensitive. Use yamllint and ansible-lint:
pip install yamllint ansible-lint
ansible-lint site.ymlFAQ
Ansible vs. Puppet/Chef — which should I choose?
For most new projects, Ansible. It has a shallower learning curve (YAML beats Ruby DSLs), no agent to manage, and a faster feedback loop. Puppet and Chef are still strong in large, legacy enterprises where pull-based architectures (agents checking into a central server) are already embedded. Pick Puppet/Chef if your team already runs them; pick Ansible if you're starting fresh.
Ansible vs. Terraform — aren't they the same thing?
No. Terraform provisions infrastructure (create a VPS, a database, a load balancer). Ansible configures infrastructure (install packages, write config files, start services). They're complementary:
- Terraform creates the VPS.
- Ansible installs and configures software on it.
Do I need Ansible for just one server?
Probably not for a single static server — plain shell scripts are fine. But the moment you have two (staging + prod, or two web nodes), Ansible pays off. You get repeatability, documentation-as-code, and the ability to rebuild a server from scratch in minutes.
Is Ansible slow?
Ad-hoc, it can be. With pipelining = True, forks tuned up, fact caching enabled, and the mitogen strategy plugin, a well-configured Ansible run is plenty fast for fleets into the hundreds. For thousands of nodes, consider AWX with execution-environment containers and distributed workers.
What Python version does Ansible need on targets?
Ansible-core 2.17 requires Python 3.7+ on targets. Ubuntu 24.04 ships Python 3.12 by default, so you're good. For older targets (CentOS 7, Ubuntu 18.04), you may need to install Python explicitly or use the raw module to bootstrap.
Can Ansible manage Windows?
Yes. Ansible connects over WinRM (or SSH on modern Windows) and ships a full suite of win_* modules (win_package, win_service, win_regedit, etc.). The control node must still be Linux or macOS — Windows is supported as a target, not a controller.
Next Steps
You now have Ansible installed, your first inventory, a working playbook, and a map of the production patterns. From here:
geerlingguy.docker, geerlingguy.postgresql) and read the source — it's a masterclass in idempotent Ansible.ansible-playbook --check on every PR and ansible-playbook on merge to main.Managing a fleet of VPS nodes? Spin up a CloudCore Starter plan as your Ansible control node and start codifying your ops today. Your future self — and your team — will thank you.