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
| Issue | Solution |
|---|---|
| Variable undefined | Check defaults/main.yml and variable precedence |
| Template error | Validate Jinja2 syntax with ansible-playbook --syntax-check |
| Permission denied | Verify become: true and sudo configuration |
| Handler not triggered | Ensure task reports changed status |
| Idempotency issues | Test with --check to verify no unexpected changes |
Best Practices
- Use roles for reusable, modular automation
- Parameterize everything — avoid hardcoded values
- Test in staging first — never deploy directly to production
- Use check mode for validation:
--check --diff - Document your playbooks with comments and README files
- Version control all playbooks and roles in Git
- Use tags for selective task execution
- 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.