The Ultimate Ansible Guide

A practical reference from first contact to writing your own modules


Table of Contents

  1. What Ansible Is and How It Thinks
  2. Ansible Architecture
  3. Installing and Setting Up Ansible
  4. Inventories: Static, Dynamic, and Cloud
  5. YAML and Ansible Syntax Basics
  6. Ad-hoc Commands vs Playbooks
  7. Playbooks in Depth
  8. Variables and Variable Precedence
  9. Jinja2 Templating
  10. Conditionals, Loops, Blocks, Rescue, Always
  11. Roles: Structure and Best Practices
  12. Collections: Organizing and Sharing Content
  13. Core Built-in Modules You’ll Use Constantly
  14. Cloud Automation (AWS, Azure, GCP)
  15. Network Automation
  16. Managing Secrets with Ansible Vault
  17. Error Handling and Debugging
  18. Idempotency: Doing Things Once, Correctly
  19. Performance and Scaling Ansible
  20. Using Ansible in CI/CD Pipelines
  21. Custom Filters and Plugins
  22. Writing Your Own Ansible Module (Python)
  23. Ansible for Kubernetes
  24. Security Hardening and Compliance
  25. Common Architecture Patterns and Real Use Cases
  26. Common Gotchas and Misconceptions
  27. End-to-End Example: Provision and Deploy a Simple App
  28. Testing: Molecule and Other Strategies
  29. Terraform, Other Tools, and When Not to Use Ansible
  30. Final Cheat Sheet

What Ansible Is and How It Thinks

Ansible is:

  • Configuration management (install packages, configure services)
  • Orchestration (deploy apps across multiple hosts in order)
  • Provisioning and automation (create cloud resources, bootstrap servers)

Key properties:

  • Agentless – no agent on managed nodes; it uses SSH (Linux) or WinRM (Windows).
  • Declarative-ish – you describe the desired state; modules try to enforce it idempotently.
  • YAML-based – human-readable playbooks.
  • Extensible – you can create roles, collections, plugins, and modules.

A mental model:

  • You have a control node (where Ansible runs).
  • You have managed nodes (servers, switches, containers, etc.).
  • You describe what you want in playbooks.
  • Ansible reads inventory to know where to apply it.
  • It uses modules to perform actions (e.g., user, yum, apt, file).
  • It applies tasks in order, evaluates conditions, and tries to leave the system in a consistent state.

Ansible Architecture

At a high level:

flowchart LR
    A[Control Node] -->|SSH / WinRM / API| B[Managed Node 1]
    A --> C[Managed Node 2]
    A --> D[Network Devices]
    A --> E[Cloud APIs]

    subgraph Control Node
      P[Playbooks]
      I[Inventory]
      M[Modules]
      PL[Plugins]
      R[Roles & Collections]
    end

Key components:

  • Control Node
    Where you install Ansible and run commands (ansible, ansible-playbook). Can be your laptop, a jump host, CI runner, etc.
  • Managed Nodes
    Targets (Linux, Windows, network gear, cloud APIs). They don’t need Ansible installed, just Python (in many cases) and connectivity.
  • Inventory
    List of hosts and groups. Can be static (INI/YAML) or dynamic (scripts/plugins from AWS, Azure, etc.).
  • Modules
    Discrete units of work. Think “actions”: manage files, packages, services, cloud resources, etc.
  • Plugins
    Extend behavior: connection plugins, callback plugins (logging/output), filter plugins (for Jinja2), lookup plugins, etc.
  • Facts
    Data collected from managed nodes (ansible_facts): OS, IPs, CPUs, memory, etc.

Installing and Setting Up Ansible

On Linux

Most common:

# On modern distributions, prefer pipx or venv
python3 -m venv ~/ansible-venv
source ~/ansible-venv/bin/activate
pip install ansible

Or from your package manager (versions may lag):

# Debian/Ubuntu
sudo apt update
sudo apt install ansible

# RHEL/CentOS (may require EPEL or AppStream)
sudo dnf install ansible

On macOS

Using Homebrew:

brew install ansible

