Jenkins + Ansible for Beginners: A Simple Pipeline That Produces a Health Report

Automation

Table of Contents

Introduction

If you’re curious about network automation but don’t yet have a clear picture of what it can actually do, a good starting point is to automate something small, safe, and repeatable.

Instead of jumping straight into configuring routers/switches, we’ll begin with a system healthcheck on an Ansible runner (a Linux host). The goal is to prove the end-to-end flow:

high level workflow
  1. Jenkins can securely SSH into an Ansible runner
  2. Jenkins can trigger an Ansible playbook
  3. The playbook produces a clean “healthcheck report” (stdout)
  4. Jenkins archives the report and sends it by email

This may look “too basic” at first—but it’s exactly the foundation you’ll reuse later for real network automation tasks such as collecting device facts, auditing configuration drift, running compliance checks, or orchestrating planned changes.

Features you’ll see

  • Jenkins parameters: Use TARGET_IP, ANSIBLE_USER, NOTIFY_EMAIL to reuse the same Jenkinsfile across environments—change inputs, not code.
  • Jenkins stages: Each concern is split into a stage: establish SSH trust → verify connectivity/tools → generate the playbook → run the healthcheck → archive results.
  • Ansible playbook: Collect facts, print a system summary, and fail fast on basic thresholds (e.g., low memory / old Python).
  • Artifacts + email: Capture stdout to system_info.txt, archive it as an artifact, and include the same report in the email.
Jenkin Console output
email response from Jenkin

Pipeline overview

Prepare known_hosts – create/lock down known_hosts, add the Ansible host key to prevent interactive prompts.

SSH preflight – try SSH into the Ansible and confirm Ansible + Python are available.

Create playbook – generate a minimal healthcheck playbook on the Ansible runner.

Run + capture output – execute ansible-playbook and save the output to system_info.txt.

Archive + notify – archive system_info.txt as a Jenkins artifact and email the captured report.

