Ansible Basics

Ansible Basics

Learning Objectives

Through this document, you will learn:

  • Ansible concepts and architecture
  • Inventory and Ad-hoc commands
  • Writing and executing Playbooks
  • Roles and variable management

Difficulty: ⭐⭐⭐ (Intermediate-Advanced)


Table of Contents

  1. Ansible Overview
  2. Installation and Configuration
  3. Inventory
  4. Ad-hoc Commands
  5. Playbook
  6. Variables and Facts
  7. Roles
  8. Ansible Vault

1. Ansible Overview

What is Ansible?

Ansible is an IT automation tool that works via SSH without requiring agents.

┌─────────────────────────────────────────────────────────────┐
│                     Control Node                            │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  Ansible                                             │   │
│  │  ├── Playbooks (YAML)                                │   │
│  │  ├── Inventory                                       │   │
│  │  ├── Modules                                         │   │
│  │  └── Plugins                                         │   │
│  └───────────────────────┬─────────────────────────────┘   │
└───────────────────────────┼─────────────────────────────────┘
                            │ SSH
        ┌───────────────────┼───────────────────┐
        │                   │                   │
        ▼                   ▼                   ▼
┌───────────────┐  ┌───────────────┐  ┌───────────────┐
│ Managed Node  │  │ Managed Node  │  │ Managed Node  │
│ (web-server1) │  │ (web-server2) │  │ (db-server)   │
│   No agent    │  │   No agent    │  │   No agent    │
└───────────────┘  └───────────────┘  └───────────────┘

Ansible vs Other Tools

Feature Ansible Puppet Chef
Agent Not required Required Required
Language YAML Ruby DSL Ruby
Push/Pull Push Pull Pull
Learning Curve Low Medium High

2. Installation and Configuration

Installing Ansible

# Ubuntu/Debian
sudo apt update
sudo apt install ansible

# RHEL/CentOS
sudo yum install epel-release
sudo yum install ansible

# Install via pip (recommended)
pip3 install ansible

# Check version
ansible --version

SSH Key Configuration

# Generate SSH key
ssh-keygen -t ed25519 -f ~/.ssh/ansible_key

# Copy key to target server
ssh-copy-id -i ~/.ssh/ansible_key.pub user@target-server

# Test SSH connection
ssh -i ~/.ssh/ansible_key user@target-server

ansible.cfg Configuration

# /etc/ansible/ansible.cfg (global)
# ~/.ansible.cfg (user)
# ./ansible.cfg (project)
# ansible.cfg
[defaults]
inventory = ./inventory
remote_user = ansible
private_key_file = ~/.ssh/ansible_key
host_key_checking = False
retry_files_enabled = False
forks = 20
timeout = 30

[privilege_escalation]
become = True
become_method = sudo
become_user = root
become_ask_pass = False

[ssh_connection]
pipelining = True
ssh_args = -o ControlMaster=auto -o ControlPersist=60s

3. Inventory

INI Format

# inventory/hosts
[webservers]
web1.example.com
web2.example.com ansible_host=192.168.1.11

[dbservers]
db1.example.com ansible_host=192.168.1.21 ansible_port=2222
db2.example.com ansible_user=dbadmin

[loadbalancers]
lb1.example.com

# Group of groups
[production:children]
webservers
dbservers
loadbalancers

# Group variables
[webservers:vars]
http_port=80
max_clients=200

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

YAML Format

# inventory/hosts.yml
all:
  children:
    webservers:
      hosts:
        web1.example.com:
        web2.example.com:
          ansible_host: 192.168.1.11
      vars:
        http_port: 80
        max_clients: 200

    dbservers:
      hosts:
        db1.example.com:
          ansible_host: 192.168.1.21
          ansible_port: 2222
        db2.example.com:
          ansible_user: dbadmin

    production:
      children:
        webservers:
        dbservers:

  vars:
    ansible_python_interpreter: /usr/bin/python3

Dynamic Inventory

# AWS EC2 dynamic inventory
pip install boto3 botocore

# ansible.cfg
[defaults]
inventory = aws_ec2.yml
# aws_ec2.yml
plugin: amazon.aws.aws_ec2
regions:
  - ap-northeast-2
keyed_groups:
  - key: tags.Environment
    prefix: env
  - key: instance_type
    prefix: type
compose:
  ansible_host: public_ip_address

Verifying Inventory

# List hosts
ansible-inventory --list
ansible-inventory --graph

# Specific group
ansible webservers --list-hosts

# Check host variables
ansible-inventory --host web1.example.com

4. Ad-hoc Commands

Basic Usage

ansible <pattern> -m <module> -a "<arguments>"

