Red Hat CoP Automation Good Practices

repository·main·Indexed 43 days ago

https://github.com/redhat-cop/automation-good-practices

A set of coding style best practices for Ansible automation development. Includes guidance on managing Ansible Automation Platform (AAP) configuration as code using the infra.aap_configuration collection, structuring CaC repositories, handling secrets, implementing CI/CD pipelines with GitHub Actions, and establishing quality gates for environment promotion.

Tokens
18.8K
Snippets
51
Records
76
Agent score
56%

What's inside Ansible Automation Good Practices

  1. Overview of Ansible Good Practices (GPA)

    main

    The automation-good-practices repository provides opinionated guidelines for Ansible practitioners to improve their automation journey. It covers a broad spectrum of topics ranging from initial structural decisions to operational lifecycle management. These are intended as 'good' practices rather than absolute 'best' practices, meaning they should be adapted to fit specific organizational needs and use cases.

    The guidelines are organized into four main parts:

    1. Part I -- Foundations: Overarching principles, structural decisions, and naming conventions.
    2. Part II -- Developing Ansible Content: Best practices for writing roles, collections, playbooks, inventories, plugins, and general coding style.
    3. Part III -- AAP and Configuration as Code: Managing Ansible Automation Platform (AAP) configuration declaratively via Git.
    4. Part IV -- Lifecycle and Operations: Git workflow, testing, CI/CD, promotion, and security.
  2. Name role parameters to avoid collisions

    main

    Since Ansible does not have namespaces, all role variables and arguments should be prefixed with the role name to prevent collisions in the global namespace.

    • Public arguments/defaults: Prefix with $ROLENAME_ (e.g., foo_packages instead of packages).
    • Module parameters: Prefix with $ROLENAME_ (e.g., foo_module).
    • Internal variables: Prefix with two underscores __ (e.g., __foo_variable). This clearly marks them as local to the role and not part of the public interface.
    • Tags: Prefix with the role name or a unique descriptive prefix.
    • Role names: Do not use dashes (-) in role names, as this causes issues with collections.
  3. Separate CI (Testing) from CD (Building/Releasing)

    main

    Maintain a clear boundary between Continuous Integration (CI) and Continuous Delivery (CD) to reduce the attack surface and simplify pipelines.

    |= | CI (Testing) | CD (Building/Releasing) | **Trigger | | Pre-commit validation | Build and publish collections | Tag creation | | Ansible-lint | Build execution environments | Merge to main | | Unit and Molecule tests | Create release manifests | Manual approval | | Security scanning | Promote between environments | Pipeline trigger | |=

  4. Adopt trunk-based development with a single main branch

    main

    Use a single main branch as the single source of truth for all automation content. Instead of using long-lived environment branches (like dev, qa, or prod), use short-lived feature branches that are merged into main via pull requests after CI validation and peer review. Environment promotion is managed through immutable Git tags and release manifests, not by moving code between branches.

    # Good - short-lived feature branch from main
    git checkout main && git pull
    git checkout -b feature/add-webserver-role
    
    # Develop, test locally, then push
    git push origin feature/add-webserver-role
  5. Use Git as the audit trail for configuration changes

    main

    Manage all configuration as code (GitOps) to provide a complete audit trail of who changed what, when, and why. This is essential for compliance frameworks like SOC 2, PCI DSS, and HIPAA. Manual UI changes in AAP bypass this audit trail.

    # Who made this change?
    git log --all --oneline -- group_vars/aap_prod/
    
    # What changed in the last 30 days?
    git log --since="30 days ago" --pretty=format:'%h|%an|%ad|%s' --date=iso
    
    # Full audit export for compliance
    git log --all --pretty=format:'%H|%an|%ae|%ad|%s' --date=iso > audit.csv
  6. Choose between SemVer and CalVer versioning strategies

    main

    Select a versioning strategy based on your audience and release cadence:

    • Semantic Versioning (SemVer) (MAJOR.MINOR.PATCH): Best for public collections or libraries where consumers need explicit compatibility signals (e.g., 2.0.0 signals a breaking change).
    • Calendar Versioning (CalVer) (YY.M.D-PATCH): Best for internal automation platforms where the deployment date is the most critical information (e.g., 26.1.6-0 for Jan 6, 2026). If using CalVer for Ansible collections, ensure it is PEP 440 compatible (no leading zeros in month or day).

    Apply the chosen strategy consistently across Git tags, collections, execution environments, and AAP project references.

    # CalVer examples
    26.1.6-0    # January 6, 2026 - Initial release
    26.1.6-1    # January 6, 2026 - Same-day hotfix
    
    # SemVer examples
    1.0.0       # Initial release
    1.1.0       # New feature, backward compatible
    2.0.0       # Breaking change
  7. Restrict usage of variable types and understand precedence

    main

    To avoid the complexity of Ansible's 22 levels of variable precedence, minimize the types of variables you use.

    Recommended Variable Usage:

    • Role defaults (defaults/main.yml): Low precedence; intended to be overwritten.
    • Inventory variables: Represent your desired state. These have predictable precedence (group before host).
    • Host facts: Represent the current state (As-Is). Should not collide with To-Be variables.
    • Role vars (vars/main.yml): Constants used by the role to separate code from data.
    • Scoped vars: Local to a block or task; used for temporary runtime logic.
    • Runtime vars (register, set_facts): Represent the current state of the automation execution.
    • Extra vars (--extra-vars): Highest precedence; use sparingly.

    Avoid:

    • Playbook/Play variables.
    • include_vars (blurs the line between code and data).
    • Excessive use of scoped variables unless required for runtime loops or complex nesting.
  8. Rely on inventory structures instead of manual host lists for looping

    main

    Avoid the anti-pattern of creating a variable containing a list of hosts and looping over it in a playbook. Instead, leverage the Ansible inventory structure.

    Why use the inventory for loops?

    1. Maintainability: Inventory structures are easier to oversee than manual lists.
    2. No Duplication: You avoid duplicating information needed for provisioning.
    3. Native Features: Using the inventory allows you to use Ansible's built-in parallelization, throttling, and the --limit parameter.
    4. Control: You can easily limit plays to specific groups or patterns using --limit.

    Example of the 'Good' approach: Define host-specific data in host_vars and group-specific data in group_vars, then target the hosts directly in the playbook.

  9. Use the infra.aap_configuration.dispatch role for ordered configuration

    main

    Instead of calling individual roles for different AAP objects, use the infra.aap_configuration.dispatch role. This role automatically handles the correct dependency order of operations, ensuring that parent objects (like Organizations) are created before child objects (like Job Templates) that reference them. It also handles idempotency and provides clear change output.

    The dispatch role applies configuration in this order:

    1. Organizations
    2. Credential Types
    3. Credentials
    4. Projects
    5. Inventories
    6. Job Templates
    7. Workflows
    8. Schedules
    # playbook.yml
    ---
    - name: Configure AAP
      hosts: aap_dev
      connection: local
      gather_facts: false
    
    tasks:
        - name: Apply AAP Configuration
          ansible.builtin.include_role:
            name: infra.aap_configuration.dispatch
  10. Implement provider support in roles

    main

    If a role supports multiple implementations of the same functionality, use a provider variable.

    1. Define an input variable: $ROLENAME_provider.
    2. If $ROLENAME_provider is not defined, the role should detect the existing provider on the system or select a default based on the OS version.
    3. Export a variable $ROLENAME_provider_os_default containing the recommended default for the current OS. This allows users to set provider: "{{ $ROLENAME_provider_os_default }}" to ensure consistency across different OS versions without manual selection.
  11. Use release manifests for deployment tracking

    main

    Create release manifests to provide a single source of truth for what is running in each environment. This enables deterministic rollbacks, auditability, and faster troubleshooting by recording exact Git SHAs, collection versions, and execution environment digests.

    # releases/release-26.1.5-0.yaml
    version: "26.1.5-0"
    created: "2025-01-05T10:00:00Z"
    
    components:
      aap_configuration:
        commit: "abc123def456..."
        tag: "26.1.5-0"
      collections:
        version: "26.1.5-0"
      execution_environment:
        tag: "26.1.5-0"
        digest: "sha256:fedcba..."
    
    environments:
      dev:
        deployed_at: "2025-01-05T09:00:00Z"
      qa:
        deployed_at: "2025-01-05T11:00:00Z"
        validated: true
      prod:
        deployed_at: "2025-01-05T15:00:00Z"
        approved_by: "CAB"