Intro
Ansible is a simple, Open Source IT automation platform. It can configure systems, deploy software, execute ad hoc jobs, and orchestrate more advanced IT tasks such as continuous deployments or zero-downtime rolling updates.
Below are some features that make Ansible unique:
- It manages machines in an agent-less manner by leveraging existing SSH daemon, thus no need of extra open ports or any additional custom-agents on remote machines.
- It allows describing infrastructure in a language that is both machine and human friendly.
- It has a rich collection of Roles.
- It focuses on security by leveraging existing standards.
- It follows a predictable release cycle presented in the Public Roadmap.
- It allows module development in any dynamic language, not only in Python.
In this simple tutorial, we will provision an Ubuntu 18.04 Server with Ansible. Furthermore, we will explore things like Ansible modules, playbooks and roles.
Prerequisites
- Ansible 2.9+ installed on a local machine. If needed, please refer to this handy installation instructions.
- Basic knowledge of Unix systems (SSH, Deb packages).
- Ubuntu 18.04 Server with SSH access (via public and private SSH key pair).
Inventory file
Before we can do anything with Ansible, we must specify a list of hosts that Ansible is about to control. The easiest way is to create a static inventory file, as presented below.
[web]
85.208.21.90
[web:vars]
ansible_user=root
ansible_port=22
ansible_ssh_private_key_file=privateKey.pem
Few things to be noted here before we move forward:
- We have created one inventory group web with the public IP address of our Ubuntu 18.04 Server. There is much more to it, so please study the First Inventory Guide to get a better understanding of what is possible.
- We have also defined shared variables for group web, instructing Ansible which SSH private key, port, and user to use.
- We have used the Static Inventory, but Ansible also supports Dynamic Inventory that is particularly handy when remote machines are being spun up and shut down frequently.
If we wanted to provision more Web Servers, we would add more IPs, each one under the new line in the web group. We could even use different private SSH keys per host, as shown below.
[web]
85.208.21.90 ansible_ssh_private_key_file=privateKey1.pem
85.208.21.95 ansible_ssh_private_key_file=privateKey2.pem
[web:vars]
ansible_user=root
ansible_port=22
Note
Please, consult the Behavioral Inventory Parameters Guide for more information about other available Ansible variables.
Ansible modules
The fastest way to see Ansible in action is tinkering with its basic Ansible command-line interface:
# ansible web -i inventory -m ping
In the above example, we have used the ping module and run it on all machines from the web group defined in our inventory file. Ping is a trivial module that is commonly used to verify the ability to login and check if Python executable was properly configured on the remote machines.
Let's see another example that uses the --check flag to enable a dry run mode.
# ansible web --check -i inventory -m apt -a "name=nodejs state=latest"
In here, we have used the apt module to install the latest nodejs package on the same machines from our web group. If the nodejs package has already been installed, Ansible won't do it again, as shown below.
85.208.21.90 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"cache_update_time": 1586849592,
"cache_updated": false,
"changed": false
}
Although these ad hoc examples have shown how to work with remote hosts using Ansible modules and Ansible CLI, the best part is yet to come. Let's explore the Ansible playbooks together.
Ansible playbooks
Playbooks are Ansible’s configuration, deployment, and orchestration language. They are expressed in YAML format, designed to be human-readable and meant to be kept in source control.
Let's start with a simple stats.yml playbook which is going to collect various information about our remote machines.
- name: Stats
hosts: all
gather_facts: True
tasks:
- name: Debug Gathered Facts
debug:
var: item
with_items:
- "{{ ansible_lsb }}"
- "{{ ansible_default_ipv4 }}"
- "{{ ansible_date_time }}"
- "{{ ansible_dns }}"
- "{{ ansible_env }}"
- "{{ ansible_hostname }}"
- "{{ ansible_memory_mb }}"
- "{{ ansible_memfree_mb }}"
- name: Capture OS Name
command: uname -a
register: os_name
- name: Debug OS Name
debug:
var: os_name.stdout
Before moving forward, let's try to understand what has just happened
- gather_facts is a boolean that controls whether the play will automatically run the setup task to gather facts for the hosts. We could omit it since it's True by default.
- hosts defines a list of groups, hosts, or host patterns that translates into a list of remote hosts that we will target. We have used the default group all since we want to get statistics from all our hosts.
- remote_user defines a user used to log into the target via the connection plugin. In our case, it is redundant, because we have also defined ansible_user in our inventory file.
- setup and debug modules help to debug the host variables collected by Ansible. If you want to see all possible variables, head over to the Ansible Facts Variables Documentation
Having the basic knowledge of the Ansible playbook, let's try to run it with ansible-playbook CLI as shown below.
# ansible-playbook -i inventory stats.yml
Afterward, we should see something similar to the snippet below. (Please mind that some output has been removed for the sake of brevity.)
PLAY [Stats] ************************************************************************************************************************************************
TASK [Gathering Facts] **************************************************************************************************************************************
ok: [85.208.21.90]
TASK [Debug Gathered Facts] *********************************************************************************************************************************
ok: [85.208.21.90] => (item={'id': 'Ubuntu', 'description': 'Ubuntu 18.04 LTS', 'release': '18.04', 'codename': 'bionic', 'major_release': '18'}) => {
"ansible_loop_var": "item",
"item": {
"codename": "bionic",
"description": "Ubuntu 18.04 LTS",
"id": "Ubuntu",
"major_release": "18",
"release": "18.04"
}
}
...
ok: [85.208.21.90] => (item={'nameservers': ['127.0.0.53']}) => {
"ansible_loop_var": "item",
"item": {
"nameservers": [
"127.0.0.53"
]
}
}
...
ok: [85.208.21.90] => (item=578) => {
"ansible_loop_var": "item",
"item": 578
}
TASK [Capture OS Name] **************************************************************************************************************************************
changed: [85.208.21.90]
TASK [Debug OS Name] ****************************************************************************************************************************************
ok: [85.208.21.90] => {
"os_name.stdout": "Linux tekmi 4.15.0-96-generic #97-Ubuntu SMP Wed Apr 1 03:25:46 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux"
}
PLAY RECAP **************************************************************************************************************************************************
85.208.21.90 : ok=4 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
This has already proved the power of Ansible automation. Yet, to become truly dangerous, let's explore the Ansible Galaxy and Roles Ecosystem together.
Ansible playbooks with roles
You can write complex playbook in one large file, but wouldn't it be handier to reuse files and organize things better? Gladly, Ansible got that covered and offers three ways to do it:
- includes
- imports
- roles
Includes and imports were added in Ansible 2.4 and allow to break up large playbooks into smaller files, which can be used across multiple parent playbooks or even multiple times within the same playbook.
Roles allow more than just tasks to be packaged together and can include variables, handlers, or even modules and other plugins. They can also be shared via the Ansible Galaxy, a free site for finding, downloading, and sharing community developed roles.
In this tutorial, we will install two roles: the PHP Role and the Composer Role, as shown below
# ansible-galaxy install --roles-path . geerlingguy.php geerlingguy.composer
After downloading these roles in the current directory (thanks to --roles-path .), let's create our second playbook, as presented below.
- name: PHP and Composer
hosts: web
gather_facts: True
vars:
php_default_version_debian: "7.2"
php_enable_webserver: false
roles:
- geerlingguy.php
- geerlingguy.composer
Two noteworthy parts are going on there:
- vars option that in our case overwrites two variables from PHP role. This option has more to offer, so please explore the Playbook Variables Guide
- roles option that allows using any local or downloaded roles statically. As of Ansible 2.4, there is also an include_role option for using roles inline and dynamically. Please refer to the Using Roles Guide for more information.
As with every playbook, the last step is to run it via ansible-playbook CLI.
# ansible-playbook -i inventory php_composer.yml
And with that last command, we have just finished our short walk-trough of Ansible. Congratulations!
Summary
We have explored several interesting parts of the Ansible automation tool together. To summarize our journey, let's recap what we have done:
- We have learned how to create a simple, static inventory.
- We have got to know several Ansible modules, including debug, setup, and ping.
- We have created a simple Playbook and debugged a few hosts variables gathered by Ansible.
- We have discovered Ansible Galaxy and downloaded PHP and Composer roles from there.
Ansible is a powerful automation platform and it takes time to master all its features. If you want to broaden your knowledge, you may be interested in reading the comprehensive book Ansible: Up and Running: Automating Configuration Management and Deployment the Easy Way.