Or via pip in a virtualenv (same as Linux).

On Windows

Typical options:

  • Use WSL (Windows Subsystem for Linux) and install Ansible inside Ubuntu.
  • Or use a Linux VM as your control node.

Direct native support on Windows as a control node is not the common path; most people use Linux/WSL.

Using Docker

Run Ansible via container:

docker run --rm -it \
  -v $(pwd):/workdir \
  -w /workdir \
  quay.io/ansible/ansible-runner:latest bash

Then use ansible-playbook inside the container.


Inventories: Static, Dynamic, and Cloud

The inventory tells Ansible what hosts exist and how to group them.

Static Inventory (INI)

inventory.ini:

[web]
web1.example.com
web2.example.com

[db]
db1.example.com

[all:vars]
ansible_user=ubuntu
ansible_ssh_private_key_file=~/.ssh/id_rsa

Run:

ansible all -i inventory.ini -m ping

Static Inventory (YAML)

inventory.yml:

all:
  vars:
    ansible_user: ubuntu
  children:
    web:
      hosts:
        web1.example.com:
        web2.example.com:
    db:
      hosts:
        db1.example.com:

Host and Group Variables

Directory layout:

inventory/
  hosts.yml
  group_vars/
    all.yml
    web.yml
  host_vars/
    web1.example.com.yml

group_vars/web.yml:

nginx_port: 80

host_vars/web1.example.com.yml:

nginx_port: 8080  # overrides group var for this host

Dynamic Inventory

Instead of listing hosts yourself, you let a plugin/script talk to your cloud provider.

Examples:

  • AWS: aws_ec2 plugin
  • Azure: azure_rm plugin
  • GCP: gcp_compute plugin

Dynamic inventory configuration (e.g., aws_ec2.yml):

plugin: aws_ec2
regions:
  - us-east-1
keyed_groups:
  - key: tags.Role
    prefix: role_

Then:

ansible-inventory -i aws_ec2.yml --graph
ansible role_web -m ping -i aws_ec2.yml

YAML and Ansible Syntax Basics

YAML is whitespace-sensitive and absolutely hates tabs. Use spaces.

A task:

- name: Install nginx
  apt:
    name: nginx
    state: present
  become: true

Key YAML patterns:

  • Lists: packages: - nginx - git - curl
  • Dictionaries: user: name: deploy shell: /bin/bash

Common mistakes:

  • Using tabs instead of spaces → parse errors.
  • Misaligned indentation → “mapping values are not allowed here”.
  • Forgetting - for list items.

Ad-hoc Commands vs Playbooks

Ad-hoc commands are great for quick one-off tasks.

# Ping all hosts
ansible all -i inventory.ini -m ping

# Install package
ansible web -m apt -a "name=nginx state=present" --become

# Run a shell command
ansible db -m shell -a "df -h"

Playbooks are YAML files describing reproducible workflows.

  • Ad-hoc: “Do X now.”
  • Playbook: “Define how X should always be.”

Playbooks in Depth

A playbook is a list of plays. Each play targets some hosts and runs tasks.

Example site.yml:

- name: Configure web servers
  hosts: web
  become: true

  vars:
    app_user: deploy

  tasks:
    - name: Ensure app user exists
      user:
        name: "{{ app_user }}"
        shell: /bin/bash

    - name: Install nginx
      apt:
        name: nginx
        state: present
      notify: Restart nginx

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

Concepts:

  • hosts: inventory group or pattern.
  • become: use privilege escalation (usually sudo).
  • tasks: ordered list of modules with arguments.
  • handlers: triggered by notify; run at end of play or when meta: flush_handlers is used.
  • vars: play-level variables.

Includes and Imports

  • import_playbook: static import at parse time.
  • include_tasks / import_tasks: bring in task sets.

Example:

- import_playbook: db.yml

- hosts: web
  tasks:
    - import_tasks: tasks/common.yml
    - include_tasks: tasks/dynamic.yml
      when: ansible_os_family == "Debian"

Tags

Tag tasks to run subsets:

- name: Install nginx
  apt:
    name: nginx
    state: present
  tags:
    - packages
    - nginx

