Ansible is a provisioning system developed by Red Hat and is widely used in industry to save a lot of SA time and effort. It makes everyone happy- from the SAs required to build machines to the security office who needs to make sure those machines are in spec, to the CFO in charge of saving money.
For those short on time, here's a [NotebookLM](https://notebooklm.google.com/) one-click podcast created from this write-up.

# Provisioning
First, let's ask: what is provisioning, and why do people use it? What problem is it really solving, and what other problems does/can it create?
Let's imagine the following scenario: You're in charge of IT for a small mom-and-pop shop selling candy. You build a couple of Windows systems with Excel, Word, etc on them for the three employees on staff and put together a couple Netgear managed switches and the ISP's router for wired and wireless connectivity in the shop.
Throughout the first couple of years, occasionally, you're asked to troubleshoot a problem on an employee machine, create and remove accounts, or outright build or rebuild a machine for a new employee. Not a big deal. It keeps you busy on the slow days.
One day, the shop expands, opening a second location. You set up a couple more systems, install the usual software, and link the stores with a VPN. All goes smoothly - until the third store opens. Then the fourth. Now, every time a new employee is hired or a system breaks down, you’re manually setting up machines and traveling to each site.
A year later, the shop has ten locations. Each store has its quirks: different software needs, custom inventory systems, and varied machine specs. What used to be a quick setup now takes days. You’re driving all over town, USB stick in hand, building machines, installing updates, and troubleshooting everything from user accounts to network issues.
Late one night, while you're buried under a pile of checklists and half-configured machines, the owner calls: “We’re opening another store next week. Can you set up five more systems?”
You realize it’s unsustainable. There *must* be a better way.
Enter Ansible. Its one and only purpose in life is to run commands on machines. Doesn't sound super fancy, because it's not. But Ansible doesn’t **need** to be fancy. It’s simple by design - no agents, no complex setup. Just plain, easy-to-read YAML files that let you describe what you want done. Need something more advanced? You can use a Python-like language in your YAML that adds flexibility when you need it, without overwhelming you when you don't.
To install it, you set up a RHEL machine somewhere - anywhere it can reach your other systems - and `sudo dnf install ansible-core`. Everything's done from the command-line, and once you get it all laid out the way you like it, provisioning an entire machine goes from hours of running commands and manually installing software to typing a single command, hitting the Enter key, and coming back after a coffee break.
# How it works
Ansible works by connecting to your machines over SSH (or WinRM for Windows) and running tasks defined in **playbooks** - YAML files that describe the steps needed to configure your systems. You start by creating an **inventory**, which is just a list of machines you want to manage.
Here's a supplementary video from NetworkChuck - I'm not a super-fan of his "clickbaity-ness" or "hollywood hacker-ish" aesthetic but I'd be lying if I said he didn't make some of the best beginner-friendly explainer videos I'd ever seen. This one is no exception.

