Ansible OEL 8 DevOps · OEL 8 · Advanced

AnsibleMulti-Environment Patterns

The directory layout that separates dev / staging / production cleanly, the per-environment vault password discipline, the promotion workflow that minimises human error, and the safety nets that prevent a staging change from ever hitting production by accident.

The single most powerful Ansible discipline is: one playbook, many inventories. The playbook code is identical between environments. What differs is the inventory file (different hosts) and the variables (different sizes, different secrets, different credentials). Promotion from staging to production is a one-line change to the -i flag.

Same playbook · 3 environments · separate vaults site.yml one playbook roles: common roles: mysql roles: monitoring SAME code tested first in dev promoted to higher envs -i dev/ hosts.ini vault_dev.yml staging/ hosts.ini vault_stg.yml production/ hosts.ini vault_prod.yml promote promote Dev fleet · 2 hosts throwaway · weekly rebuild Staging fleet · 6 hosts prod-shaped, smaller Production fleet · 50 hosts change manager approval Vault password rotates per environment · prod creds never on dev laptops
DIR — multi-environment layout
my-project/
├── ansible.cfg
├── site.yml                                # ONE playbook, all environments
├── roles/
│   ├── common/
│   ├── mysql/
│   └── monitoring/
├── inventories/
│   ├── dev/
│   │   ├── hosts.ini                       # 2 dev hosts
│   │   ├── group_vars/
│   │   │   ├── all.yml                     # dev-wide vars
│   │   │   ├── databases.yml
│   │   │   └── vault.yml                   # encrypted with dev vault password
│   │   └── host_vars/
│   ├── staging/
│   │   ├── hosts.ini                       # 6 staging hosts
│   │   ├── group_vars/
│   │   │   ├── all.yml
│   │   │   ├── databases.yml
│   │   │   └── vault.yml                   # encrypted with staging vault password
│   │   └── host_vars/
│   └── production/
│       ├── hosts.ini                       # 50 prod hosts
│       ├── group_vars/
│       │   ├── all.yml
│       │   ├── databases.yml
│       │   └── vault.yml                   # encrypted with prod vault password
│       └── host_vars/
└── playbooks/
    ├── deploy.yml
    ├── rolling_restart.yml
    └── snapshot.yml

Same variable name, different value per environment. Playbook code reads {{ mysql_max_connections }} — Ansible resolves it from whichever inventory you point at.

YAML — inventories/dev/group_vars/databases.yml
---
mysql_max_connections: 50
mysql_innodb_buffer_pool_size: "512M"
mysql_databases:
  - name: myapp_dev
backup_retention_days: 1
mysql_host_count: 1
YAML — inventories/staging/group_vars/databases.yml
---
mysql_max_connections: 200
mysql_innodb_buffer_pool_size: "2G"
mysql_databases:
  - name: myapp_staging
backup_retention_days: 7
mysql_host_count: 3
YAML — inventories/production/group_vars/databases.yml
---
mysql_max_connections: 800
mysql_innodb_buffer_pool_size: "16G"
mysql_databases:
  - name: myapp_prod
backup_retention_days: 30
mysql_host_count: 9
require_ssl: true
require_audit_log: true

Each environment gets its own vault password. A junior dev with the dev vault password CAN'T decrypt staging or production secrets even if they have the repo.

BASH — encrypt with environment-specific vault id
# Generate three vault passwords
openssl rand -base64 32 > ~/.ansible/vault_pass_dev
openssl rand -base64 32 > ~/.ansible/vault_pass_staging
openssl rand -base64 32 > ~/.ansible/vault_pass_prod
chmod 600 ~/.ansible/vault_pass_*

# Encrypt each environment's vault.yml with the matching password + id
ansible-vault encrypt --vault-id dev@~/.ansible/vault_pass_dev \
  inventories/dev/group_vars/vault.yml

ansible-vault encrypt --vault-id staging@~/.ansible/vault_pass_staging \
  inventories/staging/group_vars/vault.yml

ansible-vault encrypt --vault-id prod@~/.ansible/vault_pass_prod \
  inventories/production/group_vars/vault.yml

# Each file is tagged with which vault id it needs
head -1 inventories/dev/group_vars/vault.yml
# $ANSIBLE_VAULT;1.2;AES256;dev

