Categories
System Administration

Managing Linux servers with Ansible

Ansible is an open source, configuration management and automation tool sponsored by Red Hat. Ansible lets you define the state that your servers should be in using YAML and then proceeds to create that state over SSH. For example, the state might be that the Apache web server should be present and enabled.

The great thing about Ansible is if the server is already in the state that you’ve defined then nothing happens. Ansible wont try to install or change anything.

In this post I’m going to show how to set up Ansible to manage Linux servers.

I have Fedora Linux installed for the control node, and I’ll be configuring Enterprise Linux machines to run Ansible against. Ensure you have at least 2 Linux machines set up and connected to each other remotely.

I also recommend generating SSH keys for passwordless remote access. This isn’t entirely necessary for using Ansible, but it improves the security of your environment and gives us better automation. On your control node, generate an SSH key with ssh-keygen.

$ ssh-keygen -t ecdsa -b 521 -f ~/.ssh/id_local

You can hit enter for each of the prompts when generating the key. For additional security in a production environment I’d recommend entering a Passphrase.

Once the key is ready, copy it to the remote host. In my case my remote host is called rhel.local, which I’ve configured in /etc/hosts.

$ ssh-copy-id [email protected]

Substituting the username for your own username on the remote server, unless your name is also Dave, then you can leave it.

The user account on the remote servers needs to have sudo privileges to be able to perform tasks with Ansible. On my remote host the user account that I’m using has been added to the wheel group and I’ve allowed the wheel group to run sudo commands without a password by editing the sudoers file with visudo.

%wheel  ALL=(ALL)       NOPASSWD: ALL

If you don’t want to enable passwordless sudo you’ll need to instruct Ansible to pass your sudo password to the host when running playbooks.

The Linux installation that I’m using for the control node doesn’t have Ansible installed by default, so we’ll get that set up first. Note, we don’t need to install anything on the remote hosts which is one of the benefits of Ansible.

On your control node, which in my case is my Fedora Linux desktop machine that I use for my development work, install the following packages.

$ sudo dnf install python3-pip
$ python3 -m pip install ansible-core ansible-navigator ansible-builder

I’m installing Ansible with Python’s PIP package manager to get access to the latest version of ansible as well as ansible-navigator and the ansible-builder tool.

Once Ansible is installed, we can test it’s working correctly by trying to communicate with the remote host. Ansible has various methods of interacting with hosts, one way is through ad-hoc commands from the command line or by running playbooks. Ad-hoc commands are fine for quick tasks or for testing something, however playbooks are usually the recommended approach for most cases.

For the sake of confirming that Ansible is working and we can reach the remote host, let’s run an ad-hoc command using the ping module.

First, create a file to store your host inventory. An inventory file is basically just a list of hosts that Ansible can work with. Hosts can be listed using either IP address or hostnames and can also be grouped together for different purposes. Ignoring that for a second, we’ll start with a simple hosts file with one host that we’re configuring.

In your users home directory, create an ansible project directory and an empty file called hosts.

$ mkdir ansible
$ cd ansible
$ touch hosts

Open the hosts file and just insert the host you wish to configure. In my case it’s rhel.local. Again, you can use hostnames if your /etc/hosts file is setup for name resolution as well.

[www]
rhel.local

I’ll also create an ansible.cfg file.

[defaults]
remote_user = dave
inventory = $HOME/ansible/hosts
interpreter_python = auto_silent

[privilege_escalation]
become=true
become_method=sudo

Now we can run our first ad-hoc command.

$ ansible -m ping all

The -m ping loads the ping module. The ping module attempts to see if the remote host is alive with a ping command, and will print the output to the screen.

Now that I can communicate with the server using Ansible, I’m going to deploy the Apache web server.

As mentioned, the previous ping command was known as an ad-hoc Ansible command and is essentially just for running once off commands across your fleet. Normally though you’d define your tasks in a playbook and instruct Ansible to run those which would allow you to ensure the state is always how you expect it to be and also allows you to store your tasks in version control to track changes.

A playbook is a text file with the .yml or .yaml extension and is structured in a specific way. I wont go into what YAML is here because it’s pretty easy to understand just by looking at the code.

To install the Apache web server, create a new file in your Ansible project directory called web-server.yml and add the following:

---
- name: Ensure Apache is installed
  ansible.builtin.dnf: 
    name: httpd
    state: present

This task instructs Ansible to use the dnf module which is the package manager module for Red Hat based Linux systems, and install the httpd package. Setting state to present ensures that if the server doesn’t have Apache installed then yum will install it.

Once it’s installed we want to make sure it’s running. So we define an additional task beneath the previous one in the same yaml file.

- name: Ensure Apache is enabled and running
  ansible.builtin.service:
    name: httpd
    state: started
    enabled: true

This task tells systemd to ensure the httpd service is started and enabled at boot.

How you structure your Ansible project will depend on the scope of tasks you need completed. For small tasks like this I can use a simple playbook, but for larger installations with many tasks you might consider using ansible roles and collections which are a bit beyond what I want to discuss here, so I’ll stick with a simple playbook. The complete playbook might look like this:

---
- name: Install and configure Web server
  hosts: www
  become: true

  tasks:
    - name: Ensure Apache is installed
      ansible.builtin.dnf:
        name: httpd
        state: present

    - name: Ensure Apache is enabled and running
      ansible.builtin.service:
        name: httpd
        state: started
        enabled: true

YAML files start with 3 dashes, and then on the next line we define which hosts we require to have the following tasks run on. In this example the hosts group is www which we created in the inventory file. Become: true is telling Ansible to elevate privileges like when you type sudo before a command.

Then run the playbook with ansible-navigator use the following command:

$ ansible-navigator run web-server.yml -m stdout 

The command ansible-navigator is used instead of ansible that we ran previously and instead of passing the -m command to load a module we pass the name of the playbook, in this case web-server.yml. We can use ansible-navigator when building our playbooks and automation code. The -m stdout part of the command displays the output to the terminal rather than opening the navigator user interface.

Ansible will read the playbook and reach out to each host in the inventory then proceed to do whatever tasks are necessary to reach the desired state, in our case that the Apache web server is installed and running.

The yellow “changed” text in the output tells you that before running the playbook the state wasn’t matching what you defined and that Ansible “changed” the state of your host. This is good, this means it worked. If you run the same playbook again you should see the output changed to green and “ok” to indicate that the server is already in the state described and nothing was changed.

To make httpd accessible remotely, we need to tell the firewall on the remote host to allow traffic to port 80. Add an extra task below the previous two.

---
- name: Install and configure Web server
  hosts: www
  become: true

  tasks:
    - name: Ensure Apache is installed
      ansible.builtin.dnf:
        name: httpd
        state: present

    - name: Ensure Apache is enabled and running
      ansible.builtin.service:
        name: httpd
        state: started
        enabled: true

    - name: Allow traffic to port 80
      ansible.builtin.firewalld:
        name: http
        permanent: true
        immediate: true
        state: enabled

Re-run the playbook and see what changed.

We should now be able to open firefox and navigate to the server IP to see the default Apache webpage.