Ansible notes

Agentless automation.

Ansible is a tool for provisioning servers, managing their configuration, and deploying applications.

Ansible = configuration management (Pupet, Chef) + deployment (Capistrano, Fabric) + ad-hoc task execution (Func, ssh).

Ansible aims to be:

Initial release: 2012.

When developers begin to think of infrastructure as part of their application, stability and performance become normative.

Ansible for DevOps by Jeff Geerling

Components

Commands

Aka tasks.

Ad-hoc commands:

ansible <group> -b -m <command> -a <arguments>  # -b == become (root)
ansible <group> -m ping -u <username>
ansible mygroup -b -m yum -a "name=ntp state=present"
ansible .... --limit "192.168.60.1"  # limit to one instance, can use patterns

Verbose:

ansible ... -vvvv

Combine commands into playbooks.

Available modules

Aka "task plugins" or "library plugins".

See Ansible modules:

Playbooks

Playbook - a set of tasks (plays) that will be run against a particular server or set of servers.

Example:

---
- hosts: all
  become: yes
  vars_files:
    - vars.yml
  vars:
    - myvar: myvalue
  pre_tasks:
    ...
  tasks:
  - name: Install ... {{ myvar }}
    yum: ...
    notity:
      - do something
  - include: my-tasks.yml
  post_tasks:
    ...
  handlers:
    - do something
      ...
    - include: my-handlers.yml

Privileged access (use sudo):

become: yes

Handlers: Running Operations On Change.

ansible-playbook utility

ansible-playbook arguments:

Conditional statements

when:

- apt:
  name: vim
  when: is_local
  # when: "(is_local is defined) and is_local"
  # when: "'ready' in my_result.stdout"
  # when: "my_result.stdout.find(another_var + '/some.js') == -1"

changed_when
failed_when
wait_for - e.g. wait for a server start listening on a port

Join playbooks

- hosts: all
  tasks:
    ...
- include: playbook1.yml
- include: playbook2.yml
  vars:
    - myvar: myvalue

And can combine with when and with loop statements.

Or use ansible roles.

Inventories

[app]
102.168.60.1
102.168.60.2

[db]
102.168.60.3

# group "multi"
[multi:children]
app
db

[multi:vars]
ansible_ssh_user=myuser
ansible_ssh_private_key_file=~/...

Can specify which inventory to use:

ansible-playbook myplaybook.yml -i inventories/myinventory

Inventories can be dynamic.

Variables

ansible variables.

Use all lowercase letters.

Passing a variables file:

ansible-playbook playbook.yaml --extra-vars "@vars.yml"

Registered variables: save command stdout and stderr.

- shell: <my_command>
  register:
    my_command_result
# "{{ my_command_result.stdout }}"

Facts are information derived from speaking with your remote systems.

Passing configuration through ENV variables

Ansible set ENV variables -> the app loads configuration from ENV variables.

Using ~/.bash_profile:

- tasks
  - name: "Set an environment variable"
    lineinfile:
      path=~/.bash_profile
      regexp=^ENV_VAR=
      line=ENV_VAR=value

For system-wide use: /etc/environment.

Per play:

- name:
  ...
  environment:
    my_env_var: my_value

Per shell command can use templates:

cd {{ app_home }} && ENV_KEY1={{ env_val1 }} ENV_KEY2={{ env_val2 }} {{ app_venv }}/bin/python myapp.py

Task:

- shell: "{{ lookup('template', 'mytemplate.j2') }}"

Per supervisor config:

[program:myapp]
command={{ app_home }}/.venv/bin/python myapp.py
process_name=myapp
user={{ app_user }}
directory={{ app_home }}
environment=ENV_KEY1="{{ env_value1 }}",ENV_KEY2="{{ env_value2 }}"

Sensitive values

ansible/variables/<env>-sensitive.yml
# (don't put it under git index)
# or specify them in command line:
ansible-playbook myplaybook.yml --extra-vars="myvar=myvalue"

So vars_files will look like:

vars_files:
  - common.yml
  - "myservice_{{ env }}.yml"
  - "myservice_{{ env }}_sensitive.yml"

And env can be specified inside inventories:

# ...
[all:vars]
env=development

Variable prompt:

vars_prompt:
  - name: username
    prompt: "What is username?"
    default: anonymous
  - name: password
    prompt: "What is password?"
    private: yes
    confirm: yes

Or use Ansible vault.

Ansible vault

Encryption: AES-256.

Edit:

ansible-vault edit

Run:

ansible-playbook playbook.yml --ask-vault-pass

Works faster with:

pip install cryptography

String encryption:

ansible-vault encrypt_string <some string> --vault-password-file <password file>

This command will output a string ready to be included in a YAML file.

File encryption:

ansible-vault encrypt <file path> --vault-password-file <password file>

Replace the file.

Roles

Combines tasks, configuration, templates in one reusable package.

my_role/
  meta/
    main.yml
  tasks/
    main.yml
- hosts: all
  roles:
    - my_role

Meta:

---
dependencied: []

Ansible galaxy

A repository for roles.

ansible-galaxy init <role name>

Usage

Documentation

ansible-doc <module name>