Run only those tasks:

ansible-playbook site.yml --tags nginx

Variables and Variable Precedence

Variables are everywhere: inventories, playbooks, roles, extra vars, etc.

Types of var sources:

  • Inventory (group_vars, host_vars)
  • Play vars (vars: in play)
  • Role defaults (roles/myrole/defaults/main.yml)
  • Role vars (roles/myrole/vars/main.yml)
  • Extra vars (-e)
  • Facts (ansible_facts)
  • Registered vars (register:)

Rough idea of precedence (highest wins):

  1. Extra vars (-e) – always win.
  2. Play vars.
  3. Host vars.
  4. Group vars.
  5. Role vars.
  6. Role defaults – lowest priority.

Practical habits:

  • Put sane defaults in roles/rolename/defaults/main.yml.
  • Use group_vars/all for cross-environment shared settings.
  • Use group_vars/prod vs group_vars/stage to separate environments.
  • Avoid -e except for overrides or secrets (or CI).

register

Capture module output:

- name: Get nginx version
  command: nginx -v
  register: nginx_version
  failed_when: false

- debug:
    var: nginx_version.stderr

Jinja2 Templating

Ansible uses Jinja2 to render:

  • Variables: {{ var_name }}
  • Conditionals and loops in templates.
  • Filters: {{ var | upper }}

Example templates/nginx.conf.j2:

user {{ nginx_user }};
worker_processes auto;

http {
  server {
    listen {{ nginx_port }};
    server_name {{ inventory_hostname }};

    location / {
      proxy_pass http://127.0.0.1:{{ app_port }};
    }
  }
}

Playbook:

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

Useful filters:

  • default: {{ myvar | default('fallback') }}
  • to_nice_json, to_nice_yaml
  • upper, lower, replace, regex_replace
  • join: {{ mylist | join(',') }}
  • unique, sort, flatten

Control structures in templates:

{% if env == 'prod' %}
  log_level warning;
{% else %}
  log_level debug;
{% endif %}

upstream app {
{% for host in groups['web'] %}
  server {{ hostvars[host].ansible_host }}:{{ app_port }};
{% endfor %}
}

Conditionals, Loops, Blocks, Rescue, Always

when

- name: Install nginx on Debian family
  apt:
    name: nginx
    state: present
  when: ansible_os_family == "Debian"

Use variables, facts, registered vars:

- name: Do something only if file changed
  debug:
    msg: "File changed!"
  when: copy_result.changed

Loops

Modern way: loop:

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

Loop over dicts:

- name: Create users
  user:
    name: "{{ item.name }}"
    groups: "{{ item.groups }}"
  loop:
    - { name: "alice", groups: "sudo" }
    - { name: "bob", groups: "docker" }

Blocks, Rescue, Always

Blocks group tasks logically and handle failures.

- block:
    - name: Try risky operation
      command: /bin/false

    - name: This won’t run if previous failed
      debug:
        msg: "Still in block"
  rescue:
    - name: Handle failure
      debug:
        msg: "The block failed; handling it."

  always:
    - name: Always run cleanup
      debug:
        msg: "Cleanup step"

Useful for:

  • Rolling back changes.
  • Ensuring cleanup happens even on failure.

Roles: Structure and Best Practices

Roles are how you package reusable logic.

Standard Role Layout

roles/
  nginx/
    defaults/
      main.yml
    vars/
      main.yml
    tasks/
      main.yml
    handlers/
      main.yml
    templates/
      nginx.conf.j2
    files/
      some-static-file
    meta/
      main.yml
    tests/
      inventory
      test.yml
  • defaults/main.yml: lowest-precedence defaults.
  • vars/main.yml: higher-precedence vars (use sparingly).
  • tasks/main.yml: entry point for tasks.
  • handlers/main.yml: notifications.
  • templates/: Jinja2 templates.
  • files/: raw files to copy.
  • meta/main.yml: role dependencies.

Example use in playbook:

- hosts: web
  become: true
  roles:
    - role: nginx
      vars:
        nginx_port: 8080

