Provision a new Ansible node without SSH public key and Python

Let's assume that we want to provision a new node targethost-01.tld which will likely be a fresh VM somewhere.

The base setup resembles:

  • a minimal Ubuntu environment,
  • without an appropriate Python version,
  • without any SSH public keys and
  • no dedicated Ansible user (just root).

Our goals are:

  • Install the required Python dependencies
  • Setup users ansible and dev with SSH access using public keys

We will have to create four files:

  • ansible.cfg
  • inventory
  • playbooks/bootstrap-python.yml
  • playbooks/bootstrap.yml

Furthermore we should have a folder public which contains our SSH public keys that we want to store in the corresponding authorized_keys file on the node. For the convenience, we store the SSH private key of the ansible user in private/id_rsa. IF the file should be stored somewhere else, just edit ansible.cfg accordingly.

The ansible.cfg contains:

[defaults]
inventory = ./inventory
remote_user = ansible
#forks = 20
#gathering = smart
#fact_caching = jsonfile
#fact_caching_connection = ./facts
#fact_caching_timeout = 600
log_path = ./ansible.log
nocows = 1
private_key_file = ./private/id_rsa
host_key_checking = false

[privilege_escalation]
become = false

[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=600s -o ServerAliveInterval=60
control_path = %(directory)s/%%h-%%r
pipelining = True
timeout = 10

This configuration comes with sane defaults and some performance optimizations for SSH connections. For even better performance, forks, gathering and fact_* settings could be enabled.

We also need an inventory file containing:

[ubuntu]
targethost-01.tld ansible_python_interpreter=/usr/bin/python2.7

Playbook bootstrap-python.yml:

- hosts: ubuntu
  gather_facts: false
  become: true
  pre_tasks:
    - name: Generate locals
      raw: export LC_ALL="de_DE.UTF-8"; locale-gen de_DE.UTF-8
      changed_when: false
    - name: install python 2
      raw: test -e /usr/bin/python || (apt -y update && apt -y install python-minimal)
      changed_when: false
    - setup: # gather facts
- hosts: alpine
  gather_facts: false
  become: true
  pre_tasks:
    - name: install python 2
      raw: test -e /usr/bin/python || (apk --update add python)
      changed_when: false
    - setup: # gather facts

Playbook bootstrap.yml:

---
- import_playbook: bootstrap-python.yml

- hosts: all
  vars:
    users:
      - ansible
      - dev
  tasks:
    - name: 'Create users with corresponding groups'
      user:
        name: "{{ item }}"
        groups: "users"
      with_items: "{{ users }}"

    - name: 'Add corresponding authorized_keys to each user'
      authorized_key:
        user: "{{ item }}"
        state: present
        # Public key file has to be named according to the user, 
        # e.g. 'ansible.ssh.pub'
        key: "{{ lookup('file', '../public/' + item + '.ssh.pub') }}"
      with_items: "{{ users }}"

Execute the following command on your Ansible control machine (e.g. your local machine):

$ ansible-playbook \
    --inventory-file=my-inventory \
    --ask-pass \
    --user root \
    playbooks/bootstrap.yml \
    --limit targethost-01.tld

This is what happens as a result:

  • Ansible connects to the node targethost-01.tld via SSH using password credentials.
  • It bootstraps Python if the binary cannot be found. As the node is in the host group ubuntu, Python will be installed using apt.
  • Ansible then creates the users, adds them to their corresponding groups and provisions the authorized_keys with the public keys under public/.

If the provisioning was successful, any subsequent run of Ansible against that node should use the SSH key:

$ ansible targethost-01.tld -m ping
Jan Beilicke

About the author

Long-time IT professional and full-time nerd. Open source enthusiast, advocating security and privacy. Sees the cloud as other people's computers. Find me on Mastodon or Twitter.