20 Ansible Interview Questions

Question 1

Describe each of the following components in Ansible, including the relationship between them:

  • Task
  • Module
  • Play
  • Playbook
  • Role

Answer

This question checks whether you are familiar with Ansible fundamental components and how they fit in. I find it a very important question as it’s the basis for everything we do with Ansible.

  • Task – a call to a specific Ansible module
  • Module – the actual unit of code executed by Ansible on your own host or a remote host. Modules are indexed by category (database, file, network, …) and also referred as task plugins.
  • Play – One or more tasks executed on a given host(s)
  • Playbook – One or more plays. Each play can be executed on the same or different hosts
  • Role – Ansible roles allows you to group resources based on certain functionality/service such that they can be easily reused. In a role, you have directories for variables, defaults, files, templates, handlers, tasks, and metadata. You can then use the role by simply specifying it in your playbook.

More details on the different Ansible core components can be found  here

Question 2

Write a task to create the directory ‘/tmp/new_directory’

Answer

Very basic question, but indicates how you work with Ansible. Many will answer this question by using the shell or command modules. It doesn’t necessarily bad, but the best practice is always to use an explicit Ansible module (in our case, the ‘file’ module).

Why? Mainly due to readability. Some actions execute differently on different operating systems, but the module use is always the same and any Ansible user will know what you meant when reading the task (especially if it’s a long shell command).

Note: it doesn’t necessarily mean that modules are faster than the command(s) you specified with ‘shell’ or ‘command’.

The task of creating the directory

- name: Create a new directory
  file:
      path: "/tmp/new_directory"
      state: directory

Question 3

What would be the result of the following play?

---
- name: Print information about my host
  hosts: localhost
  gather_facts: 'no'                                                                                                                                                                           
  tasks:
      - name: Print hostname
        debug:
            msg: "It's me, {{ ansible_hostname }}"

Answer

When given a written code, always inspect it thoroughly. If your answer is “this will fail” then you are right. We are using a fact (ansible_hostname), which is a gathered piece of information from the host we are running on. But in this case, we disabled facts gathering (gather_facts: no) so the variable would be undefined which will result in failure.

The purpose of this question is to check if you know what is a fact but also whether you are paying attention to the small details.

A similar/follow-up questions can be:

  • How to list all facts available?
  • How to set a fact of your own?

Question 4

Write a playbook to install ‘zlib’ and ‘vim’ on all hosts if the file ‘/tmp/mario’ exists on the system.

Answer

---
- hosts: all
  vars:
      mario_file: /tmp/mario
      package_list:
          - 'zlib' 
          - 'vim'
  tasks:
      - name: Check for mario file
        stat:
            path: "{{ mario_file }}"
        register: mario_f

      - name: Install zlib and vim if mario file exists
        become: "yes"
        package:
            name: "{{ item }}"
            state: present
        with_items: "{{ package_list }}"
        when: mario_f.stat.exists

To answer this question you have to be familiar with register, conditionals, and loops*.

The first task uses the ‘stat’ module to check if the file exists on each system then captures the output in a variable called ‘mario_f’ using the ‘register’ term. You can then use the registered variable in any other task. In our case, we capture the stats of ‘/tmp/mario’ file and in the next task, we install the package list if the file exists.

As you can see, for installing the packages we used the “with_items” loop which allow us to iterate over a list and perform the module/task per item in the list. Loops, as in any other language, is a fundamental part of Ansible and you should be aware of the different types of loops supported with Ansible.

Another worth to mention line is ‘become: yes’ which allows us to run the task as root, but can be used with any user we will specify (e.g. become: ‘toad’). The task installing package list will fail unless this line is included since package installation can be done only with sudo permissions on the machine.

*Bonus: Some Ansible modules can receive a list as an argument. In our case, the loop process could have been removed completely by simply providing the package_list variable straight to the ‘package’ module.
Also, we could have removed the “stat” module completely by using the Ansible ‘with_fileglob’ loop, which iterates a list of files found by a regex.
To sum this up:

