For as long as I’ve been using linux, I’ve always had the problem of setting up new machines to have the right configuration. I would always have to manualy install, update, and configure my computers and servers to have the exact specs I’d need for the software I would want to run. I always found this to be frustrating and time consuming as I would always either be distro hopping, configuring servers to run my applications, or to just add obscure configurations I would have for each of the machines.
I ended up searching for a solution and found a little piece of software called Ansible! It allowed me to configure, update, and install software across a whole fleet of computers and allowed my machines to be more stable in the long run as well as pre-configured with just an ssh connection. I found the solution to be marveling and decided to begin my automation journey for all of my computers and servers in my life.
In this article, we are going to go through how you can also automate your linux servers, for personal or for work. This is more of a comprehensive guide on how to get setup and running with your anible config. I’ll leave some resources bellow to help you out with more of the specifics. With that in mind, let’s get started!
Installing Ansible
First things first, go ahead and install ansible or if you’re like me, just install it using your package manager (e.g. chocolatey, homebrew, apt, etc…).
Tasks, Plays, and Playbooks
Tasks are the smallest unit in Ansible and it’s the core of what ansible is all about! Tasks use modules to do certain actions. You can think about modules like plugins. There are millions of diffrent modules you can use and the ansible documentation will help tons with what modules to use and how to use them. Generally if there’s a linux command, then there’s a ansible module for it!
While one tasks by itself is great, it would be helpfull to organize all of our tasks. In Ansible we call these plays! You can kind of make the connection with football, each task is a player in your team and they all come together to make plays on the field.
Next concept is the playbook. A playbook is a collection/list of plays in a single file. That simple! Coming back to our football analogy, it’s the field that you play on. With all of that in mind, let’s create a simple example you can run to get the hang of things:
local.yml
- hosts: all
become: true
connection: local
tasks:
- name: create file
file:
path: "{{ ansible_env.HOME }}/hello_ansible"
owner: jesse
group: jeese
- name: insert hello_world
lineinfile:
path: "{{ ansible_env.HOME }}/hello_ansible"
regexp: '^hello_world'
line: hello_world
let’s review what’s going on here:
- created a playbook file named local.yml
- we set our hosts variable to the desired “computers/servers” we’d like to run the play on.
- defined that we should use a local connection instead
- set become to true. This gives elevated privledges to our play. Like sudo or running-as-admin.
- define the tasks we would like for our specified play.
- we defined the name of the specified task
- the module we would like to run, in this case file and lineinfile
- specified the properties for each of the modules for the desired state.
Awesome, you’ve created your first playbook! With all of that in mind, lets run this playbook on our own machine!
sudo ansible-playbook local.yml -k -i 127.0.0.1
You should now see a file in your home dirctory called hello_ansible. it should also have the contents of hello_world! You now know enough to be dangerous with ansible! These are the building block concepts of ansible and each of these can be abstracted into further levels. Let’s move on to our next concept.
The Inventory
The inventory is a list of machines/hosts by ip’s that can be aliased/grouped by labels. You’d otherwise have to define your machines by ip’s using the commandline which can be tedious and time consuming. This will make running playbooks much easier when we start adding lots of machines. There are a few ways to define inventory but we’ll use my favorite with a directory rather than a single file.
Let’s start by creating a inventory directory:
mkdir inventory
touch inventory/local
You should now have a inventory directory like this:
inventory
└── local
All files with a depth of one in the inventory directory will be sourced in via ansible. The names of these files can be up to you, but I like to start with the local file. Let’s go ahead and add our first machine/host to our inventory/local file
inventory/local
[workstation]
127.0.0.1 ansible_connection=local
As you can see, we’ve added a label for our local machine called workstation and defined to ansible that we’re using a local connection.
Let’s also go ahead and update our playbook to use the new inventory label:
local.yml
- hosts: workstation
become: true
tasks:
- name: create file
file:
path: "{{ ansible_env.HOME }}/hello_ansible"
owner: jesse
group: jeese
- name: insert hello_world
lineinfile:
path: "{{ ansible_env.HOME }}/hello_ansible"
regexp: '^hello_world'
line: hello_world
As you can see, we modified out hosts to now use our alias of workstation. Also since we added ansible_connection=local to our inventory file, we no longer need connection: local in our playbook definition.
Now let’s go ahead and see if we can run our playbook with having to define our ip:
sudo ansible-playbook local.yml -k
Perfect! Let’s go ahead and move on to our next concept.
Roles and Taskbooks
Roles are a collection of taskbooks, files, and variables rolled up to create a giant playbook! Taskbooks are just playbooks but without the playbook definition. Don’t worry, we’ll dive deeper into the diffrences further but first, let’s create our first role:
mkdir -p roles/workstation/{files,handlers,tasks,templates,vars}
touch roles/workstation/{handlers,tasks,vars}/main.yml
You should have a directory that looks like this:
roles
└── workstation
├── files
├── handlers
│ └── main.yml
├── tasks
│ └── main.yml
├── templates
└── vars
└── main.yml
Each of these has there own function. Let’s do a quick summary of each one:
- files:
- for static configuration files or assets
- handlers:
- plays that are triggered by the notifiy attribute on plays
- tasks:
- where all of your plays/taskfiles you write will exist
- templates:
- for dynamic configurations using jinja templates
- vars:
- where all of your varaibles for the role will exist
Now that we have a basic understanding, let’s convert out existing playbook into the role:
local.yml
- hosts: workstation
become: true
roles:
- workstation
roles/workstation/tasks/main.yml
- name: create file
file:
path: "{{ ansible_env.HOME }}/hello_ansible"
owner: jesse
group: jeese
- name: insert hello_world
lineinfile:
path: "{{ ansible_env.HOME }}/hello_ansible"
regexp: '^hello_world'
line: hello_world
You should now be able to run the role using the playbook:
ansible-playbook local.yml -k
To cover what we changed:
- we removed the tasks from local.yml and put it into our taskbook main.yml
- defined a new property on our playbook definition called roles
- added our workstation role to the property roles
Yay, we’ve converted our playbook to a role! Let’s cover what some of these other directories in our role do.
Files
The name itself is pretty self explanitory but this is where you’d store files you’d use in your roles. Let’s go ahead and create a file and use it in our role:
touch roles/workstation/files/hello_files
echo "hey, this is my role file!" > roles/workstation/files/hello_files
roles/workstation/tasks/main.yml
- name: create file
file:
path: "{{ ansible_env.HOME }}/hello_ansible"
owner: jesse
group: jeese
- name: insert hello_world
lineinfile:
path: "{{ ansible_env.HOME }}/hello_ansible"
regexp: '^hello_world'
line: hello_world
- name: copy file
copy:
src: hello_files
dest: "{{ ansible_env.HOME }}/hello_files"
owner: jesse
group: jeese
Run the playbook again and we should now have a new file! This makes it easy if you need to copy a big config file but don’t need to template it. Let’s move to the next folder.
Handlers
Handlers allow us to create “side effects” whenever a certain task has updated. Easiest example for when you’d want a handler is if you update a service’s config and need to restart the daemon. I’ll use sshd as an example:
roles/workstation/tasks/sshd.yml
- name: configure sshd_config the way I like it :3
lineinfile:
dest: /etc/ssh/sshd_config
line: "{{ item.key }} {{ item.value }}"
regexp: "(^#|^){{ item.key }}"
with_dict: "{{ sshd }}"
notify: restart_sshd
vars:
sshd:
'Port': 69
roles/workstation/handlers/main.yml
- name: restart_sshd
service:
name: sshd
state: restarted
roles/workstation/tasks/main.yml
- name: create file
file:
path: "{{ ansible_env.HOME }}/hello_ansible"
owner: jesse
group: jeese
- name: insert hello_world
lineinfile:
path: "{{ ansible_env.HOME }}/hello_ansible"
regexp: '^hello_world'
line: hello_world
- name: copy file
copy:
src: hello_files
dest: "{{ ansible_env.HOME }}/hello_files"
owner: jesse
group: jeese
- include_tasks: sshd.yml
There’s a lot going on with the examples but you should be able to pick out the notify attribute. We define our handler by the name and use the notify atrribute to call that handler. Handlers are only called at the end of the play so keep that in mind when using handlers.
Templates
Templates allow us to do complex edits to files and to use those templates in our tasks. Let’s convert our lineinfile task in our main.yml to a jinja template.
roles/workstation/tasks/main.yml
- name: insert hello_world
template:
src: hello_world.j2
dest: "{{ ansible_env.HOME }}/hello_ansible"
owner: jesse
group: jesse
- name: copy file
copy:
src: hello_files
dest: "{{ ansible_env.HOME }}/hello_files"
owner: jesse
group: jeese
- include_tasks: sshd.yml
roles/workstation/templates/hello_world.j2
just a jinja template!
{% for i in range(11) %}
{{ i }}
{% endfor %}
you should now see new content in your ~/hello_world file.
Vars
Variables allow us to share values across tasks, templates, and handlers. We’ll just put one in our newly created template file
roles/workstation/vars/main.yml
my_variable: this is my variable!
roles/workstation/templates/hello_world.j2
just a jinja template!
{% for i in range(11) %}
{{ i }}
{% endfor %}
{{ my_variable }}
you should now see the varialbe in the ~/hello_world file.
Summary
Ansible is an awesome little tool and this is just the start of how to use it. The ansible documentation is your friend. There are TONS of community made modules that can help you with tons of ways to automate all the things.
stay tuned for more articles about ansible and server automation in the future!