After working with Saltstack I thought I’d do some investigation with Ansible. I’m in the market for automation and want a simple means of delivering configuration onto our physical and virtual estate.

Nothing wrong with Saltstack – but broadening the view to Ansible shows it has one key feature that is very attractive. There is no need to install an agent – it uses ssh and will sudo or su to escalate privileges as required.

I’ve been working to get to the same point on both products, a client installed with my preferred environment with OhMyZsh, OS level authentication via our LDAP servers, my public key added to my account and password free use of sudo.

Saltstack achieved this with a few entries in yaml files, and so does Ansible. Right now it’s hard to pick a winner, so the agent/no-agent may be the decider.

I began using a playbook with Ansible and whilst it worked for what I was doing, I could see it getting a bit heavy. Then I investigated roles. This then becomes much more like Saltstacks pillar and grains with separate task files and a directory structure to build in.

Create your first role with:

$ ansible-role-init my-role

It’ll create a whole directory structure to build your role in. This was the missing piece in my jigsaw. Once the structure was created I could them build the role and use import_playbook and import_tasks to link everything together.

I created my roles in my own folder ~/ansible/roles, so I didn’t have to keep sudo’ing to edit the roles under the structure /etc/ansible/roles. By editing the /etc/ansible/ansible.cfg file you can give it alternative paths to reach your roles and edit them at your leisure with only user permissions.

So far using Ansible for configuration management seems very close to my experience with Saltstack. I can configure my hosts as expected, but I have found the use of the debconf module more frustrating as it’s more input.

With Saltstack I can bundle all the debconf answers and values in one branch ,eg.

nullmailer-debconf:
  debconf.set:
    - name: nullmailer
    - data:
        'shared/mailname': {'type': 'string', 'value': 'server.domain.tld'}
        'nullmailer/relayhost': {'type': 'string', 'value': 'mail.domain.tld'}

With Ansible I’m having to create tasks for every question, which is more conversant, but I guess at least it’s clearly readable.

  - name: Debconf shared/mailname
    debconf:
      name: nullmailer
      question: shared/mailname
      value: 'server.domain.tld'
      vtype: string
  - name: Debconf ullmailer/relayhost
    debconf:
      name: nullmailer
      question: nullmailer/relayhost
      value: 'mail.domain.tld'
      vtype: string    

The main thing I like Ansible for is the ability to manage an out of the box installation of Debian. As Debian doesn’t come with sudo installed Ansible is capable of using su and processing your roles/tasks either way. Ironically I’m using Ansible to install and configure sudo.

packages-tasks.yml

---
# file: packages-tasks.yml
# Install the base applications for the node
  - name: Install sudo, curl, git, zsh, ntp
    apt:
      name: "{{ packages }}"
      state: present
    vars:
      packages: 'sudo, curl, git, zsh, ntp'
  # Create the group wheel as any members of this can sudo without a password
  - name: Group wheel exists
    group:
      name: wheel
      state: present      
  # Allow password-less sudo access
  - name: Password-less sudo for wheel
    lineinfile:
      dest: /etc/sudoers
      state: present
      regexp: '^%wheel'
      line: '%wheel ALL=(ALL) NOPASSWD: ALL'

I copy in my public key to the root user authorized_keys so I can manage the host myself.

authorized_keys-tasks.yml

---
  - name: Add sysadmin to wheel for sudo usage
    user:
      name: sysadmin
      state: present
      groups: wheel, sudo

  - name: authorized_keys for root
    authorized_key:
      user: root
      state: present
      key: "{{ lookup('file', 'mykey.pub') }}"

The big one is configuring the client to use the corporate LDAP server for authentication. It requires packages installing and configuration files modifying. All done with templates, files and variables.

ldap-client-tasks.yml

---
  # Install and configure LDAP Client
  - name: Debconf nslcd/ldap-uris
    debconf:
      name: nslcd
      question: nslcd/ldap-uris
      value: '{{ ldap_uris }}'
      vtype: string
  - name: Debconf nslcd/ldap-base
    debconf:
      name: nslcd
      question: nslcd/ldap-base
      value: '{{ ldap_base }}'
      vtype: string     
  - name: Debconf nslcd/ldap-starttls
    debconf:
      name: nslcd
      question: nslcd/ldap-starttls
      value: u'{{ ldap_starttls }}'
      vtype: boolean     
  - name: Debconf nslcd/ldap-auth-type
    debconf:
      name: nslcd
      question: nslcd/ldap-auth-type
      value: '{{ ldap_auth_type }}'
      vtype: string
  - name: Debconf shared/ldapns/base-dn
    debconf:
      name: nslcd
      question: shared/ldapns/base-dn
      value: '{{ ldap_base }}'
      vtype: boolean

  - name: debconf libnss-ldapd/clean_nsswitch
    debconf:
      name: libnss-ldap
      question: libnss-ldapd/clean_nsswitch
      value: u'False'
      vtype: boolean
  - name: debconf libnss-ldapd/nsswitch
    debconf:
      name: libnss-ldap
      question: libnss-ldapd/nsswitch
      value: 'aliases, group, passwd, shadow'
      vtype: multiselect

  - name: debconf libpam-ldap/binddn
    debconf:
      name: libpam-ldap
      question: libpam-ldap/binddn
      value: '{{ ldap_bind_dn }}'
      vtype: string
  - name: debconf libpam-ldap/dblogin
    debconf:
      name: libpam-ldap
      question: libpam-ldap/dblogin
      value: u'False'
      vtype: boolean
  - name: debconf libpam-ldap/dbrootlogin
    debconf:
      name: libpam-ldap
      question: libpam-ldap/dbrootlogin
      value: u'False'
      vtype: boolean
  - name: debconf libpam-ldap/override
    debconf:
      name: libpam-ldap
      question: libpam-ldap/override
      value: u'True'
      vtype: boolean
  - name: debconf libpam-ldap/pam_password
    debconf:
      name: libpam-ldap
      question: libpam-ldap/pam_password
      value: exop
      vtype: string
  - name: debconf shared/ldapns/base-dn
    debconf:
      name: libpam-ldap
      question: shared/ldapns/base-dn
      value: '{{ ldap_base }}'
      vtype: string
  - name: debconf shared/ldapns/ldap-server
    debconf:
      name: libpam-ldap
      question: shared/ldapns/ldap-server
      value: '{{ ldap_uris }}'
      vtype: string
  - name: debconf shared/ldapns/ldap_version
    debconf:
      name: libpam-ldap
      question: shared/ldapns/ldap_version
      value: '3'
      vtype: string

  # Install packages
  - name: Install LDAP
    apt:
      name: ['libnss-ldapd', 'libpam-ldap', 'ldap-utils', 'sudo-ldap']
      state: present      

  - name: Modify nsswitch.conf
    copy:
      src: nsswitch.conf
      dest: /etc/nsswitch.conf
  - name: Remove auth_ok
    replace:
      path: /etc/pam.d/common-password
      regexp: '\sauth_ok'
  - name: Add mkhomedir.so
    lineinfile:
      path: /etc/pam.d/common-session
      line: 'session optional pam_mkhomedir.so skel=/etc/skel umask077'
      state: present
  - name: Configure ldap.conf
    template:
      src: ldap.conf.j2
      dest: /etc/ldap/ldap.conf
      owner: root
      group: root
      mode: u=rw,g=r,o=r

templates/ldap.conf.j2

TLS_CACERT      /etc/ssl/certs/ca-certificates.crt
sudoers_base ou=SUDOers,{{ ldap_base }}
BASE {{ ldap_base }}
URI  {{ ldap_uris }}