---
- hosts: all
  vars:
      package_list:
        - 'zlib' 
        - 'vim'
  tasks:
      - name: Install zlib and vim if mario file exists
        become: "yes"
        package:
            name:"{{ package_list }}"
            state: present
        with_fileglob:
            - 'tmp/mario'

Question 5

Write a playbook to deploy the file ‘/tmp/system_info’ on all hosts except for controllers group, with the following content

I'm <HOSTNAME> and my operating system is <OS>

replace <HOSTNAME> and  <OS> with the actual data for the specific host you are running on

Answer

The playbook to deploy the system_info file

--- 
- name: Deploy /tmp/system_info file
  hosts: all:!controllers
  tasks: 
      - name: Deploy /tmp/system_info
        template:
            src: system_info.j2 
            dest: /tmp/system_info

The content of the system_info.j2 template

# {{ ansible_managed }}
I'm {{ ansible_hostname }} and my operating system is {{ ansible_distribution }}

Using a template will make your playbooks and roles much more dynamic and easily adjusted to different scenarios. Ansible uses a powerful templating engine called ‘Jinja2‘ to construct dynamic templates of files. Many popular projects and companies are using it (Flask, Mozilla, …) and we strongly recommend you take the time and learn using it as it will serve you well in the future.

When writing jinja templates for Ansible it is considered best practice to add the variable ‘ansible_managed’ at the top of the template. This variable expands to a string letting whoever reads the output file that this file was generated by Ansible and managed by it.
Next, we use ansible_hostname which is the unqualified name of the host and ansible_distribution which is the OS distro.

Be aware that this question could be implemented in other ways. For example, instead of using ansible_hostname, someone could use inventory_hostname. They are not the same, but they both would fit fine in this case.

Question 6

How do you test your Ansible based projects?

Answer

When I ask this question in some interviews, I get many different answers. Let’s go over some of them.

  • Manual run: “I simply run it and check if the system is in the desired state” – Personally I don’t like this answer when provided solely. I could agree this is the easiest one (or laziest) but it potentially quite dangerous. Even if you tested your new written role in a development environment, it doesn’t mean you’ll get the same result in a production environment.
  • Check mode – yes, check mode is a good way to test your Ansible code as it will report to you what it would have done if it would actually run without check mode. So you can easily see if the Ansible run behavior is meeting your expectations.
    But the follow-up question here is “and what about scripts?”. Usually, the answer I get here is “what about them?” 🙂 that’s fine if you didn’t have to use scripts in your roles and playbooks, but if you did, you would know that check mode doesn’t run scripts and commands. To run them, you would have to disable check mode for specific tasks with “check_mode: no”.
  • Asserts:  I like asserts as a method of testing as it also resembles how you test in other languages such as Python and more importantly, it makes sure that your system reached the desired state, not as a draft like in check mode, but as an actual verification that the task changed certain resource to the desired state.

To summarize, simply be confident when explaining your choice (also, it’s not realistic to expect someone to use them all, so don’t go this way 🙂 )

Question 7

Write a playbook to install PostgreSQL on all servers in database group (assume Redhat) and update postgresql.conf with a configuration from the template postgresql.conf.j2 (assume it’s there, don’t write one).

In addition, provide users with a way to run only the configuration update task (without installing the packages).

Answer

There are two things you should not miss here: handlers and tags. Your playbook should look similar to the following:

---

- name: Installing PostgreSQL
  hosts: databases
  vars:
      pg_packages:
        - postgresql
        - postgresql-client
      pg_service: postgresql
      pg_admin_user: postgres
      pg_admin_group: postgres
      pg_version: 1981

  tasks:
      - name: Install PostgreSQL packages
        become: yes
        yum:
            name: "{{ item }}"
            state: latest
        loop: "{{ pg_packages }}"
        notify:
            - start_postgresql

      - name: Update postgres.conf file
        template:
            src: postgres.conf.j2
            dest: /etc/postgresql/{{ pg_version }}/postgresql.conf
            owner: {{ pg_admin_user }}
            group: {{ pg_admin_group }}
            mode: 0644
        become: yes
        notify:
            - restart postgresql
        tags: 
            - postgres_config
     
  handlers:
      - name: start_postgresql
        service:
            name: "{{ pg_service }}"
            state: started

      - name: restart_postgresql
        service:
            name: "{{ pg_service }}"
            state: restarted

