Ajitabh Pandey's Soul & Syntax

Exploring systems, souls, and stories – one post at a time

Tag: ansible

  • Solving Ansible’s Flat Namespace Problem Efficiently

    In Ansible, the “Flat Namespace” problem is a frequent stumbling block for engineers managing multi-tier environments. It occurs because Ansible merges variables from various sources (global, group, and host) into a single pool for the current execution context.

    If you aren’t careful, trying to use a variable meant for “Group A” while executing tasks on “Group B” will cause the play to crash because that variable simply doesn’t exist in Group B’s scope.

    The Scenario: The “Mixed Fleet” Crash

    Imagine you are managing a fleet of Web Servers (running on port 8080) and Database Servers (running on port 5432). You want a single “Security” play to validate that the application port is open in the firewall.

    The Failing Code:

    - name: Apply Security Rules
    hosts: web:database
    vars:
    # This is the "Flat Namespace" trap!
    # Ansible tries to resolve BOTH variables for every host.
    app_port_map:
    web_servers: "{{ web_custom_port }}"
    db_servers: "{{ db_instance_port }}"

    tasks:
    - name: Validate port is defined
    ansible.builtin.assert:
    that: app_port_map[group_names[0]] is defined

    This code fails when Ansible runs this for a web_server, it looks at app_port_map. To build that dictionary, it must resolve db_instance_port. But since the host is a web server, the database group variables aren’t loaded. Result: fatal: 'db_instance_port' is undefined.

    Solution 1: The “Lazy” Logic

    By using Jinja2 whitespace control and conditional logic, we prevent Ansible from ever looking at the missing variable. It only evaluates the branch that matches the host’s group.

    - name: Apply Security Rules
    hosts: app_servers:storage_servers
    vars:
    # Use whitespace-controlled Jinja to isolate variable calls
    target_port: >-
    {%- if 'app_servers' in group_names -%}
    {{ app_service_port }}
    {%- elif 'storage_servers' in group_names -%}
    {{ storage_backend_port }}
    {%- else -%}
    22
    {%- endif -%}

    tasks:
    - name: Ensure port is allowed in firewall
    community.general.ufw:
    rule: allow
    port: "{{ target_port | int }}"

    The advantage of this approach is that it’s very explicit, prevents “Undefined Variable” errors entirely, and allows for easy defaults. However, it can become verbose/messy if you have a large number of different groups.

    Solution 2: The hostvars Lookup

    If you don’t want a giant if/else block, you can use hostvars to dynamically grab a value, but you must provide a default to keep the namespace “safe.”

    - name: Validate ports
    hosts: all
    tasks:
    - name: Check port connectivity
    ansible.builtin.wait_for:
    port: "{{ vars[group_names[0] + '_port'] | default(22) }}"
    timeout: 5

    This approach is very compact and follows a naming convention (e.g., groupname_port). But its harder to debug and relies on strict variable naming across your entire inventory.

    Solution 3: Group Variable Normalization

    The most “architecturally sound” way to solve the flat namespace problem is to use the same variable name across different group_vars files.

    # inventory/group_vars/web_servers.yml
    service_port: 80
    # inventory/group_vars/db_servers.yml
    service_port: 5432
    # Playbook - main.yml
    ---
    - name: Unified Firewall Play
    hosts: all
    tasks:
    - name: Open service port
    community.general.ufw:
    port: "{{ service_port }}" # No logic needed!
    rule: allow

    This is the cleanest playbook code; truly “Ansible-native” way of handling polymorphism but it requires refactoring your existing variable names and can be confusing if you need to see both ports at once (e.g., in a Load Balancer config).

    The “Flat Namespace” problem is really just a symptom of Ansible’s strength: it’s trying to make sure everything you’ve defined is valid. I recently solved this problem in a multi-play playbook, which I wrote for Digital Ocean infrastructure provisioning and configuration using the Lazy Logic approach, and I found this to be the best way to bridge the gap between “Group A” and “Group B” without forcing a massive inventory refactor. While I have generalized the example code, I actually faced this problem in a play that set up the host-level firewall based on dynamic inventory.

  • Get Oracle XE-ORDS-APEX Up and Running Quickly with Ansible

    Oracle APEX is a rapid application development tool and from what I have seen its quite powerful. Clubed together with ORDS, it is a powerful tool to develop enterprise applications which are highly scalable, provided you are willing to spend $$$$ as everything depends on underlying Oracle Database.

    For those who want to learn the tool, Oracle XE (Express Edition) can be used as a development environment. Setting up the full stack can be a little bit challenging for developers who wants to stick to building their core strength and not mess with infrastructure deployments etc.

    To help here, I wrote this ansible playbook, which will quickly setup the entire stack. Accompanied in the git repository are the Vagrantfile which can quickly setup the DEV environment in a VirtualBox VM. The stack is currently based on CentOS 7.x (and can run in RHEL 7.x as well).

    The README file in the git repostory is self explanatory and explains in detail on how to use this playbook to get the stack up and running.

    Upcoming Enhancements

    I will be making suitable modifications in to make it run on RHEL 8.x and Oracle Linux 8.x. I will also be working on fixing the accompanied packer file which has not been tested thoroughly for building a Docker Image. I plan to get a packer file in place to build a VirtualBox box file, a Digital Ocean droplet image, and a Docker build.

    Keep watching the GitHub repository for the changes and do report any issues or feature enhancements. Feel free to submit PRs in case you get to the planned enhancements before me.

  • Ansible Quirks – 4

    I was installing ansible on OpenSuSE Leap 42.3 machine and faced a problem with multiple python versions. I have both python 2.7 and python 3.4 installed on this machine. Python 2.7 is the default one.

    I tried installed ansible with the usual pip install and was faced with an error related to setuptools (could not capture the error message). I upgraded my pip version to the latest one 10.0.1 and then installed ansible.

    $ sudo pip install --upgrade pip
    $ sudo pip install ansible

    After that I ran an ansible-galaxy init command to create one of the roles and I was welcome with the error stating “Error: Ansible requires a minimum of Python2 version 2.6 or Python3 version 3.5. Current version: 3.4.6”. And I was wondering what happened because my default python version was 2.7.

    The usual google search did not help me go anywhere. So I looked at the /usr/bin/ansible file and found that it was using python3 interpreter.

    $ head -1 /usr/bin/ansible
    #!/usr/bin/python3

    I then checked the source of pip command and found the same thing.

    $ head -1 /usr/bin/pip
    #!/usr/bin/python3

    So I looked at pip documentation on installing pip for python 2.7 and found this link which helped me with the command to install pip for the default version of python installed –

    $ sudo python -m ensurepip --default-pip
    Requirement already satisfied: setuptools in /usr/lib/python2.7/site-packages
    Collecting pip
    Installing collected packages: pip
    Successfully installed pip-9.0.1

    Now when I reinstalled ansible, it installed the correct python interpreter for my system.

    $ sudo pip install ansible
    Collecting ansible
      Cache entry deserialization failed, entry ignored
    .....
    .....
    You are using pip version 9.0.1, however version 10.0.1 is available.
    You should consider upgrading via the 'pip install --upgrade pip' command.
    $ head -1 /usr/bin/pip /usr/bin/ansible
    ==> /usr/bin/pip <==
    #!/usr/bin/python
    
    ==> /usr/bin/ansible <==
    #!/usr/bin/python

    Now when I upgraded pip it was for the default python version

    $ sudo pip install --upgrade pip
    Collecting pip
    Using cached https://files.pythonhosted.org/packages/0f/74/ecd13431bcc456ed390b44c8a6e917c1820365cbebcb6a8974d1cd045ab4/pip-10.0.1-py2.py3-none-any.whl
    Installing collected packages: pip
    Found existing installation: pip 9.0.1
    Uninstalling pip-9.0.1:
        Successfully uninstalled pip-9.0.1
        Successfully installed pip-10.0.1
    $ head -1 /usr/bin/pip /usr/bin/ansible
    ==> /usr/bin/pip  <==
    #!/usr/bin/python
    
    ==> /usr/bin/ansible <==
    #!/usr/bin/python</p>

    In all probability all this was my fault and I think the pip which I upgraded in the beginning of this process may have been installed for python3 by me, otherwise I do not see any reason for all these issues.

    Still I thought to share this entry, just in case someone can be helped.

     

  • Ansible Quirks – 3

    I was running some playbook today and realise that the command line arguments of ansible-playbook do not behaves expected.

    My playbook has the following defined –

    remote_user: root

    When I issued the following command, my expectation was that the remote user name would be considered as specified by the “-u” option

    $ ansible-playbook -u centos -i '192.168.1.192,' play.yml

    However, this does not happen and the playbook still tries to connect as the user defined in the yml file.

    So, how do we override what is in the yml file through command line. The only way seems to be possible is using the -e or --extra-vars option as follows –

    $ ansible-playbook -e "ansible_ssh_user=centos ansible_ssh_private_key_file=~/.ssh/db_hosts_id_rsa " -i '192.168.1.192,' play.yml

  • Ansible Quirks – 2

    Today I tried to upgrade my ansible to 2.2 as I wanted to use the remote_src=yes feature from the unarchive module. My operating system on this machine is Linux Mint 17.3.

    I did –

    $ pip install --upgrade --user ansible

    and lo… I got the following error –

    ImportError: No module named packaging.version

    I next tried upgrading the pip itself and that also resulted in the same error. A bit of search on the internet did not help much as different ways worked for different people. So I decided to remove python-pip package and all that was installed along with it, reinstall pip and then try upgrading ansible. In short, following sequence of commands worked for me –

    
    $ rm -rf ~/.cache/pip/*
    $ python -m pip install -U --user pip
    $ sudo apt-get purge -y python-pip
    $ sudo apt-get autoremove
    $ sudo apt-get install python-dev
    $ python -m pip install --upgrade --user packaging
    $ python -m pip install --upgrade --user appdirs
    $ python -m pip install --upgrade --user ansible