head -1 inventories/production/group_vars/vault.yml
# $ANSIBLE_VAULT;1.2;AES256;prod
INI — ansible.cfg with all three vault ids registered
[defaults]
vault_identity_list = dev@~/.ansible/vault_pass_dev,
                      staging@~/.ansible/vault_pass_staging,
                      prod@~/.ansible/vault_pass_prod

# Ansible automatically picks the right vault password based on the file's tag
💡 Tip: Production vault password lives only in 1Password / HashiCorp Vault and CI secrets. Devs running locally use the staging or dev passwords. Prod is touched only by CI or by the on-call rotation, never from a laptop.
BASH — same playbook, three -i flags
# Dev (auto-rebuilds nightly, safe to break)
ansible-playbook -i inventories/dev/ site.yml

# Staging (prod-shaped, used for changes before prod)
ansible-playbook -i inventories/staging/ site.yml --check --diff

# Production (manual confirmation before every run)
ansible-playbook -i inventories/production/ site.yml \
  --check --diff                        # always preview first
ansible-playbook -i inventories/production/ site.yml \
  --vault-id prod@.vault_pass_prod      # then apply for real
StepActionApproval
1Developer writes playbook change, tests against devNone
2PR review by another engineer + CI passes1 reviewer
3Merge to main → auto-deploy to staging via CIAutomatic
4Soak in staging for 24h, monitoring dashboards greenTime-based
5Manual trigger of production deploy in CIChange manager
6Smoke tests run, observability dashboards watchedOn-call confirms

The single most expensive mistake is running a staging-or-dev intended change against production. Three safety nets that prevent it:

YAML — gate every play with an environment assertion
---
- hosts: all
  pre_tasks:
    - name: Refuse to run if env not declared
      ansible.builtin.assert:
        that:
          - target_env is defined
          - target_env in ["dev", "staging", "production"]
        fail_msg: "Set -e target_env=<dev|staging|production> when running"

    - name: Confirm production runs explicitly
      ansible.builtin.pause:
        prompt: "About to run against PRODUCTION. Confirm with 'yes' to continue"
      when: target_env == "production"
      register: confirmation

    - name: Abort if confirmation isn't 'yes'
      ansible.builtin.fail:
        msg: "Production run aborted by operator"
      when:
        - target_env == "production"
        - confirmation.user_input | lower != "yes"
BASH — wrap the playbook in a script that enforces the inventory
#!/usr/bin/env bash
# bin/deploy
set -euo pipefail

ENV="$1"
shift

case "$ENV" in
  dev|staging|production) ;;
  *) echo "Usage: $0 <dev|staging|production> [extra ansible args]"; exit 1 ;;
esac

if [[ "$ENV" == "production" ]]; then
  read -p "About to deploy to PRODUCTION. Type 'yes' to continue: " confirm
  [[ "$confirm" == "yes" ]] || { echo "Aborted."; exit 1; }
fi

exec ansible-playbook \
  -i "inventories/$ENV/" \
  -e "target_env=$ENV" \
  --vault-id "$ENV@~/.ansible/vault_pass_$ENV" \
  "$@" \
  site.yml

# Usage:
#   bin/deploy dev
#   bin/deploy staging --tags mysql
#   bin/deploy production --check --diff
⚠ Warning: Never let ansible.cfg's inventory = default to production. Always require -i at the CLI. A misconfigured shell environment that defaulted to production once destroyed a customer's staging-test data — they had to restore from backup.
YAML — .gitlab-ci.yml with environment promotion
---
stages:
  - test
  - deploy_staging
  - deploy_production

test:
  stage: test
  script:
    - ansible-lint .
    - ansible-playbook -i inventories/dev/ site.yml --syntax-check

deploy_staging:
  stage: deploy_staging
  environment: staging
  script:
    - echo "$VAULT_PASS_STAGING" > .vault_pass
    - ansible-playbook -i inventories/staging/ site.yml \
        --vault-id staging@.vault_pass
  after_script:
    - rm -f .vault_pass
  only:
    - main

deploy_production:
  stage: deploy_production
  environment: production
  when: manual                         # human approval required
  script:
    - echo "$VAULT_PASS_PROD" > .vault_pass
    - ansible-playbook -i inventories/production/ site.yml \
        --vault-id prod@.vault_pass
  after_script:
    - rm -f .vault_pass
  only:
    - main
✅ Tip: Multi-environment discipline turns 'I hope this works in prod' into 'we already ran this exact playbook 50 times in staging this week'. The same code path that runs daily in dev runs hourly in staging runs once-a-week in prod. No surprises.