Today I'm going to show you how what Ansible is, how to get started using it to provision machines, and common deployment commands used inside it. Without wasting any time, let's begin.
Ansible is a deployment program used to install packages and write configuration files on target machines. Ansible's official website says "Ansible is the simplest way to automate apps and IT infrastructure." OK let's go into detail on what that's supposed to mean.
There are other deployment tools such as Puppet and Chef but these require you set up a main server that which target servers ask for provisioning instructions. It turns out that this setup is complicated when you only have a few machines to provision which will often be the case with personal projects and tasks.
For those use cases you want to SSH into the servers directly and run commands in each of them. And that's exactly what Ansible does. It automates the SSH connection process and command running so that you don't have to do it, saving you valuable time and eliminating potential inconsistencies and partial deployments which result from manually provisioning the target machines. This is also much easier then setting up Puppet or Chef infrastructure.
It's very easy to install Ansible on Unix-like machines (usually the deployment is done on Linux or macOS, but it's also doable on Windows because it's a Python package). On Ubuntu you should be able to run
apt-get install ansible to get it. On macOS you can use
pip install ansible. On Red Hat and CentOS it's
yum install ansible.
As you can see, that was dead-easy. You now have all of Ansible's deployment features at your fingertips.
Before you can run an Ansible deployment, you have to create deployment files which will be used there. The files are written in the YAML language. If you do not understand YAML then there's a short and quick tutorial you can go read here and come back. YAML files have the extension .yml at the end of their filenames, though it's common to see these kind of files floating around in Linux with no filename extension at all; it reads the beginning of the file to determine the file type.
First, you create a folder to store all of your files in, fpr example,
ansible-deployment. Then depending on the complexity of your deployment, there will be two to four files/folders in that directory.
\ ansible-deployment\ | |-\ hosts | |-\ site.yml | |-\ roles\ | +-\ vars\
You are not required to have this folder structure. You can run Ansible using just a single .yml file and hosts file, or even with no files at all and specify all the deployment commands as arguments on the command line (this last part is out of scope of this post).
The entries that end with a slash are folders, the rest are files. The site.yml file is what's called the Ansible playbook. This is the file that contains all of the commands that will be ran over the SHH connection.
\hosts file inside the root directory is called the inventory. The inventory stores all of the target systems that Ansible will connect to. You specify target systems by IP address, port, user name and whether public key authentication should be used. And for the purposes of the deployment, you can give the host a label for Ansible to remember. Beware; this label is not the same concept as the hostname. Setting a label here does not change the hostname of the target. You should use the
hostname Ansible module for that (more details on that later).
Here is what a sample inventory would look like:
all: hosts: my_host1: my_variable: 1 ansible_port: 28497 ansible_host: ssh5.zenulabidin.github.io ansible_ssh_private_key_file: private-key.pem my_host2: my_variable: abc ansible_port: 28496 ansible_host: ssh4.zenulabidin.github.io ansible_ssh_private_key_file: private-key.pem mygroup_ofhosts: my_host3: my_variable: abc ansible_port: 28495 ansible_host: ssh3.zenulabidin.github.io ansible_ssh_private_key_file: private-key.pem
All of the above variables are optional except for
all (which is what an inventory file begins with) and
ansible_host. In fact,
my_variable isn't even an Ansible property: You can make unique named variables such as
my_variable for each host to give each host a different value. This is very useful if you are running commands that have different modes for each host.
Here I am configuring Ansible with two targets: my_host1 and my_host2, and both of these connect to ssh5.zenulabidin.github.io:28497 and ssh4.zenulabidin.github.io:28496 respectively. You can even have two targets that connect to the same IP address but different ports because this just makes an SSH connection for each entry internally.
I also have a group of hosts named mygroup_ofhosts which currently includes only the host my_host3. Organizing your hosts into groups allowed you to write rules for some of them at once without repeating yourself.
This is our sample playbook:
--- - name: Install Rclone, and compile GNU coreutils hosts: all remote_user: ubuntu become: yes become_method: sudo vars_files: - vars/rclone.yml - vars/coreutils.yml roles: - role: stefangweichinger.ansible_rclone - coreutils
Many things to note here. First we have
hosts which can be either the label of a single host, a group of hosts, or just
all which runs this on all defined targets.
Then we have
remote_user. This is the user that we are going to run the playbook as.
become tells us if we want to use a privilege-changing program such as su or sudo to switch users, and in
become_method we supply the command-line program that changes the user for us.
Most of the time we just want to get root privileges so use use
user: root, followed by either
become_method: sudo for Ubuntu-based hosts or
become_method: su for everything else.
This is followed by
vars_files and this is a list of the files that Ansible has to read to collect additional variable names that are not defined in the inventory file. This feature is used to segregate variables based on their business logic.
A variables file isn't very special, it's also a YAML file, but the structure of the YAML variables depends on how you are going to use them in the playbook. For example if you are going to access your variable like a list, you could have a variable like this:
list_variable: - value1 - value2 - value3
It is best practice to have the variables file under a subdirectory with a descriptive name under the
So now that we got variables out of the way, let's go over the roles. Now the roles files are segregated for the same reason that Ansible allows variables to be segregated - so different teams can work on different parts of the playbook. You give each one of them a role and they will only have to manage the role they are working with.
Under the roles folder, you have the names of each role as subfolders, and in each subfolders you see three folders:
tasks. Your YAML file for this module goes inside
tasks/main.yml. I won't be going over
handlers in this post so that folder can be left empty (but existing nonetheless).
files folder contains files that are to be copied to the target machine in a temporary directory known only to Ansible. This directory will be the current directory relative to all of the commands in the playbook. You can't just run
cd in the playbook and change the current directory for all future commands - each time a new command is run it "resets" to the temporary directory beforehand.
The advantage of this behavior is now you can refer to files in the
files folder relatively, without any path, because you have no way of knowing what the path before the copied files will be on all systems, and even if you do, you can't easily specify one path in the playbook especially if the machines are running different Linux distributions, or some are Macs and the others are Linux boxes.
At any time, if you want to change the current working directory for any command, pass
chdir: /new/path as one of the parameters to the command.
One final caveat: If any of the commands fail to run, the whole deployment is aborted for that particular host but the others go on. To prevent this, pass
ignore_errors: true as one of the parameters to any command.
roles file, or in the single .yml file if that's how you're running Ansible, are all of the commands that you want the machines to run. All commands need to have the
- name element at the beginning before the command itself and its value is just a human-readable description of what you want this command to do.
Installs RPM or DEB packages depending on the distribution.
apt name: ['build-essential', 'python3-pip', 'python3-setuptools', 'git', 'cron', 'rsyslog'] state: present
Ensures that files and folders exist at that point of the playbook.
# Creates folder file: path: /home/ubuntu/work state: directory # Creates file file: path: /home/ubuntu/work state: touch
Copies a file from one place to another. If you have any files in the
files folder of your role, these are located in the (fixed) current directory.
copy: src: /source/folder/file.txt # If src is a folder, this recursively copies it. dest: /dest/folder/file2.txt
Run an arbitrary command in a terminal. The prime difference between the two is that
shell spins up a bash shell, or whatever the default shell is, to run the commands, allowing you to use special shell operators in your Ansible playbook. Most of the time you don't need this functionality so you can choose to use the otherwise equivalent
command: cmd: make --quiet
Enables, disables, stops and starts services on the target system, using Sysvinit or Systemd, depending on what is installed on your system.
# Starts the rsyslog service service: name: rsyslog state: started
Sets the hostname of the target system.
hostname: name: web01
Reboots the target and waits for a maximum of 600 seconds (10 minutes) for it to come online again to reconnect to it. This value can be changed.
reboot: reboot_timeout: 3600 # default is 600
There was simply too many features to cover in Ansible to go over them in one session. If you want to learn more about Ansible playbooks, head over to their Getting Started guide to become an Ansible rockstar.
If you see any errors in this post, please let me know so I can correct them.