Ansible
Getting started
Use inventory
and the target "example" to ping as root
[example]
ansible101.xyz
ansible -i inventory example -m ping -u root
Use ansible.cfg
to set a default inventory
[defaults]
INVENTORY = inventory
Log-in as root and check date, memory usage, and ping
ansible example -a "date" -u root
ansible example -a "free -h" -u root
ansible example -m ping -u root
Provisioning
Using Vagrant, start with a base image
vagrant init geerlingguy/centos
edit the Vagrantfile
# -*- mode: ruby -*-
# vi: set ft=ruby :
Vagrant.configure("2") do |config|
config.vm.box = "geerlingguy/centos7"
config.vm.provision "ansible" do |ansible|
ansible.playbook = "playbook.yml"
end
end
Create a playbook.yml
---
- name: Set up NTP on all servers.
hosts: all
become: yes
tasks:
- name: Ensue NTP is installed.
yum: name=ntp state=present
# - shell: |
# if ! rpm -qa | grep -qw ntp; then
# yum install -y ntp
# fi
- name: Ensure NTP is running.
service: name=ntpd state=started enabled=yes
Run the playbook on the vagrant VM using vagrant provision
Other useful vagrant commands
vagrant ssh
vagrant ssh-config
vagrant status
Clean up
vagrant halt
vagrant destroy
Ad-hoc commands
Create 2 instance of app, 1 db with Vagrant
# -*- mode: ruby -*-
# vi: set ft=ruby :
Vagrant.configure("2") do |config|
config.vm.box = "geerlingguy/centos7"
config.ssh.insert_key = false
config.vm.synced_folder ".", "/vagrant", disabled: true
config.vm.provider :virtualbox do |v|
v.memory = 256
v.linked_clone = true
end
# App server 1
config.vm.define "app1" do |app|
app.vm.hostname = "orc-app1.test"
app.vm.network :private_network, ip: "192.168.60.4"
end
# App server 2
config.vm.define "app2" do |app|
app.vm.hostname = "orc-app2.test"
app.vm.network :private_network, ip: "192.168.60.5"
end
# App server 3
config.vm.define "db" do |db|
db.vm.hostname = "orc-db.test"
db.vm.network :private_network, ip: "192.168.60.6"
end
end
Note: vagrant plugin install vagrant-hostsupdater
required
vagrant up
a typical inventory file
#Application servers
[app]
192.168.60.4
192.168.60.5
#Database server
[db]
192.168.60.6
#Group has all the servers
[multi:children]
app
db
# Variables for all the servers
[multi:vars]
ansible_ssh_user=vagrant
ansible_ssh_private_key_file=~/.vagrant.d/insecure_private_key
Check inventory and ssh ok
vagrant ssh app1
...
ansible-inventory -i inventory --all
sudo ansible -i inventory all --list-hosts
run ad-hoc tasks on inventory groups or individual servers
# hostnames for all servers
ansible multi -i inventory -a "hostname"
# Check full config for db server
ansible -i inventory db -m setup
Checking facts
ansible multi -i inventory -a "df -h"
ansible multi -i inventory -a "date"
ansible multi -i inventory -a "free -h"
Use become
flag to install (as sudo)
## become superuse to install ntp
ansible -i inventory multi --become -m yum -a "name=ntp state=present"
ansible -i inventory multi -b -m yum -a "name=ntp state=present"
## passwords
ansible -i inventory multi -K -m yum -a "name=ntp state=present"
other ad-hoc tasks
## ansible inventory enable service ntpd
ansible -i inventory multi -b -m service -a "name=ntpd state=started enabled=yes"
##stop
ansible -i inventory multi -b -a "service ntpd stop"
## change date
ansible -i inventory multi -b -a "ntpdate -q 0.rhel.pool.ntp.org"
## start
ansible -i inventory multi -b -a "service ntpd start"
## memory from db
ansible -i inventory db -b -a "free -m"
## check using mysql_user module in the db server to setup a user
ansible -i inventory db -b -m mysql_user -a "name=django host=% password=12345 priv=*.*:ALL state=present"
## limit to a single server (or ending in .4)
ansible -i inventory app -a "free -m" --limit "192.168.60.4"
ansible -i inventory app -a "free -m" --limit "*.4"
Use module whenever possible install of action -a
, such as service
. Check docs via command lineansible-doc service
Other useful commands
# run comment date on inventory in parallel (default)
ansible -i inventory multi -a "date"
ansible -i inventory multi -m command -a "date"
# command date but sequential (slower)
ansible -i inventory multi -a "date" -f 1
# run yum update in bg with an id to check progress and exit
ansible -i inventory multi -b -B 3600 -P 0 -a "yum -y update"
# check the status using the id from prev step
ansible -i inventory multi -b -m async_status -a "jid=xxxxxxxxx.yyyy"
# get logs for all servers
ansible multi -b -m shell -a "tail /var/log/messages | grep ansible-command | wc-l"
#cron
##ansible -i inventory multi -b -m cron -a "name=smt hour=2 job=/scr.sh"
# git to update repo
ansible -i inventory multi - b -m git -a "repo-gith.ur dest=/opt update=yes version=1.2.4"
Playbooks
from a shell script like
# install apache
yum instal --quiet -y httpd httpd-devel
# copy configuration file
cp httpd.cond /etc/httpd/conf/httpd.conf
cp httpd-vhosts /etc/httpd/conf/httpd-vhosts.conf
# Start Apache and configure it to run at boot.
service httpd start
chkconfig httpd on
to a basic playbook like (simply add names to each instruction or use shell: |
)
---
- name: Install Apache
hosts: all
tasks:
- name: Install Apache.
command: yum install --quiet -y httpd httpd-devel
- name: Copy configuration files.
command: >
cp httpd.conf /etc/httpd/httpd.conf
- command: >
cp httpd-vhosts /etc/httpd/conf/httpd-vhosts
- name: Start Apache and configure it to run at boot.
command: service httpd start
- command: chkconfig httpd on
or even better
---
- name: Install Apache
hosts: all
tasks:
- name: Install Apache.
yum:
name:
- httpd
- httpd-devel
state: present
# yum: name=httpd state=present
- name: Copy configuration files.
copy:
src: "{{ item.src }}"
destination: "{{ item.dest }}"
owner: root
group: root
mode: 0644
with_items:
- src: httpd.conf
dest: /etc/httpd/httpd.conf
- src: httpd-vhosts
dest: /etc/httpd/conf/httpd-vhosts
- name: Ensure Apache started and boot
service:
name: httpd
state: started
enabled: true
Import and include
A simple way to organize task using import_tasks
or include_tasks
handlers:
- import_tasks: handlers/apache.yml
- import_tasks: handlers/app.yml
#...
tasks:
- import_tasks: tasks/apache.yml
vars:
apache_package: apache2
# override var for specific task
- import_tasks: tasks/app.yml
- include_tasks: tasks/dynamically_defined.yml
---
- name: dynamically defined task X
find:
paths: {{ item }}
patterns: '*.log'
register: found_log_file_paths
with_items: "{{ log_file_paths }}"
Or import a complete playbook
#...
- import_playbook: api.yml
A better way to organize is using Roles...
Real-world playbook
Installing solr
on Ubuntu instance
#inventory
[solr]
192.168.0.20 ansible_user=root
#vars.yml
---
download_dir: /tmp
solr_dir: /opt/solr
solr_version: 8.2.1
solr_checksum: sha512:b372f44baeafa12ec9c06239080e04c75328c37c92e5d27e6f819b139a69eacbbb879d73f032b20db66d33ee33efb41283648727a169ce4eds67095b780a5d0c
#main.yml
---
- hosts: solr
become: true
vars_files:
- vars.yml
pre_tasks:
- name: Update apt cache if needed.
apt:
update_cache: true
cache_valid_time: 3600
handlers:
- name: Restart solr.
service:
name: solr
state: restarted
tasks:
- name: Install Java.
apt:
name: openjdk-8-jdk
state: present
- name: Install solr.
get_url:
url: "https://mirrors.ocf.berkeley.edu/apache/lucene/solr/{{ solr_version }}/solr-{{ solr_version }}.tgz"
dest: "{{ download_dir }}/solr-{{solr_version}}.tgz"
checksum: "{{ solr_checksum }}"
# unarchive and expand in one step not possible with solr
- name: Expand solr.
unarchive:
src: "{{ download_dir }}/solr-{{solr_version}}.tgz"
dest: "{{ download_dir }}"
remote_src: true
creates: "{{ download_dir }}/solr-{{solr_version}}/README.txt"
# check solr installation docs for options
- name: Run solr installation script.
command: >
{{ download_dir }}/solr-{{solr_version}}/bin/install_solr_service.sh
{{ download_dir }}/solr-{{solr_version}}.tgz
-i /opt
-d /var/solr
-u solr
-s solr
-p 8983
creates={{ solr_dir }}/bin/solr
- name: Ensure solr is started and enabled at boot.
service:
name: solr
state: started
enabled: yes
Check syntax and deploy to inventory named solr
ansible-playbook main.yml --syntax-check
ansible-playbook -i inventory main.yml
Handlers
just like another task but...
- will be called at the end of a completed block using
notify
- will be triggered only once even if called multiple times
- are usually called to restart services after config or installation changes
- won't flush at the end (as usual) if a task fails unless the flag
--force-handlers
is used - can be called by other handlers too
---
- name: Install Apache.
hosts: centos
become: true
handlers:
- name: restart apache
service:
name: httpd
state: restarted
notify: restart memcached
## just like tasks can be called upon by other handlers
- name: restart memcached
service:
name: memcached
state: restarted
tasks:
- name: Ensure Apache is installed
yum:
name: httpd
state: present
- name: Copy test config file.
copy:
src: files/test.conf
dest: /etc/httpd/conf.d/test.conf
notify:
- restart apache
# - restart memcached
# as opposed to flush them at the end of the playbook
- name: Make sure handlers are flush right away
meta: flush_handlers
- name: Ensure Apache is running and starts at boot.
service:
name: httpd
state: started
enabled: true
Use flush_handlers
to execute early, otherwise handlers flush at the very end if no fail
Pro tip: User the task fail (no parameters) to trigger a failure and test handlers
- fail:
Environment variables
Download a file using a proxy, two ways
#...
## use block `vars` applied where injected only
vars:
proxy_vars:
http_proxy: http://example-proxy:80/
https_proxy: https://example-proxy:80/
#....handlers
#....tasks
# define per task via environment
- name: Download a 20MB file
get_url:
url: http://ipv4.download.thinkbroadband.com/20MB.zip
dest: /tmp
environment:
http_proxy: http://example-proxy:80/
https_proxy: https://example-proxy:80/
# inject from vars via environment
- name: Download a 10MB file
get_url:
url: http://ipv4.download.thinkbroadband.com/10MB.zip
dest: /tmp
environment: proxy_vars
Or make the variables available to all task using environment
or import from files
## apply to entire playbook, no vars needed
environment:
http_proxy: http://example-proxy:80/
https_proxy: https://example-proxy:80/
vars_files:
- "vars/apache_default.yml"
- "vars/apache_{{ ansible_os_family }}.yml"
Another way to load variables using the pre_tasks
block
pre_tasks:
- name: Load variable files.
include_vars: "{{ item }}"
with_first_found:
- "vars/apache_{{ ansible_os_family }}.yml"
- "vars/apache_default.yml"
use to load first match, otherwise used default, check another example here.
In this example, choose the Apache variable file based on the value for ansible_os_family
obtained from ansible gather facts pre task
---
- name: Install Apache.
hosts: all
# ubuntu OR centos
become: true
vars_files:
- "vars/apache_default.yml"
- "vars/apache_{{ ansible_os_family }}.yml"
apache_default.yml
---
apache_package: apache2
apache_service: apache2
apache_config_dir: /etc/apache2/sites-enabled
apache_RedHat.yml
---
apache_package: httpd
apache_service: httpd
apache_config_dir: /etc/httpd/conf.d
Add environment to remote host
tasks:
- name: Add an environment variable to the shell
lineinfile:
# dest: "~/.bash_profile"
dest: "/etc/environment"
regexp: '^ENV_VAR='
line: 'ENV_VAR=value'
become: false
- name: Get the value of an env value
shell: 'source ~/.bash_profile && echo $ENV_VAR'
register: foo
- debug: msg="The variable is {{ foo.stdout }}"
working playbook: load variables (from files) based on OS, verify installation, copies config file, verify service is running, and restarts service (via handler)
---
- name: Install Apache.
hosts: all
become: true
handlers:
- name: restart apache
service:
name: "{{ apache_service }}"
state: restarted
pre_tasks:
- debug: var=ansible_os_family
- name: Load variable files.
include_vars: "{{ item }}"
with_first_found:
- "vars/apache_{{ ansible_os_family }}.yml"
- "vars/apache_default.yml"
tasks:
- name: Download a file
get_url:
url: http://ipv4.download.thinkbroadband.com/20MB.zip
dest: /tmp
- name: Ensure Apache is installed
package:
name: "{{ apache_package }}"
state: present
- name: Copy test config file.
copy:
src: files/test.conf
dest: "{{ apache_config_dir }}/test.conf"
notify:
- restart apache
- name: Ensure Apache is running and starts at boot.
service:
name: "{{ apache_service }}"
state: started
enabled: true
Vault
encrypt and decrypt sensitive info, like passwords, keys, users
# encrypt your keys file using AES256
ansible-vault encrypt vars/api_key.yml
# run playbook that uses encrypted file
ansible-playbook main.yml --ask-vault-pass
ansible-playbok main.yml --vault-password-file ~/.ansible/api-pwd.txt
# decrypt when neeeded (unadvisable)
ansible-vault decrypt vars/api_key.yml
# edit file and edit inline without decrypting it
ansible-vault edit vars/api_key.yml
A playbook using encrypted API key.
---
- hosts: localhost
connection: local
gather_facts: no
vars_files:
- vars/api_key.yml
tasks:
- name: Echo the API key which was injected into the env.
shell: echo $API_KEY
environment:
API_KEY: "{{ myapp_api_key }}"
register: echo_result
- name: Show the echo_result
debug: var=echo_result.stdout
Roles
Each role needs a meta
and tasks
folder. The meta
folder contains role dependencies for this role (must run first) – helps compartmentalize variables and other role specific variables and files
.
├── ansible.cfg
├── inventory
├── main.yml
└── roles
└── nodejs
├── defaults
├── files
├── handlers
├── meta
│ └── main.yml
├── tasks
│ └── main.yml
├── templates
├── tests
└── vars
# /main.yml
---
- hosts: all
become: yes
vars:
locations: /usr/local
pre_tasks: []
roles:
- nodejs
tasks:
- name: #...
- include_role: nodejs
# /meta/main.yml
---
dependencies: []
Testing
The Ansible testing spectrum:
+ 1. yamlint
|
| 2. ansible-playbook --syntax-check
|
| 3. ansible-lint
|
| 4. molecule test (integration)
|
| 5. ansible-playbook --check (against prod)
|
v 6. Parallel infrastructure
https://docs.ansible.com/ansible/latest/user_guide/playbooks_tests.html
debug
use register
to capture status codes/outputs and use debug: var
to print them to console:
---
- hosts: servers
tasks:
- name: Register the value of 'uptime'.
command: uptime
register: system_uptime
- name: Print the value to console.
debug:
var: system_uptime.stdout
- name: If has value changed, notify.
debug:
msg: "Command resulted in a change!"
when: system_uptime is changed
Download the roles to be used locally (at the project level) to avoid version mismatches between projects, this will create a roles
# ansible.cfg
[defaults]
nocows = True
roles_path = ./roles
Use a requirements.yml
file to describe roles and versions, and install locally via
ansible-galaxy install -r requirements.yml
# requirements.yml
---
roles:
- name: owner1.role1
version: x.y.z
- name: owner2.role2
version: a.b.c
Finally, add the roles in your main.yml
playbook with privileges if needed (become: yes
)
fail/assert
Use variables (with debug
?) to create flags and use fail
& assert
such as:
tasks:
- name: Fail or fail.
fail:
msg: "Always fails."
when: true
- name: Check for something and fails.
assert:
that: will_fail != false
- name: Assertions can have contain conditions.
assert:
that:
- check_one
- check_two
- check_three == false
yamllint & --syntax-check
Install via pip3 install yamllint
and run in your directory with yamllint .
Override defaults by adding a .yamllint
file
---
extends: default
rules:
truthy:
allowed-values:
- 'true'
- 'false'
- 'yes'
- 'no'
ansible-playbook main.yml --syntax-check
will run static check, more details here.
Note: When referencing files in a playbook, prefer import_tasks as can be checked statically (file exists), whereas include_tasks not.