Role Dependencies

roles/app/meta/main.yml:

dependencies:
  - role: nginx
  - role: postgresql

Ansible will apply nginx and postgresql roles before app.

Best Practices for Roles

  • Keep roles single responsibility (e.g., nginx, app_java, prometheus_node_exporter).
  • Avoid hardcoding environment-specific stuff in roles; push that to group_vars.
  • Use defaults/ instead of vars/ unless you really want to override users.
  • Document variables in README.md inside the role.

Collections: Organizing and Sharing Content

Collections bundle:

  • Roles
  • Modules
  • Plugins
  • Playbooks

Layout:

my_namespace-my_collection/
  plugins/
    modules/
    filters/
    callbacks/
  roles/
  playbooks/
  docs/
  galaxy.yml

Use content from a collection with fully qualified collection names (FQCN):

- name: Use community.general module
  community.general.htpasswd:
    path: /etc/nginx/.htpasswd
    name: admin
    password: secret

Installing collections:

ansible-galaxy collection install community.general

Custom collection publishing is done via ansible-galaxy collection build and ansible-galaxy collection publish (often to private Galaxy servers or Automation Hub).


Core Built-in Modules You’ll Use Constantly

Instead of listing every module, here’s the useful families.

File Management

  • file: create directories, change ownership/mode, symlinks.
  • copy: copy files to remote.
  • template: render Jinja2 templates.
  • lineinfile, blockinfile: modify specific lines or blocks in a file.

Example:

- name: Ensure directory exists
  file:
    path: /opt/myapp
    state: directory
    owner: deploy
    group: deploy
    mode: "0755"

Package Management

  • apt (Debian/Ubuntu)
  • yum, dnf (RHEL/CentOS/Fedora)
  • package (generic wrapper)
- name: Install nginx using generic module
  package:
    name: nginx
    state: present

Service Management

  • service, systemd
- name: Enable and start nginx
  systemd:
    name: nginx
    enabled: true
    state: started

User / Group Management

  • user, group, authorized_key
- name: Create deploy user
  user:
    name: deploy
    shell: /bin/bash

- name: Add SSH key
  authorized_key:
    user: deploy
    key: "{{ lookup('file', 'files/deploy_id_rsa.pub') }}"

Command Execution

  • shell: runs commands through shell (supports pipes, redirects).
  • command: runs commands directly (no shell).
  • raw: sends raw commands (useful when Python is not installed yet).

Use these sparingly; prefer idempotent modules.


Cloud Automation (AWS, Azure, GCP)

Ansible has rich cloud collections:

  • AWS: amazon.aws, community.aws
  • Azure: azure.azcollection
  • GCP: google.cloud

Example: create an EC2 instance (simplified):

- hosts: localhost
  connection: local
  gather_facts: false

  collections:
    - amazon.aws

  tasks:
    - name: Launch EC2 instance
      ec2_instance:
        name: "web-server"
        key_name: my-key
        instance_type: t3.micro
        image_id: ami-1234567890abcdef0
        wait: true
        region: us-east-1
      register: ec2

Common patterns:

  • Use dynamic inventory from the same provider.
  • Use Ansible to bootstrap after initial provisioning (install agents, configure app).

Network Automation

Network device support is largely via collections:

  • cisco.ios, arista.eos, junipernetworks.junos, etc.

Core concepts:

  • Use connection types like network_cli or httpapi.
  • Use NAPALM-based modules or vendor-specific modules.
  • Always test safely (check_mode, proper backups).

Example:

- hosts: switches
  connection: network_cli
  gather_facts: false

  tasks:
    - name: Configure interface
      ios_config:
        lines:
          - description Uplink
          - switchport mode trunk
        parents: interface GigabitEthernet0/1

Managing Secrets with Ansible Vault

Vault encrypts sensitive data (passwords, API keys).

Create an encrypted file:

ansible-vault create group_vars/prod/vault.yml

Inside:

db_password: supersecret

Use in playbooks:

- name: Use secret
  debug:
    msg: "DB password is {{ db_password }}"

Run with vault password:

