Ansible Multi-Environment — Dev Staging Production

Introduction

Managing Ansible inventory across dev, staging, and production environments requires a clear structure for hosts, groups, and environment-specific variables. This guide shows you how to organize your Ansible inventory to deploy consistently across multiple environments while keeping configurations separate and maintainable.

Overview

Effective multi-environment Ansible deployments depend on proper inventory organization. You'll learn to structure host groups by environment, manage environment-specific variables, validate deployments across environments, and implement best practices for consistency and reliability.

Inventory Structure

Define dev, staging, and production as separate groups in your inventory file, then keep each environment's variables in its own group_vars file:

[dev]
dev-web1.example.com
dev-web2.example.com

[staging]
staging-web1.example.com

[production]
prod-web1.example.com
prod-web2.example.com

[dev:vars]
environment=development

[staging:vars]
environment=staging

[production:vars]
environment=production
inventory/
├── hosts.ini
└── group_vars/
    ├── dev.yml
    ├── staging.yml
    └── production.yml

With this layout, ansible-playbook -i inventory/hosts.ini site.yml --limit staging targets only the staging hosts, while group_vars/<environment>.yml supplies the variables specific to that group.

Prerequisites

---
- name: Ensure prerequisites
  hosts: all
  become: true
  tasks:
    - name: Install required packages
      ansible.builtin.package:
        name:
          - python3
          - python3-pip
        state: present

Basic Implementation

---
- name: Dev Staging Production
  hosts: all
  become: true
  vars:
    app_name: myapp
    environment: production

  tasks:
    - name: Validate environment
      ansible.builtin.assert:
        that:
          - environment in ['development', 'staging', 'production']
        fail_msg: "Invalid environment: {{ environment }}"

    - name: Create directory structure
      ansible.builtin.file:
        path: "{{ item }}"
        state: directory
        mode: '0755'
      loop:
        - /opt/{{ app_name }}
        - /opt/{{ app_name }}/config
        - /opt/{{ app_name }}/data
        - /opt/{{ app_name }}/logs

    - name: Deploy configuration
      ansible.builtin.template:
        src: "config.j2"
        dest: "/opt/{{ app_name }}/config/main.yml"
        mode: '0644'
        validate: 'python3 -c "import yaml; yaml.safe_load(open(\"%s\"))"'
      notify: Restart application

Advanced Configuration

    - name: Configure with environment-specific values
      ansible.builtin.template:
        src: "{{ environment }}-config.j2"
        dest: "/opt/{{ app_name }}/config/env.yml"
        mode: '0644'
      vars:
        debug_mode: "{{ environment != 'production' }}"
        log_level: "{{ 'info' if environment == 'production' else 'debug' }}"

    - name: Set up monitoring
      ansible.builtin.template:
        src: monitoring.j2
        dest: "/opt/{{ app_name }}/config/monitoring.yml"
      when: monitoring_enabled | default(true)

Role Structure

roles/multi-environment/
├── defaults/
│   └── main.yml          # Default variables
├── handlers/
│   └── main.yml          # Service restart handlers
├── meta/
│   └── main.yml          # Role metadata and dependencies
├── tasks/
│   ├── main.yml          # Main task entry point
│   ├── install.yml       # Installation tasks
│   ├── configure.yml     # Configuration tasks
│   └── verify.yml        # Verification tasks
├── templates/
│   └── config.j2         # Jinja2 templates
├── tests/
│   └── test.yml          # Test playbook
└── vars/
    └── main.yml          # Role-specific variables

Error Handling

    - name: Deploy with rollback
      block:
        - name: Back up current configuration
          ansible.builtin.copy:
            src: "/opt/{{ app_name }}/config/"
            dest: "/opt/{{ app_name }}/config.bak/"
            remote_src: true

        - name: Apply new configuration
          ansible.builtin.template:
            src: config.j2
            dest: "/opt/{{ app_name }}/config/main.yml"

        - name: Verify new configuration
          ansible.builtin.command:
            cmd: "/opt/{{ app_name }}/bin/validate-config"
          changed_when: false

      rescue:
        - name: Rollback on failure
          ansible.builtin.copy:
            src: "/opt/{{ app_name }}/config.bak/"
            dest: "/opt/{{ app_name }}/config/"
            remote_src: true

        - name: Notify about failure
          ansible.builtin.debug:
            msg: "Deployment failed on {{ inventory_hostname }} — rolled back"

      always:
        - name: Clean up backup
          ansible.builtin.file:
            path: "/opt/{{ app_name }}/config.bak"
            state: absent

Handlers

  handlers:
    - name: Restart application
      ansible.builtin.systemd:
        name: "{{ app_name }}"
        state: restarted
        daemon_reload: true

    - name: Reload configuration
      ansible.builtin.systemd:
        name: "{{ app_name }}"
        state: reloaded

Testing

# Dry run
ansible-playbook site.yml --check --diff

# Limit to specific hosts
ansible-playbook site.yml --limit webservers

# Run specific tags
ansible-playbook site.yml --tags configure

# Verbose output for debugging
ansible-playbook site.yml -vvv

Troubleshooting

IssueSolution
Variable undefinedCheck defaults/main.yml and variable precedence
Template errorValidate Jinja2 syntax with ansible-playbook --syntax-check
Permission deniedVerify become: true and sudo configuration
Handler not triggeredEnsure task reports changed status
Idempotency issuesTest with --check to verify no unexpected changes

Best Practices

  1. Use roles for reusable, modular automation
  2. Parameterize everything — avoid hardcoded values
  3. Test in staging first — never deploy directly to production
  4. Use check mode for validation: --check --diff
  5. Document your playbooks with comments and README files
  6. Version control all playbooks and roles in Git
  7. Use tags for selective task execution
  8. Implement error handling with block/rescue/always

Conclusion

This guide covered the practical implementation of Ansible Multi-Environment. Start with the basic examples, customize for your environment, and gradually add error handling, monitoring, and CI/CD integration as your automation matures.