Ansible at Lightspeed ⚡
Tags: devops, ansible
Published: 2021-01-04 04:44:40

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.

## What is Ansible?

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.

## How can I get started with Ansible?

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.

## How do I run Ansible?

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](https://learnxinyminutes.com/docs/yaml/) 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.

### The inventory

The `\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:

```yml
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. 

### The playbook

This is our sample playbook:

```yml
---
- 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:

```yml
list_variable:
  - value1
  - value2
  - value3
```

*It is best practice to have the variables file under a subdirectory with a descriptive name under the `vars` folder.*

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: `files`, `handlers` and `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).

The `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.

## Commonly used commands

In the `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.

### `yum` and `apt`

Installs RPM or DEB packages depending on the distribution.

Example:
```yaml
apt
    name: ['build-essential', 'python3-pip', 'python3-setuptools', 'git', 'cron', 'rsyslog']
   state: present
```

### `file`

Ensures that files and folders exist at that point of the playbook.

Example:
```yaml
  # Creates folder
  file:
    path: /home/ubuntu/work
    state: directory

  # Creates file
  file:
    path: /home/ubuntu/work
    state: touch
```

### `copy`

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.

Example:
```yaml
  copy:
    src: /source/folder/file.txt  # If src is a folder, this recursively copies it.
    dest: /dest/folder/file2.txt
```

### `command` and `shell`

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` instead.

Example:
```yml
  command:
    cmd: make --quiet
```

### `service`

Enables, disables, stops and starts services on the target system, using Sysvinit or Systemd, depending on what is installed on your system.

Example:
```yml
  # Starts the rsyslog service
  service:
    name: rsyslog
    state: started
```

### `hostname`

Sets the hostname of the target system.

Example:
```yml
  hostname:
    name: web01
```

### `reboot`

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.

Example:
```yaml
  reboot:
    reboot_timeout: 3600  # default is 600
```

## And we're done

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](https://docs.ansible.com/ansible/latest/user_guide/index.html) to become an Ansible rockstar.

If you see any errors in this post, please let me know so I can correct them.