Ansible in CI/CD: Vault Passwords, Check Mode Gates, and Molecule Before Merge
Run Ansible in Jenkins and GitHub Actions with vault secrets, --check mode dry-run gates, and Molecule testing.
20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.
- ✓Production DevOps experience
- ✓Deep understanding of the tool's internals
- ✓Experience debugging distributed systems
Ansible automates configuration management and application deployment in CI/CD pipelines by defining idempotent playbooks that run across environments. The key takeaway: use ansible-pull for agentless, pull-based deployments in dynamic infrastructures, and always test playbooks with --check and --diff to prevent drift and failures.
Ansible in CI/CD means integrating Ansible playbook execution into automated pipelines (Jenkins, GitHub Actions, GitLab CI, etc.) to provision, configure, or deploy infrastructure as part of a software delivery process. It transforms ad-hoc automation into a repeatable, auditable, and gated deployment mechanism.
In the Ansible ecosystem, CI/CD pipelines act as the control plane. They handle secrets injection (vault passwords, API tokens), host selection (inventory limits), and execution modes (check vs. live). They also enforce quality gates like syntax checks, linting, and Molecule testing before playbooks ever touch production.
The problem CI/CD solves for Ansible is consistency and safety. Without a pipeline, engineers run playbooks from their laptops with varying environments, secrets may be exposed, and there's no audit trail. CI/CD forces every run through the same process, with the same variables, and logs every execution.
Imagine you're a chef with a recipe book for setting up a restaurant kitchen. You have secret ingredients (like vault passwords) that only you know. Now, you want to let junior chefs use your recipes, but you don't want them to see the secret ingredients or accidentally set the kitchen on fire. Ansible in CI/CD is like having a automated assistant that: (1) hides the secret ingredients in a locked safe (CI secrets), (2) does a 'pretend cook' first to check if anything would burn (--check mode), and (3) tests the recipe on a small countertop (Molecule) before the real kitchen. The assistant also ensures the junior chefs only work on specific stations (--limit) and not the whole kitchen.
A few years ago, I was on-call for a critical infrastructure deployment. A junior engineer had merged a PR that ran an Ansible playbook against all production hosts instead of the intended canary group. The playbook accidentally restarted a database service across 200 servers. The --limit flag was missing in the CI pipeline. We learned the hard way that CI/CD for Ansible needs explicit host scoping and dry-run gates.
Historically, Ansible was run ad-hoc from laptops. As teams grew, they needed repeatable, auditable deployments. CI/CD pipelines became the standard way to run playbooks, but they introduced new failure modes: secrets leaking, running against wrong hosts, and no validation before merge.
This article covers how to run Ansible in Jenkins Declarative Pipelines and GitHub Actions safely. You'll learn to store vault passwords in CI secrets, limit hosts, use --check mode as a gate, and test with Molecule before merge. These patterns come from real production incidents and have saved my teams countless times.
We'll dive into specific code examples for Jenkinsfile and GitHub Actions workflows, with exact flags and secret injection methods. By the end, you'll have a production-grade CI/CD pipeline for Ansible that prevents the most common disasters.
Jenkins Declarative Pipeline: Ansible with Vault Secrets
In Jenkins, use the Declarative Pipeline syntax with a sh step to run ansible-playbook. Store vault passwords as Jenkins Credentials (Secret text) and inject them via withCredentials.
``groovy pipeline { agent any environment { ANSIBLE_HOST_KEY_CHECKING = 'False' ANSIBLE_VAULT_PASSWORD_FILE = "${WORKSPACE}/vault-pass" } stages { stage('Create vault password file') { steps { withCredentials([string(credentialsId: 'ansible-vault-password', variable: 'VAULT_PASS')]) { sh 'echo "$VAULT_PASS" > ${ANSIBLE_VAULT_PASSWORD_FILE}' } } } stage('Run Ansible') { steps { sh 'ansible-playbook -i inventory/production.yml site.yml --limit "${HOSTS}" --vault-password-file ${ANSIBLE_VAULT_PASSWORD_FILE}' } } } post { always { sh 'rm -f ${ANSIBLE_VAULT_PASSWORD_FILE}' } } } ``
Gotcha: Never echo the vault password into the pipeline log. Use withCredentials to mask it. Also, clean up the password file in post.always.
For multiple vault IDs, use --vault-id: `` ansible-playbook --vault-id dev@${ANSIBLE_VAULT_PASSWORD_FILE} --vault-id prod@${ANSIBLE_VAULT_PASSWORD_FILE2} ``
pipeline {
agent any
environment {
VAULT_PASS = credentials('ansible-vault-password')
}
stages {
stage('Deploy') {
steps {
withVault([vaultSecrets: [[path: 'secret/ansible', engineVersion: 2]]]) {
sh '''
ansible-playbook -i inventory/prod.yml \
--vault-password-file <(echo "$VAULT_PASS") \
site.yml
'''
}
}
}
}
}chmod 600 ${ANSIBLE_VAULT_PASSWORD_FILE}. Otherwise, other processes on the same agent could read it.cat on that file and the password leaked into logs. We now always use chmod 600 and delete the file immediately after use.GitHub Actions: ansible-playbook-action and Secrets
GitHub Actions has a community action ansible/ansible-playbook-action that simplifies running playbooks. Store vault passwords as repository secrets and pass them via env.
``yaml name: Ansible Deploy on: push: branches: [ main ] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Run Ansible playbook uses: ansible/ansible-playbook-action@v1 with: playbook: site.yml inventory: inventory/production.yml limit: '${{ vars.HOSTS }}' vault_password: '${{ secrets.ANSIBLE_VAULT_PASSWORD }}' options: | --check --diff ``
Important: The action writes the vault password to a temporary file and passes it via --vault-password-file. It also supports vault_password_file input if you have a file.
If you need more control, use a raw run step: ``yaml - name: Run Ansible env: ANSIBLE_VAULT_PASSWORD_FILE: /tmp/vault-pass run: | echo '${{ secrets.ANSIBLE_VAULT_PASSWORD }}' > $ANSIBLE_VAULT_PASSWORD_FILE chmod 600 $ANSIBLE_VAULT_PASSWORD_FILE ansible-playbook -i inventory/production.yml site.yml --limit "${{ vars.HOSTS }}" ``
Gotcha: The ansible/ansible-playbook-action does not support multiple vault IDs natively. Use the raw run step with --vault-id for that.
name: Ansible Deploy on: [push] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Run Ansible playbook uses: dawidd6/action-ansible-playbook@v2 with: playbook: site.yml inventory: inventory/prod.yml key: ${{ secrets.SSH_PRIVATE_KEY }} vault_password: ${{ secrets.VAULT_PASSWORD }} options: | --verbose
echo carefully.ACTIONS_STEP_DEBUG: false in the workflow.ansible/ansible-playbook-action for simplicity, but switch to raw run for advanced needs like multiple vault IDs.Limiting Hosts in CI: The --limit Safety Net
The --limit flag is your best friend in CI. Always use it with a parameter or variable that defaults to a safe group (e.g., canary or staging). Never allow an empty limit.
Jenkins example with input parameter: ``groovy parameters { string(name: 'HOSTS', defaultValue: 'canary', description: 'Host pattern for --limit') } stage('Validate HOSTS') { steps { sh ''' if [ -z "${HOSTS}" ]; then echo "ERROR: HOSTS parameter cannot be empty" exit 1 fi # Optional: validate against known groups ansible-inventory -i inventory/production.yml --list | jq -e '."'"${HOSTS}"'"' || { echo "ERROR: ${HOSTS} not found in inventory" exit 1 } ''' } } ``
GitHub Actions with environment variable: ``yaml env: HOSTS: ${{ github.event_name == 'pull_request' && 'staging' || 'canary' }} ``
Gotcha: Ansible's --limit accepts patterns like hostname, group:&group2, !group3. But in CI, avoid complex patterns that may be misinterpreted. Stick to simple hostnames or group names.
Multiple limits: Use --limit multiple times or comma-separated: --limit 'group1,group2'.
# Limit deployment to a specific host group in CI ansible-playbook -i inventory/prod.yml \ --limit web_servers \ --vault-password-file vault_pass \ site.yml
Using --check Mode as a Dry-Run Gate
A common pattern is to run --check --diff in a separate stage and fail the pipeline if it detects changes. This acts as a dry-run gate before actual deployment.
Jenkins example: ``groovy stage('Dry Run (Check Mode)') { steps { sh ''' ansible-playbook -i inventory/production.yml site.yml --limit "${HOSTS}" --check --diff \ --vault-password-file ${ANSIBLE_VAULT_PASSWORD_FILE} 2>&1 | tee check_output.log # Fail if any changes detected if grep -q 'changed=' check_output.log; then echo "ERROR: Check mode detected changes. Pipeline aborted." exit 1 fi ''' } } ``
GitHub Actions example: ``yaml - name: Check Mode run: | ansible-playbook -i inventory/production.yml site.yml --limit "${{ vars.HOSTS }}" --check --diff 2>&1 | tee check.log if grep -q 'changed=' check.log; then echo "Changes detected in check mode. Failing." exit 1 fi ``
Gotcha: --check mode is not perfect. Some modules (like command or shell) do not support check mode and will report changed even if they wouldn't change anything. Use check_mode: no on such tasks or handle them specially.
Alternative: Use --diff only, without --check, to see what would change, but that's riskier.
# Run check mode before actual deployment ansible-playbook -i inventory/prod.yml \ --check \ --vault-password-file vault_pass \ site.yml # If check passes, run without --check ansible-playbook -i inventory/prod.yml \ --vault-password-file vault_pass \ site.yml
command, shell, raw, script do not support check mode. They will always report changed in check mode. Consider using changed_when: false or check_mode: no for those tasks.command to restart a service. In check mode, it always reported changed, causing the gate to fail. We added changed_when: false to that task and used a separate handler for restarts.--check --diff as a gate, but be aware of modules that don't support check mode. Adjust tasks with changed_when or check_mode: no.Testing with Molecule Before Merge
Molecule is the standard testing framework for Ansible roles. Run it in CI on every PR to validate playbooks against Docker containers or VMs before merge.
Molecule scenario example (molecule/default/molecule.yml): ``yaml --- provisioner: name: ansible playbooks: converge: converge.yml platforms: - name: instance image: geerlingguy/docker-ubuntu2204-ansible:latest pre_build_image: true verifier: name: ansible ``
GitHub Actions workflow for Molecule: ``yaml name: Molecule Test on: [pull_request] jobs: molecule: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.11' - name: Install dependencies run: | pip install molecule molecule-plugins[docker] ansible-lint - name: Run Molecule run: molecule test env: ANSIBLE_VAULT_PASSWORD_FILE: /dev/null # or provide a test vault password ``
Jenkins pipeline for Molecule: ``groovy stage('Molecule Test') { steps { sh ''' pip install molecule molecule-plugins[docker] ansible-lint molecule test ''' } } ``
Gotcha: Molecule requires Docker or Vagrant. Ensure the CI runner has Docker installed and the user has permissions. Use molecule docker driver for most CI environments.
---
dependency:
name: galaxy
driver:
name: docker
platforms:
- name: instance
image: geerlingguy/docker-ubuntu2204-ansible:latest
pre_build_image: true
provisioner:
name: ansible
playbooks:
converge: converge.yml
verifier:
name: ansiblemolecule/ubuntu2204, molecule/centos9) to test against different OS versions. Run them in parallel in CI.molecule test in CI on every PR to catch playbook errors early. Use Docker-based scenarios for speed.Secrets Management: Vault Passwords and API Tokens
Beyond vault passwords, Ansible playbooks often need API tokens (e.g., for cloud modules). Inject them via CI secrets and use Ansible's environment or vars.
Jenkins example with multiple secrets: ``groovy withCredentials([ string(credentialsId: 'ansible-vault-password', variable: 'VAULT_PASS'), string(credentialsId: 'aws-access-key', variable: 'AWS_ACCESS_KEY_ID'), string(credentialsId: 'aws-secret-key', variable: 'AWS_SECRET_ACCESS_KEY') ]) { sh ''' ansible-playbook -i inventory/production.yml site.yml \ --vault-password-file <(echo "$VAULT_PASS") \ -e aws_access_key="$AWS_ACCESS_KEY_ID" \ -e aws_secret_key="$AWS_SECRET_ACCESS_KEY" ''' } ``
GitHub Actions example: ``yaml - name: Run Ansible env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} run: | ansible-playbook -i inventory/production.yml site.yml \ --vault-password-file <(echo "${{ secrets.ANSIBLE_VAULT_PASSWORD }}") \ -e aws_access_key="$AWS_ACCESS_KEY_ID" \ -e aws_secret_key="$AWS_SECRET_ACCESS_KEY" ``
Gotcha: Using process substitution <() may not work in all shells (e.g., some CI environments use sh instead of bash). Use a temp file instead for portability.
# Encrypt a variable file with ansible-vault ansible-vault encrypt group_vars/all/vault.yml # Use vault password from file ansible-playbook -i inventory/prod.yml \ --vault-password-file vault_pass \ site.yml
sh (dash) which doesn't support <(). Use a temp file: echo "$VAULT_PASS" > /tmp/vault-pass && chmod 600 /tmp/vault-pass.sh. We switched to a temp file and it worked consistently.Syntax Checks and Linting as Early Gates
Before running any playbook, validate syntax and style. Add these as separate pipeline stages to fail fast.
Jenkins stages: ``groovy stage('Syntax Check') { steps { sh 'ansible-playbook --syntax-check -i inventory/staging.yml site.yml' } } stage('Lint') { steps { sh 'ansible-lint site.yml' } } ``
GitHub Actions steps: ``yaml - name: Syntax Check run: ansible-playbook --syntax-check -i inventory/staging.yml site.yml - name: Lint run: ansible-lint site.yml ``
ansible-lint configuration (.ansible-lint): ``yaml --- exclude_paths: - .git/ - molecule/ parseable: true quiet: true ``
Gotcha: ansible-lint may have different rules across versions. Pin the version in requirements.txt or use a container image.
# Syntax check and linting in CI ansible-playbook --syntax-check site.yml ansible-lint site.yml
ansible-lint on all YAML files, not just playbooks. Use ansible-lint . to check roles, tasks, and handlers.--syntax-check and ansible-lint as early pipeline stages to catch errors before any execution.Handling Multiple Environments in CI
CI pipelines often deploy to multiple environments (dev, staging, prod). Use environment-specific inventories and variables.
Jenkins with environment matrix: ``groovy pipeline { parameters { choice(name: 'ENV', choices: ['dev', 'staging', 'prod'], description: 'Target environment') } stages { stage('Deploy') { steps { sh "ansible-playbook -i inventory/${ENV}.yml site.yml --limit '${HOSTS}'" } } } } ``
GitHub Actions with environment-specific secrets: ``yaml jobs: deploy: runs-on: ubuntu-latest environment: ${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }} steps: - uses: actions/checkout@v4 - name: Run Ansible run: ansible-playbook -i inventory/${{ vars.ENV }}.yml site.yml ``
Gotcha: Environment-specific secrets in GitHub Actions require the environment to be defined in the repository settings. Use environment: to scope secrets.
# Deploy to staging first, then production ansible-playbook -i inventory/staging.yml --vault-password-file vault_pass site.yml # If staging succeeds, deploy to production ansible-playbook -i inventory/prod.yml --vault-password-file vault_pass site.yml
inventory/dev.yml, inventory/prod.yml) with appropriate host groups and variables.Error Handling and Rollback Strategies
Ansible playbooks can fail mid-deployment. Implement error handling and rollback in CI.
Jenkins with try-catch: ``groovy stage('Deploy') { steps { script { try { sh 'ansible-playbook site.yml --limit "${HOSTS}"' } catch (Exception e) { echo "Deployment failed, initiating rollback..." sh 'ansible-playbook rollback.yml --limit "${HOSTS}"' throw e } } } } ``
GitHub Actions with rollback job: ``yaml jobs: deploy: runs-on: ubuntu-latest steps: - run: ansible-playbook site.yml --limit "${{ vars.HOSTS }}" rollback: if: ``failure() needs: deploy runs-on: ubuntu-latest steps: - run: ansible-playbook rollback.yml --limit "${{ vars.HOSTS }}"
Gotcha: Rollback playbooks must be idempotent and tested. They should restore previous state, not just reverse changes.
--- - name: Deploy with rollback hosts: web tasks: - name: Backup current deployment ansible.builtin.archive: path: /var/www/app dest: /var/backups/app_{{ ansible_date_time.epoch }}.tar.gz - name: Deploy new version ansible.builtin.copy: src: /tmp/new_app/ dest: /var/www/app/ notify: rollback handlers: - name: rollback ansible.builtin.shell: | rm -rf /var/www/app tar -xzf /var/backups/app_{{ ansible_date_time.epoch }}.tar.gz -C /var/www/
Parallel Execution and Idempotency
Running Ansible on multiple hosts in parallel speeds up deployments, but requires idempotent playbooks.
Jenkins with forks: ``groovy sh 'ansible-playbook -i inventory/production.yml site.yml --forks 10 --limit "${HOSTS}"' ``
GitHub Actions with forks: ``yaml run: ansible-playbook -i inventory/production.yml site.yml --forks 10 --limit "${{ vars.HOSTS }}" ``
Idempotency check: Use --diff to verify no changes on second run. `` ansible-playbook site.yml --check --diff ``
Gotcha: Parallel execution can cause race conditions if tasks modify shared resources (e.g., a database). Use serial or throttle in playbook to limit concurrency.
# Run playbook in parallel with forks ansible-playbook -i inventory/prod.yml \ --forks 10 \ --vault-password-file vault_pass \ site.yml # Check idempotency by running twice ansible-playbook -i inventory/prod.yml --vault-password-file vault_pass site.yml ansible-playbook -i inventory/prod.yml --vault-password-file vault_pass site.yml | grep -q "changed=0" && echo "Idempotent"
ok=... changed=0. If not, fix tasks that are not idempotent.file module, but the second run always showed 'changed' because of a missing state: directory. Idempotency testing caught it.--forks for speed, but ensure playbooks are idempotent. Test by running twice.Audit Logging and Notifications
CI pipelines should log all Ansible runs and notify teams on failures.
Jenkins with Slack notification: ``groovy post { failure { slackSend( channel: '#infra-alerts', color: 'danger', message: "Ansible deployment failed: ${env.JOB_NAME} ${env.BUILD_NUMBER} (<${env.BUILD_URL}|Open>)" ) } success { slackSend( channel: '#infra-deployments', color: 'good', message: "Ansible deployment succeeded: ${env.JOB_NAME} ${env.BUILD_NUMBER}" ) } } ``
GitHub Actions with Slack notification using action: ``yaml - name: Notify Slack if: ``always() uses: slackapi/slack-github-action@v1.24.0 with: payload: | { "channel": "#infra-alerts", "text": "Ansible deployment ${{ job.status }}" } env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
Store Ansible logs as artifacts: ``yaml - name: Upload logs uses: actions/upload-artifact@v4 with: name: ansible-logs path: /tmp/ansible.log ``
--- - name: Deploy with audit logging hosts: web tasks: - name: Log deployment start ansible.builtin.uri: url: https://logs.example.com/deploy method: POST body: '{"status":"started","host":"{{ inventory_hostname }}"}' headers: Content-Type: application/json - name: Deploy app ansible.builtin.copy: src: /tmp/app/ dest: /var/www/app/ - name: Log deployment end ansible.builtin.uri: url: https://logs.example.com/deploy method: POST body: '{"status":"completed","host":"{{ inventory_hostname }}"}' headers: Content-Type: application/json
ansible.posix.log_plays.Advanced: Dynamic Inventory from CI Variables
Use CI environment variables to dynamically generate inventory. This is useful for ephemeral environments.
Jenkins with scripted inventory: ``groovy stage('Generate Inventory') { steps { sh ''' cat > inventory/dynamic.yml <<EOF all: hosts: ${HOSTNAME}: ansible_host: ${IP} EOF ''' } } stage('Run Ansible') { steps { sh 'ansible-playbook -i inventory/dynamic.yml site.yml' } } ``
GitHub Actions with dynamic inventory: ``yaml - name: Generate inventory run: | cat > inventory/dynamic.yml <<EOF all: hosts: ${{ vars.HOSTNAME }}: ansible_host: ${{ vars.IP }} EOF - name: Run playbook run: ansible-playbook -i inventory/dynamic.yml site.yml ``
Gotcha: Be careful with YAML injection if variables contain special characters. Use | to escape.
---
plugin: community.general.direct
hosts:
web:
hosts:
"{{ lookup('env', 'CI_HOST') }}":
ansible_user: "{{ lookup('env', 'CI_USER') }}"
ansible_ssh_private_key_file: "{{ lookup('env', 'CI_SSH_KEY') }}"ansible.builtin.template to generate inventory safely.template module to generate inventory from a Jinja2 template.add_host module instead of manual YAML generation.The Missing --limit Disaster
restart postgresql as a handler.HOSTS defaulted to the canary group, but it was empty, so Ansible targeted all hosts.--limit $HOSTS where HOSTS was an empty string because the input parameter wasn't required and defaulted to ''. Ansible treats empty --limit as no limit, so it ran on all hosts in inventory.HOSTS is empty: if [[ -z "$HOSTS" ]]; then echo 'ERROR: HOSTS cannot be empty'; exit 1; fi. Also added a required parameter with a default of 'canary'.- Always validate that --limit is non-empty and matches expected host patterns.
- Use parameter validation in CI and consider a default safe group like 'canary'.
--check mode was accidentally left enabled. Also verify the inventory file is correct and hosts match the limit. Run ansible-inventory --list to debug.--vault-password-file or ANSIBLE_VAULT_PASSWORD_FILE environment variable.requirements.yml is installed via ansible-galaxy collection install -r requirements.yml before running the playbook.molecule docker driver and ensure Docker is available. Alternatively, use molecule vagrant with Vagrant installed.echo $ANSIBLE_VAULT_PASSWORD_FILEansible-playbook --vault-password-file $ANSIBLE_VAULT_PASSWORD_FILE playbook.ymlecho "$HOSTS"ansible-playbook -i inventory playbook.yml --limit "$HOSTS"ansible-playbook --check --diff playbook.yml | grep -q 'changed=' && exit 1docker pull geerlingguy/docker-ubuntu2204-ansiblemolecule testpre_build_image: true in molecule.ymlansible-lint --versionansible-lint -c .ansible-lint playbook.yml| Feature | Jenkins Declarative Pipeline | GitHub Actions | Notes |
|---|---|---|---|
| Secret injection | withCredentials step | Secrets in env or inputs | Both support secrets; Jenkins requires plugin |
| Vault password | Write to file from secret | vault_password input or env | GitHub Actions has dedicated input |
| Host limiting | Parameter with validation | env or vars with validation | Both need explicit validation |
| Check mode gate | Manual grep in sh step | Manual grep in run step | Similar approach |
| Molecule testing | sh step with pip install | Steps with pip install | Identical |
| Dynamic inventory | Scripted generation | Run step generation | Both support inline generation |
| Notifications | Slack plugin | Slack action | Jenkins has richer plugin ecosystem |
| Cost | Self-hosted (infra cost) | Free for public repos | GitHub Actions has usage limits |
| File | Command / Code | Purpose |
|---|---|---|
| Jenkinsfile | pipeline { | Jenkins Declarative Pipeline |
| .github | name: Ansible Deploy | GitHub Actions |
| playbook.yml | ansible-playbook -i inventory/prod.yml \ | Limiting Hosts in CI |
| molecule | dependency: | Testing with Molecule Before Merge |
| playbook.yml | ansible-vault encrypt group_vars/all/vault.yml | Secrets Management |
| playbook.yml | ansible-playbook --syntax-check site.yml | Syntax Checks and Linting as Early Gates |
| playbook.yml | ansible-playbook -i inventory/staging.yml --vault-password-file vault_pass site.... | Handling Multiple Environments in CI |
| site.yml | - name: Deploy with rollback | Error Handling and Rollback Strategies |
| site.yml | - name: Deploy with audit logging | Audit Logging and Notifications |
| inventory | plugin: community.general.direct | Advanced |
Key takeaways
Interview Questions on This Topic
How would you securely pass an Ansible vault password in a Jenkins Declarative Pipeline?
withCredentials step with a 'Secret text' credential. Write the password to a temporary file with restricted permissions, then pass it via --vault-password-file. Clean up the file in the post.always block. Example: withCredentials([string(credentialsId: 'vault-pass', variable: 'VAULT')]) { sh 'echo $VAULT > /tmp/vault-pass && chmod 600 /tmp/vault-pass && ansible-playbook --vault-password-file /tmp/vault-pass site.yml' }.What is the purpose of `--check` mode in Ansible CI/CD pipelines?
--check mode runs the playbook in dry-run mode, showing what changes would be made without actually applying them. In CI/CD, it's used as a gate: if check mode detects changes, the pipeline can fail before the real deployment. This prevents unexpected changes from being applied. However, some modules (like command) do not support check mode and always report 'changed'.How do you limit Ansible to specific hosts in a CI pipeline?
--limit flag with a host pattern (hostname, group name, or expression). In CI, pass this as a parameter or environment variable. Always validate that the limit is non-empty and optionally check that the host/group exists in inventory. For example, in Jenkins: --limit "${HOSTS}" with a default of 'canary'.How would you test an Ansible role in CI before merging a PR?
molecule test. This validates syntax, idempotency, and functionality. The CI job should be triggered on pull requests and must pass before merge.What are the limitations of `--check` mode in Ansible?
command, shell, raw, and script do not support check mode and will always report 'changed'. Also, modules that modify external systems (e.g., cloud APIs) may still make changes if not properly implemented. Use check_mode: no on such tasks or handle them with changed_when: false.How do you handle multiple vault passwords in a CI pipeline?
--vault-id with multiple password files. Each vault ID can have its own password file. In CI, store each password as a separate secret and write to separate files. For example: --vault-id dev@/tmp/dev-pass --vault-id prod@/tmp/prod-pass.What is the difference between `ansible-playbook` and `ansible-pull` in CI/CD?
ansible-playbook pushes playbooks from a control node to managed nodes. ansible-pull runs on each managed node, pulling playbooks from a Git repository. In CI/CD, ansible-playbook is more common because CI acts as the control node. ansible-pull is used for self-updating systems or when you can't have a central control node.How do you ensure idempotency in Ansible playbooks?
command/shell unless necessary, use state parameters (e.g., state: present), and test by running the playbook twice in CI and verifying changed=0 on the second run.Frequently Asked Questions
Technically yes, but it's dangerous. Without --limit, Ansible runs on all hosts in the inventory, which could include production. Always use --limit with a validated non-empty value.
The ansible/ansible-playbook-action does not support multiple vault IDs. Use a raw run step with --vault-id and separate password files created from secrets.
Use Docker-based scenarios (fast), limit to one platform per PR, and run full matrix on main branch merge.
Use GitHub Environments. Define secrets per environment and use the environment: key in the job to scope which secrets are available.
Use Slack, email, or PagerDuty integrations. In Jenkins, use the Slack plugin. In GitHub Actions, use the slackapi/slack-github-action.
Yes, you can trigger job templates via API from CI. This offloads execution to Tower/AWX, which provides RBAC, logging, and scheduling.
Use `-v` (verbose) levels: -v for basic, -vvv for connection details. Store logs as artifacts and use grep to filter. Also use ANSIBLE_STDOUT_CALLBACK=unixy for cleaner output.
ansible-lint checks playbooks for best practices, syntax errors, and potential issues. It should run before any execution to catch problems early. Pin the version to avoid rule changes.
20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.
That's Ansible. Mark it forged?
6 min read · try the examples if you haven't