ansible-playbook site.yml --ask-vault-pass
# or
ansible-playbook site.yml --vault-password-file .vault_pass.txt

Best practices:

  • Keep secrets in separate vars files, e.g. vault.yml.
  • Never store vault pass file in the same repo.
  • Consider external secret managers (HashiCorp Vault, AWS SSM) with lookup plugins for large environments.

Error Handling and Debugging

Useful CLI flags:

  • -v, -vv, -vvv: increase verbosity.
  • --step: confirm each task interactively.
  • --start-at-task: resume from a specific task.

Debug in tasks:

- debug:
    var: some_variable

- debug:
    msg: "Value is {{ some_variable | default('not set') }}"

Control failures:

- name: This may fail but is not fatal
  command: /usr/bin/false
  register: cmd
  ignore_errors: true

- name: Fail if condition
  fail:
    msg: "Something went wrong"
  when: cmd.rc != 0

Or:

failed_when: "'CRITICAL' in cmd.stderr"
changed_when: false

Idempotency: Doing Things Once, Correctly

Idempotency means: running the playbook again doesn’t break things; it leaves the system in the same state.

Good example:

- name: Install nginx (idempotent)
  apt:
    name: nginx
    state: present

Bad example:

- name: Append line (not idempotent)
  shell: "echo 'include /etc/nginx/conf.d/*.conf;' >> /etc/nginx/nginx.conf"

Better:

- name: Ensure line exists
  lineinfile:
    path: /etc/nginx/nginx.conf
    line: "include /etc/nginx/conf.d/*.conf;"

Use module options like creates, removes, chdir to ensure idempotency when using command/shell:

- name: Run migration only once
  command: /opt/app/bin/migrate
  args:
    creates: /opt/app/.migration_done

Performance and Scaling Ansible

When you grow to hundreds/thousands of hosts, performance matters.

Tips:

  • Increase forks: parallelism. # ansible.cfg [defaults] forks = 50
  • Enable pipelining and SSH multiplexing: [ssh_connection] pipelining = True control_path = ~/.ssh/ansible-%%h-%%p-%%r Ensure requiretty is disabled in sudoers if you use pipelining.
  • Use fact caching (Redis, JSON files) to avoid recollecting facts every run.
  • Use serial for rolling updates: - hosts: web serial: 10 tasks: - name: Update app include_role: app
  • Use strategy: free to let hosts run tasks independently: - hosts: all strategy: free tasks: - name: Long task per host command: /usr/local/bin/do_something_long

Using Ansible in CI/CD Pipelines

Typical stages:

  1. Lint: ansible-lint
  2. Unit/integration tests: molecule
  3. Dry-run: ansible-playbook --check
  4. Apply: ansible-playbook to staging, then prod.

Example GitHub Actions job (idea, not full YAML):

jobs:
  ansible:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.12'

      - name: Install Ansible
        run: |
          python -m pip install --upgrade pip
          pip install ansible ansible-lint molecule

      - name: Lint playbooks
        run: ansible-lint

      - name: Run playbook in check mode
        run: ansible-playbook -i inventory site.yml --check

For production, you might:

  • Run playbooks from a bastion host via CI.
  • Use vault or external secret managers for credentials.

Custom Filters and Plugins

You can extend Jinja2 with custom filters.

Filter Plugin Example

filter_plugins/my_filters.py:

def reverse_string(value):
    return value[::-1]

class FilterModule(object):
    def filters(self):
        return {
            'reverse_string': reverse_string
        }

Use in playbook/template:

