Getting Started with Ansible
Intro
Alright folks this is gonna be a long one so grab some tea. This wont be a walk through really of how to do this on your system tho there will be some simple examples to follow if you are so inclined, and to help with navigating it here’s a table of contents to keep track.
What is Ansible
Ansible is an IT automation tool. Simply put, you can automate the maintenance and creation of your infrastructure saving you time on deployments. Wether you are administering a simple homelab or orchestrating an entire datacenter it can help simplify your workload making most if not all your tasks manageable within a simple file.
How to Install
To ensure you always have the most up-to-date versions of Ansible and Ansible Navigator, it’s recommended to use pipx
, a tool that avoids dependency issues and system conflicts. Here are the steps to install them:
1
pipx install --include-deps ansible ansible-navigator argscomplete
If you want shell completion for the commands, you can add the following:
1
pipx inject --include-apps ansible argscomplete
To configure argscomplete
, follow these steps (please note that this method requires Bash 4.2 or later):
1
activate-global-python-argcomplete --user
Congratulations! You now have Ansible installed with tab completion.
Basic Commands
I won’t be going into detail on all the commands; that’s what manpages are for. However, I can provide a few examples of the commands you will be running and where some of the default files will be located.
FIles located in /etc/ansible
ansible.cfg
- This file is the default configuration file for Ansible, allowing you to specify or customize its behavior. It includes settings such as SSH configurations, default inventory files, remote users, roles paths, module paths, logging, and plugin configurations. hosts
- Often referred to as the inventory file, this document defines the hosts and groups within your environment, along with some variables you can specify for the hosts. roles
- This directory allows you to store specific roles that you can call and reuse in your playbooks. The common file structure for the directory is as follows:
1
2
3
4
5
6
7
8
9
10
$/etc/ansible> roles/
- tasks/ #contains the main playbook tasks for the role
- handlers/ # contains handler tasks that can be notified by other tasks
- templates/ # holds jinja2 templates that can be used in tasks.
- files/ # contains files that can me copied to remote hosts
- vars/ # stores variables specific to the role
- defaults/ #stores default values for role variables
- meta/ # contains metadata about the role
- readme.md # documentation about the role
- When you include a role in your playbook, Ansible will execute the tasks defined in the role’s
tasks/main.yml
file.
Host and Inventory file configuration
This file is written in the .ini format and contains host information for devices you want to automate. You can define specific groups for your devices or assign variables to them, such as login credentials.
Here is an example structure:
1
2
3
4
5
6
7
8
9
10
11
[web_servers]
wb1.example.com
192.168.0.254
[db_servers]
db1.example.com
db2.example.com
[dev_env:children]
web_servers
db_servers
As you can see, you can specify a machine either with a domain name or an IP address and group them together in a group by using a semicolon :
.
How I will be using it
In my homelab, I host multiple systems, primarily Linux-based, on my network, including Docker containers. There are three primary tasks I want to be able to accomplish:
- Update all systems: Ensure all systems are up to date.
- Initialize and set up systems and containers: Configure new systems and containers as needed.
- Automate Docker updates, installations, and configurations: Automatically manage Docker updates, installations, and configurations across the network.
Updating Systems
Since I am constantly changing my homelab, I won’t be providing any specifics about what I am currently using. If you would like updated playbooks for what I am currently running, stop by my GitHub and take a look.
Here is an example of how to update an Ubuntu system and a Windows system:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
---
- name: Update Ubuntu and Windows 10 machines
hosts: all
become: yes
tasks:
- name: Update Ubuntu packages
apt:
upgrade: yes
update_cache: yes
when: "'ubuntu' in inventory_hostname"
- name: Update Windows 10 using win_updates
win_updates:
category_names: SecurityUpdates,UpdateRollups
state: installed
when: "'windows' in inventory_hostname"
System Initialization
Because I’m constantly changing things, having the ability to hit a delete button and run a script to rebuild everything is incredibly valuable. It’s a liberating feeling, knowing that if something goes down or gets corrupted, I can bring it back with just one command. As I mentioned before, if you want to stay updated on the playbooks I currently have in place, stop by my GitHub. If you’re looking for an example of how to build a system, here’s one on setting up a web server on Ubuntu (keep in mind this doesn’t follow best practices):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
---
- name: Setup Web Server
hosts: webserver # Host group containing your target Linux system(s)
become: yes # Run tasks as sudo
tasks:
- name: Update apt package cache
apt:
update_cache: yes
- name: Install Apache web server
apt:
name: apache2
state: present # Ensure Apache is installed
- name: Start Apache service and enable it on boot
service:
name: apache2
state: started
enabled: yes
- name: Ensure Apache service is running
wait_for:
port: 80
timeout: 300 # Wait for 5 minutes for Apache to start
Docker Automation and Deployment
Now, for the main bread and butter! Docker deployments via Ansible. This has got to be my favorite thing to do so far, and it makes my homelab deployment super quick. Because I am still writing more playbooks, please visit my GitHub for my updated scripts. Just as an example, here is how I am currently deploying Portainer in my homelab.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
---
- name: Portainer Deployment
hosts: docker-hosts
tasks:
- name: Deploy Portainer
community.docker.docker_container:
name: portainer
image: portainer/portainer-ce
ports:
- "9443:9443"
- "8000:8000"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- portainer_data:data
restart_policy: always
Network and System Automation
How to control network devices
Your scripts can vary widely depending on your network’s complexity and specific use cases. A good starting point is ansible-galaxy
. By checking online here, you can search for extra plugins to assist in automating various systems. For example, there are plugins available for Cisco, Juniper, and UniFi products. In my case, I need to manage a PFSense box, so I will be using the pfsensible.core
plugin, which can be installed using the following command:
1
ansible-galaxy collection install pfsensible.core
This module enables me to manage my firewall settings via Ansible. If you’d like to see example playbooks, you can check them out on my GitHub!
If you are not using PFSense and just want to try configuring something simple like ufw
on the host then you can do so like this:
1
2
3
4
5
6
7
8
9
10
11
12
---
- name: Configure ufw
hosts: host
become: yes
tasks:
# FIREWALL SETUP
- name: Open SSH port
ufw:
state: enabled
rule: allow
port: "22"
proto: tcp
How to control systems
You can control other systems similarly; however, most tasks can be completed with the built-in modules.
For Linux machines, you can call the module for the various package managers and execute commands just as you would in a terminal within the playbook.
Similarly, for Windows, it works the same way. Their built-in modules can assist in controlling their systems and servers as well.
Playbook Creation
Condensing jobs into Playbooks
Playbooks
A playbook is a set of tasks that you want to run in a specific order or by a specific condition, all under one YAML file.
For example, if you want to update your repositories and packages on a Debian-based system, you can do the following:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
---
- name: Update debian servers
hosts: your_server_group # Replace this with the group of servers you want to update
become: true # Enable privilege escalation (sudo)
tasks:
- name: Update apt cache
apt:
update_cache: yes
become: true
- name: Upgrade all packages
apt:
upgrade: dist
autoremove: yes
autoclean: yes
become: true
Now, let’s understand what is going on in the playbook:
- We defined the name of our playbook as
update debian servers
. - We specified the hosts we want the playbook to execute on (these are specified in our
inventory.ini
file). - For this task, we chose to execute it with elevated privileges via sudo, using the
become: true
directive. - Once that has been set, we can define the tasks we want to run. Refer to [[#Tasks]] for more information.
- Under this section, we specify the name of the task we are performing and then execute our commands.
Tasks
Tasks are simply actions that you want to perform on the specified systems. The variables you use will vary based on your specific use case, so you will need to refer to the base documentation or the documentation for the module from
ansible-galaxy
for guidance.Roles
Roles enable you to organize and structure your automation tasks, playbooks, and associated files in a reusable and modular manner. They promote code reusability, maintainability, and organization, making it easier to manage complex automation projects. This concept is described further under [[#Files located in /etc/ansible]].
Handlers
Handlers are a specific type of task used to manage service states on remote hosts, usually in response to changes made by other tasks in a playbook. They are commonly employed to ensure that specific actions, such as restarting a service or reloading a configuration, are only triggered when necessary. This reduces unnecessary service interruptions and improves playbook efficiency. Here is an example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
---
- name: Install and configure Apache
hosts: your_servers
tasks:
- name: Update apt package cache
apt:
update_cache: yes
- name: Install Apache web server
apt:
name: apache2
state: present
notify:
- Restart Apache
handlers:
- name: Restart Apache
service:
name: apache2
state: restarted
In this playbook:
- The first task updates the apt package cache on the target servers.
- The second task installs the Apache web server (
apache2
package) using theapt
module. If the package is installed or updated, it triggers the handler namedRestart Apache
. - The handler
Restart Apache
is defined under thehandlers
section. When notified, it restarts the Apache service using theservice
module.
In this example, the handler will only be triggered if the Apache package is installed or updated, ensuring that the service is restarted specifically when the installation state changes.
Running a Playbook
Using the “ansible-playbook” command
1
ansible-playbook -i inventory_file playbook.yml
Here’s what each part of the command does:
ansible-playbook
: This is the command used to run Ansible playbooks.-i inventory_file
: Specifies the path to the inventory file, which contains information about the hosts you want to manage with Ansible. Replaceinventory_file
with the actual path to your inventory file.playbook.yml
: Specifies the path to the playbook file that you want to execute. Replaceplaybook.yml
with the actual path to your playbook file.Using the “ansible-navigator” command
1
ansible-navigator run playbook.yml
In this command:
ansible-navigator run
: This part of the command indicates that you want to run an Ansible playbook usingansible-navigator
.playbook.yml
: Specifies the path to your playbook file. Replaceplaybook.yml
with the actual path to your playbook file.Authentication methods
Now, for user credentials, you have two options: you can either store them in plain text, which is not recommended for security reasons, or you can use Ansible Vault to securely encrypt sensitive data with a password. This password should be managed in a secure manner, either in a physically secure location or by using a trusted password manager.
Password Authentication
Just as an example, here is how you would set it up in plain text:
Add the following to your playbook.
1
2
ansible_user=<remote-user>
ansible_password=<remote-password>
- This is only for password authentication.
- If you are using passwords, you need to disable host key checking.
- Go to the
ansible.cfg
file. - Find
host_key_checking
and ensure it is set tofalse
.
Now, let’s get to the proper way to do this. Here is an example of using ansible-vault
:
1
ansible-vault encrypt_string 'my-password' --name 'db-password'
Here, we are instructing ansible-vault
to encrypt the password “my-password” and tag it with the name “db-password”. Once executed, it will prompt for a password and generate the encrypted password.
Here is an example output:
1
2
!vault |
$ANSBLE_VAULT;1.1;AES256 658975795687065786089598769a75965085665085975746760945694669659564975965085497675087597650864767657660795479856976a546087659786596508497a60959568567956807547680795684356789059486367606464395395407498769647a59650835542412145907579653423412143754a597659674354234312549460707454243612a
With this, you can copy it and use it either in your playbook or add it to the device in your inventory file. You can also reference it within your playbook as a variable, where in this case it is named “db-password”.
SSH Authentication
You can also enable SSH login for the servers by first generating an SSH key pair.
Here are some examples:
This first example follows a centralized control model where your Ansible controller has control over the key pair. This can be both good and bad:
Good:
- Simple to deploy and manage.
- Centralized control.
Bad:
- Single point of failure: If the private key is compromised, the attacker can take over everything.
- Lack of granular control: Limited access control for individual machines if you want to restrict access.
Now, here are your steps:
- On your Ansible control machine, generate the key pair:
1
ssh-keygen -t rsa -b 4096
- Next, copy the public key to the remote hosts. Make sure to change the
remote-user
to the user you want to use on the remote machine, and theremote-host
to the domain name or IPv4 address of the remote machine:
1
ssh-copy-id <remote-user>@<remote-host>
- You can test the SSH connection by making a connection with the following command:
1
ssh <remote-user>@<remote-host>
- Now, update your Ansible inventory file with the user and host information.
1
2
[web-servers]
wb1.example.com ansible_ssh_user=<remote-user> ansible_ssh_host=<remote-host>
Now, for the second method, it’s a bit more complex but adds better security. You will be making an SSH key for each machine and pushing it to the remote host like before, but this time naming the SSH key file and specifying it in the inventory.ini
file.
- On your Ansible control machine, generate the key pair and name it:
1
ssh-keygen -t rsa -b 4096 -f /path/to/keyfile
- Next, copy the public key to the remote hosts. Ensure you change the
remote-user
to the user you want to use on the remote machine, and theremote-host
to the domain name or IPv4 address of the remote machine. Make sure you specify the public key file with the-i
option and the path to the file.
1
ssh-copy-id -i /path/to/keyfile.pub <remote-user>@<remote-host>
- You can test the SSH connection by making a connection with the following command:
1
ssh -i /path/to/keyfile <remote-user>@<remote-host>
- Now, update your Ansible inventory file with the user and file location information.
1
2
[web-servers]
wb1.example.com ansible_ssh_user=<remote-user> ansible_ssh_private_key_file=/path/to/keyfile.pem
Secrets management
Securing passwords, API keys, and other sensitive credentials is crucial in ensuring the safety of your deployments. With Ansible-Vault, you are able to encrypt and secure this sensitive information. Here’s how you can impliment it in your docker deployments.
Installing Ansible-Galaxy Collection
1
ansible-galaxy collection install community.docker`
Before proceeding, ensure Docker and Docker Compose are installed, along with the necessary SDK for the community.docker extension (Docker Python package).
Creating a Docker Container
To start off, here is a basic example of how a container can be made with the community.docker extension.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
---
- name: Portainer Deployment
hosts: docker-hosts
tasks:
- name: Deploy Portainer
community.docker.docker_container:
name: portainer
image: portainer/portainer-ce
ports:
- "9443:9443"
- "8000:8000"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- portainer_data:data
restart_policy: always
In this example we are using the docker_container
command to create the container. from here it is esentialy the same as creating a docker compose file. however here we do not have any secrets to protect.
In scenarios where sensitive information like API keys needs to be added to a configuration file within the container, Ansible can handle this securely using Ansible-Vault.
Securely Adding API Key to Configuration
1
2
3
4
5
6
7
8
9
- hosts: host
vars_prompt:
- name: api_key
prompt: Enter the API key
tasks:
- name: Update API Key
ansible.builtin.lineinfile:
path: /path/to/config/file
line: "API_KEY={{ api_key }}{% endrAw %}"
In this example we are prompting the user of this playbook to enter in the API Key before continuing on. While this can work for small or homelab deployments. For large enterprise use this can be cumbersome, and remembering lengthy, complex strings can be challenging. Ansible-Vault simplifies this process by allowing you to secure and automate sensitive data.
Encrypting Sensitive Data with Ansible-Vault
To encrypt the API key using Ansible-Vault we will need to create a file that will store all our sensitive information. To do so we can run the following command:
1
vim password_file.enc
Now this can be anywhere but preferable within the folder structure where your playbooks are kept. We can add in our password and label it in the file in the following format
1
api_key: my_password123
now you can use ansible-vault
to encrypt the file.
1
ansible-vault encrypt password_file.enc
when executed it will ask for a vault password to encrypt the file. once complete, you can verify that it is encrypted. you should see something like this in the file
1
2
$ANSIBLE_VAULT;1.1;AES256
43891207412936486234871230682346802349897231047123648732487324896213084603286408123640871326487123847132876408132640812370461230874610236408123640871236408732434876923741234021649123046032784238794927384692130423046021386498132640123640123747812340812360471236471236940234072364012374613294012360413264087132054601328740213412369460324012374601234612836402647326673263073860407820784590320410365365486235759624929
now you can remove the unnessesary vars in the playbook we just made so that it is cleaner and more efficient.
1
2
3
4
5
6
7
-hosts: host
gather_facts: flase
- tasks:
- name
ansible.builtin.lineinfile:
path: /path/to/config/file
line: "API_KEY= }{{ api_key }}"
now when you go to run the playbook you will need to specify the password file. ansible will decrypt the file and use the variables that are defined just like if you defined it within the playbook.
1
ansible-playbook -i inventory.ini -e @password_file.enc --ask-vault-pass playbook.yaml
this will ask for your vault password to decrypt the file and then execute the playbook.
Automate Further
if you would like to automate this even more so that you do not have to enter in a password. you can use a password file that contains the vault password. (make sure to set proper permissions on this file so that only the ansible user can access this as to not leak the master password. this method while being more convinent, if not properly done can be worse than just having credentials in your playbooks)
for this method you just need to create a file that contains your vault password:
1
vim vault-pass
now just add your password by itself
1
2
cat vault-pass
master-pass123
now simply add this to your playbook command and your good.
1
ansible-playbook -i inventory.ini -e @password_file.enc --ask-vault-pass vault-pass playbook.yaml
note that you will need to be executing this playbook as the owner of the vault-pass file otherwise it will not work.
Managing Credentials
now to edit the passwords in the vault you can either decrypt > edit > reencrypt, or you can use the edit command with ansible-vault. here are examples of both.
ansible-vault edit
1
ansible-vault edit password_file.enc
now you can just edit it within the terminal
- decrypt > edit > encrypt way
1
2
3
4
5
ansible-vault decrypt password_file.enc
vim password_file.enc
ansible-vault encrypt password_file.enc
there you have it! now you know how to manage secrets within your playbooks. now their are ways to integrate a password manager to manage the secrets tho i will not be going over that method here as it is a bit out of scope.
Conclusion
Whewww… Thank you all for reading! I hope this was a good starting place for you to get into Ansible within your homelab. trying to find a guide that would fit my needs had me going from article to article and video to video ultimately ending in me just reading the docs and playing around. if you know any ways i can make improvements to either the projects in my git repos or information in general feel free to reach out!