> [!note]
> He uses different commands and file locations than in this document. Both the video and this document are valid ways to do all of this.
## Workflow
1. **Define your inventory**
A list of machines you want Ansible to manage.
2. **Write a playbook**
A playbook contains tasks like installing software or setting up configurations.
3. **Run the playbook**
Ansible connects to the machines and applies the necessary tasks.
## Inventory
The **inventory** file is where you define the machines that Ansible will manage. In this example, we have three groups of machines: webservers, workstations, and a database server. Each group contains the IP addresses or hostnames of the machines, allowing Ansible to apply different configurations depending on the role of the server.
```ini
[webservers]
192.168.1.10 # websrv1.candy.shop
192.168.1.11 # websrv2.candy.shop
[workstations]
192.168.2.52 # wks1.candy.shop
192.168.2.53 # wks2.candy.shop
192.168.2.54 # wks3.candy.shop
[databases]
192.168.3.5 # db1.candy.shop
```
## Playbook
The **playbook** is where the real magic happens. Here, we define tasks for each group of machines. In this example, we’ll:
- Install and configure Apache on Linux web servers.
- Install common software like Google Chrome and 7-Zip on Windows workstations.
- Install and configure MariaDB on a Linux database server.
Ansible will connect to the machines in each group and run the tasks defined for that group. Let’s look at how this playbook is structured:
```yaml
---
- hosts: webservers
tasks:
- name: Install Apache
yum:
name: httpd
state: present
- name: Start and enable Apache
service:
name: httpd
state: started
enabled: true
- hosts: workstations
tasks:
- name: Install Google Chrome on Windows workstations
win_chocolatey:
name: googlechrome
state: present
- name: Install 7-Zip on Windows workstations
win_chocolatey:
name: 7zip
state: present
- name: Set PowerShell execution policy to unrestricted
win_shell: "Set-ExecutionPolicy Unrestricted -Force"
args:
executable: powershell
- hosts: databases
tasks:
- name: Install MariaDB
yum:
name: mariadb-server
state: present
- name: Start and enable MariaDB
service:
name: mariadb
state: started
enabled: true
```
Under the hood, those tasks - even `win_chocolatey` and `service` - jut run commands on the remote system. It's just that Ansible abstracted away all those ugly "check, then install, then verify" commands behind a single `win_chocolatey` task set.
## Hit the go button
When you've saved your playbook and inventory files to a directory on your new "Ansible provisioning server", you can run this in that directory to start provisioning all of your systems:
```bash
ansible-playbook -i inventory.ini playbook.yml
```
And, finally, go get some coffee! ☕
---
For larger projects or environments with many systems, you’ll likely need more than one playbook. While it’s tempting to cram everything into one massive YAML file, it’s much better to break things up. One playbook per "logical task" helps keep things organized and manageable.
For example, instead of having a single playbook for everything:
- One playbook for setting up web servers.
- Another for configuring workstations.
- And maybe a separate playbook for managing your database servers.
This way, you can reuse tasks and target specific groups of machines without sifting through a gigantic playbook for every small change.
If your system is complex enough, you may even organize your playbooks into "common/core items", "specific items for that system type", and "specific items for X software on that system" - it all depends on how you want to organize things. Since Ansible is all files-based, you can have multiple directories for other sites, projects, or testing. Ansible doesn't care, it just runs the tasks you give it in the order you tell it to.
# Advanced Usage
As you grow more comfortable with Ansible, there are several features you can use to optimize and customize your automation even more.
## Roles
Roles are a way to break down tasks into reusable components that can be shared across multiple playbooks. Think of roles as modular pieces that can be plugged into different projects.
For example, you could have a `powershell` role that installs and configures your PS environment, a `7zip` role that handles 7-zip setup, and a `chrome` role for installing and hardening Chrome. Each role has its own directory structure for tasks, variables, handlers, and files, which keeps things clean and easy to manage.
To use a role, you would structure your playbook like this:
```yaml
---
- hosts: workstations
tasks:
- name: Include Powershell roll
include_role:
name: powershell
- name: Include 7zip roll
include_role:
name: 7zip
- name: Include Chrome roll
include_role:
name: chrome
```
You would then [create new directories](https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_reuse_roles.html#role-directory-structure), `powershell`, `7zip`, and `chrome` and place your tasks, templates, variables, etc into specific subdirectories of that role. It keeps everything nice and organized!
Roles make it easy to keep your playbooks modular and allow you to build on top of them without starting from scratch each time.
## Tags
You can use tags to run specific parts of a playbook instead of running (or re-running) the whole thing. This is especially useful when you only want to run a subset of tasks, such as software updates, without rerunning everything else in the playbook.
Here’s an example:
```yaml
---
- hosts: all
tasks:
- name: Install Apache
yum:
name: httpd
state: present
tags: apache
- name: Install MySQL
yum:
name: mariadb-server
state: present
tags: database
```
You can then run only the tasks with a specific tag like this:
```bash
ansible-playbook -i inventory.ini playbook.yml --tags "apache"
```
Or skip tags like this:
```bash
ansible-playbook -i inventory.ini playbook.yml --skip-tags "apache,database"
```
This would effectively run nothing, since you're telling Ansible to skip both of the tasks in that playbook.
## Variables
Instead of hardcoding paths, usernames, IP addresses, etc, you can define variables and use them throughout your playbook. You can even define different variables for different groups or systems.
Example:
```yaml
---
- hosts: all
vars:
apache_port: 8080
tasks:
- name: Configure Apache to listen on custom port
lineinfile:
path: /etc/httpd/conf/httpd.conf
regexp: '^Listen'
line: "Listen {{ apache_port }}"
```
Variables can be stored in your inventory file, defined directly in the playbook, or loaded from external files.
## Secrets
Ansible Vault allows you to encrypt sensitive data like passwords or API keys. You can then reference these secrets in your playbooks (as variables) without exposing them in plain text.
To create an encrypted file:
```bash
ansible-vault create secrets.yml
```
Choose a password that you'll remember, since you'll be using it every time you run any playbooks that require this secrets file.
Inside this file, you can define variables like:
```yaml
db_password: myS3cr3tP@ssw0rd
admin_password: abcXYZ123!
```
Then, in your playbook, reference the secrets file as a variables file:
```yaml
---
- hosts: databases
vars_files:
- secrets.yml
tasks:
- name: Set up the database
mysql_user:
name: admin
password: "{{ db_password }}"
```
And, finally, run the playbook with:
```bash
ansible-playbook -i inventory.ini playbook.yml --ask-vault-pass
```
This will prompt you for the password you set for the secrets vault earlier, so hopefully you still remember it.
Vaults are a great way to do encryption-at-rest, and many other Dev\*Ops projects support handling Ansible Vaults (secrets files).
## Templates
Templates allow you to dynamically generate configuration files or scripts using [Jinja2 templating](https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_templating.html). Instead of hardcoding values into configuration files, you can use variables within templates, which Ansible will substitute when the playbook runs.
Here’s an example of a simple Apache configuration template:
**apache.conf.j2**:
```jinja2
<VirtualHost *:{{ apache_port }}>
ServerName {{ server_name }}
DocumentRoot {{ document_root }}
</VirtualHost>
```
You can then reference this template in your playbook and pass in its variables:
```yaml
---
- hosts: webserver
vars:
apache_port: 8080
server_name: www.example.com
document_root: /var/www/html
tasks:
- name: Deploy Apache config from template
template:
src: apache.conf.j2
dest: /etc/httpd/conf.d/apache.conf
```
Templates are especially useful for generating configuration files that change across environments or systems.
## Playbooks on playbooks
Sometimes, playbooks need to call other playbooks to keep things organized and manageable. Instead of having one massive playbook, you can structure your automation by calling smaller, more specific playbooks from a "master" playbook.
Here’s how:
```yaml
---
- hosts: localhost
tasks:
- name: Run common setup tasks
include_tasks: common_setup.yml
- name: Run webserver playbook
include_tasks: webserver.yml
- name: Run database playbook
include_tasks: database.yml
```