Requirements: Linode user account, and an Ansible compatible server (RHEL, CentOS, Ubuntu Server, etc.).

Install Ansible (by following the official guide https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html#installation-guide).

The server you install Ansible on is your Control node, from this Control node you deploy to Managed nodes.

Managed nodes are usually configured in a hosts file, called your Inventory. We never install Ansible on the managed nodes.

Install Linode’s official Python library for the Linode API v4.

$ pip install linode_api4

We need an Ansible configuration file, I prefer to keep my configuration file in /opt/ansible/, especially if multiple users will utilize the Control node.

sudo mkdir /opt/ansible/
sudo touch /opt/ansible/.ansible.cfg

Export the ANSIBLE_CONFIG environment variable, and source it.

$ echo 'export ANSIBLE_CONFIG=/opt/ansible/.ansible.cfg' | sudo tee /etc/profile.d/ansible.sh
$ source /etc/profile.d/ansible.sh

Change ownership of the ansible directory and cd into it.

$ sudo chown -R <your_user>:<your_user> /opt/ansible/ && cd /opt/ansible/

Edit ansible.cfg.

[defaults]
host_key_checking = False
VAULT_PASSWORD_FILE = ./vault-pass

[inventory]
enable_plugins = linode

Roles

We will use Ansible’s concept of Roles. With roles, we can group things that logically belong together in separate directories. In this article we will create three roles: linode, security, and webserver.

Ansible uses a defined directory structure for roles: seven main standard directories, and you must include at least one of these directories in each role (https://docs.ansible.com/ansible/latest/user_guide/playbooks_reuse_roles.html).

At this point, this is what my directory tree looks like.

ansible/
└── roles
    ├── linode
    │   └── tasks
    │       └── main.yml
    ├── security
    │   └── tasks
    │       └── main.yml
    └── webserver
        └── tasks
            └── main.yml

Edit roles/linode/tasks/main.yml.

- name: Deploy Linode
  hosts: localhost
  vars_files:
      - /opt/ansible/group_vars/vars

  tasks:

    - name: Deploy new Linode
      linode_v4:
        label: "{{ label }}{{ 100 |random }}"
        access_token: "{{ token }}"
        type: g6-nanode-1
        region: eu-central
        image: linode/centos8
        root_pass: "{{ password }}"
        authorized_keys: "{{ ssh_keys }}"
        group: ansible_managed_node
        tags: ansible_managed_node
        state: present
      register: deployed_linode

To find the name of Linode types (size), regions, and images, you can use linode-cli. Remember to create a Personal Access Token on your Linode account page.

pip install linode-cli

In /opt/ansible/, create the directory group_vars, and inside a file vars.

ssh_keys: ~/.ssh/id_rsa.pub
label: simple-linode

Use ssh-keygen to create your key pair, and paste the public key into the file above.

Ansible Vault

For secure storage of passwords and access tokens we will use Ansible Vault to encrypt them.

echo 'My.ANS1BLEvault-c00lPassw0rd' > /opt/ansible/vault-pass

Remember to add vault-pass to .gitignore

Run ansible-vault encrypt_string 'My.c00lPassw0rd' --name 'password', and paste the output in /opt/ansible/group_vars/vars.

Do the same with your Linode Personal Access Token: ansible-vault encrypt_string '<your Linode Personal Access Token>' --name 'token', again, paste the output into your vars file.

My var file now looks like this:

ansible_ssh_user: root
ssh_keys: ~/.ssh/id_rsa.pub

label: linode

password: !vault |
          $ANSIBLE_VAULT;1.1;AES256
          3463383764 ... secret

token: !vault |
          $ANSIBLE_VAULT;1.1;AES256
          396665323 ... secret

Remember to also add your public SSH key to your Linode account.

Run the playbook, ansible-playbook roles/linode/tasks/main.yml.

You can add -v, -vv, or -vvv, specifying levels of verbosity for troubleshooting.

/opt/ansible$ ansible-playbook roles/linode/tasks/main.yml
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does
not match 'all'

PLAY [Deploy Linode] ******************************************************************************************

TASK [Gathering Facts] ****************************************************************************************
ok: [localhost]

TASK [Deploy Linode] ******************************************************************************************
changed: [localhost]

PLAY RECAP ****************************************************************************************************
localhost                  : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

At this point you should be able to SSH into your new Linode server, as root. Find its IP using linode-cli linodes list.

Inventory

Use linode-cli linodes list to find the IP of your newly deployed server.

Create a file /opt/ansible/hosts.

[webserver]
<server IP>

Edit ansible.cfg and add the path to your inventory (hosts) file. My .ansible.cfg now looks like this:

[defaults]
host_key_checking = False
VAULT_PASSWORD_FILE = ./vault-pass
inventory = /opt/ansible/hosts
enable_plugins = linode
pipelining = True

Run ansible all --list-hosts to verify Ansible can use your inventory.

For sake of example, let us say our new server will be a web server, maybe it will host your blog. Remember, we talked about Roles earlier, webserver is the name of our role.

Our web server will have need of some security changes, so let us create the security playbook first.

Edit /opt/ansible/roles/security/tasks/main.yml.

- hosts: webserver
  vars_files:
      - /opt/ansible/group_vars/vars
  become: true

  tasks:
    - name: "Update security packages"
      yum:
        name: '*'
        security: yes
        state: latest

    - name: "Enable firewalld"
      firewalld:
        permanent: true
        state: enabled
        service: "{{ item }}"
      notify: reload firewalld
      loop:
        - ssh

For sake of this article, and in the name of example, this is far from a complete security playbook. You should look into best practices and security configurations standards like DISA STIG.

Run the playbook: ansible-playbook -i /opt/ansible/hosts roles/security/tasks/main.yml -l webserver

Here we specify which hosts file we would like to use, with -i, then follows the playbook, and lastly the role name (as specified in the hosts file).

Now for the web server playbook.

Edit /opt/ansible/roles/webserver/tasks/main.yml

- hosts: webserver
  vars_files:
      - /opt/ansible/group_vars/vars
  become: true

  tasks:
    - name: "Install Nginx"
      yum:
        name: nginx
        state: present

    - name: "Start nginx"
      service:
        name: nginx
        state: started

    - name: "Enable systemd service for nginx"
      systemd:
        name: nginx
        enabled: yes
        masked: no

    - name: "Enable firewalld"
      firewalld:
        permanent: true
        state: enabled
        service: "{{ item }}"
        immediate: true
      loop:
        - https
        - http

ansible-playbook -i /opt/ansible/hosts roles/webserver/tasks/main.yml -l webserver

If you are running playbooks multiple times, e.g. for troubleshooting, it can be handy to start at a certain task, using --start-at-task <task name>.

Example:

ansible-playbook -i /opt/ansible/hosts roles/webserver/tasks/main.yml -l webserver --start-at-task "Enable firewalld