Intermediate 8 min · 2026-06-21

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.

N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.

Follow
Production
production tested
June 21, 2026
last updated
1,596
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer

Store vault passwords in CI secrets (e.g., Jenkins Credentials or GitHub Actions secrets) and pass via --vault-password-file or env var. Limit Ansible to specific hosts in CI using --limit with inventory hostnames or group names from env vars. Use --check --diff in a separate CI job stage as a dry-run gate; fail the pipeline if check mode reveals changes. Run Molecule scenarios (e.g., molecule test) in PR checks before merge to validate playbooks against Docker or Vagrant. In Jenkins Declarative Pipeline, use ansible-playbook inside a sh step; for GitHub Actions, use the ansible/ansible-playbook-action. Never hardcode vault passwords in playbooks or inventory; always inject via CI secrets and use --vault-id for multiple vaults. Ansible's --check mode skips tasks that modify state but still runs facts and conditionals; beware of side effects. Use ansible-lint and ansible-playbook --syntax-check as early pipeline gates to catch errors fast.

โœฆ Definition~90s read
What is Ansible in CI/CD Pipelines?

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.

โ˜…
Imagine you're a chef with a recipe book for setting up a restaurant kitchen.

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.

Plain-English First

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} ``

Vault Password File Permissions
Ensure the vault password file is readable only by the Jenkins user: chmod 600 ${ANSIBLE_VAULT_PASSWORD_FILE}. Otherwise, other processes on the same agent could read it.
Production Insight
We once had a pipeline that wrote the vault password to a world-readable temp file. A junior engineer ran a debug cat on that file and the password leaked into logs. We now always use chmod 600 and delete the file immediately after use.
Key Takeaway
Always use Jenkins Credentials Binding plugin to inject vault passwords and clean up the password file after the playbook runs.

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.

GitHub Actions Secret Masking
GitHub automatically masks secrets in logs, but be careful: if you copy the secret to a file and then cat it, the secret may appear in logs. Use echo carefully.
Production Insight
We had a pipeline where the vault password was accidentally printed because the action's debug mode was enabled. We now set ACTIONS_STEP_DEBUG: false in the workflow.
Key Takeaway
Use the 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'.

Default to Safe Group
Set the default value of HOSTS to a non-production group like 'staging' or 'canary'. Require explicit override for production deployments.
Production Insight
We once had a pipeline where the HOSTS variable was set to an empty string because the Jenkins parameter was optional. Ansible treated no limit as 'all hosts' and we accidentally deployed to production. Now we always validate non-empty and also check against inventory.
Key Takeaway
Always validate that --limit is non-empty and optionally verify the host/group exists in inventory before running the playbook.

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.

Check Mode Limitations
Modules like 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.
Production Insight
We had a playbook that used 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.
Key Takeaway
Use --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.

Molecule Scenarios for Different OS
Create multiple Molecule scenarios (e.g., molecule/ubuntu2204, molecule/centos9) to test against different OS versions. Run them in parallel in CI.
Production Insight
We once merged a role that worked on Ubuntu but broke on CentOS because of package name differences. Molecule testing with multiple scenarios caught it before production deployment.
Key Takeaway
Run 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.

Process Substitution in CI
Some CI runners use sh (dash) which doesn't support <(). Use a temp file: echo "$VAULT_PASS" > /tmp/vault-pass && chmod 600 /tmp/vault-pass.
Production Insight
We had a pipeline that used process substitution and it worked locally but failed in Jenkins because the default shell was sh. We switched to a temp file and it worked consistently.
Key Takeaway
Inject secrets via CI secret management and use temp files for vault passwords to avoid shell compatibility issues.

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.

Lint All Files
Run ansible-lint on all YAML files, not just playbooks. Use ansible-lint . to check roles, tasks, and handlers.
Production Insight
We once had a syntax error (missing colon) in a vars file that only showed up at runtime. Adding syntax check to CI caught it immediately.
Key Takeaway
Add --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.

Inventory per Environment
Keep separate inventory files for each environment (e.g., inventory/dev.yml, inventory/prod.yml) with appropriate host groups and variables.
Production Insight
We once used a single inventory with group_vars per environment, but a misconfiguration caused prod variables to leak into dev. Separate files are safer.
Key Takeaway
Use separate inventory files per environment and scope secrets accordingly.

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.