As you can see I have put everything in the same file, but a better solution would be to create a role and put each section into its own directory (vars, handlers, tasks, …). We’ll have detailed question and answer on roles later on.

The first thing to note here is handlers. Handlers allow us to trigger action (usually a task) upon a change. As you can see, the syntax is quite simple, using the keyword ‘notify’ we can provide a list of actions to execute. This makes a clear separation between what are the main tasks, required for installing and configuring PostgreSQL and what are the small “sub” actions required for the tasks to be complete.

The second thing to note here is ‘tags’. This answers the second part of the question on how to allow users to run only the configuration update part. Imagine you have 100 tasks and you want only run a small portion of them, let’s say four tasks which responsible for updating your application. Without tags, you would have to run everything in your playbook, but tags allow you to run only those you marked with a specific tag.

Question 8

Let’s say you used the same variable name (whoami) in several places with different values:

  • role defaults -> whoami: mario
  • extra vars (variables you pass to Ansible CLI with -e) -> whoami: toad
  • host facts -> whoami: luigi
  • inventory variables (doesn’t matter which type) -> whoami: browser

Which one will be used eventually? Why?

Answer

The right answer is ‘toad’.

Variable precedence is about how variables override each other when they set in different locations. If you didn’t experience it so far I’m sure at some point you will, which makes it a useful topic to be aware of.

In the context of our question, the order will be extra vars (always override any other variable) -> host facts -> inventory variables -> role defaults (the weakest).

A full list can be found at the link above. Also, note there is a significant difference between Ansible 1.x and 2.x.

Question 9

Which Ansible best practices are you familiar with? name at least three

Answer

  • When using several parameters it’s better to use the YAML dictionary format. I personally find it much more readable.
# BAD

- name: Update postgres.conf file
  template: src=postgres.conf.j2
            dest=/etc/postgresql/{{ pg_version }}/postgresql.conf
            owner={{ pg_admin_user }} group={{ pg_admin_group }}
            mode=0644
# GOOD

- name: Update postgres.conf file
  template:
      src: postgres.conf.j2
      dest: /etc/postgresql/{{ pg_version }}/postgresql.conf
      owner: {{ pg_admin_user }}
      group: {{ pg_admin_group }}
      mode: 0644
  • “Always Name Tasks”. It’s good to get used to name every task you add. Even the simple among them, like using the ‘debug’ module. The name of the task gives some indication as to what it does and why it was added. It can be a workaround a known bug or it can be a long and juicy command so the user reads/using your playbook will appreciate if you describe shortly what it does.
# BAD

- template:
    src: postgres.conf.j2
    dest: /etc/postgresql/{{ pg_version }}/postgresql.conf

# GOOD

- name: Update postgres.conf file
  template:
      src: postgres.conf.j2
      dest: /etc/postgresql/{{ pg_version }}/postgresql.conf
  • Any change to Ansible code should pass ansible-lint. This is another non-official best practice which is basically a best-practices checker.  This is why I consider it as one of the most important best practices to implement as it makes it easier to make sure other best practices are being followed, especially in a shared repository where you have several contributors.

As already stated, not every mentioned best practice is an official one (= in Ansible docs) and that’s totally fine as long as you can explain why it’s a best practice (or more accurately a good practice).

Question 10

Write an Ansible Ad-Hoc command to create the file ‘/tmp/info” on all hosts with the content: “The inventory file is in <inventory_file_path> and the inventory groups are <inventory_groups>”

Note: you should also list the hosts which included in the  inventory groups

Answer

Knowing the different ways to execute Ansible can save you time in the future. One of them is the Ad-Hoc way which is a very quick way to run anything on a remote host.

In our case, our ad-hoc command will look like this

ansible all -m copy -a 'content="The inventory file is in {{ inventory_dir }} and the inventory groups are {{ groups }}" dest=/tmp/info'