{{ "ansible" | reverse_string }}  {# outputs 'elbisna' #}

Other plugin types:

  • Lookup plugins: fetch data from external systems.
  • Callback plugins: custom logging/formatting of output.
  • Action plugins: change how modules are executed.

Writing Your Own Ansible Module (Python)

This is the “advanced mode” you asked for.

Modules are usually:

  • Single Python file in library/ or inside a collection.
  • Use AnsibleModule from ansible.module_utils.basic.
  • Accept arguments, do work, return JSON.

Minimal example: library/hello_module.py:

#!/usr/bin/python

from ansible.module_utils.basic import AnsibleModule

def run_module():
    module_args = dict(
        name=dict(type='str', required=False, default='world')
    )

    result = dict(
        changed=False,
        message=''
    )

    module = AnsibleModule(
        argument_spec=module_args,
        supports_check_mode=True
    )

    name = module.params['name']
    result['message'] = f"Hello, {name}!"

    # No state change so changed=False
    module.exit_json(**result)

def main():
    run_module()

if __name__ == '__main__':
    main()

Use it in a playbook:

- hosts: localhost
  tasks:
    - name: Test custom module
      hello_module:
        name: Ansible
      register: result

    - debug:
        var: result

Key steps for real modules:

  1. Define argument_spec with types, required/optional, choices.
  2. Use supports_check_mode to respect --check where possible.
  3. Determine whether something changed and set changed=True appropriately.
  4. Use module.fail_json(msg="...") for errors.
  5. Put shared code in ansible.module_utils for reuse across modules.

For collections, modules live at plugins/modules/my_module.py and are referenced by FQCN: my_namespace.my_collection.my_module.


Ansible for Kubernetes

Ansible is not a Kubernetes-native tool like Helm, but it can:

  • Apply manifests.
  • Manage cluster resources (deployments, services, configmaps).
  • Orchestrate around Kubernetes (create infra, update DNS, deploy apps).

Use kubernetes.core collection:

- hosts: localhost
  gather_facts: false
  collections:
    - kubernetes.core

  tasks:
    - name: Apply manifest
      k8s:
        state: present
        src: k8s/deployment.yml

Common patterns:

  • Use Ansible to deploy Helm charts with community.kubernetes.helm.
  • Use Ansible for environment bootstrapping and let Kubernetes handle the inner app lifecycle.

Security Hardening and Compliance

Ansible is great for repeatable hardening:

  • Apply CIS Benchmarks, STIGs, etc.
  • Ensure consistent OS baseline across fleets.

Typical approach:

  • Use existing hardening roles like devsec.hardening.
  • Customize vars for your baseline.
  • Run in audit mode first (when supported), then enforce.

Example:

- hosts: linux
  become: true
  roles:
    - devsec.hardening.os_hardening

Combine with:

  • ansible-playbook --check for dry-run.
  • Reports from custom callback plugins or CI artifacts.

Common Architecture Patterns and Real Use Cases

1. Golden Image + Ansible

  • Build a base image (Packer) with OS + basic tools.
  • Use Ansible to apply environment-specific config on boot.

2. Immutable Infrastructure with Ansible

  • Use Terraform to create instances.
  • Use Ansible to configure and deploy.
  • Destroy and recreate instead of modifying in place.

3. Simple App Deployment

  • Role for nginx as reverse proxy.
  • Role for app (deploy artifact, configure systemd).
  • Pipeline triggers Ansible to roll out new version.

4. Multi-environment Layout

inventories/
  dev/
    hosts.yml
    group_vars/
  stage/
  prod/

roles/
  nginx/
  app/
playbooks/
  site.yml

site.yml selects roles; inventory selects environment.


Common Gotchas and Misconceptions

  • “Ansible is only for configuration” – not true. It also does orchestration and some provisioning.
  • Mixing shell for everything instead of using modules – leads to fragile, non-idempotent setups.
  • Forgetting become: true and wondering why changes fail.
  • Relying on facts but disabling gather_facts without manually calling setup.
  • Using command to edit config files instead of template / lineinfile / blockinfile.

Example:

- name: Ensure facts available explicitly
  setup:

End-to-End Example: Provision and Deploy a Simple App

Goal: configure a web server that serves a simple app behind nginx.

Inventory

inventory.ini:

[web]
web1.example.com ansible_user=ubuntu

Role: nginx

roles/nginx/tasks/main.yml:

- name: Install nginx
  apt:
    name: nginx
    state: present
    update_cache: true

- name: Deploy nginx config
  template:
    src: nginx.conf.j2
    dest: /etc/nginx/sites-available/app.conf
  notify: Reload nginx

- name: Enable site
  file:
    src: /etc/nginx/sites-available/app.conf
    dest: /etc/nginx/sites-enabled/app.conf
    state: link
  notify: Reload nginx

- name: Ensure default site disabled
  file:
    path: /etc/nginx/sites-enabled/default
    state: absent
  notify: Reload nginx

roles/nginx/handlers/main.yml:

- name: Reload nginx
  systemd:
    name: nginx
    state: reloaded

roles/nginx/templates/nginx.conf.j2:

server {
  listen 80;
  server_name {{ inventory_hostname }};

  location / {
    proxy_pass http://127.0.0.1:{{ app_port }};
  }
}

Role: app

roles/app/tasks/main.yml:

- name: Ensure app directory
  file:
    path: /opt/app
    state: directory
    owner: "{{ app_user }}"
    group: "{{ app_user }}"
    mode: "0755"

- name: Deploy app script
  copy:
    src: app.py
    dest: /opt/app/app.py
    owner: "{{ app_user }}"
    group: "{{ app_user }}"
    mode: "0755"

- name: Deploy systemd service
  template:
    src: app.service.j2
    dest: /etc/systemd/system/app.service
  notify: Restart app

- name: Ensure app running
  systemd:
    name: app
    enabled: true
    state: started

- name: Reload systemd daemon
  systemd:
    daemon_reload: true
  when: ansible_service_mgr == "systemd"

roles/app/handlers/main.yml:

- name: Restart app
  systemd:
    name: app
    state: restarted

Playbook

site.yml:

- hosts: web
  become: true

  vars:
    app_user: deploy
    app_port: 5000

  roles:
    - nginx
    - app

Run:

ansible-playbook -i inventory.ini site.yml

You now have:

  • App running on localhost:5000.
  • Nginx proxying from port 80 to the app.

Testing: Molecule and Other Strategies

Molecule helps you test roles in isolation.

Initialize role with Molecule:

molecule init role myrole -d docker
cd myrole
molecule test

molecule/default/molecule.yml describes how to create instances (Docker, EC2, etc.) and run tests.

Typical flow:

  1. molecule converge: apply role to test instance.
  2. molecule verify: run tests (often via testinfra or similar).
  3. molecule destroy: tear down.

Combine this with CI to ensure your roles remain stable over time.


Terraform, Other Tools, and When Not to Use Ansible

Ansible is strong at config management and orchestration, weak at complex stateful infrastructure planning.

Good fits for Ansible:

  • Configuring running servers.
  • Application deployments.
  • Rolling out OS patches.
  • Simple provisioning and glue tasks.

Better handled by other tools:

  • Complex infra graph (VPCs, subnets, peering, complex dependencies) → Terraform, CloudFormation, Pulumi.
  • Kubernetes-native app lifecycle → Helm, ArgoCD, Flux.
  • Continuous sync of desired state → GitOps tools.

Hybrid pattern (common and sane):

  • Terraform builds infrastructure (instances, networking, security groups).
  • Ansible configures OS and deploys applications.

Final Cheat Sheet

Quick reference of useful commands and concepts.

CLI

# Ad-hoc ping
ansible all -m ping -i inventory.ini

# Run a playbook
ansible-playbook -i inventory.ini site.yml

# Check syntax only
ansible-playbook site.yml --syntax-check

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

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

# Show inventory graph
ansible-inventory -i inventory.yml --graph

Files and Layout

project/
  ansible.cfg
  inventory/
    dev/
    prod/
  group_vars/
  host_vars/
  roles/
    nginx/
    app/
  playbooks/
    site.yml

Config (ansible.cfg)

[defaults]
inventory = inventory
remote_user = ubuntu
host_key_checking = False
retry_files_enabled = False
forks = 20

[ssh_connection]
pipelining = True

Vault

ansible-vault create secrets.yml
ansible-vault edit secrets.yml
ansible-playbook site.yml --ask-vault-pass

That’s the big-picture Ansible reference: from “what is this thing” all the way to writing your own modules and wiring it into CI/CD.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top