Code Walkthrough: Pipeline Header + Shared SSH Settings + Parameters (Line#1–33)

pipeline {
  agent any
  environment {
    // SSH private key path on Jenkins agent
    SSH_KEY = '=====PLEASE UPDATE TO YOUR SSH PRIVATE KEY PATH VALUE====='
    // known_hosts path on Jenkins agent
    SSH_KNOWN_HOSTS = '=====PLEASE UPDATE TO YOUR KNOWN_HOSTS PATH VALUE====='
    // Enforce host key checking (recommended)
    SSH_OPTS = "-o StrictHostKeyChecking=yes -o UserKnownHostsFile=${SSH_KNOWN_HOSTS}"
  }
  parameters {
    string(
      name: 'TARGET_IP',
      defaultValue: '=====PLEASE UPDATE TO YOUR ANSIBLE RUNNER PRIVATE IP VALUE=====',
      description: 'Ansible runner private IP (e.g. 10.x / 172.31.x inside your VPC)'
    )
    string(
      name: 'ANSIBLE_USER',
      defaultValue: '=====PLEASE UPDATE TO YOUR SSH USER VALUE=====',
      description: 'SSH username on the Ansible runner (e.g. ec2-user, ubuntu)'
    )
    string(
      name: 'NOTIFY_EMAIL',
      defaultValue: '=====PLEASE UPDATE TO YOUR NOTIFICATION EMAIL VALUE=====',
      description: 'Healthcheck notification email'
    )
  }

pipeline { agent any } (Line 1-2)

This is a Declarative Pipeline. agent any means the job can run on any available Jenkins agent.

environment (Line 3–11)

  • SSH_KEY: The SSH private key path on the Jenkins agent. Jenkins uses it to SSH into the Ansible runner.
  • SSH_KNOWN_HOSTS: The known_hosts file path on the Jenkins agent. This stores and verifies the target host key.
  • SSH_OPTS: Enforces safer SSH behavior:
    • StrictHostKeyChecking=yes: fail if the host key is unknown/changed (prevents accidentally trusting the wrong host).
    • UserKnownHostsFile=...: forces SSH to use your managed known_hosts file.

parameters (Line 12–33)

These are the “things you’ll change per environment” exposed as Jenkins inputs—so you reuse the same Jenkinsfile by changing values, not code:

  • TARGET_IP: Private IP of the Ansible runner (inside your VPC).
  • ANSIBLE_USER: SSH username (e.g., ec2-user, ubuntu).
  • NOTIFY_EMAIL: Email address that receives the healthcheck report.

Code Walkthrough: SSH Trust Bootstrap (Prepare known_hosts) (Line 35–51)

  stages {
    stage('Prepare known_hosts') {
      steps {
        sh '''
          set -e
          mkdir -p /var/lib/jenkins/.ssh
          touch "$SSH_KNOWN_HOSTS" || true
          chown jenkins:jenkins "$SSH_KNOWN_HOSTS" || true
          chmod 600 "$SSH_KNOWN_HOSTS" || true
          grep -qE "^${TARGET_IP}[ ,]" "$SSH_KNOWN_HOSTS" 2>/dev/null || {
            ssh-keyscan -H "${TARGET_IP}" >> "$SSH_KNOWN_HOSTS"
          }
        '''
      }
    }

stage('Prepare known_hosts') (Line 35–51)

This stage bootstraps SSH trust so the rest of the pipeline can run non-interactively with StrictHostKeyChecking=yes:

If the target host key isn’t already recorded, it adds it via ssh-keyscan (so SSH won’t prompt later).

Creates the Jenkins ~/.ssh folder (if missing).

Ensures the known_hosts file exists with safe permissions (600).

Code Walkthrough: SSH Health Check (Line 53–60)

    stage('SSH Health Check') {
      steps {
        sh """
          ssh -i "$SSH_KEY" $SSH_OPTS "${params.ANSIBLE_USER}@${params.TARGET_IP}" \
            'whoami && hostname && ansible-playbook --version && python3 --version'
        """
      }
    }

SSH Health Check (Line 53–60)

This stage does a quick “can we actually run?” sanity check by SSH’ing into the Ansible runner and printing:

  • whoami / hostname: confirms you logged in as the expected user on the expected host
  • ansible-playbook --version: confirms Ansible is installed and reachable
  • python3 --version: confirms Python 3 is available (needed for Ansible facts/tasks)

If this stage fails, the rest of the pipeline is skipped—so you catch SSH/key/user/tooling issues early.

Code Walkthrough: Prepare Healthcheck Playbook (Line 62–101)

    stage('Prepare Healthcheck Playbook') {
      steps {
        sh """
          ssh -i "$SSH_KEY" $SSH_OPTS "${params.ANSIBLE_USER}@${params.TARGET_IP}" '
            set -eux
            mkdir -p ~/playbooks
            cat > ~/playbooks/system_healthcheck.yml <<'"'"'EOF'"'"'
- hosts: localhost
  connection: local
  gather_facts: true
  tasks:
    - name: Show basic system info (stdout report)
      debug:
        msg: |
          Hostname: {{ ansible_facts.hostname }}
          OS: {{ ansible_facts.distribution }} {{ ansible_facts.distribution_version }}
          Kernel: {{ ansible_facts.kernel }}
          CPU (vCPU): {{ ansible_facts.processor_vcpus | default(ansible_facts.processor_cores, true) }}
          Memory (MB): {{ ansible_facts.memtotal_mb }}
          Primary IP: {{ ansible_facts.default_ipv4.address | default("n/a") }}
          Python: {{ ansible_facts.python.version.major }}.{{ ansible_facts.python.version.minor }}.{{ ansible_facts.python.version.micro }}
    - name: Fail if memory is too low
      fail:
        msg: "Memory too low: {{ ansible_facts.memtotal_mb }} MB"
      when: ansible_facts.memtotal_mb < 800
    - name: Fail if Python version is too old
      fail:
        msg: "Python version too old: {{ ansible_facts.python.version.full }}"
      when: ansible_facts.python.version.major < 3
EOF
            chmod 644 ~/playbooks/system_healthcheck.yml
          '
        """
      }
    }

Prepare Healthcheck Playbook (Line 62–101)

This stage SSHes into the Ansible runner and writes a minimal Ansible playbook (~/playbooks/system_healthcheck.yml) that the next stage will execute.

  • Creates a working folder on the runner: ~/playbooks
  • Generates the playbook using a heredoc (cat > ... << 'EOF' ... EOF)
  • Sets safe file permissions: chmod 644

Inside the playbook:

  • gather_facts: true collects system facts (OS, kernel, CPU, memory, IP, Python version, etc.).
  • Debug output prints a readable system summary to stdout (so Jenkins can capture it).
  • Fail-fast checks stop the run if:
    • memory is below 800 MB, or
    • Python major version is below 3.

Code Walkthrough: Ansible Healthcheck (Line 103–115)

    stage('Ansible Healthcheck') {
      steps {
        sh """
          set -e
          ssh -i "$SSH_KEY" $SSH_OPTS "${params.ANSIBLE_USER}@${params.TARGET_IP}" "
            set -eux
            ansible-playbook -i localhost, -c local \
              -e ansible_python_interpreter=/usr/bin/python3 \
              ~/playbooks/system_healthcheck.yml
          " 2>&1 | tee system_info.txt
        """
      }
    }

Ansible Healthcheck (Line 103–115)

This stage runs the healthcheck playbook on the Ansible runner and captures the output back in Jenkins.

  • ssh ... ansible-playbook ...: executes ~/playbooks/system_healthcheck.yml on the runner.
  • -i localhost, -c local: runs against localhost using a local connection (no remote inventory needed).
  • -e ansible_python_interpreter=/usr/bin/python3: ensures Ansible uses Python 3.

Output handling:

  • 2>&1 merges stderr into stdout (so errors are captured too).
  • | tee system_info.txt prints the output to the Jenkins console and saves the same content into system_info.txt for archiving/email later.

If the playbook hits a fail task (e.g., low memory / old Python), this stage fails and the pipeline goes to the post { failure { ... } } flow.

Code Walkthrough: Archive Result + Notifications (Line 117–167)

    stage('Archive Result') {
      steps {
        archiveArtifacts artifacts: 'system_info.txt', fingerprint: true
      }
    }
  }
  post {
    success {
      script {
        env.SYSTEM_INFO = fileExists('system_info.txt')
          ? readFile('system_info.txt')
          : 'N/A (system_info.txt not found)'
      }
      catchError(buildResult: 'SUCCESS', stageResult: 'UNSTABLE') {
        mail to: "${params.NOTIFY_EMAIL}",
             subject: "[Jenkins] ✅ Ansible Healthcheck SUCCESS",
             body: """\
Ansible healthcheck completed successfully.
Job: ${env.JOB_NAME}
Build: #${env.BUILD_NUMBER}
Target: ${params.TARGET_IP}
User: ${params.ANSIBLE_USER}
===== Captured Output =====
${env.SYSTEM_INFO}
"""
      }
    }
    failure {
      script {
        env.SYSTEM_INFO = fileExists('system_info.txt')
          ? readFile('system_info.txt')
          : 'N/A (system_info.txt not found)'
      }
      catchError(buildResult: 'FAILURE', stageResult: 'UNSTABLE') {
        mail to: "${params.NOTIFY_EMAIL}",
             subject: "[Jenkins] ❌ Ansible Healthcheck FAILED",
             body: """\
Ansible healthcheck FAILED.
Job: ${env.JOB_NAME}
Build: #${env.BUILD_NUMBER}
Target: ${params.TARGET_IP}
User: ${params.ANSIBLE_USER}
===== Captured Output =====
${env.SYSTEM_INFO}
"""

Archive Result + Notifications (Line 117–167)

This final part stores the report and sends an email based on success/failure.

  • Archive Result: archiveArtifacts artifacts: 'system_info.txt' saves the captured output as a Jenkins artifact for later download.
    fingerprint: true adds traceability (Jenkins can track the same artifact across builds).
  • post { success / failure }: runs after the pipeline finishes.
    • Reads system_info.txt into env.SYSTEM_INFO (or falls back to N/A if missing).
    • Sends an email to NOTIFY_EMAIL with job/build/target info and the captured output.
    • catchError(...) prevents the build result from being changed just because the email step fails (e.g., mail server issue).

Jenkins file

Note: The Jenkins file below uses masked/demo placeholders (example private IP, example email, and a generic SSH key path). Replace TARGET_IP, the SSH key/credentials, and mail recipients with your own values.

pipeline {
  agent any

  environment {
    // SSH private key path on Jenkins agent
    SSH_KEY = '=====PLEASE UPDATE TO YOUR SSH PRIVATE KEY PATH VALUE====='

    // known_hosts path on Jenkins agent
    SSH_KNOWN_HOSTS = '=====PLEASE UPDATE TO YOUR KNOWN_HOSTS PATH VALUE====='

    // Enforce host key checking (recommended)
    SSH_OPTS = "-o StrictHostKeyChecking=yes -o UserKnownHostsFile=${SSH_KNOWN_HOSTS}"
  }

  parameters {
    string(
      name: 'TARGET_IP',
      defaultValue: '=====PLEASE UPDATE TO YOUR ANSIBLE RUNNER PRIVATE IP VALUE=====',
      description: 'Ansible runner private IP (e.g. 10.x / 172.31.x inside your VPC)'
    )

    string(
      name: 'ANSIBLE_USER',
      defaultValue: '=====PLEASE UPDATE TO YOUR SSH USER VALUE=====',
      description: 'SSH username on the Ansible runner (e.g. ec2-user, ubuntu)'
    )

    string(
      name: 'NOTIFY_EMAIL',
      defaultValue: '=====PLEASE UPDATE TO YOUR NOTIFICATION EMAIL VALUE=====',
      description: 'Healthcheck notification email'
    )
  }

  stages {
    stage('Prepare known_hosts') {
      steps {
        sh '''
          set -e
          mkdir -p /var/lib/jenkins/.ssh

          touch "$SSH_KNOWN_HOSTS" || true
          chown jenkins:jenkins "$SSH_KNOWN_HOSTS" || true
          chmod 600 "$SSH_KNOWN_HOSTS" || true

          grep -qE "^${TARGET_IP}[ ,]" "$SSH_KNOWN_HOSTS" 2>/dev/null || {
            ssh-keyscan -H "${TARGET_IP}" >> "$SSH_KNOWN_HOSTS"
          }
        '''
      }
    }

    stage('SSH Health Check') {
      steps {
        sh """
          ssh -i "$SSH_KEY" $SSH_OPTS "${params.ANSIBLE_USER}@${params.TARGET_IP}" \
            'whoami && hostname && ansible-playbook --version && python3 --version'
        """
      }
    }

    stage('Prepare Healthcheck Playbook') {
      steps {
        sh """
          ssh -i "$SSH_KEY" $SSH_OPTS "${params.ANSIBLE_USER}@${params.TARGET_IP}" '
            set -eux
            mkdir -p ~/playbooks

            cat > ~/playbooks/system_healthcheck.yml <<'"'"'EOF'"'"'
- hosts: localhost
  connection: local
  gather_facts: true

  tasks:
    - name: Show basic system info (stdout report)
      debug:
        msg: |
          Hostname: {{ ansible_facts.hostname }}
          OS: {{ ansible_facts.distribution }} {{ ansible_facts.distribution_version }}
          Kernel: {{ ansible_facts.kernel }}
          CPU (vCPU): {{ ansible_facts.processor_vcpus | default(ansible_facts.processor_cores, true) }}
          Memory (MB): {{ ansible_facts.memtotal_mb }}
          Primary IP: {{ ansible_facts.default_ipv4.address | default("n/a") }}
          Python: {{ ansible_facts.python.version.major }}.{{ ansible_facts.python.version.minor }}.{{ ansible_facts.python.version.micro }}

    - name: Fail if memory is too low
      fail:
        msg: "Memory too low: {{ ansible_facts.memtotal_mb }} MB"
      when: ansible_facts.memtotal_mb < 800

    - name: Fail if Python version is too old
      fail:
        msg: "Python version too old: {{ ansible_facts.python.version.full }}"
      when: ansible_facts.python.version.major < 3
EOF

            chmod 644 ~/playbooks/system_healthcheck.yml
          '
        """
      }
    }

    stage('Ansible Healthcheck') {
      steps {
        sh """
          set -e
          ssh -i "$SSH_KEY" $SSH_OPTS "${params.ANSIBLE_USER}@${params.TARGET_IP}" "
            set -eux
            ansible-playbook -i localhost, -c local \
              -e ansible_python_interpreter=/usr/bin/python3 \
              ~/playbooks/system_healthcheck.yml
          " 2>&1 | tee system_info.txt
        """
      }
    }

    stage('Archive Result') {
      steps {
        archiveArtifacts artifacts: 'system_info.txt', fingerprint: true
      }
    }
  }

  post {
    success {
      script {
        env.SYSTEM_INFO = fileExists('system_info.txt')
          ? readFile('system_info.txt')
          : 'N/A (system_info.txt not found)'
      }
      catchError(buildResult: 'SUCCESS', stageResult: 'UNSTABLE') {
        mail to: "${params.NOTIFY_EMAIL}",
             subject: "[Jenkins] ✅ Ansible Healthcheck SUCCESS",
             body: """\
Ansible healthcheck completed successfully.

Job: ${env.JOB_NAME}
Build: #${env.BUILD_NUMBER}
Target: ${params.TARGET_IP}
User: ${params.ANSIBLE_USER}

===== Captured Output =====
${env.SYSTEM_INFO}
"""
      }
    }

    failure {
      script {
        env.SYSTEM_INFO = fileExists('system_info.txt')
          ? readFile('system_info.txt')
          : 'N/A (system_info.txt not found)'
      }
      catchError(buildResult: 'FAILURE', stageResult: 'UNSTABLE') {
        mail to: "${params.NOTIFY_EMAIL}",
             subject: "[Jenkins] ❌ Ansible Healthcheck FAILED",
             body: """\
Ansible healthcheck FAILED.

Job: ${env.JOB_NAME}
Build: #${env.BUILD_NUMBER}
Target: ${params.TARGET_IP}
User: ${params.ANSIBLE_USER}

===== Captured Output =====
${env.SYSTEM_INFO}
"""
      }
    }
  }
}

Summary

In this first demo, we built a minimal but practical Jenkins → SSH → Ansible pipeline that you can reuse as a template for bigger automation.

  • Jenkins takes three inputs (TARGET_IP, ANSIBLE_USER, NOTIFY_EMAIL) so you can reuse the same Jenkinsfile without editing code.
  • It bootstraps SSH trust (known_hosts) to keep runs non-interactive while still enforcing host key checking.
  • It generates and runs a tiny Ansible healthcheck playbook on the runner to collect facts and perform basic fail-fast checks.
  • The playbook output is captured into system_info.txt, then archived as an artifact and sent by email for easy auditing.

From here, you can swap the healthcheck playbook with real automation tasks (patching, deployments, or network device workflows) while keeping the same Jenkins pipeline structure.

Comments

Copied title and URL