-m for specifying the module name. We used ‘copy’ but you could also use other modules for achieving the same result.

-a are the arguments we are passing to the module like the content of the file and where to create it (‘dest’)

I suggest running a couple of ad-hoc commands in order to be familiar with it and feel comfortable enough to run them on demand.

Question 11

What is ansible-pull?  How it’s different compared to ansible-playbook?

Answer

We know that running ansible-playbook will enforce certain configuration on the hosts we are operating on from what is known as the control node (the node we are running the commands from).

ansible-pull is also applying configuration but it’s running from a managed host and not from the control host. It’s pulling the configuration from a given URL of a repository.

It could be useful for certain use cases in which you need a reversed architecture of enforcing configuration on the host you are connected to, from a central place

Question 12

You have a list of directories and you want to write a task which will copy the first directory it finds to a remote server

Answer

- name: Copy directory to a remote host
  synchronize:
      src: "{{ item }}"
      dest: "{{ ansible_env.HOME }}"
  with_first_found:
      - "/home/mario/dir1"
      - "/tmp/dir2"
      - "/tmp/dir3"

Question 13

What is a dynamic inventory?

Which conventions are important to follow when writing a new dynamic inventory script?

Answer

As opposed to the default/static inventory where you update the list hostname manually or by pushing/removing lines, a dynamic inventory is generated by extracting information from external sources like a cloud (OpenStack, GCE, …) or LDAP. Most of these dynamic repositories scripts can be found in contrib/inventory directory

when developing a new dynamic inventory it’s important the result of such script when called with the ‘–list’ argument will output the groups in JSON format. It can be group:managed_hosts like this:

