Happy Dussehra! May good always win. Happy Diwali! Wishing you a bright year ahead. Merry Christmas! Warm wishes from Vizag. Happy New Year! Here’s to a great year ahead.

Archive ยท DevOps

Ansible: Configure Servers Without Logging In

Inventories, idempotent playbooks, variables, handlers, templates, roles and Vault with ansible-core 2.13, plus safe rollout and troubleshooting.

4 min read

  • ansible-core 2.13
  • Python 3.10
  • Ubuntu 22.04 LTS

Ansible connects over SSH and applies tasks described in YAML. No agent is needed on the servers; they only need Python and an SSH login. It suits configuration of virtual machines and bare metal (packages, users, files, services) and complements Terraform, which creates the infrastructure underneath.

Versions in use on this date. ansible-core 2.13 (released June 2022; it needs Python 3.8 or newer on the control machine) and Ubuntu 22.04 LTS (released April 2022) as the target, which ships Python 3.10. Modules now live in collections, so use fully qualified names such as ansible.builtin.apt.

How a run flows

flowchart TB
  INV[Inventory<br/>hosts and groups] --> PB
  VARS[group_vars and host_vars<br/>Vault encrypted secrets] --> PB[Playbook with roles]
  PB --> CTRL[Control node runs ansible-playbook]
  CTRL -->|SSH + Python module upload| W1[web-1]
  CTRL -->|SSH| W2[web-2]
  W1 --> R1[Each task reports ok, changed or failed]
  W2 --> R1
  R1 --> H{Task changed something?}
  H -->|yes| HAND[Notify handler, for example reload nginx once at end]
  H -->|no| SKIP[Idempotent - nothing done]

Project layout

infra/
  ansible.cfg
  inventory/
    production.ini
    group_vars/
      all.yml
      web.yml
      vault.yml          # encrypted with ansible-vault
  site.yml
  roles/
    nginx/
      tasks/main.yml
      handlers/main.yml
      templates/site.conf.j2
      defaults/main.yml

ansible.cfg:

[defaults]
inventory = inventory/production.ini
host_key_checking = True
forks = 10
stdout_callback = yaml
[privilege_escalation]
become = True

inventory/production.ini:

[web]
web-1 ansible_host=10.0.1.11
web-2 ansible_host=10.0.1.12

[web:vars]
ansible_user=ubuntu
ansible_python_interpreter=/usr/bin/python3

A playbook with a handler and a template

site.yml:

- name: Web servers
  hosts: web
  become: true
  serial: 1                    # roll one host at a time
  vars:
    server_name: shop.example.com
  tasks:
    - name: Install nginx
      ansible.builtin.apt:
        name: nginx
        state: present
        update_cache: true
        cache_valid_time: 3600

    - name: Render site configuration
      ansible.builtin.template:
        src: site.conf.j2
        dest: /etc/nginx/conf.d/shop.conf
        owner: root
        group: root
        mode: "0644"
      notify: Test and reload nginx

    - name: Start and enable nginx
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: true

  handlers:
    - name: Test and reload nginx
      ansible.builtin.command: nginx -t
      changed_when: false
      notify: Reload nginx
    - name: Reload nginx
      ansible.builtin.service:
        name: nginx
        state: reloaded

The first handler tests the configuration, and only if it is valid does it notify the reload handler; a failed test stops the play before a broken config is loaded. Where a tool accepts a file path, the validate: option of template and copy is even better, for example validate: visudo -cf %s for sudoers files.

Run:

python3 -m pip install --user "ansible-core==2.13.*"
ansible-galaxy collection install ansible.posix community.general
ansible web -m ping
ansible-playbook site.yml --check --diff       # dry run, show differences
ansible-playbook site.yml --limit web-1        # canary on one host
ansible-playbook site.yml

Ideas that matter

  • Idempotent: a second run changes nothing if the server is already correct. Prefer modules (apt, user, template, lineinfile) over command and shell; if you must use them, add creates: or changed_when: so reports stay truthful.
  • Handlers run once at the end of a play, only when notified, so a config change triggers one reload, not five.
  • Variables come from many places; keep defaults in roles/*/defaults, environment values in group_vars, and remember that extra vars (-e) win.
  • Roles package tasks, templates and defaults for reuse; share them through collections or a Git repository.
  • Tags and --limit let you apply part of a playbook or one host.
  • serial and max_fail_percentage give rolling updates and stop a bad change from hitting every server.

Secrets with Vault

ansible-vault create inventory/group_vars/vault.yml     # contains db_password: ...
ansible-playbook site.yml --ask-vault-pass

Reference {{ db_password }} in templates, set no_log: true on tasks that handle secrets so they are not printed, and keep the vault password in your password manager or CI secret store, never in the repository.

Safe operation

  1. Review changes by pull request, and run ansible-lint and yamllint in CI.
  2. Use SSH keys and a dedicated automation user with scoped sudo; keep host_key_checking on.
  3. Test roles on a disposable VM or container first (Molecule if you want automation).
  4. Always run with --check --diff on production before the real run, remembering that check mode cannot predict every command task.
  5. Pin collection versions in requirements.yml for reproducible runs.

Troubleshooting

  • UNREACHABLE: SSH user, key, security group or host key problem; test with plain ssh and -vvv.
  • /usr/bin/python: not found: set ansible_python_interpreter=/usr/bin/python3.
  • Missing sudo password: configure passwordless sudo for the automation user, or pass --ask-become-pass.
  • Module not found after upgrade: install the collection that now contains it.