Ansible or How to Mess up AT SCALE!
Getting Started
To get started you will need two things: Text Editor, Python 3. You can use whatever text editor you want to use, Visual Studio Code is a popular one that runs almost anywhere. As long as you're comfortable with it, use it. If you can run python and ssh you have everything else you need.
Note
-
It is possible to run this on Windows itself, I do not use Windows so I can not help too much. If you have WSL working then you should be able to everything. If you're having problems we do have other systems that can be used
Ansible is in most package repos, for this we're using ansible-core. I recommend you install it via pip to you can ensure you have the same thing as we use in prod
Installing ansible is pretty easy, either use your package manager or pip
# pip
pip install --user ansible-core
# Redhat Systems
dnf install ansible
# Debian Systems
apt install ansible
Once you have ansible installed let's run our first command to make sure everything is in good working order ansible -i localhost localhost -m ping
See that was pretty easy!
My First Inventory File
The next thing we're going to do is create a very simple inventory.ini file.
This file does one thing, tells ansible that localhost
is in my_group
That's all it does! Let's run our command again, this time using the inventory file.
Success
So we told ansible to use our inventory file -i inventory.ini
and that we want to connect to my_group
and run the ping module.
What if we wanted to run against all the hosts in our inventory file? We would use the special group named all
[!success]
β― ansible -i inventory.ini all -m ping localhost | SUCCESS => { "ansible_facts": { "discovered_interpreter_python": "/usr/bin/python3" }, "changed": false, "ping": "pong" }
We can run commands too!
Success
This told ansible that I want to run ls
on localhost
Let's try something a little more fun!
Success
What changed there? We told ansible that we want to use the ansible.builtin.command
module. Ansible assumes that if you do not specify a module name that you just want to run an adhoc command. So what's a module? Simply put module is a connection of stuff that tells ansible how to do do stuff.
Let's take this another step farther, how about we use ansible to make sure my mac does not have the dreaded sl
command!
Example
We can see that it says "changed:" false
that means that it was not there. So no worries about being punished for messing up up a ls
And now let's make sure I have the wonderful toilet
command!
Example
So now we know how to use ad hoc commands. We can run them on all the hosts in our inventory file or just a group. What if we want to do more?
Using the flag --limit
we can limit ansible to one host or just a group of hosts e.g. -- limit hostname.here.local
Yaml? What the Hell is a Yaml?
YAML stands for YAML Ainβt Markup Language. It's what most things use to store key value pairs in a way that humans can manage and understand without too much help. Pretty much everything with Ansible, (and most cloud stuff), uses yaml. When it comes to ansible it only cares about the content of the file and not the name, so you can use .yml
or.yaml
it only really matters to the humans (and sometimes Windows)
Yaml can be a real pain so do yourself a favor, use a linter
pip install --user yamllint yamlfix prettier
What the hell is a linter?
A linter is something that makes sure you code is functional. It will let you know where things are broken and maybe give you a hint on how to fix it. A formatter can help with that, while It doesn't check if it is actually good or functional it tries to make it readable. So you should be using yamllint
to check your files for problems and yamlfix
to help fix them automatically. Once your good you can use prettier -w
to make it pretty. Don't worry, there's ways of automating this
Let's just jump right into the deep end and make an inventory file using yaml. Ideally any inventory file you will be using will be some kind of yaml. It doesn't matter if it's for ansible or kubernetes for example. Let's take a look at a simple inventory.yaml file
Here way have two hosts: alice & bob. If this was in ini it would look like
So not much of a different from our first inventory.ini We can check this file with the ansible-inventory
command
Nothing really complex so far, let's add some more hosts and groups into our inventory.yml
Example
As you can see we can have many groups even groups that have other groups inside them. We can keep nesting groups as long as we follow the hosts, children, hosts naming. We can make sure this is a valid inventory by looking at with ansible-inventory
β― ansible-inventory -i inventory.yml --graph
@all:
|--@ungrouped:
|--@group_1:
| |--strangechicken
| |--tumescentcloaca
|--@group_2:
| |--wowzer
| |--imaproblem
|--@group_3:
| |--group_1
| |--alice
|--@group_4:
| |--group_2
| |--bob
|--@group_5:
| |--group_3
| |--group_4
My First Playbook
Now running commands one at a time is fine but it's a lot more fun to have something that does a lot of things on a lot of things. This is where Ansible shines!
This is also where things start to get more information dense. So buckle up!
Ansible will not change anything that doesn't need to be changed, this is important for ensuring something called idempotency. That means that no matter how many times we run the same thing, we're going to get the same results.
Let's take a look at a simple but real playbook: change_genesis.yml
Example
---
- name: Change Genesis Server
hosts: all
become: true
vars:
genesis_ip: 10.194.118.129
files_to_change:
- /etc/profile.d/genesis.sh
- /etc/resolv.conf
- /etc/profile.d/yum.sh
tasks:
- name: Replace Bad Genesis
ansible.builtin.replace:
dest: "{{ item }}"
regexp: 10\.173\.0\.129
replace: "{{ genesis_ip }}"
loop: "{{ files_to_change }}"
So what does this do? It may look complicated but it's fairly simple. This says look at these files for this string and replace it with that string.
Starting from the top we have name:
this is the name of the play. The play is what ansible calls it's group of actions or a single action. All plays should be named starting with a capital letter and end with a letter. It will still work if you do not do this, but it will complain the entire time. Kinda like when you don't put on your seat beltβ¦
Next hosts:
and become:
this is the host, hosts, and or group name to run this on. Most of the time this is going to set to all
. become:
tells ansible that it needs to use sudo aka root powers to do these tasks. The higher up in the play the more things will use sudo powers.
Now vars:
is a special:
Like become:
it can be used most anywhere in a play, or it can reference other yaml files. So if we were to put vars in something like our inventory file it would apply per host, per group, or even for everything! In this case we're only saying that the plays before need these variables. Variables can also be anything we want. If I want foo to have the value of bar then I put foo: bar
.
genesis_ip:
is the ip to the genesis server in this case we're using10.194.118.129
files_to_change:
lists the files that we need to check.
Further down we get to tasks:
tasks are the plays that that playbook is running. In this case we have single play named Replace Bad Genesis
. The module name is ansible.builtin.replace
that makes it function like sed
. The next lines are telling it where and what. dest:
is the destination file, regexp:
is a regular expression, and replace:
is what is going to replace the matched expression.
This play has something special attached to it called a loop:
this tells the play that there's going to be more things to do so it will function in a loop, kind of like for x in
a bash loop.
When you call a variable in you have to use another kind of language aside from yaml. It's a templating language/engine called Jinja2. Templating languages (engines) have been around for long time. They allow us to define how things should look while still allowing us to program-maticly change things. DO NOT WORRY, much like regex, you will not need to know it like you will need to understand yaml.
To call variables we're using jinja values. So in this case the files are represented by "{{ item }}"
and the list of files is represented by " {{ files_to_change }}"
this is more than likely the most complex jinja you will have to remember. The various linter's will handle the rest if you mess it up. So do not worry to much!
Let's look at the same thing done in bash
Example
Now lets look at a script to do the same thing
Example
You could also use the get a bigger hammer approach too grep -rl "10.173.0.129" | xargs sed 's/10.173.0.129/10.194.118.129/g'
there's a lot of ways to do the same thing. So why do it with Ansible? Well idempotency aside we do not have to worry about what system is on the other end. That same module will work the same on Linux, Mac, Windows, BSD, etc. We can also chain multiple playbooks together in ways that trying to string together multiple shell scripts start to fail at. Radssh is an incredible tool but it will only get you so far. Ansible provides a way to validate your configurations and document them in ways that other methods just simply can not do.
Digressions Aside; how Do You Actually Run the Damn Thing!?
> ansible-playbook -i inventory.yml all -u USERNAME -k change_genesis.yml
Let's break this down. ansible-playbook
is the command that handles playbooks, kind of like we used ansible-inventory
to look at out inventory file. -i inventory.yml all
says use all the hosts in the inventory file. -u USERNAME -k
says use this username and prompt for a sudo password so it can be used when needed. Some depending on how sudoers is configured you can leave this out. For most admin/op account -k
isn't needed. Lastly the name of the playbook change_genesis.yml
How-to: Write an Ansible Playbook
The General Layout of the a Playbook
---
# How to use:
# ansible-playbook -i inventory/inventory.file.here playbooks/template.yml
- name: Playbook name
hosts: all
become: false # Change to true for sudo powers
vars: # Any vars needed for this playbook
handlers: # Any handlers needed for this playbook
pre_tasks: # Any pre-tasks needed for this playbook
post_tasks: # Any post-tasks needed for this playbook
tasks: # The main body of the playbook
Playbooks should follow this layout where possible. You may have some things that require more or less.
Formatting and General Rules
General Rules
- Every play should be named started with a capital letter
- Try to to keep lines under 80 col in length
- Use true or false to maintain truthy (yaml spec)
- Place tags where it makes sense
- Place options at the top of a block when possible; this helps with readability
- Place optionals above the module where possible; this helps with readability
- Comments should have two spaces # a single space, then the text (yaml & markdown spec)
- Names should end with a letter where possible
- Registers & variable names should not contain non alpha characters when possible.
- Anything that makes changes to the system when run should be handler when at all possible
- If you have to do ignore errors on something you either have a very niche edge case or it's lazy code. Anything that ignores errors should be a handler too.
Bad Formatting
What's wrong with this?
- There is no name that starts with a capital letter
- There is a comment that isn't spaced out correctly and goes way past 80 col.
- Options are at the bottom rather than the top.
- Contains ignore_errors and yes
- No tags
Good Formatting
Why is this better?
- The task is named clearly and correctly
- You know right away that it is tagged and will trigger a handler
- Does not break truthy or ignore errors
- Does not have an unnecessary or overly long comment
Formatting & Linting
If you follow the general rules then you should be able to format your playbooks with something like prettier
and run ansible-lint
on them. This will let you know if you have any errors that require fixing. You goal is to need none or as few ignores in your .ansible-lint-ignore
file and be as close to the production ready profile as possible.
If you encounter problems with your playbook that are formatting related running yamlfix
then prettier
on them will normally fix them for you.
Once your playbook has been formatted and linted you can try it out.
The End-ish
This is has been an overview on Ansible. It is by no means a complete or even good overview. It's purely from my memory and there are bounds to be gaps or things I'm making assumptions about. I will be adding this document to github in the future once I am mostly satisfied with it's content.
As with everything in life; YMMV, caveat emptor, pass performance doesn't equal future success, andβ¦
Have an Average Day :|