# Examples
ansible all -m ping
ansible webservers -m command -a "uptime"
ansible dbservers -m shell -a "df -h | grep /dev/sda"

Frequently Used Modules

# ping (connection test)
ansible all -m ping

# command (basic command execution)
ansible all -m command -a "hostname"

# shell (use shell features)
ansible all -m shell -a "cat /etc/os-release | grep VERSION"

# copy (file copy)
ansible all -m copy -a "src=/local/file dest=/remote/path mode=0644"

# file (file/directory management)
ansible all -m file -a "path=/tmp/testdir state=directory mode=0755"

# yum/apt (package management)
ansible webservers -m yum -a "name=httpd state=present"
ansible webservers -m apt -a "name=nginx state=present update_cache=yes"

# service (service management)
ansible webservers -m service -a "name=nginx state=started enabled=yes"

# user (user management)
ansible all -m user -a "name=deploy state=present groups=sudo"

# setup (gather system information)
ansible web1 -m setup
ansible web1 -m setup -a "filter=ansible_distribution*"

Privilege Escalation

# Use sudo
ansible all -m apt -a "name=vim state=present" --become

# As specific user
ansible all -m command -a "whoami" --become --become-user=postgres

Parallel Execution

# Adjust number of concurrent hosts
ansible all -m ping -f 50  # 50 concurrent executions

# Sequential execution
ansible all -m ping -f 1

5. Playbook

Basic Structure

# site.yml
---
- name: Configure webservers
  hosts: webservers
  become: yes
  vars:
    http_port: 80

  tasks:
    - name: Install nginx
      apt:
        name: nginx
        state: present
        update_cache: yes

    - name: Start nginx service
      service:
        name: nginx
        state: started
        enabled: yes

- name: Configure databases
  hosts: dbservers
  become: yes

  tasks:
    - name: Install PostgreSQL
      apt:
        name: postgresql
        state: present

Running Playbooks

# Basic execution
ansible-playbook site.yml

# Specify inventory
ansible-playbook -i inventory/production site.yml

# Dry run (check without changes)
ansible-playbook site.yml --check

# Show differences
ansible-playbook site.yml --diff

# Verbose output
ansible-playbook site.yml -v    # verbose
ansible-playbook site.yml -vvv  # very verbose

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

# Use tags
ansible-playbook site.yml --tags "install,configure"
ansible-playbook site.yml --skip-tags "debug"

# Start at specific task
ansible-playbook site.yml --start-at-task "Start nginx service"

Conditionals (when)

tasks:
  - name: Install Apache (RHEL)
    yum:
      name: httpd
      state: present
    when: ansible_os_family == "RedHat"

  - name: Install Apache (Debian)
    apt:
      name: apache2
      state: present
    when: ansible_os_family == "Debian"

  - name: Check if file exists
    stat:
      path: /etc/myapp.conf
    register: config_file

  - name: Create config if not exists
    template:
      src: myapp.conf.j2
      dest: /etc/myapp.conf
    when: not config_file.stat.exists

Loops

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

  - name: Create users
    user:
      name: "{{ item.name }}"
      groups: "{{ item.groups }}"
      state: present
    loop:
      - { name: 'alice', groups: 'sudo' }
      - { name: 'bob', groups: 'developers' }

  - name: Install packages with apt
    apt:
      name: "{{ packages }}"
      state: present
    vars:
      packages:
        - nginx
        - postgresql
        - redis

Handlers

tasks:
  - name: Copy nginx config
    template:
      src: nginx.conf.j2
      dest: /etc/nginx/nginx.conf
    notify: Restart nginx

  - name: Copy site config
    template:
      src: site.conf.j2
      dest: /etc/nginx/sites-available/mysite
    notify:
      - Reload nginx
      - Clear cache

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

  - name: Reload nginx
    service:
      name: nginx
      state: reloaded

  - name: Clear cache
    file:
      path: /var/cache/nginx
      state: absent

Blocks

tasks:
  - name: Install and configure app
    block:
      - name: Install package
        apt:
          name: myapp
          state: present

      - name: Configure app
        template:
          src: myapp.conf.j2
          dest: /etc/myapp/config.yml

    rescue:
      - name: Rollback on failure
        apt:
          name: myapp
          state: absent

    always:
      - name: Always run cleanup
        file:
          path: /tmp/install_temp
          state: absent

    when: install_myapp | default(true)
    become: yes

6. Variables and Facts

Variable Definition Locations