{
    "database":["host1", host2"],
    "web":["host3", "host4"],
    "video":["host5", "host6"]
}

Or group:dict_of_variables like this:

{
    "databases": {
        "hosts": ["host1"],
        "vars": {
            "this_is_cool": true
                }
                 },
    "web": {
        "hosts": ["host2", "host3"],
        "vars": {
            "this_is_also_cool": true
                }
           }
}

Of course, it can also be a mix of the two

{
    "databases": {
        "hosts": ["host1"],
        "vars": {
            "x": 2
                }
                 },
    "app": ["host2", host3"]
}

More information on dynamic inventory can be found here.

Question 14

Given a full or relative path of a file (by passing a variable with -e) do the following:

  1. Look for it in the location given or the current working directory
  2. If you managed to find the file, copy it to a remote host to the user home directory. If you didn’t manage to find it, fail the run/execution

Answer

 name: find a given file                                                                                                                                                                  
  find:
      file_type: file
      pattern: "{{ given_path | basename }}"
      paths:
          - "{{ lookup('env', 'PWD') }}/{{ given_path | dirname }}"
          - "{{ given_path | expanduser | dirname }}"
  register: find_results
  delegate_to: localhost
 
- name: fail when file was not found
  fail:
      msg: "Unable to find the file: {{ given_path | basename }}"
  when: given_path|default('') and find_results.matched == 0
 
- name: copy file to remote host
  vars:
      file_src: "{{ find_results.files[0].path }}"
  copy:
      src: "{{ file_src }}"
      dest: "{{ ansible_env.HOME }}"

Question 15

Write a role to install Apache

Answer

I imagine this question would be very popular in interviews since ‘role’ is a key component in Ansible and it makes use of many of components we discussed in previous questions like tasks, templates, and variables.

Note that there is more than one answer for this question. I will provide a short but comprehensive answer. A detailed answer with the use of all the directories in a role will probably give you extra points.

Let’s start with the structure

apache-role/
    vars/
    handlers/
    tasks/
    defaults/
    meta/

It’s a very standard role structure, but you need to be familiar with each directory – what is it used for, why and how you implement it in order to create an Apache installation in our case.

Let’s start with vars. vars hold all the variables we’ll use for this role. What can be a variable? if we are using this role on different operating systems, then the service name of apach2 could be a variable. On Fedora, it would be httpd while on Debian it’s called apache2. Another variable could be apache2_packages to define which packages should be installed on each operating system. Let’s see how the implementation of this looks

apache-role/
    vars/
        RedHat.yml
        Debian.yml

Let’s have a look at RedHat.yml

---
# This file is vars/RedHat.yml

apache2_service: httpd
apache2_packages:
    - 'httpd'
    - 'httpd-devel'

Debian.yml would look like this

---
# This file is vars/Debian.yml

apache2_service: apache2
apache2_packages:
    - 'apache2'
    - 'httpd-devel'

Now let’s move to handlers. Handlers are triggered actions upon certain change. In our case, a very common handler could be ‘restart service’.

---
# This file is handlers/main.yml

- name: Restart apache2
  service:
      name: "{{ apache2_service }}"
      state: 'started'

This handler will be used to restart apache2 after making changes to the configuration. Remember {{ apache2_service }} is defined in RedHat.yml and Debian.yml.

Next, we’ll define default variables in defaults/main.yml

apache2_listen_port: 80
apache2_listen_port_ssl: 443

If you wonder what is the difference between defaults and vars then you should think on defaults as common variables, used for every type of operating system and scenario while vars are adjusted variables to a specific use case/environment. In our case, the operating system type.

The meta folder acts as a place to define extra behaviors in a role. The most commonly used definition is the role dependencies.
There are cases such that a role is dependent on a different role, for example, installing a Java based application requires Java to be installed.
Lets build on this example, say we have two roles:
1. Java installation role
2. Elasticsearch installation role
Our play might look like:

- hosts: elasticsearch
  roles:
    - { role: java, version: 8 }
    - elasticsearch

But since we love sharing our roles with other people, a user that would like to use our elasticsearch role might not know it depends on another role called ‘Java’, therefore by adding a line under the meta folder:

---
# This file is meta/main.yml

dependencies:
  - { role: java, version: 8 }

It will try and run a role called ‘java’ prior to invoking the ‘elasticsearch’ role.
This will allow us to refactor our previous play:

- hosts: elasticsearch
  roles:
    - elasticsearch

Finally, we’ll define some tasks. The “heart” of the role, the part that defines what will be executed. We will have a simple main file which will include additional tasks based on purpose (e.g. install, configure, …) and will do it based on OS type. In order to install right packages, we will first include variables we earlier defined, per the OS we are using.

# This is tasks/main.yml file

- name: Include OS variables
  include_vars: "{{ ansible_os_family }}.yml"

- name: Install Apache
  include_tasks: "install-{{ ansible_os_family }}.yml"

Now, let’s have a look at install-RedHat.yml

# This is tasks/install-RedHat.yml

- name: Install Apache
  become: yes
  yum:
      name: "{{ item }}"
      state: present
  with_items: "{{ apache2_packages }}"

Finally, this is how our structure looks like after creating all the above files:

apache-role/
    vars/
        Debian.yml
        RedHat.yml
    handlers/
        main.yml
    tasks/
        main.yml
        install-RedHat.yml
    defaults/
        main.yml
    meta/
        main.yml

Remember, this is not a full solution and the more verbose and detailed answer you’ll give,  the more credit you’ll get. For example: add configuring Apache tasks and adding templates directory with templates for the configuration.

Question 16

You have the following play

---
- name: Print variable    
  hosts: localhost    
  tasks:         
      - name: Print test variable
        debug:    
            msg: "{{ test }}"

And you run the following command

ansible-playbook -e test="test 1 2 3" debug.yaml

What would be the output of the test variable?

  • test 1 2 3
  • test
  • empty string
  • the variable is not defined

 Answer

The answer is ‘test’.

This one is tricky and it checks whether you understand shell and Ansible. In this case, since the command is first processed by the shell, whatever in the quotes and after a backslash or space will be ignored.

As an interviewer, you should know that most of the candidates will not answer this correctly or simply guess and it shouldn’t indicate whether they are familiar with Ansible or not.

The solution would be to quote whatever is after ‘-e’ as a whole

ansible-playbook -e 'test="test 1 2 3"' debug.yaml

Question 17

How would you change the output format of Ansible execution to display only the name of the tasks?

Answer

The answer is “callback plugin”. It doesn’t let you only to change what you see but basically do everything you want based on different events in Ansible. So a similar question might be: “how would you log everything to a file?” and the answer would still be a callback plugin.

Here some of the main points you need to know about callback plugins:

  • There are several callback plugins in Ansible tree
  • To enable callback plugin, include it in ansible.cfg like this:
    callback_whitelist = my_cool_plugin
  • To change the default stdout of Ansible execution, modify ansible.cfg to include
    stdout_callback = my_new_stdout_callback_plugin
  • when developing a new callback plugin, you need to inherit from this parent class: “CallbackBase” and override methods such as
    • v2_runner_on_skipped
    • v2_runner_on_unreachable
    • v2_runner_on_ok
    • v2_runner_on_failed

Callback plugin is a great way to fully customize your Ansible execution and the way you consume Ansible output.

Question 18

File ‘/tmp/exercise’ includes the following content

Goku = 9001
Vegeta = 5200
Trunks = 6000
Gotenks = 32

With one task, switch the content to:

Goku = 9001
Vegeta = 250
Trunks = 40
Gotenks = 32

Answer

 - name: Change saiyans level
   lineinfile:
       dest: /tmp/exercise
       regexp: "{{ item.regexp }}"
       line: "{{ item.line }}"
   with_items:
       - { regexp: '^Vegeta', line: 'Vegeta = 250' }
       - { regexp: '^Trunks', line: 'Trunks = 40' }

No new concepts here, just a little more advanced for loop where we use two items every iteration (regexp and line).

Note that you could also solve this with ‘blockinfile’.

Question 19

Write a filter to captialize a string

ansible localhost -m debug -a 'msg="{{ "i love pizza" | cap }}"'
localhost | SUCCESS => {
 "msg": "I love pizza"
}

Answer

def cap(self, string):
    return string.capitalize()

We just wrote a very simple filter. Writing filters is a good skill you want to own when mastering Ansible and there are several reasons for that.

First, believe it or not, filters make your playbooks more readable in some cases. Especially when you are using a long juicy command that user needs a couple of minutes to understand what you wrote and why. ‘hostname_to_ip’ is easy to understand, right?

In addition, don’t try to use Ansible blindly for everything. That’s not what I meant in the 2nd interview question. Use Ansible when you can, when it’s simple for others to understand and when performances not impacted but don’t stick to it at any cost.

Question 20

The last question is a summary question. You have 10 statements, for each one determine if it’s true or false (one of them is “depends” :D)

  1. A module is a collection of tasks
  2. It’s better to use shell or command instead of a specific module
  3. Host facts override play variables
  4. A role might include the following: vars, meta, and handlers
  5. Dynamic inventory is generated by extracting information from external sources
  6. It’s a best practice to use indention of 2 spaces instead of 4
  7. The following task will run successfully
- name: Install zlib
  yum:
      name: zlib
      state: present

8. ‘notify’ used to trigger handlers

9. This “hosts: all:!controllers” means ‘run only on controllers group hosts’

10. role defaults override role vars

Answer

  1. False. A play is a collection of tasks. A task is an execution of a specific module.
  2. False. While it can be quicker to use shell or command, it’s always good to use a specific module as it’s usually more readable for Ansible users.
  3. False. Play variables override host facts
  4. True
  5. True. It can be clouds like OpenStack. It can also be LDAP
  6. False. It’s really doesn’t matter (at least for some people 🙂 )
  7. Depends. If the playbook or role has ‘become: yes’ it will run successfully but if not, it will fail due to permissions.
  8. True
  9. False. It means run on all hosts except for controllers
  10. False. Role vars override role defaults.

Practice is everything

That’s it. If you managed to answer most of the questions correctly, it definitely indicates you are familiar with Ansible concepts and usage. But remember, practice makes perfect, not answering interview questions 🙂

More interview questions!

For more interview questions or a checklist of subjects to go over when learning Ansible, visit our repository in GitHub

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s