Hearing about Ansible lately? Everyone's using it! But what's the buzz about, what is ansible actually? And why should you use it? Lot's of questions and lot's of answers. Let's dive!
What is it?
Well Ansible is a tool to automate the setup of an EC2 instance, a vagrant box, and almost anything that you can ssh into.
What's the usecase?
Imagine you've to setup an instance and use it as a server using nginx. Simple, right? Just ssh into it, do the nginx conf and leave it running. But wait, you did the setup on say an Ubuntu instance, but for some reasons, you have to change your server OS, to something like a Fedora may be? Or an Amazon linux? Now, you need to repeat all the steps you just did for ubuntu setup to perform the exactly same task. Another usecase is to setup multiple instances automatically, all you need to define is the tasks, roles etc. for Ansible. We'll be getting to it shortly.
Installing Ansible
sudo apt-get install ansible
should do just fine. Or dnf
in place of apt-get
for RHEL. Interestingly, you don't need to install ansible on the target machine! You just install it on your local machine once. And just exploit it using the tasks you define. This is because Ansible basically just SSH into your remote machine, and executes task if required.
Getting started
We'll start with doing everything in a get it done way. And then move to the way of how it should be done. Firstly you define the target(s) to act upon. This is called the inventory. You can name this file anything, inventory, hosts, instances, etc. it's upto you. In this file, type in the IPs of the instances you wish to control in format:
[name]
0.0.0.1 ansible_user='ubuntu' ansible_ssh_private_key_file='path/to/privateKey/for/server'
0.0.1.2 ansible_user='userName_to_login_as' ansible_ssh_private_key_file='path/to/private/key'
You can access these IPs using the *name* you provided. If you want to test this on vagrant, write an inventory file in the format:
[vag]
127.0.0.1 ansible_port=2222 ansible_user='vagrant' ansible_ssh_private_key_file='/path/toVagrantfile/.vagrant/machines/default/virtualbox/private_key'
If you wish, you can follow along by performing the commands on your machine instead of a server too. Using localhost:
[me]
localhost ansible_connection=local
Now, if you wish to use localhost, just use me
in place of vag
/groupName
.
If the inventory is ready we can begin executing the commands!
Note: Though most of the commands used in this blog are non destructive and would not change anything in your PC to cause problems. Caution should be excercised when hacking through the different commands on your own. Especially if the target machine hosts important data, or you are performing all the commands on your localhost
Note: If you get an error/warning related to usage of python2 interpreter, you can create a file ansible.cfg
and specify python3 interpreter by writing this in the file:
[defaults]
interpreter_python = /usr/bin/python3
Executing some commands
Ansible can work only when the target machine is ready to ssh, and the credentials provided in the inventory file are correct. Now that ansible knows how to authenticate itself to the machines, it'll have no problems executing any commands.
A little idea about Modules and arguments, and we are ready to go.
Modules are small pieces of python code, that ansible uses to handle resources on the target machine. And some modules require arguments to act upon. Let's see first an example that doesn't require an argument.
Ping module. Since we have already provided the inventory file, we don't need to provide ansible with the IPs of the instances to ping.
We can simply do:
ansible -i inventory_file groupName -m ping
groupName, like we provided vag
in the inventory file example above. It's just a collection of IPs or domains along with their options under one name.
-m
: states that we're going to use a module Whose name in this case is ping
If you've provided inventory file and group name correctly you should get an output like:
default | SUCCESS => { "changed": false, "ping": "pong" }
Congratulations, you just executed your first command using ansible.
Now let's try one example which takes an argument. How about a Hello World sample? For this we're going to use shell
module.
ansible -i inventory groupName -m "shell" -a "echo Hello World!"
Output comes out to be:
default | SUCCESS | rc=0 >> Hello World!
Let's try one last thing, installing a package. We'll be using apt module.
ansible -i inventory groupName -m apt -a "name=nginx state=present update_cache=true"
The argument here is self explanatory. apt module, takes arguments name, state mainly. Name states which package to act upon, and state tells what action is needed to be performed if any. This makes sure idempotence is there. Means, the same action performed repeatedly should not alter the state of the resource. state can be absent, present, latest, etc.
Update_cache is suggested because most of the images used to setup an instance would require you to update repository cache before you install anything.
This command should give you an error
default | FAILED! => { "changed": false, "msg": "Failed to lock apt for exclusive operation" }
And that is because the default user doesn't have enough privileges. For privilege escalation, we need to supply another flag. -b
.
ansible -i inventory groupName -b -m apt -a "name=nginx state=present update_cache=true"
-b
or --become
tells ansible whether to activate or deactivate privilege escalation. Default for privilege escalation is root, and hence the command will be executed as root. But you can also specify another user, as whom you'd like to execute the command. This can be done using --become-user userName
. Note that, if you the target system has requires a password, you need to pass -K
flag to ask for become pass.
Now the terminal will take time depending on the internet connection available to the target machine and will generate output like:
default | SUCCESS => {
"cache_update_time": 1526681063,
"cache_updated": true,
"changed": true,
"stderr": "",
"stderr_lines": [],
...
}
In every output the value for
"changed"
variable is the gist of success or failure. This and the color of the output are enough to denote unchanged, changed, failure.
But ansible can do a lot more. This method is easy if you need to run one single module. But this is often not the case. You'd almost always require a collection of modules to setup a target machine. To see how many modules ansible provides head on to this page. In that case giving arguments to ansible command wont be the best choice. Now enters ansible playbook.
Ansible playbook
Playbook is like a collection of ansible commands put together in a single YAML file. We performed 3 tasks all unrelated, but in case of a setup used to install and configure a single package inside the target machine, like setting up nginx, would require more tasks. And it'd be more logical to group all of them in a single playbook. Let's create a simple playbook, to say hello world, and then we'll move to installing nginx using playbook.
A bit of YAML
Before typing out your first YAML, let's just get a basic idea of it's syntax. These syntax basics have nothing specific with ansible. These basics will be same in every YAML file, so if you are already familiar with it's syntax, feel free to skip to the playbook part.
A YAML file can optionally begin with ---
(triple dash) and end with ...
(triple dot)
#
before any line comments it.
A list is represented by lines starting with a -
at same indentation level. For example:
---
numbers:
- one
- two
- three
- four
...
And a dictionary is represented like:
---
name_of_dictionary:
key1: value1
key2: value2
...
Now there can be combinations of keys, dictionaries. Let's do one example of list of dictionary that is used in playbooks.
---
- dictionary1:
key1: value1
key2: value2
key3:
- one
- two
- dictionary2:
key4: value4
key5: value5
key6:
- foo
- bar
...
Here we have a list of dictionaries, dictionary1 and dictionary2. In these dictionaries, some of the values are themselves a list. Like key3 in dictionary1 and key6 in dictionary2.
If you're a pythonista, there's another format that'd be more familiar to you:
---
dictionary1: {key1: value1, key2: value2}
list1: ['one', 'two', 'three']
...
There's whole lot of other details that you might like to have a glance. They are available with the ansible documentation.
Back to the playbooks
Let's create the playbook 'nginx.yml' now, and then we'll see that in detail:
---
- hosts: vag
become: yes
tasks:
- name: Install Nginx
apt:
name: nginx
state: present
update_cache: true
This is just the playbook version of the bash command we performed earlier using ansible to install nginx.
First let's see what each key means and then we'll run this playbook.
We can perform a lot of tasks for different hosts defined in the inventory, inside a single playbook. We can have a list of dictionaries, and depending upon the hosts specified, the tasks will be executed for that specific host. If you want to perform the tasks for all the hosts defined in the inventory file, you can also give the value all
for hosts key. Here we are considering the host vag
we defined previously in the inventory.
become
is required to use apt. Otherwise, we'd come across an error (as we did while executing directly using bash). Note that, yes
is just one of the ways to provide a boolean value. You can specify True
, TRUE
, yes
and so on...
tasks
is again a list of dictionaries defining the tasks we want to perform against the target machine.
Inside tasks, the first key is name
, we can name it anything, this will be shown when we run the playbook, so keep the name quite descriptive.
Secondly we specify the module to use for this first task. Here apt
is used.
Now we are supplying arguments to apt
using YAML syntax, values are same as we did while executing from terminal. The arguments are just key value pair, where the module name acts as dictionary name.
You can also specify latest
as a value to state
key. This will update the package too, if an update comes. Whereas present
would just ensure that the package is installed on the target machine.
Run the playbook using:
ansible-playbook nginx.yml -i inventory
This is same as the single line command we performed earlier, just that we have a named task here. Output here will be similar to this:
(If you are trying this on your local machine, you might need to pass a : --ask-become-pass
or -K
as a flag to ansible-playbook
command.)
PLAY [me] **********... TASK [Gathering Facts] **********... ok: [vag] TASK [Install Nginx] **********... changed: [vag] PLAY RECAP **********... vag : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Now you got a playbook running, you can add more tasks, find more modules and hack through. Bigger projects even have a playbook spread over folders, where, tasks, defaults, vars, etc. all have a separate folder and a main.yml in each of them.
Hope this will get you started on ansible.
Thanks a lot for reading!