Ansible Course
Beginner Best Practices
In this lesson
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.
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.
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".
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.
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.
- name: nginx
ansible.builtin.package:
name: nginx
state: present
- 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.
ansible.cfg — project-level configREADME.md and any documentationEncrypt 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.
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.
--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.
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.
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
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
--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.