Rollback Complexity
Rollbacks are hard. Prefer blue-green deployments or canary releases over rollback playbooks. If you must rollback, test the rollback playbook as rigorously as the deploy playbook.
Production Insight
We had a rollback playbook that failed because it required a secret that was already rotated. Now we store previous secrets temporarily during deployment.
Key Takeaway
Implement rollback strategies in CI, but prefer safer deployment patterns like blue-green.

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.

Test Idempotency
Run the playbook twice in CI. The second run should produce ok=... changed=0. If not, fix tasks that are not idempotent.
Production Insight
We had a playbook that created a directory with file module, but the second run always showed 'changed' because of a missing state: directory. Idempotency testing caught it.
Key Takeaway
Use --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 ``

Centralized Logging
Forward Ansible logs to a centralized system (ELK, Splunk) for long-term retention and analysis. Use callback plugins like ansible.posix.log_plays.
Production Insight
We missed a failure notification because the Slack webhook was misconfigured. Now we test notifications with a manual trigger.
Key Takeaway
Always notify on failures and store logs as artifacts for debugging.

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.

YAML Injection
If variables contain colons or quotes, they may break YAML. Use ansible.builtin.template to generate inventory safely.
Production Insight
We generated inventory with a variable containing a colon, which broke YAML parsing. Now we use Ansible's template module to generate inventory from a Jinja2 template.
Key Takeaway
For dynamic inventory, use Ansible templates or the add_host module instead of manual YAML generation.
● Production incidentPOST-MORTEMseverity: high

The Missing --limit Disaster

Symptom
Engineer saw 'PLAY RECAP: ok=500 changed=500' and then database alerts flooded. The playbook had restart postgresql as a handler.
Assumption
They assumed the pipeline's inventory variable HOSTS defaulted to the canary group, but it was empty, so Ansible targeted all hosts.
Root cause
The pipeline script used --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.
Fix
Changed the pipeline to fail early if 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'.
Key lesson
  • 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'.
Production debug guideSymptom โ†’ Root cause โ†’ Fix4 entries
Symptom · 01
Pipeline succeeds but no changes applied (ok=0 changed=0) when you expected changes
Fix
Check if --check mode was accidentally left enabled. Also verify the inventory file is correct and hosts match the limit. Run ansible-inventory --list to debug.
Symptom · 02
Vault password prompt hangs pipeline indefinitely
Fix
The pipeline is not providing the vault password. Ensure the vault password is stored as a CI secret and passed via --vault-password-file or ANSIBLE_VAULT_PASSWORD_FILE environment variable.
Symptom · 03
Playbook fails with 'ERROR! no action detected in task'
Fix
Often caused by missing collections or roles. Ensure requirements.yml is installed via ansible-galaxy collection install -r requirements.yml before running the playbook.
Symptom · 04
Molecule test passes locally but fails in CI with 'docker: not found'
Fix
CI runner lacks Docker or the molecule driver. Use molecule docker driver and ensure Docker is available. Alternatively, use molecule vagrant with Vagrant installed.
★ Ansible in CI/CD Pipelines Quick Referenceprint this for your desk
Pipeline hangs waiting for vault password
Immediate action
Check if ANSIBLE_VAULT_PASSWORD_FILE is set
Commands
echo $ANSIBLE_VAULT_PASSWORD_FILE
ansible-playbook --vault-password-file $ANSIBLE_VAULT_PASSWORD_FILE playbook.yml
Fix now
export ANSIBLE_VAULT_PASSWORD_FILE=/path/to/vault-pass
Playbook runs on all hosts instead of limited set+
Immediate action
Check --limit value is non-empty
Commands
echo "$HOSTS"
ansible-playbook -i inventory playbook.yml --limit "$HOSTS"
Fix now
Add validation: if [ -z "$HOSTS" ]; then exit 1; fi
Check mode shows changes but pipeline proceeds to apply+
Immediate action
Gate the apply stage on check mode result
Commands
ansible-playbook --check --diff playbook.yml | grep -q 'changed=' && exit 1
Fix now
Add a stage that runs --check and fails if any changes detected
Molecule test fails with 'No such image'+
Immediate action
Pull the Docker image used in molecule.yml
Commands
docker pull geerlingguy/docker-ubuntu2204-ansible
molecule test
Fix now
Add pre_build_image: true in molecule.yml
ansible-lint fails in CI but passes locally+
Immediate action
Check ansible-lint version and config
Commands
ansible-lint --version
ansible-lint -c .ansible-lint playbook.yml
Fix now
Pin ansible-lint version in requirements.txt and use same config
CI/CD Platforms for Ansible: Jenkins vs GitHub Actions
FeatureJenkins Declarative PipelineGitHub ActionsNotes
Secret injectionwithCredentials stepSecrets in env or inputsBoth support secrets; Jenkins requires plugin
Vault passwordWrite to file from secretvault_password input or envGitHub Actions has dedicated input
Host limitingParameter with validationenv or vars with validationBoth need explicit validation
Check mode gateManual grep in sh stepManual grep in run stepSimilar approach
Molecule testingsh step with pip installSteps with pip installIdentical
Dynamic inventoryScripted generationRun step generationBoth support inline generation
NotificationsSlack pluginSlack actionJenkins has richer plugin ecosystem
CostSelf-hosted (infra cost)Free for public reposGitHub Actions has usage limits

