paulgorman.org/technical

Ansible

(2017, 2019)

Ansible is an IT automation package. Its big competitor is SaltStack.

Overview:

Installation & Getting Started

Ansible works over ssh, so we don’t need to install anything on clients. The control machine must have Python (2.6+ or 3.5+).

# apt-get install ansible

Even on the control machine, there’s no database, no running daemon, just commands to run as needed.

http://docs.ansible.com/ansible/latest/intro_installation.html

Ansible uses any settings it finds in ~/.ssh/config (e.g., jump host). By default, it uses SFTP but can be configured to use SCP. Ansible assumes the use of SSH keys, but can be given the --ask-pass or --ask-become-pass option.

Ansible doesn’t just have to connect remotely over SSH. The transports are pluggable, and there are options for managing things locally, as well as managing chroot, lxc, and jail containers. A mode called ‘ansible-pull’ can also invert the system and have systems ‘phone home’ via scheduled git checkouts to pull configuration directives from a central repository.

http://docs.ansible.com/ansible/latest/intro_getting_started.html

Basic definitions:

Ansible looks for its configuration in this order:

  1. ANSIBLE_CONFIG environment variable
  2. $PWD/ansible.cfg
  3. $HOME/ansible.cfg
  4. /etc/ansible/ansible.cfg

The config file defines the location of the hosts inventory:

[defaults]
inventory      = /etc/ansible/hosts

Inventory

Ansible acts on multiple machines from the inventory. A dynamic inventory is possible (LDAP, Cobbler, etc.), but the file /etc/ansible/hosts is the default inventory (or specify with the -i flag):

# Ex 1: Ungrouped hosts, specify before any group headers.
bigby.example.com
192.168.100.22
www[1:26].example.com
[dbservers]
everest.example.com
db-[a:f].example.com
[kvm]
mizzen.example.com
zol.example.com
tiger.example.com

For hosts with non-22 SSH ports, specify them like foo.example.com:22000. The inventory can contain variable definitions, per-host or per-group, for later use in playbooks:

firefly.example.com http_port=8080
[flint]
super_secret=true

Make groups-of-groups using the :children suffix:

[north_america:children]
michigan
illinois
newyork

Two implicit groups exist: all and ungrouped.

Have Ansible scan the hosts file to list all hosts it recognizes or all hosts from a particular group:

$ ansible all --list-hosts
$ ansible webservers --list-hosts

Check which of the hosts in the inventory respond to pings:

$ansible all -m ping
firefly.example.com | SUCCESS => {
	"changed": false,
	"ping": "pong"
}
tiger.example.com | SUCCESS => {
	"changed": false,
	"ping": "pong"
}
kosmokrator.example.com | SUCCESS => {
	"changed": false,
	"ping": "pong"
}
mizzen.example.com | SUCCESS => {
	"changed": false,
	"ping": "pong"
}
nostromo.example.com | SUCCESS => {
	"changed": false,
	"ping": "pong"
}

Ad-Hoc Commands

Ad-hoc commands are things too quick, simple, or single-use to write as a playbook.

http://docs.ansible.com/ansible/latest/intro_adhoc.html

Ping all the hosts:

$ ansible all -m ping

We have a [kvm] group of hypervisors specified in /etc/ansible/hosts. What guests are on each?

$ ansible kvm --ask-become-pass --become -a "virsh list --all"

For commands that require shell features (even ones as basic as pipes) use the “shell” module:

$ ansible detroit -m shell -a 'echo $TERM'

(The default module is command.)

The -f flag sets the number of parallel processes (default 5).

$ ansible kvm -f 10 -m shell -a 'cat /etc/group | tac'

So far we’ve been demoing simple command execution, but most Ansible modules are not simple imperative scripts. Instead, they use a declarative model, calculating and executing the actions required to reach a specified final state. Furthermore, they achieve a form of idempotence by checking the current state before they begin, and if the current state matches the specified final state, doing nothing. However, we also recognize that running arbitrary commands can be valuable, so Ansible easily supports both.

Transfer a file directly to many servers:

$ ansible atlanta -m copy -a "src=/etc/hosts dest=/tmp/hosts"

Change file ownership or permissions with the file module:

$ ansible webservers -m file -a "dest=/srv/foo/b.txt mode=600 owner=mdehaan group=mdehaan"

Delete directories (recursively) and delete files:

$ ansible webservers -m file -a "dest=/path/to/c state=absent"

Add or remove users:

$ ansible all -m user -a "name=foo password=<crypted password here>"
$ ansible all -m user -a "name=foo state=absent"

Trigger a Git pull:

$ ansible webservers -m git -a "repo=https://foo.example.org/repo.git dest=/srv/myapp version=HEAD"

Make sure a service is started, restarted, or stopped:

$ ansible webservers -m service -a "name=httpd state=started"
$ ansible webservers -m service -a "name=httpd state=restarted"
$ ansible webservers -m service -a "name=httpd state=stopped

Long-running operations can run in the background, with optional timeout and status polling:

$ ansible all -B 3600 -P 0 -a "/usr/bin/long_running_operation --do-stuff"
$ ansible web1.example.com -m async_status -a "jid=488359678239.2844"
$ ansible all -B 1800 -P 60 -a "/usr/bin/long_running_operation --do-stuff"

Report “facts” — discovered variables about the client(s):

$ ansible all -m setup

Playbooks

Playbooks are Ansible’s configuration, deployment, and orchestration language.

http://docs.ansible.com/ansible/latest/playbooks.html

Write playbooks in YAML. http://docs.ansible.com/ansible/latest/YAMLSyntax.html

Before trying to edit YAML with Vim, add this to your .vimrc:

au! BufNewFile,BufReadPost *.{yaml,yml} set filetype=yaml foldmethod=indent
autocmd FileType yaml setl ts=2 sw=2 sts=2 expandtab

Each playbook lists one or more “plays”. A “play” links hosts to tasks. Tasks are calls to Ansible modules.

---
- hosts: webservers
  vars:
	http_port: 80
	max_clients: 200
  remote_user: root
  tasks:
  - name: ensure apache is at the latest version
	yum: name=httpd state=latest
  - name: write the apache config file
	template: src=/srv/httpd.j2 dest=/etc/httpd.conf
	notify:
	- restart apache
  - name: ensure apache is running (and enable it at boot)
	service: name=httpd state=started enabled=yes
  handlers:
	- name: restart apache
	  service: name=httpd state=restarted

Run a playbook:

$ ansible-playbook myplaybook.yml

Check the playbook syntax:

$ ansible-playbook --check myplaybook.yml

Lint a playbook (Debian has an ansible-lint package):

$  ansible-lint myplaybook.yml

Walk through a playbook step-by-step:

$ ansible-playbook --step myplaybook.yml

Make a playbook executable and head it with this shebang like to run it like a shell script:

#!/usr/bin/ansible-playbook

A playbooks can contain {{ variables }}. Register the results of shell commands as variables like:

tasks:
  - shell: /usr/bin/foo
    register: foo_result
    ignore_errors: True

Modules

http://docs.ansible.com/ansible/latest/modules.html

Show available modules:

$ ansible-doc -l

Ansible offers many modules from the very basic and universal to the esoteric.

Interesting modules include: apt, at, copy, cron, dnf, docker*, git, group, hostname, htpasswd, include, ipnetns (v2.5+), iptables, letsencrypt, lvol (lvm), mail, mount, mysql, nmcli, package, sysctl, systemd, user, virt (libvirt), vyos_, win_*, yum.

Ansible Vault

Ansible Vault stores secrets. Vault does file-level symmetric encryption. One of the main problems it solves is preserving secrets as part of a versioned repository without exposing them.

$  ansible-vault create secrets.yml
New Vault password:
Confirm New Vault password:
$  cat secrets.yml 
$ANSIBLE_VAULT;1.1;AES256
32323261326237346266386136343866346538303061336562326132313536663137656664306266
3435326361666537373536613638323935393662336263370a323065646233366464346432646664
39373837386531306435636438363066393838356364376533636230346630613163643437396235
3538326537346362300a363966386138363037643930393561663436653664653030666234303965
61333934633935383931646166613533643636666334303337353337396536653937
$  ansible-vault edit secrets.yml
Vault password:
$  ansible-vault view secrets.yml
Vault password:
A very secret thing.
$  ansible-vault rekey secrets.yml
Vault password:
New Vault password:
Confirm New Vault password:
Rekey successful

How is this more useful that any generic symmetric encryption utility? We can name the secret values, and use them as variables in an Ansible playbook.

$  ansible-vault encrypt_string --stdin-name 'key-for-secret'
New Vault password: 
Confirm New Vault password: 
Reading plaintext input from stdin. (ctrl-d to end input)
My secret value.key-for-secret: !vault |
          $ANSIBLE_VAULT;1.1;AES256
          32623531636165636338343630373936613133346130336532393338306330326663646132643339
          6266363365643937333837383934663732646631353335620a333033363362383839336565323966
          39616332323039363461363930396633643735366135633465653936643065356266313965343063
          6436383833646163660a306162626136366563303261663636643330666263613839303235633532
          38343964613037316434353762373765663138626464663466656631303633353331
Encryption successful

(Terminate value entry with CTRL-d, not ENTER!)

Paste the resulting encrypted variable definition into the Ansible playbooks along with any other variables. Use the variable later in the playbook by enclosing it in (quoted for YAML safety) Jinja-style double-curly-brackets.

- hosts: webservers
  vars:
    http_port: 8080
    key-for-secret: !vault |
          $ANSIBLE_VAULT;1.1;AES256
          32663335323337646165663536633432303633396131346432643231353632316539626537343236
          3064613431666565623237666136363561636634633936340a383236363137353663346666613334
          65623831613664653036396130386461623066363532386133653363326566323539616336363065
          6134313630303132350a393031353735626237356238653430373239653635323837363666303733
          6261
[…]
- name: Add web config
  webconfig:
    set_passwd: "{{ key-for-secret }}"

When running a playbook with encrypted variables, use --ask-vault-pass:

ansible-playbook --ask-vault-pass -i inventory_file my_playabook.yml