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.
- Use the
users
module to manage users, assign groups, and define custom aliases in theprofile
property. - Put a time limit on the availability of the
sudo
command - Use Ansible Tower to produce a GUI interface to restart certain services.
- Use Ansible Tower to look for files larger than a particular size in a directory.
- 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.
- 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.
- 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.
- hosts: all
vars_files:
- vars/name.yml
tasks:
- copy:
dest: /etc/motd
content: Hello, {{ greet_name }}!
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.
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
- hosts: all
roles:
- role: roles/motd
vars:
greet_name: Dgiapusccu # (1)
- Without providing an overriding variable value:
- hosts: all roles: - roles/motd
# motd/tasks/main.yml
- copy:
content: Hello, {{ greet_name }}!
dest: /etc/motd
# motd/defaults/main.yml
greet_name: World # (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.
- 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
roles:
- src: git@ssh.dev.azure.com:v3/PODS-LLC/SWE/devops_inventory_role
version: master # (2)
scm: git
name: inventory # (1)
- This apparently determines how the directory is renamed.
- 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.
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/ansibleansible ALL=(ALL) NOPASSWD: ALL # (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 hostsansible all --list-hosts
- Without this line, plays will fail with the message "Missing sudo password"
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)
- 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.ymlusername: newuser password: password
Run playbook providing a password fileansible-playbook --vault-password-file vault-pw playbooks/user.yml
Commands
ansible
-
Used to run ad-hoc commands from the command-line.
Ad-hoc commandsansible all -m shell -a env ansible all -a env # (1)
- The command module is default and does not have to be made explicit
Display all available hostsansible 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.ymlroles: - src: git+https://jasperzanjani@dev.azure.com/jasperzanjani/NewDevOpsProject/_git/motd-role # (1) version: master
- 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
- By default, ansible-galaxy will expect a tarball, unless
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
- 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
# 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
- 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
- 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
- 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
- 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'
# 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:
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
- 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
- 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
- name: Comment out a line in a config
ansible.builtin.replace:
path: /etc/motd
regexp: '^Hello, (.*)'
replace: '# Hello, \1'
- 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
# 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*"
- name: Install VS Code
snap:
name: code
state: present
classic: yes
# 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)!
- 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.