Ansible configures servers without installing agents. SSH in, run tasks, done. Here’s how to write playbooks that actually work.

Why Ansible?

  • Agentless: Uses SSH, nothing to install on targets
  • Idempotent: Run it twice, same result
  • Readable: YAML syntax, easy to understand
  • Extensible: Huge module library

Inventory

Define your servers in /etc/ansible/hosts or a custom file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# inventory.ini
[webservers]
web1.example.com
web2.example.com

[databases]
db1.example.com ansible_user=postgres

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

Your First Playbook

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# site.yml
---
- name: Configure web servers
  hosts: webservers
  become: yes
  
  tasks:
    - name: Install nginx
      apt:
        name: nginx
        state: present
        update_cache: yes
    
    - name: Start nginx
      service:
        name: nginx
        state: started
        enabled: yes

Run it:

1
ansible-playbook -i inventory.ini site.yml

Common Modules

Package Management

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# Debian/Ubuntu
- name: Install packages
  apt:
    name:
      - nginx
      - curl
      - vim
    state: present
    update_cache: yes

# RHEL/CentOS
- name: Install packages
  yum:
    name: nginx
    state: present

Files and Templates

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
- name: Copy config file
  copy:
    src: nginx.conf
    dest: /etc/nginx/nginx.conf
    owner: root
    mode: '0644'
  notify: Restart nginx

- name: Create directory
  file:
    path: /var/www/app
    state: directory
    owner: www-data
    mode: '0755'

- name: Template config
  template:
    src: app.conf.j2
    dest: /etc/app/config.conf
  notify: Restart app

Services

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
- name: Ensure nginx is running
  service:
    name: nginx
    state: started
    enabled: yes

- name: Restart service
  service:
    name: nginx
    state: restarted

Users

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
- name: Create app user
  user:
    name: appuser
    shell: /bin/bash
    groups: www-data
    append: yes

- name: Add SSH key
  authorized_key:
    user: appuser
    key: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"

Commands

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
- name: Run a command
  command: /opt/app/setup.sh
  args:
    creates: /opt/app/.installed

- name: Run shell command
  shell: cat /etc/passwd | grep root
  register: result

- name: Show output
  debug:
    var: result.stdout

Variables

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# In playbook
- hosts: webservers
  vars:
    app_port: 8080
    app_name: myapp
  
  tasks:
    - name: Configure app
      template:
        src: app.conf.j2
        dest: "/etc/{{ app_name }}/config.conf"
1
2
3
4
# In separate file (group_vars/webservers.yml)
app_port: 8080
app_name: myapp
db_host: db1.example.com
1
2
3
4
5
6
7
8
9
# In template (app.conf.j2)
server {
    listen {{ app_port }};
    server_name {{ ansible_hostname }};
    
    location / {
        proxy_pass http://localhost:3000;
    }
}

Handlers

Handlers run once at the end, only if notified:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
- hosts: webservers
  tasks:
    - name: Update nginx config
      template:
        src: nginx.conf.j2
        dest: /etc/nginx/nginx.conf
      notify: Restart nginx
    
    - name: Update SSL cert
      copy:
        src: ssl.crt
        dest: /etc/nginx/ssl.crt
      notify: Restart nginx
  
  handlers:
    - name: Restart nginx
      service:
        name: nginx
        state: restarted

Conditionals

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
- name: Install on Debian
  apt:
    name: nginx
  when: ansible_os_family == "Debian"

- name: Install on RedHat
  yum:
    name: nginx
  when: ansible_os_family == "RedHat"

- name: Only if variable set
  debug:
    msg: "App is {{ app_name }}"
  when: app_name is defined

Loops

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- name: Create users
  user:
    name: "{{ item }}"
    state: present
  loop:
    - alice
    - bob
    - charlie

- name: Install packages
  apt:
    name: "{{ item }}"
    state: present
  loop:
    - nginx
    - curl
    - git

- name: Create files from dict
  copy:
    dest: "/etc/app/{{ item.key }}.conf"
    content: "{{ item.value }}"
  loop: "{{ config_files | dict2items }}"

Roles

Organize playbooks into reusable roles:

rolnegsithtdv/naaeeaxsnmfr/kdpassllu//ealmmrttaasesii/snnmm..anayyigimmninll.n.yxym.mlclonf.j2
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# roles/nginx/tasks/main.yml
---
- name: Install nginx
  apt:
    name: nginx
    state: present

- name: Configure nginx
  template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
  notify: Restart nginx
1
2
3
4
5
6
# site.yml
---
- hosts: webservers
  roles:
    - nginx
    - app

Ansible Vault

Encrypt sensitive data:

1
2
3
4
5
6
7
8
# Create encrypted file
ansible-vault create secrets.yml

# Edit encrypted file
ansible-vault edit secrets.yml

# Run playbook with vault
ansible-playbook site.yml --ask-vault-pass
1
2
3
# secrets.yml (encrypted)
db_password: supersecret123
api_key: sk-xxxxx
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# playbook using secrets
- hosts: webservers
  vars_files:
    - secrets.yml
  
  tasks:
    - name: Configure database
      template:
        src: db.conf.j2
        dest: /etc/app/db.conf

Best Practices

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# Use meaningful names
- name: Install nginx web server  # Good
- name: Install                   # Bad

# Use become for privilege escalation
- hosts: all
  become: yes  # Run as root

# Check before making changes
ansible-playbook site.yml --check --diff

# Limit to specific hosts
ansible-playbook site.yml --limit web1.example.com

Quick Reference

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Run playbook
ansible-playbook site.yml

# With inventory file
ansible-playbook -i inventory.ini site.yml

# Check mode (dry run)
ansible-playbook site.yml --check

# Limit hosts
ansible-playbook site.yml --limit webservers

# Extra variables
ansible-playbook site.yml -e "app_port=9000"

# With vault password
ansible-playbook site.yml --ask-vault-pass

# Verbose output
ansible-playbook site.yml -vvv

# List tasks
ansible-playbook site.yml --list-tasks

Ansible turns server configuration into version-controlled code. Write once, run anywhere, get consistent results every time.