Debug

Verbose:

ansible-playbook playbook.yml -v

Environments

Can specify environment inside inventories:

# ...
[all:vars]
env=development

And then use it when loading variables:

vars_files:
  - "myservice_{{ env }}.yml"

Tags

Allow to run (or exclude) subset of a playbook's tasks, roles, and handlers.

roles:
  - {role: myrole, tags: ['install', 'setup']}
tasks:
  - name: "My task"
    someaction: someargs
    tags:
      - setup
ansible-playbook myplaybook.yml --tags "install"
ansible-playbook myplaybook.yml --skip-tags "install"

Vagrant

Use for testing on development machine.

Vagrant is a server templating tool (other are Docker, Packer), create an image instead of configuring each server.

Working with Vagrant:

vagrant box add <repository>/<image>
vagrant init <repository>/<image>
vagrant up
vagrant ssh
vagrant halt  # shut down
vagrant destroy  # completely destroy the box
vagrant ssh-config  # ssh details

Vagrant features:

Example Vagrantfile:

# -*- mode: ruby -*-
# vi: set ft=ruby :

# All Vagrant configuration is done below. The "2" in Vagrant.configure
# configures the configuration version (we support older styles for
# backwards compatibility). Please don't change it unless you know what
# you're doing.

Vagrant.configure("2") do |config|

  config.vm.box = "ubuntu/xenial64"

  # NFS shared folder
  # config.vm.synced_folder "./.shared/", "/var/townintel_shared", type: "nfs"

  config.vm.define "myinstance" do |app|
    app.vm.hostname = "myservice.mysite.com"
    app.vm.network:private_network, ip: "10.100.0.10"

    app.vm.provision "ansible" do |ansible|
        ansible.verbose = "v"
        ansible.inventory_path = "./inventories/development"
        ansible.playbook = "myservice.yml"
    end
  end

  # ...

end

Reach host machine

Host machine is available at 10.0.2.2.

Download Vagrant.

AWS guide

See Ansible AWS Guide.
Ansible for AWS book.

Run ansible through a bastion host

See Running Ansible Through an SSH Bastion Host by Scott Lowe.

Example:

# custom ssh configuration file
Host 10.10.10.*
  ProxyCommand ssh -W %h:%p bastion.example.com
  IdentityFile ~/.ssh/private_key.pem

Host bastion.example.com
  Hostname bastion.example.com
  User ubuntu
  IdentityFile ~/.ssh/private_key.pem
  ControlMaster auto
  ControlPath ~/.ssh/ansible-%r@%h:%p
  ControlPersist 5m
# ansible.cfg
[ssh_connection]
ssh_args = -F ./ssh.cfg -o ControlMaster=auto -o ControlPersist=30m
control_path = ~/.ssh/ansible-%%r@%%h:%%p

Doesn't work for me ^, so I just put the configuration to ~/.ssh/config.

Python3

Specify it in inventories:

[all:vars]
ansible_python_interpreter=/usr/bin/python3

Copy source code

One way:

---
- tasks
  - name: "Synchronize the source"
      synchronize:
        dest: "/home/{{ user }}/{{ app }}/"
        src: "{{ item }}"
        rsync_opts:
          - "--exclude=*.pyc"
      become_user: "{{ user }}"
      with_items:
        - ../<some folder>
        - ../<some file>
      notify: server restart

Examples

Simple Python code deploy

---
- name: Deploy
  hosts: <hosts>
  become: true
  vars_files:
    - variables/base.yml
  vars:
    - user: deploy
  tasks:
    - name: "apt-get update"
      apt:
        update_cache: yes
        cache_valid_time: 3600
    - name: "apt-get install"
      apt:
        name: "{{ item }}"
        state: latest
      with_items:
        - build-essential
        - virtualenv
        - python3-dev
    - name: "Create deploy group"
      group:
        name="{{ user }}"
        state=present
    - name: "Create deploy user"
      user:
        name="{{ user }}"
        shell=/bin/bash
        groups="{{ user }}"
        append=yes
    - name: "Synchronize the source"
      synchronize:
        dest: "/home/{{ user }}/<project name>/"
        src: "{{ item }}"
        rsync_opts:
          - "--exclude=*.pyc"
      become_user: "{{ user }}"
      with_items:
        - ../<source>
        - ../requirements.txt
      tags:
        - reload
    - name: "Install requirements"
      pip:
        requirements: "/home/{{ user }}/<project name>/requirements.txt"
        virtualenv: "/home/{{ user }}/.venv"
      become_user: "{{ user }}"

Best practices

See Ansible Best Practices.

Role names

ansible-role-... - easier to find on GitHub.

Project structure

- myproject
  - inventories/
  - roles/
  - variables/
  - playbook1.yml
  - playbook2.yml
  - Vagrantfile

Vocabulary

See also Ansible Glossary.

Idempotence

Idempotence is the ability to run an operation which produces the same result whether run once or multiple times.

ad-hoc

Ad-hoc - made or happening only for a particular purpose or need, not planned before it happens.

Ansible for DevOps by Jeff Geerling
Ansible documentation
Ansible blog

Licensed under CC BY-SA 3.0