7. Ansible Playbooks, Roles and Advanced AutomationIntroduction and ContextInfrastructure as Code is not limited to creating servers, networks, or cloud resources. Once infrastructure exists, it must be configured, secured, updated, monitored, and eventually retired. Terraform is primarily concerned with provisioning and managing infrastructure state, whereas Ansible is commonly used to perform configuration management, application deployment, orchestration, and operational tasks. In a mature DevOps environment, the two tools are often complementary: Terraform creates or changes the target environment, and Ansible establishes the desired operating-system and application state within it. Ansible represents automation as readable YAML documents called playbooks. A playbook describes which hosts should be affected and which tasks should be executed. Its agentless architecture, reliance on standard remote protocols such as SSH and WinRM, and extensive module ecosystem make it suitable for heterogeneous environments. However, simple task lists are insufficient for large organisations. Sustainable automation requires careful use of handlers, templates, variables, loops, conditions, tags, blocks, error handling, roles, collections, and dynamic inventories. This chapter examines these capabilities as parts of one design problem: how to build automation that is repeatable, testable, secure, understandable, and adaptable to change. The central concern is not merely whether a playbook works once, but whether it remains trustworthy when executed repeatedly across development, testing, and production environments. Learning ObjectivesAfter studying this chapter, the learner should be able to: - Explain the structure and execution model of Ansible playbooks.
- Design tasks using modules, variables, facts, loops, conditions, tags, and handlers.
- Use templates, blocks, rescue sections, and always sections for controlled automation.
- Organise reusable automation through roles and collections.
- Select and configure static or dynamic inventories.
- Apply idempotence, privilege control, validation, testing, and secure secret management.
- Analyse an operational scenario and design an appropriate Ansible solution.
- Evaluate the organisational and governance implications of large-scale automation.
The Playbook as an Executable Operations ModelAn Ansible playbook is a YAML file containing one or more plays. A play maps a set of hosts to a sequence of tasks and may define variables, privilege escalation, handlers, tags, and execution controls. A minimal playbook is conceptually simple: ```yaml - name: Configure web servers
hosts: web become: true tasks: ansible.builtin.package: name: nginx state: present - name: Ensure Nginx is enabled and running
ansible.builtin.service: name: nginx enabled: true state: started ``` The first play targets the inventory group `web`. Each task invokes a module. Modules are the units that perform operations such as installing packages, managing files, creating users, configuring services, or interacting with cloud APIs. Fully qualified collection names, such as `ansible.builtin.package`, improve clarity and reduce ambiguity. Ansible normally gathers facts before executing tasks. Facts describe the target system, including its operating-system family, network interfaces, processor information, and memory. They can be referenced through variables such as `ansible_facts['os_family']`. Fact gathering is useful for cross-platform automation, although it can be disabled when performance or privacy considerations require it. A task should express a desired state rather than an imperative sequence whenever the selected module supports that approach. For example, `state: present` communicates that a package must exist. Re-running the task should not reinstall an already present package. This property is called idempotence. It is central to reliable configuration management because playbooks may be executed after failures, during scheduled reconciliation, or as part of continuous delivery. Variables, Precedence, and FactsVariables make playbooks adaptable. Values may be defined in inventory files, group variables, host variables, playbooks, roles, or supplied at runtime. Ansible applies a precedence system to determine which value is effective. The precise hierarchy is extensive, but a sound design principle is to keep ordinary configuration in appropriately scoped variable files and reserve high-precedence mechanisms, such as extra variables, for deliberate overrides. Variables should describe policy or environment-specific data, not conceal complex program logic. For example: ```yaml app_port: 8080 app_user: orders app_directories: - /opt/orders
- /var/log/orders
``` Sensitive values should not be placed in ordinary YAML files. Ansible Vault can encrypt variables or complete files, but encryption does not remove the need for access control, rotation, auditability, and careful handling of decrypted values in logs and temporary files. Handlers and Event-Driven ChangesHandlers are special tasks triggered by notifications. They are useful when a configuration change requires a service restart or reload: ```yaml - name: Deploy service configuration
ansible.builtin.template: src: orders.conf.j2 dest: /etc/orders.conf mode: '0644' notify: Reload orders service handlers: - name: Reload orders service
ansible.builtin.service: name: orders state: reloaded ``` A handler normally runs once at the end of a play, even if several tasks notify it. This prevents unnecessary repeated restarts. Handlers therefore express a useful distinction between changing a resource and responding to that change. The `meta: flush_handlers` action can force pending handlers to run at a controlled point, although frequent use may make execution harder to reason about. Building Expressive TasksTemplates and Configuration GenerationThe `template` module renders Jinja2 templates using variables and facts. Templates are more maintainable than embedding long configuration strings in tasks because presentation and control data remain separate. ```jinja2 server { listen {{ app_port }}; server_name {{ inventory_hostname }}; location / { proxy_pass http://127.0.0.1:{{ backend_port }}; } } ``` A production-quality template should have explicit file ownership and permissions, and its syntax should be validated before replacing a live configuration. Many modules provide a `validate` option; otherwise, a validation task can be executed before a handler reloads the service. Templates should also avoid exposing secrets through rendered files unless the file itself is appropriately protected. Loops and Data-Driven AutomationLoops eliminate repeated tasks and make the intended data model visible: ```yaml - name: Create application directories
ansible.builtin.file: path: "{{ item }}" state: directory owner: "{{ app_user }}" mode: '0755' loop: "{{ app_directories }}" ``` The variable `item` represents the current element. For dictionaries or structured data, `loop_control` can assign a meaningful label or variable. Excessively complicated loops, however, reduce readability. If a loop contains several unrelated conditions, a role task file or a data transformation may be more appropriate. Conditions and Platform VariationThe `when` keyword controls whether a task runs. Conditions should generally be written without Jinja2 delimiters: ```yaml - name: Install a Debian-specific package
ansible.builtin.apt: name: unattended-upgrades state: present update_cache: true when: ansible_facts['os_family'] == 'Debian' ``` Conditions are valuable for operating-system differences, optional components, feature flags, and safe rollout rules. They should not be used to disguise an unclear design. When platforms differ substantially, separate task files or role defaults may communicate the distinction better than a single task containing many conditions. Tags and Selective ExecutionTags allow operators to select a subset of a playbook, for example `--tags hardening` or `--skip-tags database`. They are useful for development, maintenance windows, and emergency operations. Tags must not become a substitute for dependency management. Running a tag in isolation should be safe only when its prerequisites are documented and satisfied. A tag taxonomy should be agreed upon at organisational level so that operators interpret tags consistently. Blocks, Failure Handling, and Controlled RecoveryA block groups tasks and applies shared conditions, privilege settings, or error handling. The `rescue` section runs if a task in the block fails, while `always` runs regardless of success or failure. ```yaml - name: Deploy and verify application
block: - name: Copy release artifact
ansible.builtin.copy: src: orders.tar.gz dest: /opt/orders/orders.tar.gz - name: Restart application
ansible.builtin.service: name: orders state: restarted rescue: - name: Record deployment failure
ansible.builtin.debug: msg: "Deployment failed on {{ inventory_hostname }}" always: - name: Remove temporary marker
ansible.builtin.file: path: /tmp/orders-deploying state: absent ``` Error handling requires an explicit operational policy. `ignore_errors: true` may allow a playbook to continue, but it can also conceal a serious failure. `failed_when` and `changed_when` allow a task’s result to be interpreted according to domain-specific rules. These controls should be narrowly scoped and documented. A system that silently continues after a security-hardening failure may be worse than one that stops visibly. For rolling changes, `serial` limits the number of hosts processed at once. `max_fail_percentage` can stop a rollout when the failure rate becomes unacceptable. `any_errors_fatal` can stop all hosts when a failure on one host invalidates the entire operation. These options turn a task sequence into a deployment strategy and should be selected according to application availability and consistency requirements. Roles as the Primary Unit of ReuseA role provides a conventional directory structure for reusable automation. A typical role contains: ```text roles/orders/ ├── defaults/main.yml ├── vars/main.yml ├── tasks/main.yml ├── handlers/main.yml ├── templates/ ├── files/ ├── meta/main.yml └── README.md ``` `defaults` contains low-precedence values intended for users to override. `vars` contains role-internal values that should rarely be overridden. `tasks/main.yml` is the role entry point, while `handlers`, `templates`, and `files` hold related implementation assets. `meta` can declare dependencies and metadata. Roles separate what a component does from the playbook that composes components. A playbook might include roles for common operating-system hardening, a reverse proxy, an application runtime, and a monitoring agent. This separation supports code review, independent testing, versioning, and reuse across teams. A good role has a narrow responsibility, sensible defaults, documented variables, predictable handlers, and minimal assumptions about its caller. It should not unexpectedly modify unrelated services or overwrite administrator-managed files. Role dependencies must be evaluated carefully: implicit chains can make execution difficult to understand and can introduce version conflicts. Collections and the Ansible EcosystemCollections package modules, plugins, roles, inventory plugins, and documentation under a namespace and name, such as `community.general` or a vendor-specific collection. They provide a distribution mechanism beyond the core Ansible package. A `requirements.yml` file can declare required collections and their versions: ```yaml collections: version: '>=8.0.0,<9.0.0' ``` Dependencies should be pinned or constrained through a controlled process. Unreviewed collection upgrades may change module behavior or introduce security concerns. Organisations should maintain an approved dependency catalogue, scan dependencies, and test automation against supported Ansible and collection versions. Inventory: From Static Lists to Dynamic TopologyInventory maps names to hosts and groups. A static inventory may be adequate for a small laboratory: ```ini [web] web01 ansible_host=192.0.2.10 web02 ansible_host=192.0.2.11 [db] db01 ansible_host=192.0.2.20 ``` In cloud and container environments, hosts are frequently created and destroyed. Dynamic inventory plugins query an external source, such as a cloud provider, virtualization platform, or configuration-management database, and construct groups from metadata. Grouping by tags such as `environment=production` and `role=web` enables playbooks to target logical services rather than fixed addresses. Dynamic inventory improves adaptability but creates dependencies on credentials, APIs, naming conventions, and source availability. It should be tested independently, cached where appropriate, and governed so that an incorrect cloud tag cannot accidentally place a host in a sensitive production group. Inventory is therefore not merely a list; it is an operational model of topology and responsibility. A Lifecycle for Maintainable AutomationA professional automation change normally passes through several stages. First, the desired state and scope are defined. Next, the author selects appropriate modules and models variables. The implementation is placed in a role or another reusable unit. Linting checks syntax, naming, and common errors. Molecule or equivalent tests can create ephemeral targets and verify convergence, idempotence, and behavior under failure. In a delivery pipeline, the change is reviewed, scanned, tested in a non-production environment, and promoted with an auditable approval process. Idempotence should be tested by running the automation twice and examining the second run. A well-designed playbook should report few or no changes on the second execution. Imperative shell commands are sometimes necessary, but they should be avoided when a dedicated module exists. If a command is used, `creates`, `removes`, `changed_when`, or a carefully designed check should communicate when a change actually occurred. Security is part of design rather than a later enhancement. Use least-privilege accounts, controlled privilege escalation, protected Vault keys, restrictive file modes, encrypted transport, and redaction with `no_log` where sensitive task output could appear. `no_log` should be targeted because excessive use reduces operational visibility. Playbooks should be reviewed for command injection, unsafe variable interpolation, insecure temporary files, and accidental disclosure in debugging output. Applied Case: Standardising a Multi-Tier Application PlatformBackgroundThe following is an illustrative case. A financial-services company operates a customer-order platform consisting of web servers, application servers, and database nodes. Terraform provisions virtual machines in two environments, but post-provisioning configuration is performed manually. Over time, servers differ in package versions, service settings, user permissions, and monitoring configuration. Situation and ProblemA security review identifies inconsistent SSH settings and missing audit agents. Application releases also require manual edits to reverse-proxy configuration. A failed release on one application server occasionally leaves the service stopped, while operators lack a consistent rollback procedure. The company needs repeatable automation without causing simultaneous outages across all nodes. AnalysisThe environment has three distinct concerns: common operating-system controls, tier-specific configuration, and release orchestration. A single large playbook would create excessive coupling. Static inventory would also become unreliable because Terraform replaces instances during scaling and recovery. The solution therefore requires roles, dynamic inventory, templates, handlers, and controlled rollout parameters. Approach and SolutionThe team creates four roles: `baseline`, `web_proxy`, `orders_app`, and `audit_agent`. The `baseline` role manages approved packages, users, SSH policy, time synchronisation, and file permissions. The proxy role renders configuration from environment variables and notifies a reload handler only when the template changes. The application role installs a versioned artifact, creates directories, manages a system service, and runs a health check. The audit role installs and configures the organisation’s monitoring component. A cloud inventory plugin groups instances by tags. The playbook applies baseline controls to all managed hosts, then applies tier-specific roles: ```yaml - name: Apply common controls
hosts: all become: true roles: - name: Configure application tier gradually
hosts: app serial: 1 become: true roles: ``` The application role uses a block with `rescue` to restore the previous artifact if the health check fails. A handler restarts the service only after a successful configuration change. Tags distinguish baseline, proxy, application, and audit operations, while CI executes linting, role tests, and a second-run idempotence check. OutcomeThe company obtains consistent configuration across newly provisioned and existing hosts. Rolling deployment limits the blast radius of an application failure. Dynamic inventory removes the need to update host addresses manually, and role documentation clarifies which variables are safe to override. The most important improvement is not merely faster deployment; it is the creation of an auditable, repeatable operational process. Learning PointsThe case demonstrates that reusable automation begins with modelling boundaries. Common controls belong in a common role, application behavior belongs in an application role, and rollout policy belongs in the playbook that composes those roles. Error handling, inventory design, testing, and governance must be considered together rather than added independently. Challenges, Limitations, and Common MistakesAnsible’s flexibility can create inconsistency. Common problems include placing too much logic in one playbook, using shell commands where modules exist, relying on undocumented variable precedence, storing secrets in source control, and treating `ignore_errors` as a general recovery mechanism. Another mistake is writing roles that assume a particular distribution, directory layout, or service manager without documenting that assumption. Performance can decline when fact gathering, serial execution, or highly granular tasks are used across thousands of hosts. Parallelism must be balanced against API rate limits, service capacity, and change risk. Network interruptions and partial execution are unavoidable in distributed systems; playbooks should therefore be safely re-runnable and should provide useful failure information. Ansible also has boundaries. It is not a replacement for a configuration database, a full deployment approval system, or specialised database migration tooling. It can orchestrate these systems, but responsibility for transactional guarantees and domain-specific correctness remains with the appropriate technology. Governance, Professional Practice, and Emerging DevelopmentsAt enterprise scale, automation is an organisational control. Changes should be versioned, reviewed, traceable to an approved requirement, and executed using controlled identities. Separation of duties may require different permissions for authoring, approving, and running production automation. Audit records should show the playbook version, target inventory, operator or pipeline identity, and result. Current practice increasingly combines Ansible with Git-based workflows, execution platforms, policy checks, and ephemeral test environments. Event-driven automation can respond to monitoring events, although automated remediation must be bounded to prevent loops and unintended escalation. Ansible content collections encourage sharing and standardisation, while automation analytics can reveal failure rates, duration, drift, and frequently changed components. AI-assisted code generation may accelerate drafting tasks, but generated automation still requires human review for idempotence, security, privilege boundaries, and operational consequences. Best-Practice Design Principles1. Express desired state with purpose-built modules whenever possible. 2. Design every change to be safely repeatable. 3. Keep roles cohesive, documented, and minimally coupled. 4. Use defaults for supported customisation and protect internal invariants. 5. Separate secrets from ordinary configuration and restrict their exposure. 6. Validate rendered configuration before reloading services. 7. Use handlers to avoid unnecessary restarts. 8. Make rollout scope explicit with `serial` and failure thresholds. 9. Test syntax, convergence, idempotence, failure paths, and supported platforms. 10. Treat inventory, dependencies, permissions, and auditability as part of the automation product. Chapter SummaryAnsible playbooks describe desired configuration and operational workflows through plays and tasks. Modules provide the main mechanism for idempotent change, while variables, facts, templates, loops, conditions, and tags make automation adaptable. Handlers connect configuration changes to controlled service actions. Blocks, rescue sections, and rollout controls provide structured failure management. Roles organise tasks, templates, handlers, and defaults into reusable units; collections distribute Ansible content and its dependencies. Static inventories suit stable environments, whereas dynamic inventory plugins represent changing cloud and virtualised topologies. Reliable enterprise automation depends on testing, secure secret handling, least privilege, dependency control, versioning, review, and auditability. The professional objective is not simply automation that executes, but automation that remains understandable and safe under repetition, failure, and organisational change. Key TermsIdempotence: The property that repeated execution produces the same desired state without unnecessary changes. Playbook: A YAML document containing one or more Ansible plays. Play: A mapping between target hosts and a sequence of tasks. Task: A named unit of work that invokes an Ansible module. Module: An Ansible component that performs a specific operation on a managed resource. Handler: A task triggered by notification, normally after ordinary tasks complete. Template: A Jinja2-based file containing variables and expressions for generated configuration. Fact: Information gathered about a managed host. Variable precedence: The rules determining which value is selected when variables have multiple definitions. Loop: A mechanism for applying a task to multiple data elements. Condition: A logical expression, commonly expressed with `when`, that controls task execution. Tag: A label used to select or exclude tasks during execution. Block: A group of tasks supporting shared control and error-handling sections. Role: A standardised, reusable directory structure for Ansible automation. Collection: A distributable package of Ansible modules, plugins, roles, and documentation. Inventory: The hosts and groups known to Ansible. Dynamic inventory: An inventory generated from an external system or service. Ansible Vault: An encryption mechanism for protecting sensitive Ansible data. Privilege escalation: The controlled execution of tasks with elevated permissions. Convergence: The process by which repeated automation moves a system toward its desired state. Review QuestionsShort Answer Questions1. What is the relationship between a play, a task, and a module? 2. Why are handlers preferable to unconditional service restarts? 3. Distinguish between role defaults and role variables. 4. What is the purpose of dynamic inventory? 5. How does idempotence improve operational reliability? Long Answer Questions1. Explain how templates, variables, facts, loops, and conditions work together in a cross-platform playbook. 2. Describe the structure of an Ansible role and explain how roles improve maintainability. 3. Discuss blocks, rescue sections, always sections, and rollout controls as mechanisms for handling failure. 4. Explain the security and governance controls required for enterprise Ansible automation. 5. Compare static and dynamic inventories, including their operational benefits and risks. Analytical / Application Questions1. Design an Ansible automation structure for a web, application, and database platform. Justify the roles, inventory groups, handlers, and rollout strategy you select. 2. A playbook reports success but leaves a service misconfigured after a template change. Analyse possible design faults and propose validation and error-handling improvements. 3. An organisation’s second playbook run still reports many changes and occasionally restarts every service. Diagnose likely causes and recommend a testing and redesign approach. 22. Multiple Choice Questions with AnswersQuestion 1A task installs a package with `state: present` and reports changed only when the package is absent. Which property does this primarily demonstrate? A. Dynamic inventory B. Idempotence C. Privilege separation D. Template inheritance Correct Answer: B. Idempotence Question 2Why is a handler generally preferred for restarting a service after a configuration change? A. It runs before all ordinary tasks B. It runs once after notifications, avoiding unnecessary repeated restarts C. It ignores failures automatically D. It encrypts the configuration file Correct Answer: B. It runs once after notifications, avoiding unnecessary repeated restarts Question 3Which design is most appropriate for a cloud environment where hosts are frequently replaced? A. A manually maintained list of IP addresses B. A dynamic inventory plugin using host metadata C. A separate playbook for every individual host D. Hard-coded hostnames inside templates Correct Answer: B. A dynamic inventory plugin using host metadata Question 4What is the main purpose of the `rescue` section in an Ansible block? A. To execute tasks when a task in the block fails B. To define role defaults C. To gather facts more quickly D. To force all tasks to run in parallel Correct Answer: A. To execute tasks when a task in the block fails Question 5A role contains a default value that users are expected to override for different environments. Where should that value normally be placed? A. `defaults/main.yml` B. `handlers/main.yml` C. `meta/main.yml` only D. The inventory plugin source code Correct Answer: A. `defaults/main.yml` Question 6Which approach best reduces the risk of replacing a live service configuration with invalid content? A. Always restart the service before rendering the template B. Validate the rendered configuration before notifying a reload handler C. Use `ignore_errors: true` on every task D. Put the configuration in a debug message Correct Answer: B. Validate the rendered configuration before notifying a reload handler Question 7An operator uses `ignore_errors: true` on a security-hardening task. What is the principal danger? A. The task will always execute twice B. A serious failure may be concealed while the play continues C. The inventory will become dynamic D. Handlers will be encrypted Correct Answer: B. A serious failure may be concealed while the play continues Question 8What is the strongest reason to use fully qualified collection names such as `ansible.builtin.package`? A. They make every task run with root privileges B. They identify the module’s namespace and reduce ambiguity C. They eliminate the need for testing D. They automatically create inventory groups Correct Answer: B. They identify the module’s namespace and reduce ambiguity Question 9A deployment must update one application host at a time and stop if failures exceed an acceptable threshold. Which controls are most relevant? A. `serial` and failure-threshold settings B. Only `debug` and `gather_facts` C. Only `loop` and `with_items` D. `defaults` and `templates` Correct Answer: A. `serial` and failure-threshold settings Question 10Which practice most directly tests whether an automation design is convergent? A. Running it once with maximum verbosity B. Running it twice and examining whether the second run makes unnecessary changes C. Replacing modules with shell commands D. Removing all handlers Correct Answer: B. Running it twice and examining whether the second run makes unnecessary changes
|