Skip to content

Ansible

Projects for learning

There are [several areas][https://opensource.com/article/19/8/ops-tasks-ansible] where Ansible can be used in personal projects for learning purposes.

  1. Use the users module to manage users, assign groups, and define custom aliases in the profile property.
  2. Put a time limit on the availability of the sudo command
  3. Use Ansible Tower to produce a GUI interface to restart certain services.
  4. Use Ansible Tower to look for files larger than a particular size in a directory.
  5. Debug a system performance problem.

Ansible is an automation tool used for configuration management using human-readable YAML templates. Ansible is distinguished for being agentless, meaning no special software is required on the nodes it manages.

Ansible can be used in one of two ways:

  • Running ad hoc commands, executed in realtime by an administrator working at the terminal using the ansible command
  • Running playbooks, YAML documents that represent a sequence of scripted actions which apply changes uniformly over a set of hosts, using the ansible-playbook command.

A playbook is a YAML document that represents a sequence of scripted actions called tasks which apply changes uniformly over a set of hosts. Any ad hoc command can be rewritten as a playbook, but some modules can only be used effectively as playbooks.

playbooks/motd.yml
- hosts: all
  tasks:
  - copy:
    dest: /etc/motd
    content: "Hello, World!"

Ansible host management relies on an inventory file containing a list of IP addresses or hostnames organized in groups. Inventories can be INI or YAML format. Inventories are conventionally organized as a file named hosts at the root of a project directory, although a system hosts file can be defined at /etc/ansible/hosts.

Variables

Variables can be defined under vars (as properties), and they are referenced using Jinja2-style double braces: {{ }}. YAML syntax requires a value starting with double braces to be quoted.

playbooks/motd.yml
- hosts: all
  vars:
    name: World
  tasks:
  - command: echo "Hello, {{ name }}!"

Variables can also be defined in variables files, YAML-format dictionaries conventionally placed in the vars directory, and referenced using the vars_files property. The path for vars files appears to be interpreted relative to the location of the playbook.

playbooks/motd.yml
- hosts: all
  vars_files:
  - vars/name.yml
  tasks:
  - copy:
    dest: /etc/motd
    content: Hello, {{ greet_name }}!
playbooks/vars/name.yml
greet_name: World

Variables can also be defined at runtime using the --extra-vars/-e option. Variables can be passed as space-delimited or JSON format.

ansible-playbook release.yml -e "version=1.23.45 other_variable=foo"

Variables cane be encrypted inline in an otherwise cleartext vars file.

Jinja2

Various effects are possible using Jinja2 templates:

Jinja2 control structures support control flow features like loops and conditionals inside {% ... %} blocks.

- name: Find any YUM/DNF variables
  find:
    paths: "/etc/{% 'dnf' if ansible_distribution_major_version == '8' else 'yum' %}/vars"
  register: _repository_vars_files

Filters follow a pipe in the template.

Capitalize a value
line: " {{ hypervisor | upper }}

Ansible provides additional filters. Here both the basename, b64decode, and combine Ansible filters are used as well as trim which is native to Jinja2.

- set_fact:
    repository_vars: "{{ (repository_vars | default({})) | combine({ (_file.source | basename): _file.content | b64decode | trim }) }}"
  loop: "{{ _repository_vars_slurped_files.results }}"
  loop_control:
    loop_var: _file

Roles

Ansible roles group content in a way that allows it to be shared. They typically correspond to the service offered (web servers or databases, etc).

Roles have a highly standardized directory structure.

  • defaults: default values for variables with low perecedence that can be overriden by inventory variables
  • files: static files referenced by role tasks
  • handlers: handler definitions
  • meta: metadata about the role, such as author, license, platforms, dependencies, etc
  • tasks: task definitions
  • vars: role variables with high precedence that cannot be overriden by inventory variables

A skeleton directory can be created with ansible-galaxy.

ansible-galaxy init $ROLENAME

Roles can be called in a playbook under the roles property. The value of the role is interpreted as a path, appended to the project directory or various other potential locations. Because roles are meant to be reused by many playbooks, a central location is recommended:

  • ~/.ansible/roles
  • /etc/ansible/roles
  • /usr/share/ansible/roles
playbooks/motd.yml
- hosts: all
  roles:
  - role: roles/motd
    vars:
      greet_name: Dgiapusccu # (1)
  1. Without providing an overriding variable value:
    - hosts: all
      roles: 
      - roles/motd
    
motd role
# motd/tasks/main.yml
- copy:
    content: Hello, {{ greet_name }}!
    dest: /etc/motd

# motd/defaults/main.yml
greet_name: World # (1)
  1. Role variables defined in vars have a high precedence and cannot be overriden. Only values defined in defaults can be overriden.

It appears that variables with values defined in the main.yml file located vars or defaults are automatically picked up. But if variables are defined in additional files they must be explicitly imported.

motd/tasks/main.yml
- include_vars:
    file: "{{ role_path }}/defaults/secure.yml"
- copy:
    content: Hello, {{ greet_name }}!
    dest: /etc/motd

Normally, tasks in a role execute before the other tasks of a playbook. pre_tasks and post_tasks can be defined as well.

Roles can have dependencies on other dependencies, as defined in the meta directory.

dependencies:
- { role: apache, port: 80 }
- { role: mariadb, dbname: addresses, admin_user: bob }

Collections

Ansible collections comprise a standardized format for Ansible content distribution, allowing it to be delivered asynchronously and on-demand separately from Ansible Automation Platform releases. Ansible content can include playbooks, modules, roles, documentation, tests, plugins. Ansible collections are delivered using Ansible Galaxy and each collection needs a galaxy.yml file that describes the collection.

Collections can be installed from a YAML-format requirements file:

ansible-galaxy collection install -r ansible/requirements.yml

ansible/requirements.yml
roles:
- src: git@ssh.dev.azure.com:v3/PODS-LLC/SWE/devops_inventory_role
  version: master # (2)
  scm: git
  name: inventory # (1)
  1. This apparently determines how the directory is renamed.
  2. Branch name

Handlers

Handlers are tasks that are executed when notified by a task. They are only run once, and only if the notifying task has made a change to the system.

Here, Enable Apache will be called if Install Apache makes a change. If apache2 is already installed, the handler is not called.

- hosts: webservers
  become: yes
  tasks:
  - name: Install Apache
    apt: name=apache2 update_cache=yes state=latest
    notify: enable apaches

  handlers:
  - name: Enable Apache
    service: name=apache2 enabled=yes state=started
- hosts: all
  vars:
    package_name: apache2
  tasks:
  - name: this installs a package
    apt: "name={{ package_name }} update_cache=yes state=latest"
    notify: enable apache
  handlers:
  - name: enable apache
    service: "name={{ package_name }} enabled=yes state=started" 

Conditional logic is [implemented][https://www.linuxjournal.com/content/ansible-part-iii-playbooks] on each task by defining a value for the when statement:

- hosts: all
  vars:
    startme: true
  tasks:
  - command: echo Hello, World!
    when: startme

Vault

Ansible Vault is a place to safely keep passwords. There are two types of vaulted content:

  • Vaulted files, where the full file, which can contain Ansible variables or other content, is encrypted
  • Single encrypted variables, where only specific variables within a normal "variable file" are encrypted.
Run a playbook providing a vault password file
ansible-playbook --vault-password-file $pwfile playbooks/motd.yml

Tasks

Setup

The Ansible control node needs to be configured to elevate privileges. This is done by modifying ansible.cfg in the relevant project directory or the system config at /etc/ansible/ansible.cfg

ansible.cfg
[privilege_escalation]
become=yes

An Ansible service account is created on each managed node.

useradd ansible -s /usr/bin/bash -mG wheel # sudo
passwd ansible
su - ansible
ssh-keygen -A

Now the service account is given the ability to sudo any command without a password.

/etc/sudoers.d/ansible
ansible ALL=(ALL) NOPASSWD: ALL
# (1)!
  1. Without this line, plays will fail with the message "Missing sudo password"
    PLAY [Running motd role] *******************************************************
    
    TASK [Gathering Facts] *********************************************************
    fatal: [hyperv-centos9]: FAILED! => {"msg": "Missing sudo password"}
    
    PLAY RECAP *********************************************************************
    hyperv-centos9             : ok=0    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0   
    

The system inventory is an INI-format config located at etc/ansible/hosts and defines the clients which are to be controlled by the server.

The group name all is implicitly defined, and the following command will display all defined hosts.

Display all available hosts
ansible all --list-hosts

Apache

- hosts: all
  tasks:
  - package:
      name: httpd
      state: latest
  - service:
      name: httpd
      enabled: true
      state: started
  - ansible.posix.firewalld:
      immediate: true
      permanent: true
      service: http
      state: enabled

Using vars
- name: Install Apache
  hosts: all
  vars:
    apache_package: httpd
  tasks:
  - name: Install {{ apache_package }} package
    package:
      name: "{{ apache_package }}"
      state: latest
  - name: Enable and start {{ apache_package }} service
    service:
      name: "{{ apache_package }}"
      enabled: true
      state: started
  - name: Open firewall
    ansible.posix.firewalld:
      immediate: true
      permanent: true
      service: http
      state: enabled

Files

Create file
- copy:
    dest: /etc/motd
    content: "Hello, World!\n" # (1)
  1. Alma Linux 9 additionally requires the libselinux-python package to handle SELinux contexts. It is incorrectly identified as "libselinux-python" in the Ansible error message.
Delete file
- file:
    path: /etc/motd
    state: absent
Create directory
- file:
    path: /home/ansible/.vim/autoload
    state: directory
    mode: 0755

User creation

- hosts: all
  vars:
  - username: newuser
  - password: password
  tasks:
  - user:
      name: "{{ username }}"
      password: "{{ password }}"

More secure is using a separate vaulted variables file to keep the credential secure.

playbooks/user.yml
- hosts: all
  vars_files:
  - vars/user.yml
  tasks:
  - user:
      name: "{{ username }}"
      password: "{{ password }}"
playbooks/vars/user.yml
username: newuser
password: password
Run playbook providing a password file
ansible-playbook --vault-password-file vault-pw playbooks/user.yml

Commands

ansible

Used to run ad-hoc commands from the command-line.

Ad-hoc commands
ansible all -m shell -a env
ansible all -a env # (1)
  1. The command module is default and does not have to be made explicit
Display all available hosts
ansible localhost --list-hosts

ansible-config

Display non-default settings
ansible-config dump --only-changed

ansible-doc

# List currently installed modules
ansible-doc -l

# Get module-specific information
ansible-doc $MODULE

# Get example code
ansible-doc -s $MODULE

ansible-galaxy

# Log in
ansible-galaxy login

# Search for roles
ansible-galaxy search $ROLE

# Install a public role (to ~/.ansible/roles by default)
ansible-galaxy install $USER.$ROLE

# Initiate the skeleton structure of a role
ansible-galaxy init $ROLENAME

# Upload a role
ansible-galaxy import $USERNAME $REPONAME
ansible-galaxy import --no-wait $USERNAME $REPONAME # send job to background

A requirements file can also be used.

ansible-galaxy role install -r requirements.yml
requirements.yml
roles:
- src: git+https://jasperzanjani@dev.azure.com/jasperzanjani/NewDevOpsProject/_git/motd-role # (1)
  version: master

  1. By default, ansible-galaxy will expect a tarball, unless git+ is prepended to the URL.

As long as a public key is registered with Azure DevOps (and an outbound SSH connection isn't blocked by the firewall), the requirements file can use an SSH connection.

roles:
- src: git@ssh.dev.azure.com:v3/jasperzanjani/NewDevOpsProject/motd-role
  version: master

Variables in

ansible-playbook

Verify YAML syntax
ansible-playbook --syntax-check $FILE

ansible-vault

# Create an encrypted file, providing password interactively
ansible-vault create $file

# Use a cleartext password file
ansible-vault view --vault-password-file=vault-pw $file

# Encrypt/decrypt a file in-place, overwriting original file
ansible-vault encrypt $file
ansible-vault decrypt $file

ansible-vault edit secret.yml

Modules

archive
- name: Compress directory /path/to/foo/ into /path/to/foo.tgz
  archive:
    path: /path/to/foo
    dest: /path/to/foo.tgz
- name: Create a bz2 archive of multiple files, rooted at /path
  archive:
    path:
    - /path/to/foo
    - /path/wong/foo
    dest: /path/file.tar.bz2
    format: bz2
cli_config
# Platform-agnostic way of [pushing text-based configurations](https://opensource.com/article/19/9/must-know-ansible-modules) to network devices over the **network_cli_connection** plugin.
- name: Set hostname for a switch and exit with a commit message
  cli_config:
    config: set system host-name foo
    commit_comment: this is a test
- name: Back up a config to a different destination file
  cli_config:
    config: "{{ lookup('template', 'basic/config.j2') }}"
    backup: yes
    backup_options:
      filename: backup.cfg
      dir_path: /home/user
command
- name: Return motd to registered var
  command: cat /etc/motd
  register: mymotd
- name: Change the working directory to somedir/ and run the command as db_owner if /path/to/database does not exist.
  command: /usr/bin/make_database.sh db_user db_name
  become: yes
  become_user: db_owner
  args:
    chdir: somedir/
    creates: /path/to/database
copy
- name: Copy a new "ntp.conf file into place, backing up the original if it differs from the copied version
  copy:
    src: /mine/ntp.conf
    dest: /etc/ntp.conf
    owner: root
    group: root
    mode: '0644'
    backup: yes
- name: Copy file with owner and permission, using symbolic representation
  copy:
    src: /srv/myfiles/foo.conf
    dest: /etc/foo.conf
    owner: foo
    group: foo
    mode: u=rw,g=r,o=r
debug
- name: Display all variables/facts known for a host
  debug:
    var: hostvars[inventory_hostname]
    verbosity: 4
# Display content of copy module only when verbosity of 2 is specified
- name: Write some content in a file /tmp/foo.txt
  copy:
    dest: /tmp/foo.txt
    content: |
    Good Morning!
      Awesome sunshine today.
    register: display_file_content
- name: Debug display_file_content
  debug:
    var: display_file_content
    verbosity: 2

file
- name: Change file ownership, group and permissions
  file:
    path: /etc/foo.conf
    owner: foo
    group: foo
    mode: '0644'
- name: Create a directory if it does not exist
  file:
    path: /etc/foo
    state: directory
    mode: '0755'
file
# Create a symlink
ansible $CLIENT -b -m file -a "src=/etc/ntp.conf dest=/home/user/ntp.conf owner=user group=user state=link"

# Create a folder using an ad hoc command
ansible $CLIENT -b -m file -a "path=/etc/newfolder state=directory mode=0755"

git
- git:
    name: Create git archive from repo
    repo: https://github.com/ansible/ansible-examples.git
    dest: /src/ansible-examples
    archive: /tmp/ansible-examples.zip
- git:
    repo: https://github.com/ansible/ansible-examples.git
    dest: /src/ansible-examples
    separate_git_dir: /src/ansible-examples.git
lineinfile
- name: Ensure SELinux is set to enforcing mode
  lineinfile:
    path: /etc/selinux/config
    regexp: '^SELINUX='
    line: SELINUX=enforcing
- name: Add a line to a file if the file does not exist, without passing regexp
  lineinfile:
    path: /etc/resolv.conf
    line: 192.168.1.99 foo.lab.net foo
    create: yes
package
- name: Install Apache and MariaDB
  dnf:
    name:
    - httpd
    - mariadb-server
    state: latest
- name: Install PostgreSQL and NGINX
  yum:
    name:
    - nginx
    - postgresql
    - postgresql-server
    state: present
replace
- name: Comment out a line in a config
  ansible.builtin.replace:
    path: /etc/motd
    regexp: '^Hello, (.*)'
    replace: '# Hello, \1'
service
- name: Start service foo, based on running process /usr/bin/foo
  service:
    name: foo
    pattern: /usr/bin/foo
    state: started
- name: Restart network service for interface eth0
  service:
    name: network
    state: restarted
    args: eth0
setup
# Display all available information about a system
ansible $CLIENT -b -m setup

# Filter results to ansible_os_family, which indicates if the OS is Debian or Red Hat
ansible $CLIENT -b -m setup -a "filter=*family*"
snap
- name: Install VS Code
  snap:
    name: code
    state: present
    classic: yes   
template
# This example creates a HTML document on each client that is customized using Ansible variables. 
---
- hosts: webservers
  become: yes

  tasks:
  - name: install apache2
    apt: name=apache2 state=latest update_cache=yes
    when: ansible_os_family == "Debian"

  - name: install httpd
    yum: name=httpd state=latest
    when: ansible_os_family == "RedHat"

  - name: start apache2
    service: name=apache2 state=started enable=yes
    when: ansible_os_family == "Debian"

  - name: start httpd
    service: name=httpd state=started enable=yes
    when: ansible_os_family == "RedHat

  - name: install index
    template:
      src: index.html.j2
      dest: /var/www/html/index.html
# (1)!
  1. Jinja2 template file
    <html>
      <h1>This computer is running {{ ansible_os_family }},
      and its hostname is:</h1>
      <h3>{{ ansible_hostname }}</h3>
      {# this is a comment, which won't be copied to the index.html file #}
    </html>
    

Glossary

  • Ad Hoc: type of command run in realtime by an administrator working at the terminal
  • Ansible Galaxy: online portal where a gallery of roles made by the Ansible community can be found
  • Ansible Tower: web-based RESTful API endpoint that provides the officially supported GUI frontend to Ansible configuration management, available in two versions: standard ($13,000/yr) and premium ($17,500/yr)
  • Ansible Vault: place to keep encrypted passwords
  • AWX: Open-source project upon which Ansible Tower was built
  • Fact: System property gathered by Ansible when it executes a playbook on a node
  • Inventory: INI-format file containing a list of servers or nodes that you are managing and configuring
  • Module: standalone scripts that enable a particular task across many OSes, services, applications, etc. Predefined modules are available in the module library, and new ones can be defined via Python or JSON.
  • Play: script or instruction that defines the task to be carried out in a server
  • Playbook:
  • Role: organize components of playbooks, allowing them to be reused
  • Task: A single scripted action in a playbook, equivalent to an ad hoc command
  • Vault: feature of Ansible that allows you to keep sensitive data such as passwords or keys protected at rest, rather than as plaintext in playbooks or roles.