Ansible
Ansible is a radically simple IT automation engine that automates cloud provisioning,configuration management, application deployment, intra-service orchestration, and many other IT needs.
Designed for multi-tier deployments since day one, Ansible models your IT infrastructure by describing how all of your systems inter-relate, rather than just managing one system at a time.
It uses no agents and no additional custom security infrastructure, so it’s easy to deploy – and most importantly, it uses a very simple language (YAML, in the form of Ansible Playbooks) that allow you to describe your automation jobs in a way that approaches plain English.
On this page, we’ll give you a really quick overview so you can see things in context. For more detail, hop over to docs.ansible.com.
What can we do with Ansible?
Ansible is a tool for automate deployments and it can be included among Puppet or Chef, but its main feature is to be agentless, I mean, it doesn’t requires to install any agent on the managed hosts.
Ansible does a dployment of configurations, installation and actions over several machines, that allows an efective, fast and low resources consumption automate management. It doesn’t requires any database to store options or task to execute. Ansible is based in plain text files written in YAML language that define machines, variables and tasks. To execute task Ansible provides modules written in python that interacts with the managed hosts. The number of modules grows up on every Ansible revision and at the moment of this post it was more than 780.
The way Ansible applies configurations is through a remote connection from the manager host. By default it uses a SSH connection. That means that we cannot manage Windows machines? In fact Ansible allows Windows machines management through WinRM, but the options and the compatible modules will be limited.
Although it is agentless there are two ways to make it agent-like-tool, by executing from the host manager (the one with ansible installed) or from the managed hosts (that requires to have ansible installed to allow this behavior). It doesn’t matter which one we chose, we need any scheduling tools executing ansible, the easy way is with Cron.
Installing Ansible
Depending on the Operative System there are different ways to install Ansible:
- Source Code: only for Linux and MacOSX
- Pip: only for Linux and MacOSX
- Packages: only for Linux distros variants
- Windows: Windows is not supported as manager hosts, so it is not posible to intall ansible on it
For this tutorial I will install ansible over CentOS 7. For the Red Hat based installations we need to install EPEL repositories, except in Fedora that is available from the official repositories.
I will have 2 virtual machines to work with. One will be the Manager and the other Node-1. Manager will be the one with Ansible installed, but both requires Python.
Ansible depends on Python 2.6 or 2.7. Python 3 usage is under Technology Preview for Ansible 2.2
$ sudo yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm $ sudo yum install -y python ansible $ ansible --version ansible 2.2.0.0 config file = /etc/ansible/ansible.cfg configured module search path = Default w/o overrides
As I will use SSH connections the first thing I need is to create the pair of ssh keys and to copy them to the managed hosts. I will use the manager host as a managed host too, so I need to copy the key on localhost.
$ ssh-keygen Generating public/private rsa key pair. Enter file in which to save the key (/home/centos/.ssh/id_rsa): Enter passphrase (empty for no passphrase): Enter same passphrase again: Your identification has been saved in /home/centos/.ssh/id_rsa. Your public key has been saved in /home/centos/.ssh/id_rsa.pub. The key fingerprint is: 30:7e:59:4c:70:97:6c:67:5b:94:29:ef:f4:48:9b:a8 centos@manager.ansible.test.com The key's randomart image is: +--[ RSA 2048]----+ | ..o... .+| | + .+.oo.| | o o. ooo | | . o o oo | | . S oo+.| | . . +..| | . | | E | | | +-----------------+ $ ssh-copy-id -i .ssh/id_rsa.pub localhost The authenticity of host 'localhost (::1)' can't be established. ECDSA key fingerprint is b5:47:7b:dd:d7:16:07:0e:97:5a:bd:6b:21:e9:b9:e6. Are you sure you want to continue connecting (yes/no)? yes /usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed /usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys Number of key(s) added: 1 Now try logging into the machine, with: "ssh 'localhost'" and check to make sure that only the key(s) you wanted were added. $ ssh-copy-id -i .ssh/id_rsa.pub node-1.ansible.test.com The authenticity of host 'node-1.ansible.test.com (192.168.150.110)' can't be established. ECDSA key fingerprint is 9c:de:24:4e:ff:2a:55:05:35:b3:76:41:86:d3:58:21. Are you sure you want to continue connecting (yes/no)? yes /usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed</pre> <pre>/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys Number of key(s) added: 1 Now try logging into the machine, with: "ssh 'node-1.ansible.test.com'" and check to make sure that only the key(s) you wanted were added.
Configuring Ansible
Ansible has its config file in /etc/ansible/ansible.cfg. There are several options that we can use grouped into blocks. This are the most used options:
- [defaults]: block for default Ansible configuration
- inventory: defines the inventory file path, by default /etc/ansible/hosts
- sudo_user: the user for sudo login, by default root
- forks: the number of parallel processes that ansible will run, by default 5
- timeout: the time to wait for SSH connection establish, by default 10 seconds
- log_path: the path for the log file, /var/log/ansible.log by default
- nocows: if the valu is 0 and it is installed cowsay, it will show animals informing about the playbooks, by default is 1
- [privilege_escalation]: options relate to privilege escalation
- become: if its value is True, the remote user will try to escalate privileges, default value is False
- become_method: the method employed for privilege escalation by default sudo
- become_user: the user that is escalate, by default root
- [ssh_connection]: options for the SSH connections
- ssh_args: options to be passed to de ssh client command, by default -C -o ComtrolMaster=auto -o ControlPersist=60s
- control_path: ansible uses multiplexing to reduce the number of connections, this options declares the socket file to be created/used, by default %(directory)s/ansible-ssh-%%h-%%p-%%r
- scp_if_ssh: the mechanism to transfer files, by default is set to smart tries to use sftp and if fails tries scp
- [colors]: defines the colors used by the ansible playbooks messages
Ansible offers different locations for the predecence of the configuration file. The default one could be overriden by ~/.ansible.cfg and it could be ovverriden by ${CWD}/ansible.cfg.
The inventory file
In this file are defined the managed hosts. They can be identified by their IP or hostname. Also they can be grouped in order to configure several hosts at same way. Hosts not belonging to any group must be declared before any group. Lets take this example:
mkdir ~/project1 && cd ~/project1 cat <<EOF> inventory another-manager.ansible.test.com node-2.ansible.test.com [manager] manager.ansible.test.com [webservers] manager.ansible.test.com node-1.ansible.test.com EOF
There are two hosts at the beginning , another-manager y node-2. There is also a group called manager with one host only, and it doesn’t need to be defined before. The other group called webservers contains two hosts. A host can be declared in several groups. The main focus of the groups is to configure several machines in the same way, as for example the webservers installs the Apache package and they will have the same content deployed. We will see this later.
First running
Once we have our inventory file, lets run something. The way to do it is:
ansible -i <path/to/custom/inventory> <group|host> -m <module> -a “<module arguments>”
I will show a simple module, the ping module, will trie to connect the hosts. This modules doesn’t require arguments
$ cd project1 $ ansible -i inventory manager -m ping manager.ansible.test.com | SUCCESS => { "changed": false, "ping": "pong" } $ ansible -i project1/inventory webservers -m ping manager.ansible.test.com | SUCCESS => { "changed": false, "ping": "pong" } node-1.ansible.test.com | SUCCESS => { "changed": false, "ping": "pong" } $ ansible -i project1/inventory another-manager.ansible.test.com -m ping another-manager.ansible.test.com | UNREACHABLE! => { "changed": false, "msg": "Failed to connect to the host via ssh: ssh: connect to host another-manager.ansible.test.com port 22: Connection refused\r\n", "unreachable": true } $ ansible -i project1/inventory nonexistent-manager.ansible.test.com -m ping [WARNING]: No hosts matched, nothing to do
This exampled showed 4 different runs:
- With the manager group, only one host
- With the webservers group, that has two hosts
- With the non-grup belonging host another-manager, that is in the inventory file but is not accesible
- With the nonexistent-manager host not defined in the inventory file and therefore not used by ansible
Stucked? Ansible helps you
Ansible grants a huge online documentation to become an expert, but there is also available an offline one. In point of fact there is a command,, ansible-doc, that show the available modules and their usage. Lets take a look how to get information for a module that allows us to run a command on the remote hosts.
$ ansible-doc -h Usage: ansible-doc [options] [module...] Options: -h, --help show this help message and exit -l, --list List available modules -M MODULE_PATH, --module-path=MODULE_PATH specify path(s) to module library (default=None) -s, --snippet Show playbook snippet for specified module(s) -v, --verbose verbose mode (-vvv for more, -vvvv to enable connection debugging) --version show program's version number and exit $ ansible-doc -l|grep command alternatives Manages alternative programs for common commands asa_command Run arbitrary commands on Cisco ASA devices. at Schedule the execution of a command or script file via the at command. command Executes a command on a remote node dellos10_command Run commands on remote devices running Dell OS10 dellos6_command Run commands on remote devices running Dell OS6 dellos9_command Run commands on remote devices running Dell OS9 eos_command Run arbitrary commands on an Arista EOS device expect Executes a command and responds to prompts haproxy Enable, disable, and set weights for HAProxy backend servers using socket commands. ... $ ansible-doc command > COMMAND The [command] module takes the command name followed by a list of space-delimited arguments. The given command will be executed on all selected nodes. It will not be processed through the shell, so variables like `$HOME' and operations like `"<"', `">"', `"|"', `";"' and `"&"' will not work (use the module if you need these features). Options (= is mandatory): - chdir cd into this directory before running the command [Default: None] - creates a filename or (since 2.0) glob pattern, when it already exists, this step will *not* be run. [Default: None] - executable change the shell used to execute the command. Should be an absolute path to the executable. [Default: None] = free_form the command module takes a free form command to run. There is no parameter actually named 'free form'. See the examples! [Default: None] - removes a filename or (since 2.0) glob pattern, when it does not exist, this step will *not* be run. [Default: None] - warn if command warnings are on in ansible.cfg, do not warn about this particular line if set to no/false. [Default: True] $ ansible-doc -s command - name: Executes a command on a remote node action: command chdir # cd into this directory before running the command creates # a filename or (since 2.0) glob pattern, when it already exists, this step will *not* be run. executable # change the shell used to execute the command. Should be an absolute path to the executable. free_form= # the command module takes a free form command to run. There is no parameter actually named 'free form'. See the examples! removes # a filename or (since 2.0) glob pattern, when it does not exist, this step will *not* be run. warn # if command warnings are on in ansible.cfg, do not warn about this particular line if set to no/false.
So suppose we want to run the command /usr/bin/hostname only if the file /home/centos/project1/inventory exists, this is how we can do it and the result:
$ cd project1 $ ansible -i inventory webservers -m command -a '/usr/bin/hostname removes=/home/centos/project1/inventory' node-1.ansible.test.com | SUCCESS | rc=0 >> skipped, since /home/centos/project1/inventory does not exist manager.ansible.test.com | SUCCESS | rc=0 >> manager.ansible.test.com
Arranging task with playbooks
Playbooks are plain text files written in YAML (YAML Ain’t Markup Language). Their sport analogy with the playbooks used by football or basketball coaches explains their behavior. This files will define plays that processes several tasks. This tasks will be processes in the order they are defined. A playbook file can contains several plays, also processed in the written order, but Ansible can run only one playbook at a time, though it can be linked, something I will show later.
The tasks will use the modules seen on the previous ad-hoc runs. Each play must define the hosts to execute the tasks.
Playbooks should be created in a way that task don’t make unnecessary changes on the systems if executed more than once; this is called idempotent. Almost all the modules are designed to be idempotent. e.g., the yum module allows to install a package, but if it is already installed, by a previous playbook run, it will not be reinstaled, but this task will be skipped. Some of the modules and modules options are not idempotent, so better is to consult on the documentation to understand what the options do.
There are two modules that breaks the idempotent, command and shell, so we avoid whenever is possible to use that modules.
YAML has a begin document marker (—) and another one for the end (…), both could be omitted whe writing the playbook, but I will use then in the examples. Another thing is the playbooks defines a list format of plays; YAML starts every element on the list with (–). Every play is a YAML dictionary object defines attributes: values. Most common attributes are divided in 5 categories:
- Name
- The name attribute defines the play name
- Systems
- The hosts attribute will contain a list of hosts or groups or both to run the play
- User
- The remote_user attributes allows to define the user to connect with
- The become attribute with a value of True/False define if the play requires to scale privileges on the remote hosts
- The become_method attribute defines the command to use for privilege escalation
- The become_user attribute declares the user to escale
- Variables
- The vars attribute defines a list of variables used by the play tasks
- Tasks
- The tasks contains a list of task to be run. Every task will have an unique name in the play and a module with its correspondent options
When writing playbooks we must be aware of YAML format and syntax conventions, as well as the most important rule, the indentation. YAML is very strict with the indentation to separate dictionaries, lists, etc.
One useful tip when writing ansible playbooks (or any other yaml file) for me is to use VIM as editor, and adding this line to .vimrc:
autocmd FileType yaml,yml setlocal ai ts=2 sw=2 et colorcolumn=3,5,7,9,11
- autocmd defines some automatic commands to be run after opening a file
- FileType is used to declare file extension to be used. Multiple extensions separated by comma.
- setlocal set the commands available only for the current window or buffer
- ai or autoindent. When an indent is introduced, the next line keeps the indentation
- ts or tabstop defines the number of columns skipped when Tab key is pressed. By default is 8
- sw or shiftwidth allows to set the same number of columns of tabstop, but when used “<” or “>” in VIM command mode
- et or expandtab allows to set the same number of columns of tabstop, but when used Tab key in VIM insert mode
- colorcolumn allows to colorize the columns correspondent to numbers declared. This helps to see indentation mistakes quickly
Lets create a basic playbook and we will extend it later.
$ cd project1 $ cat playbooks/prepare-WebServers.yaml --- - name: Install and configure webservers services hosts: webservers remote_user: centos become: True tasks: - name: Install Apache package yum: name: httpd state: installed ...
To run a playbook we need a different command. ansible is for run ad-hoc modules. ansible-playbook.
$ cd project1 $ ansible-playbook -i inventory playbooks/prepare-WebServers.yaml
Before run a playbook we can use two options that help us to avoid execution issues. The –syntax-check options checks the YAML file is well written.
$ cd project1 $ ansible-playbook --syntax-check -i inventory playbooks/prepare-WebServers.yaml ERROR! Syntax Error while loading YAML. The error appears to have been in '/home/centos/project1/playbooks/prepare-WebServers.yaml': line 3, column 8, but may be elsewhere in the file depending on the exact syntax problem. The offending line appears to be: - name:Install and configure webservers services hosts: webservers ^ here
Sometimes the errors are not clear enough, in this case it is not in the hosts line as it seems, but in the previous one, it needs a blank after name:
Once fixed we can use the option -C, that will “dry-run” the playbook, it means that the command acts as if it is running the playbooks but only checks if the tasks can be done
$ cd project1 $ ansible-playbook -C -i inventory playbooks/prepare-WebServers.yaml PLAY [Install and configure webservers services] ******************************* TASK [setup] ******************************************************************* ok: [manager.ansible.test.com] ok: [node-1.ansible.test.com] TASK [Install Apache package] ************************************************** changed: [node-1.ansible.test.com] changed: [manager.ansible.test.com] PLAY RECAP ********************************************************************* manager.ansible.test.com : ok=2 changed=1 unreachable=0 failed=0 node-1.ansible.test.com : ok=2 changed=1 unreachable=0 failed=0 $ rpm -q httpd package httpd is not installed
Once verified lets run it:
$ cd project1 $ ansible-playbook -i inventory playbooks/prepare-WebServers.yaml PLAY [Install and configure webservers services] ******************************* TASK [setup] ******************************************************************* ok: [manager.ansible.test.com] ok: [node-1.ansible.test.com] TASK [Install Apache package] ************************************************** changed: [node-1.ansible.test.com] changed: [manager.ansible.test.com] PLAY RECAP ********************************************************************* manager.ansible.test.com : ok=2 changed=1 unreachable=0 failed=0 node-1.ansible.test.com : ok=2 changed=1 unreachable=0 failed=0 $ rpm -q httpd httpd-2.4.6-40.el7.centos.4.x86_64
Lets extend the playbook. I will add new tasks and separate them into blocks. The playbook is self-explanatory:
$ cat playbooks/prepare-WebServers.yaml --- - name: Install and configure webservers services hosts: webservers remote_user: centos become: True tasks: - block: - name: Install Apache package yum: name: httpd state: installed - name: Install Firewalld package yum: name: firewalld state: installed - block: - name: Start and enable Apache service service: name: httpd enabled: true state: started - name: Start and enable Firewalld service service: name: firewalld enabled: true state: started - block: - name: Enable firewall rules for Apache firewalld: state: enabled service: http permanent: true immediate: true - block: - name: create index file shell: echo 'Hello World!' > /var/www/html/index.html ...
And when run:
$ ansible-playbook -i inventory playbooks/prepare-WebServers.yaml PLAY [Install and configure webservers services] ******************************* TASK [setup] ******************************************************************* ok: [node-1.ansible.test.com] ok: [manager.ansible.test.com] TASK [Install Apache package] ************************************************** changed: [manager.ansible.test.com] changed: [node-1.ansible.test.com] TASK [Install Firewalld package] *********************************************** changed: [manager.ansible.test.com] changed: [node-1.ansible.test.com] TASK [Start and enable Apache service] ***************************************** changed: [node-1.ansible.test.com] changed: [manager.ansible.test.com] TASK [Start and enable Firewalld service] ************************************** changed: [manager.ansible.test.com] changed: [node-1.ansible.test.com] TASK [Enable firewall rules for Apache] **************************************** ok: [node-1.ansible.test.com] ok: [manager.ansible.test.com] TASK [create index file] ******************************************************* changed: [manager.ansible.test.com] changed: [node-1.ansible.test.com] PLAY RECAP ********************************************************************* manager.ansible.test.com : ok=7 changed=5 unreachable=0 failed=0 node-1.ansible.test.com : ok=7 changed=5 unreachable=0 failed=0 $ curl node-1.ansible.test.com Hello World!
Up to here the Ansible 2 introduction. In a future post we will see some stuff as variables, linked tasks, linked playbooks, roles and… Ansible Galaxy. See you.