Ansible Lesson 10 – Beginner Best Practices | Dataplexa
Section I · Lesson 10

Beginner Best Practices

In this lesson

Project structure Naming conventions Idempotency habits Version control Testing & linting

Beginner best practices are the habits, conventions, and structural decisions that make Ansible projects readable, maintainable, and trustworthy from the very first commit. They are not advanced topics reserved for experienced engineers — they are the foundation that prevents the most common and painful mistakes. Learning them before writing your first real playbook means you build good instincts from the start, rather than spending months unlearning bad ones. This lesson consolidates the most important principles from Section I into a practical checklist you can apply immediately.

The Cost of Skipping Best Practices

Best practices are not bureaucracy — they are the distilled lessons of teams who shipped automation that broke at 3am, who spent hours debugging a playbook nobody understood, or who accidentally ran a playbook against the wrong environment because their inventory was unclear. Every practice in this lesson exists because someone learned it the hard way.

Hours

debugging a playbook with no task names, no comments, and variables scattered everywhere

Incident

caused by running a non-idempotent playbook twice when a deployment failed halfway through

Minutes

for a new team member to understand a well-structured project with clear names and a README

Zero

anxiety running a playbook for the tenth time when you know it is idempotent and version controlled

Standard Project Structure

One of the most impactful decisions you make at the start of an Ansible project is how to organise your files. A consistent, predictable project structure means any engineer who has worked with Ansible before can navigate your project instantly — even if they have never seen it. Ansible does not enforce a specific layout, but the community has converged on a standard that scales from single-file projects to enterprise-wide automation platforms.

myproject/
├── ansible.cfg          # project-level config — committed to Git
├── inventory/
│   ├── production/
│   │   ├── hosts.ini    # production host list
│   │   └── group_vars/
│   │       ├── all.yml  # vars for all production hosts
│   │       └── webservers.yml
│   └── staging/
│       ├── hosts.ini    # staging host list (separate from production!)
│       └── group_vars/
├── site.yml             # master playbook — the entry point
├── playbooks/
│   ├── deploy.yml       # deployment playbook
│   └── hardening.yml    # security hardening playbook
├── roles/               # reusable roles (covered in Lessons 23–24)
│   └── nginx/
├── files/               # static files to copy to managed nodes
├── templates/           # Jinja2 templates (covered in Lesson 17)
└── README.md            # tells the next engineer how to use this project

What just happened?

This layout separates concerns cleanly: inventories live in their own directory and are split by environment so production and staging can never be confused; playbooks are named by function; roles are isolated and reusable; and a README explains the project to whoever comes next. Start every project with this structure — even if most directories start empty.

The Mise en Place Analogy

Professional chefs practise mise en place — everything in its place before cooking starts. Ingredients are prepped and labelled, tools are laid out in a consistent position, the workspace is clean. A well-structured Ansible project is the engineering equivalent: when everything has a predictable location and a clear name, you can work faster, make fewer mistakes, and hand off your work to anyone without explanation.

Naming and Clarity Conventions

The single most underrated best practice in Ansible is writing clear, descriptive task names. Every task in a playbook must have a name: field that reads like a plain English sentence describing what the task does and why. This is what you see in output during a run, what your colleagues read in a code review, and what appears in logs when a deployment fails.

Rule 1

Name every task — no exceptions

A task without a name: field shows as its module and arguments in output — unreadable at a glance and impossible to track in long playbook runs.

Rule 2

Use verb-noun format for task names

Task names should read like instructions: "Install Nginx web server", "Create deploy user", "Copy application config". Not: "nginx" or "package".

Rule 3

Use lowercase_with_underscores for variable names

Variable names should be lowercase and use underscores as separators: nginx_port, deploy_user, app_version. Never camelCase or UPPER_CASE for user-defined variables.

Rule 4

Name playbook files by function, not by server

Call playbooks deploy.yml, hardening.yml, monitoring.yml — not web01.yml or server.yml. Playbooks describe actions, not targets.

❌ Unclear task name
- name: nginx
  ansible.builtin.package:
    name: nginx
    state: present
✓ Clear task name
- name: Install Nginx web server
  ansible.builtin.package:
    name: nginx
    state: present

Idempotency Habits

Idempotency is Ansible's most important guarantee, but it is not automatic — it requires deliberate choices at the task level. These five habits protect idempotency and make your playbooks safe to re-run in any situation, including mid-deployment recovery scenarios.

Habit 1

Always use dedicated modules — never raw commands for things modules can do

ansible.builtin.package checks if a package is installed before installing it. ansible.builtin.shell running apt install does not. The module does the idempotency check for you — the shell command never does.

Habit 2

Declare state explicitly in every task

Always write state: present or state: started explicitly — never rely on defaults. Explicit state makes intent obvious to anyone reading the playbook and prevents subtle bugs when module defaults change between versions.

Habit 3

Run every playbook twice in development — the second run must report zero changes

This is the idempotency test. If the second run shows changed tasks, something is not idempotent. Fix it before merging. A playbook that cannot pass this test is not safe for production.

Habit 4

Use --check mode before running against production

