Ansible Course
Ansible with Jenkins
In this lesson
Jenkins remains the most widely deployed CI/CD platform in enterprise environments — and many teams that already run Jenkins want to integrate Ansible into existing pipelines rather than migrating to GitHub Actions or GitLab CI. Ansible and Jenkins complement each other well: Jenkins orchestrates the pipeline stages, manages build triggers, and provides the web UI and audit trail; Ansible handles the actual configuration and deployment work that Jenkins stages invoke. This lesson covers the Ansible Jenkins plugin, writing Declarative Jenkinsfiles that call Ansible, injecting vault passwords and SSH keys from Jenkins credentials, parameterised builds for version and environment selection, and packaging reusable Ansible pipeline logic in a Jenkins shared library.
The Ansible Jenkins Plugin
The Ansible plugin for Jenkins
provides two dedicated pipeline steps — ansiblePlaybook and
ansibleAdhoc — that handle Ansible installation path resolution,
inventory specification, vault password injection, and verbose output formatting.
Using the plugin steps is cleaner than calling ansible-playbook directly
in a shell step, but both approaches work.
Plugin installation
In Jenkins:
Manage Jenkins → Plugins → Available → search "Ansible" →
install Ansible plugin. After installation, configure the Ansible
installation path under Manage Jenkins → Tools → Ansible installations.
Point it to the ansible binary on the Jenkins agent.
// Jenkinsfile — using the Ansible plugin steps
pipeline {
agent any
stages {
stage('Deploy') {
steps {
// ansiblePlaybook plugin step
ansiblePlaybook(
playbook: 'site.yml',
inventory: 'inventory/production/',
credentialsId: 'ansible-ssh-key', // Jenkins SSH credential
vaultCredentialsId: 'ansible-vault-pass', // Jenkins secret text
colorized: true,
extras: '-e app_version=${APP_VERSION}'
)
}
}
}
}
Jenkins Credentials for Ansible
Jenkins has a built-in credentials store that securely holds SSH keys, secret text, and username/password pairs. Ansible needs two credential types — an SSH private key for connecting to managed nodes and a secret text for the vault password. Both are stored in Jenkins and injected at runtime without ever appearing in the Jenkinsfile or build logs.
ID:
ansible-ssh-keyUsername:
ansiblePrivate Key: paste the PEM key directly
Jenkins writes it to a temp file and sets
--private-key automatically when referenced via credentialsId.
ID:
ansible-vault-passSecret: the vault password
Jenkins writes it to a temp file and passes it as
--vault-password-file. The password never appears in logs or the Jenkinsfile.
// Manually injecting credentials when not using the plugin step
pipeline {
agent any
stages {
stage('Deploy') {
steps {
withCredentials([
sshUserPrivateKey(
credentialsId: 'ansible-ssh-key',
keyFileVariable: 'SSH_KEY_FILE',
usernameVariable: 'SSH_USER'
),
string(
credentialsId: 'ansible-vault-pass',
variable: 'VAULT_PASS'
)
]) {
sh '''
echo "$VAULT_PASS" > .vault_pass
chmod 600 .vault_pass
ansible-playbook site.yml \\
-i inventory/production/ \\
--private-key "$SSH_KEY_FILE" \\
-u "$SSH_USER" \\
--vault-password-file .vault_pass
rm -f .vault_pass
'''
}
}
}
}
}
Complete Declarative Jenkinsfile
The complete pipeline below implements all four stages from Lesson 37 — lint, test, staging, production — as a declarative Jenkinsfile. The production stage uses Jenkins' built-in input step as the manual approval gate.
// Jenkinsfile
pipeline {
agent {
docker {
image 'python:3.12-slim'
args '-v /var/run/docker.sock:/var/run/docker.sock'
}
}
parameters {
string(name: 'APP_VERSION', defaultValue: 'latest', description: 'Application version to deploy')
choice(name: 'ENVIRONMENT', choices: ['staging', 'production'], description: 'Target environment')
booleanParam(name: 'SKIP_TESTS', defaultValue: false, description: 'Skip Molecule tests (emergency only)')
}
environment {
ANSIBLE_FORCE_COLOR = '1'
ANSIBLE_HOST_KEY_CHECKING = 'False'
}
stages {
// ── Stage 1: Install dependencies ──────────────────────────
stage('Setup') {
steps {
sh 'pip install ansible ansible-lint yamllint molecule molecule-plugins[docker]'
sh 'ansible-galaxy install -r requirements.yml'
}
}
// ── Stage 2: Lint and syntax check ─────────────────────────
stage('Validate') {
steps {
sh 'yamllint .'
sh 'ansible-lint'
sh 'ansible-playbook site.yml --syntax-check -i inventory/staging/'
}
post {
failure {
echo 'Lint failed — fix errors before deploying'
}
}
}
// ── Stage 3: Molecule role tests ───────────────────────────
stage('Test') {
when {
expression { return !params.SKIP_TESTS }
}
parallel {
stage('Test: common') {
steps { sh 'cd roles/common && molecule test' }
}
stage('Test: nginx') {
steps { sh 'cd roles/nginx && molecule test' }
}
stage('Test: postgresql') {
steps { sh 'cd roles/postgresql && molecule test' }
}
}
}
// ── Stage 4: Deploy to staging ─────────────────────────────
stage('Deploy Staging') {
when {
anyOf {
branch 'main'
expression { params.ENVIRONMENT == 'staging' }
}
}
steps {
withCredentials([
sshUserPrivateKey(credentialsId: 'ansible-ssh-key', keyFileVariable: 'SSH_KEY'),
string(credentialsId: 'ansible-vault-pass', variable: 'VAULT_PASS')
]) {
sh '''
echo "$VAULT_PASS" > .vault_pass
ansible-playbook site.yml \\
-i inventory/staging/ \\
--private-key "$SSH_KEY" \\
--vault-password-file .vault_pass \\
-e app_version=${APP_VERSION} \\
--check --diff
ansible-playbook site.yml \\
-i inventory/staging/ \\
--private-key "$SSH_KEY" \\
--vault-password-file .vault_pass \\
-e app_version=${APP_VERSION}
ansible-playbook smoke_tests.yml \\
-i inventory/staging/ \\
--private-key "$SSH_KEY" \\
--vault-password-file .vault_pass \\
-e app_version=${APP_VERSION}
rm -f .vault_pass
'''
}
}
}
// ── Stage 5: Manual approval gate ──────────────────────────
stage('Approve Production') {
when {
expression { params.ENVIRONMENT == 'production' }
}
steps {
timeout(time: 24, unit: 'HOURS') {
input(
message: "Deploy ${params.APP_VERSION} to PRODUCTION?",
ok: 'Deploy',
submitterParameter: 'APPROVED_BY'
)
}
}
}
// ── Stage 6: Deploy to production ──────────────────────────
stage('Deploy Production') {
when {
expression { params.ENVIRONMENT == 'production' }
}
steps {
withCredentials([
sshUserPrivateKey(credentialsId: 'ansible-ssh-key-prod', keyFileVariable: 'SSH_KEY'),
string(credentialsId: 'ansible-vault-pass-prod', variable: 'VAULT_PASS')
]) {
sh '''
echo "$VAULT_PASS" > .vault_pass
ansible-playbook site.yml \\
-i inventory/production/ \\
--private-key "$SSH_KEY" \\
--vault-password-file .vault_pass \\
-e app_version=${APP_VERSION}
ansible-playbook smoke_tests.yml \\
-i inventory/production/ \\
--private-key "$SSH_KEY" \\
--vault-password-file .vault_pass \\
-e app_version=${APP_VERSION}
rm -f .vault_pass
'''
}
}
}
}
post {
always {
sh 'rm -f .vault_pass' // safety net — ensure cleanup even on crash
cleanWs() // clean Jenkins workspace after every run
}
success {
echo "Pipeline completed: ${params.APP_VERSION} → ${params.ENVIRONMENT}"
}
failure {
mail(
to: 'ops@example.com',
subject: "FAILED: Ansible pipeline ${env.JOB_NAME} #${env.BUILD_NUMBER}",
body: "Build failed: ${env.BUILD_URL}"
)
}
}
}
Started by user ops-engineer (approved by: senior-engineer)
[Pipeline] stage: Setup
+ pip install ansible ansible-lint yamllint molecule ...
[Pipeline] stage: Validate
+ yamllint .
+ ansible-lint
Passed: 0 violation(s) found.
+ ansible-playbook site.yml --syntax-check
playbook: site.yml — syntax OK
[Pipeline] stage: Test (parallel)
[Test: common] PASSED 2m14s
[Test: nginx] PASSED 1m58s
[Test: postgresql] PASSED 2m41s
[Pipeline] stage: Deploy Staging
TASK [Deploy Nginx configuration]
--- before +++ after
- worker_connections 1024;
+ worker_connections 2048;
changed: [staging-web01]
PLAY RECAP: staging-web01 ok=18 changed=3 failed=0
[Pipeline] stage: Approve Production
Waiting for approval... (approved by: senior-engineer)
[Pipeline] stage: Deploy Production
PLAY RECAP: web01 ok=18 changed=3 failed=0
web02 ok=18 changed=3 failed=0
web03 ok=18 changed=3 failed=0
Finished: SUCCESSWhat just happened?
The pipeline ran all six stages end to end.
The three Molecule tests ran in parallel, completing in under 3 minutes. The staging
deployment showed the --check --diff output before applying —
confirming only worker_connections would change. The production stage
paused for 24 hours waiting for manual approval; a senior engineer reviewed and
approved, and the deployment ran successfully across all three production hosts.
Parameterised Builds
The Jenkinsfile above already uses
parameters — APP_VERSION, ENVIRONMENT, and
SKIP_TESTS. Parameterised builds make the pipeline reusable across
different versions and environments without code changes. Engineers trigger a build
from the Jenkins UI and fill in the parameters, or CI/CD triggers set them
automatically.
// Triggering a parameterised Jenkins build via the REST API (from another pipeline)
curl -X POST \
"https://jenkins.example.com/job/ansible-deploy/buildWithParameters" \
--user "ci-user:${JENKINS_API_TOKEN}" \
--data "APP_VERSION=2.4.1" \
--data "ENVIRONMENT=production" \
--data "SKIP_TESTS=false"
// Triggering from another Jenkinsfile (upstream/downstream pipeline chaining)
stage('Trigger Ansible Deploy') {
steps {
build(
job: 'ansible-deploy',
parameters: [
string(name: 'APP_VERSION', value: env.BUILD_VERSION),
string(name: 'ENVIRONMENT', value: 'staging')
],
wait: true, // wait for the triggered pipeline to complete
propagate: true // fail this pipeline if the triggered one fails
)
}
}
Jenkins Shared Library
When multiple projects need the same Ansible pipeline logic, a Jenkins shared library packages that logic into reusable Groovy functions. Projects import the library and call a single function — the pipeline stages, credential handling, and cleanup are all encapsulated. Changes to the shared library propagate to all pipelines that use it without requiring changes in each repository.
// vars/ansibleDeploy.groovy — shared library function
// Stored in a separate Git repo registered in Jenkins as a shared library
def call(Map config) {
// config keys: playbook, inventory, version, vaultCredId, sshCredId
def playbook = config.playbook ?: 'site.yml'
def inventory = config.inventory ?: "inventory/${config.environment}/"
def version = config.version ?: error('version is required')
withCredentials([
sshUserPrivateKey(credentialsId: config.sshCredId, keyFileVariable: 'SSH_KEY'),
string(credentialsId: config.vaultCredId, variable: 'VAULT_PASS')
]) {
sh """
echo "\$VAULT_PASS" > .vault_pass
pip install ansible -q
ansible-galaxy install -r requirements.yml -q
ansible-playbook ${playbook} \\
-i ${inventory} \\
--private-key "\$SSH_KEY" \\
--vault-password-file .vault_pass \\
-e app_version=${version}
rm -f .vault_pass
"""
}
}
// ─── How any project uses the shared library ─────────────────────────────
// Jenkinsfile in the application repo:
@Library('ansible-shared-library') _
pipeline {
agent any
stages {
stage('Deploy Staging') {
steps {
ansibleDeploy(
environment: 'staging',
version: params.APP_VERSION,
sshCredId: 'ansible-ssh-key',
vaultCredId: 'ansible-vault-pass'
)
}
}
stage('Deploy Production') {
steps {
input 'Deploy to production?'
ansibleDeploy(
environment: 'production',
version: params.APP_VERSION,
sshCredId: 'ansible-ssh-key-prod',
vaultCredId: 'ansible-vault-pass-prod'
)
}
}
}
}
The Shared Library Analogy
A Jenkins shared library is to Jenkinsfiles what an Ansible role is to playbooks. The deployment logic is written once, tested, and versioned in its own repository. Every team that needs Ansible deployment calls the shared function — they get the correct credential handling, cleanup, and error reporting without writing it themselves. When the platform team updates the shared library (new Ansible version, improved error handling), every pipeline using it benefits automatically.
Jenkins vs GitHub Actions — Choosing the Right Tool
The Jenkins Agent Must Have Ansible Installed and Network Access to Managed Nodes
The most common Jenkins + Ansible integration failure is a pipeline that calls ansible-playbook on a Jenkins agent that either does not have Ansible installed or cannot reach the managed nodes over SSH. Always configure a dedicated Jenkins agent (or Docker agent image) with Ansible pre-installed and network connectivity to your infrastructure. Using the Docker agent (image: python:3.12-slim) with pip install ansible in the Setup stage is the most reproducible approach — the agent environment is identical on every run.
Key Takeaways
withCredentials; they never appear in Jenkinsfiles or logs.
input step with a timeout for production gates — the pipeline pauses and waits for a named approver. Set a reasonable timeout (24 hours) so stale approvals do not block the pipeline indefinitely.
parallel block to test multiple roles simultaneously, matching the matrix pattern from GitHub Actions.
ansibleDeploy(...) instead of duplicating credential handling and cleanup logic across every project's Jenkinsfile.
post { always {} } — delete vault password files and SSH keys in the post block so they are removed even when the pipeline fails mid-stage.
Teacher's Note
If your team runs Jenkins, take the complete Jenkinsfile from this lesson and create a new pipeline job pointing at your Ansible repository. Run it with ENVIRONMENT=staging and SKIP_TESTS=true for your first run to verify the credential injection and playbook invocation work correctly before enabling the full pipeline.
Practice Questions
1. Which Jenkins pipeline step injects credentials from the Jenkins credentials store into environment variables within a scoped block — without exposing them in the Jenkinsfile?
2. Which Jenkins declarative pipeline step pauses execution and waits for a human to click an approval button before the next stage runs?
3. In which Jenkinsfile section should you delete the .vault_pass file to ensure it is removed even when the pipeline fails mid-stage?
Quiz
1. What credential type should an SSH private key be stored as in Jenkins, and how does Jenkins make it available to Ansible?
2. A Jenkins pipeline runs Molecule tests for 4 roles sequentially in a single stage, taking 12 minutes total. How do you make them run simultaneously?
3. Ten application teams each have their own Jenkinsfile with duplicated Ansible credential handling and deployment logic. When the platform team changes the SSH key credential ID, all ten Jenkinsfiles need updating. What is the solution?
Up Next · Lesson 39
Ansible Anti-patterns
Learn the most common Ansible mistakes that experienced engineers still make — and the correct patterns to replace them with. Recognising anti-patterns is what separates maintainable automation from the kind that breaks under pressure.