Priority (low  high):
1. role defaults
2. inventory file vars
3. inventory group_vars
4. inventory host_vars
5. playbook group_vars
6. playbook host_vars
7. host facts
8. play vars
9. play vars_prompt
10. play vars_files
11. role vars
12. block vars
13. task vars
14. include_vars
15. set_facts / registered vars
16. role parameters
17. extra vars (-e)

Variable File Structure

project/
├── ansible.cfg
├── inventory/
   ├── hosts
   ├── group_vars/
      ├── all.yml
      ├── webservers.yml
      └── dbservers.yml
   └── host_vars/
       ├── web1.example.com.yml
       └── db1.example.com.yml
├── playbooks/
   └── site.yml
└── roles/

group_vars/host_vars Examples

# inventory/group_vars/all.yml
---
ansible_user: deploy
ntp_servers:
  - 0.pool.ntp.org
  - 1.pool.ntp.org

# inventory/group_vars/webservers.yml
---
http_port: 80
nginx_worker_processes: auto

# inventory/host_vars/web1.example.com.yml
---
nginx_worker_processes: 4
custom_config: true

Using Variables in Playbooks

---
- name: Configure web servers
  hosts: webservers
  vars:
    app_name: myapp
    app_version: "1.2.3"
    app_env: production

  vars_files:
    - vars/common.yml
    - "vars/{{ ansible_os_family }}.yml"

  tasks:
    - name: Deploy application
      template:
        src: app.conf.j2
        dest: "/etc/{{ app_name }}/config.yml"

    - name: Set runtime variable
      set_fact:
        runtime_var: "{{ ansible_hostname }}-{{ app_version }}"

Facts

tasks:
  # Use auto-gathered facts
  - name: Print OS info
    debug:
      msg: "OS: {{ ansible_distribution }} {{ ansible_distribution_version }}"

  # Custom facts (stored on target host)
  # /etc/ansible/facts.d/custom.fact
  - name: Create custom fact
    copy:
      content: |
        [app]
        version=1.2.3
        environment=production
      dest: /etc/ansible/facts.d/custom.fact
      mode: '0644'

  # Refresh facts
  - name: Refresh facts
    setup:
      filter: ansible_local

  - name: Use custom fact
    debug:
      msg: "App version: {{ ansible_local.custom.app.version }}"

Jinja2 Templates