ansible-playbook --check runs a dry run — Ansible reports what it would change without actually changing anything. Use it every time before applying automation to a production environment for the first time.

Habit 5

Guard non-idempotent tasks with creates or when

When you must use command or shell, add a guard condition. creates: /path/to/marker skips the task if the file already exists. when: condition skips it based on a variable or fact. Every non-idempotent task must have a guard.

Version Control and Secrets

Every Ansible project belongs in a Git repository. This is not optional. The moment your automation exists only on a laptop or a shared server, it is one disk failure away from being lost and one engineer departure away from being undocumented. Version control also gives you the single most important safety feature in automation: rollback.

Always commit to Git
ansible.cfg — project-level config
All playbooks and roles
Inventory files (hosts only, no secrets)
Variable files without sensitive values
README.md and any documentation
Never commit in plain text
Passwords and API keys
SSH private keys
Database credentials
TLS certificates and private keys
Any value that grants access to a system

Encrypt all sensitive values with Ansible Vault before committing — covered in Lesson 28. For now, add a .gitignore that excludes any unencrypted secrets file and use environment variables for values that are injected at CI/CD runtime.

Linting and Testing Your Automation

Playbooks are code — they deserve the same quality checks that application code gets. A small investment in tooling catches errors before they reach production and enforces the conventions your team has agreed on. These four tools form the standard quality pipeline for Ansible projects.

🔍

ansible-lint

Catches best-practice violations, deprecated syntax, and common mistakes in your playbooks and roles. Integrates into VS Code and CI pipelines. The first tool to add to any project.

Docs ↗
🧪

Molecule

A testing framework for Ansible roles. Spins up a temporary VM or container, runs your role against it, verifies the result, then destroys the environment. The standard for role testing in production teams.

Docs ↗

--check mode

Ansible's built-in dry run flag. Run ansible-playbook --check site.yml to see what would change without applying any changes. Use this as a pre-flight check before every production deployment.

Built-in
📋

yamllint

A YAML syntax linter that catches indentation errors, trailing spaces, and formatting inconsistencies before Ansible even sees the file. Fast, lightweight, and a natural companion to ansible-lint.

Docs ↗

Section I Recap

You have completed Section I — Ansible Fundamentals. Before moving into Section II's hands-on playbook work, here is a summary of every concept covered so far and where it fits in your mental model of Ansible.

Section I — what you now know

Lesson 1 What Ansible is — agentless, idempotent, declarative automation over SSH
Lesson 2 Configuration management — drift, snowflake servers, desired state, IaC
Lesson 3 Architecture — control node, managed nodes, inventory, modules, playbooks
Lesson 4 Control vs managed nodes — requirements, SSH, become, forks, Windows support
Lesson 5 Installation — apt / dnf / pip, SSH key setup, first ping test, common errors
Lesson 6 ansible.cfg — precedence order, essential directives, environment variable overrides
Lesson 7 Inventory — INI vs YAML, groups, children, host/group vars, dynamic inventory
Lesson 8 Ad-hoc commands — syntax, flags, host patterns, common modules, ad-hoc vs playbooks
Lesson 9 Modules — how they work, categories, essential 15, collections, ansible-doc
Lesson 10 Best practices — project structure, naming, idempotency habits, version control, linting

Never Run an Untested Playbook Directly Against Production

The most dangerous Ansible mistake beginners make is running a new playbook directly against production hosts because "it looks fine." Always run against a staging environment first, then use --check mode, then run with --limit against a single production host before rolling out to the full fleet. This sequence has prevented countless incidents — skipping any step of it is how incidents start.

Key Takeaways

Structure your project from day one — separate inventories by environment, name playbooks by function, and always include a README. A consistent layout takes minutes to set up and saves hours of confusion later.
Name every task with a clear verb-noun description — task names appear in output, logs, and code reviews. They are your free documentation layer and the fastest way to make a playbook readable to someone who did not write it.
Run every playbook twice in development — the second run must report zero changes. This is the idempotency test, and no playbook should merge without passing it.
Everything goes in Git — except unencrypted secrets — version control gives you rollback, history, peer review, and the ability to rebuild from scratch. Secrets go in Ansible Vault.
Add ansible-lint and yamllint to every project — they catch errors that reach production otherwise. Use --check mode as a pre-flight before any production run.

Teacher's Note

Before starting Section II, take 20 minutes to set up your project directory with the structure shown in this lesson, initialise a Git repository, and install ansible-lint — you will use all three in every lesson from here forward.

Practice Questions

1. What flag do you add to ansible-playbook to run a dry run that shows what would change without making any actual changes?



2. What tool catches best-practice violations and deprecated syntax in Ansible playbooks and roles?



3. You run a playbook for the second time in development to test idempotency. What should the play recap show?



Quiz

1. Why does the recommended project structure place production and staging inventories in separate directories?


2. You must use ansible.builtin.command for a task that has no dedicated module. How do you make it idempotent?


3. What is the correct sequence for safely deploying a new playbook to a production fleet for the first time?


Up Next · Lesson 11 — Section II

YAML Basics for Ansible

Section II starts now. Learn YAML — the language every Ansible playbook is written in — from syntax fundamentals to the patterns you will use every single day.