Key takeaways

1
Always store Ansible vault passwords in CI secrets and inject via --vault-password-file.
2
Never run Ansible in CI without a non-empty --limit; validate it in the pipeline.
3
Use --check --diff as a dry-run gate that fails the pipeline if changes detected.
4
Run Molecule tests in PR checks to catch playbook errors before merge.
5
Add ansible-playbook --syntax-check and ansible-lint as early pipeline stages.
6
Use separate inventory files per environment and scope secrets accordingly.
7
Implement rollback strategies but prefer blue-green or canary deployments.
8
Test idempotency by running playbooks twice and ensuring no changes on second run.

Common mistakes to avoid

6 patterns
×

Hardcoding vault passwords in playbooks or inventory

Symptom
Password exposed in repository or logs
Fix
Store in CI secrets and inject via --vault-password-file or ANSIBLE_VAULT_PASSWORD_FILE
×

Not using --limit or using empty string

Symptom
Playbook runs on all hosts, potentially causing wide impact
Fix
Always use --limit with a non-empty parameter; validate in pipeline
×

Skipping --check mode before deployment

Symptom
Unexpected changes applied in production
Fix
Add a dry-run stage that fails if changes detected
×

Not running Molecule tests before merge

Symptom
Playbook fails in production due to OS differences or missing dependencies
Fix
Add Molecule testing to PR checks
×

Using process substitution for vault password in CI

Symptom
Pipeline fails with syntax error on sh shell
Fix
Use a temp file: echo password > /tmp/vault-pass
×

Not cleaning up vault password file after use

Symptom
Password persists on disk, potential security leak
Fix
Delete file in post/always block
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
How would you securely pass an Ansible vault password in a Jenkins Decla...
Q02SENIOR
What is the purpose of `--check` mode in Ansible CI/CD pipelines?
Q03JUNIOR
How do you limit Ansible to specific hosts in a CI pipeline?
Q04SENIOR
How would you test an Ansible role in CI before merging a PR?
Q05SENIOR
What are the limitations of `--check` mode in Ansible?
Q06SENIOR
How do you handle multiple vault passwords in a CI pipeline?
Q07SENIOR
What is the difference between `ansible-playbook` and `ansible-pull` in ...
Q08SENIOR
How do you ensure idempotency in Ansible playbooks?
Q01 of 08SENIOR

How would you securely pass an Ansible vault password in a Jenkins Declarative Pipeline?

ANSWER
Use the 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' }.
FAQ · 8 QUESTIONS

Frequently Asked Questions

01
Can I run Ansible without --limit in CI?
02
How do I pass multiple vault passwords in GitHub Actions?
03
What if Molecule tests take too long?
04
How do I handle secrets for different environments in GitHub Actions?
05
What is the best way to notify on Ansible failures?
06
Can I use Ansible Tower/AWX in CI/CD?
07
How do I debug Ansible in CI when logs are too verbose?
08
What is the role of ansible-lint in CI?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.

Follow
Verified
production tested
June 21, 2026
last updated
1,596
articles · all by Naren
🔥

That's Ansible. Mark it forged?

8 min read · try the examples if you haven't

Previous
Ansible Docker Management
19 / 23 · Ansible
Next
Ansible Performance Tuning