{# templates/nginx.conf.j2 #}
worker_processes {{ nginx_worker_processes | default('auto') }};

events {
    worker_connections {{ nginx_worker_connections | default(1024) }};
}

http {
    server {
        listen {{ http_port }};
        server_name {{ ansible_fqdn }};

        {% for location in nginx_locations | default([]) %}
        location {{ location.path }} {
            proxy_pass {{ location.proxy }};
        }
        {% endfor %}

        {% if enable_ssl | default(false) %}
        ssl_certificate {{ ssl_cert_path }};
        ssl_certificate_key {{ ssl_key_path }};
        {% endif %}
    }
}

7. Roles

Role Structure

roles/
└── webserver/
    ├── defaults/
       └── main.yml      # Default variables (low priority)
    ├── files/
       └── static.conf   # Static files
    ├── handlers/
       └── main.yml      # Handlers
    ├── meta/
       └── main.yml      # Dependencies, metadata
    ├── tasks/
       └── main.yml      # Tasks
    ├── templates/
       └── nginx.conf.j2 # Jinja2 templates
    └── vars/
        └── main.yml      # Variables (high priority)

Creating Roles

# Create basic structure
ansible-galaxy init roles/webserver

Role Example

# roles/webserver/defaults/main.yml
---
nginx_port: 80
nginx_worker_processes: auto
nginx_sites: []
# roles/webserver/tasks/main.yml
---
- name: Install nginx
  apt:
    name: nginx
    state: present
    update_cache: yes
  tags: install

- name: Configure nginx
  template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
  notify: Restart nginx
  tags: configure

- name: Configure sites
  template:
    src: site.conf.j2
    dest: "/etc/nginx/sites-available/{{ item.name }}"
  loop: "{{ nginx_sites }}"
  notify: Reload nginx
  tags: configure

- name: Enable sites
  file:
    src: "/etc/nginx/sites-available/{{ item.name }}"
    dest: "/etc/nginx/sites-enabled/{{ item.name }}"
    state: link
  loop: "{{ nginx_sites }}"
  notify: Reload nginx
  tags: configure

- name: Start nginx
  service:
    name: nginx
    state: started
    enabled: yes
  tags: service
# roles/webserver/handlers/main.yml
---
- name: Restart nginx
  service:
    name: nginx
    state: restarted

- name: Reload nginx
  service:
    name: nginx
    state: reloaded
# roles/webserver/meta/main.yml
---
dependencies:
  - role: common
  - role: firewall
    vars:
      firewall_allowed_ports:
        - "{{ nginx_port }}"

Using Roles

# site.yml
---
- name: Configure web servers
  hosts: webservers
  become: yes
  roles:
    - common
    - role: webserver
      vars:
        nginx_port: 8080
        nginx_sites:
          - name: mysite
            domain: example.com
            root: /var/www/mysite

- name: Configure databases
  hosts: dbservers
  become: yes
  roles:
    - common
    - postgresql

Ansible Galaxy

# Search roles
ansible-galaxy search nginx

# Install role
ansible-galaxy install geerlingguy.nginx

# Install collection
ansible-galaxy collection install community.general

# Install from requirements.yml
ansible-galaxy install -r requirements.yml
# requirements.yml
---
roles:
  - name: geerlingguy.nginx
    version: "3.1.0"
  - name: geerlingguy.postgresql
    version: "3.3.1"

collections:
  - name: community.general
    version: ">=6.0.0"
  - name: amazon.aws
    version: "5.0.0"

8. Ansible Vault

Basic Vault Usage

# Create new encrypted file
ansible-vault create secrets.yml

# Encrypt existing file
ansible-vault encrypt vars/secrets.yml

# Decrypt file
ansible-vault decrypt vars/secrets.yml

# Edit file
ansible-vault edit secrets.yml

# View file contents
ansible-vault view secrets.yml

# Change password
ansible-vault rekey secrets.yml

Vault Usage Example

# vars/secrets.yml (encrypted)
---
db_password: "SuperSecretPassword123"
api_key: "sk-1234567890abcdef"
# playbook.yml
---
- name: Deploy application
  hosts: webservers
  vars_files:
    - vars/secrets.yml

  tasks:
    - name: Configure database
      template:
        src: db.conf.j2
        dest: /etc/app/db.conf
      # {{ db_password }} is available

Running Playbooks

# Password prompt
ansible-playbook site.yml --ask-vault-pass

# Password file
ansible-playbook site.yml --vault-password-file ~/.vault_pass

# Environment variable
export ANSIBLE_VAULT_PASSWORD_FILE=~/.vault_pass
ansible-playbook site.yml

String Encryption

# Encrypt string only
ansible-vault encrypt_string 'MySecretPassword' --name 'db_password'

# Output (use directly in YAML)
db_password: !vault |
          $ANSIBLE_VAULT;1.1;AES256
          66386439653236...

Practice Problems

Problem 1: Write Inventory

Write the inventory in YAML format for the following servers: - app1, app2 (webservers group) - db1 (dbservers group) - All servers: ansible_user=deploy - webservers: http_port=8080

Problem 2: Write Playbook

Write a Playbook that installs and starts nginx: - Support both RedHat and Debian families - Use handlers for service restart - Use tags (install, configure)

Problem 3: Write Role

Write tasks/main.yml for a Role that installs PostgreSQL: - Install packages - Start service - Create initial database


Answers

Problem 1 Answer

# inventory/hosts.yml
all:
  vars:
    ansible_user: deploy
  children:
    webservers:
      hosts:
        app1:
        app2:
      vars:
        http_port: 8080
    dbservers:
      hosts:
        db1:

Problem 2 Answer

---
- name: Install and configure nginx
  hosts: webservers
  become: yes

  tasks:
    - name: Install nginx (RedHat)
      yum:
        name: nginx
        state: present
      when: ansible_os_family == "RedHat"
      tags: install

    - name: Install nginx (Debian)
      apt:
        name: nginx
        state: present
        update_cache: yes
      when: ansible_os_family == "Debian"
      tags: install

    - name: Copy nginx config
      template:
        src: nginx.conf.j2
        dest: /etc/nginx/nginx.conf
      notify: Restart nginx
      tags: configure

    - name: Start nginx
      service:
        name: nginx
        state: started
        enabled: yes
      tags: install

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

Problem 3 Answer

# roles/postgresql/tasks/main.yml
---
- name: Install PostgreSQL
  apt:
    name:
      - postgresql
      - postgresql-contrib
      - python3-psycopg2
    state: present
    update_cache: yes

- name: Start PostgreSQL service
  service:
    name: postgresql
    state: started
    enabled: yes

- name: Create application database
  become_user: postgres
  postgresql_db:
    name: "{{ pg_database | default('appdb') }}"
    state: present

- name: Create database user
  become_user: postgres
  postgresql_user:
    name: "{{ pg_user | default('appuser') }}"
    password: "{{ pg_password }}"
    db: "{{ pg_database | default('appdb') }}"
    priv: ALL
    state: present

Next Steps


References

to navigate between lessons