[推荐]docker+jenkins+jenkinsfile+ansible实现多机批量部署

1)gitea+postgres、jenkins软件安装

复制代码
[root@localhost soft]# cat docker-compose.yml 
version: '3.8'
services:
  postgres:
    image: postgres:15-alpine
    container_name: gitea-postgres
    restart: always
    volumes:
      - ./postgres-data:/var/lib/postgresql/data
    environment:
      - POSTGRES_USER=gitea
      - POSTGRES_PASSWORD=gitea_prod_2026
      - POSTGRES_DB=gitea
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U gitea"]
      interval: 10s
      timeout: 5s
      retries: 5

  gitea:
    image: gitea/gitea:1.22.3
    container_name: gitea-prod
    restart: always
    depends_on:
      postgres:
        condition: service_healthy
    volumes:
      - ./gitea-data:/data
      - /etc/localtime:/etc/localtime:ro
    ports:
      - "3000:3000"
      - "222:22"
    environment:
      # 时区
      - TZ=Asia/Shanghai

      # 🔥 核心:自动指定数据库为 Postgres 🔥
      - GITEA__database__DB_TYPE=postgres
      - GITEA__database__HOST=postgres:5432
      - GITEA__database__NAME=gitea
      - GITEA__database__USER=gitea
      - GITEA__database__PASSWD=gitea_prod_2026

      # 自动配置站点 URL(生产必须)
      - GITEA__server__DOMAIN=192.168.3.203
      - GITEA__server__ROOT_URL=http://192.168.3.203:3000
      - GITEA__server__HTTP_PORT=3000
      - GITEA__server__SSH_PORT=222

  jenkins:
    build:
      context: ./jenkins
      dockerfile: Dockerfile
    image: my-jenkins:ansible-jdk21
    container_name: jenkins-jdk21
    restart: always
    ports:
      - "8080:8080"
    volumes:
      - ./jenkins-data:/var/jenkins_home
    environment:
      - TZ=Asia/Shanghai
    user: root



[root@localhost soft]# 

2)由于jenkins是安装在docker内的,jenkins所在机器内部需要执行ansible,所以需要python环境

所以这个jenkins软件还需要内部安装ansible和python

复制代码
[root@localhost jenkins]# cat Dockerfile 
FROM jenkins/jenkins:jdk21

USER root

RUN apt-get update \
 && apt-get install -y --no-install-recommends python3 python3-pip sshpass \
 # 关键:使用阿里云 PyPI 镜像加速安装 ansible
 && python3 -m pip install --break-system-packages "ansible>=9,<11" \
    -i https://mirrors.aliyun.com/pypi/simple/ \
    --trusted-host mirrors.aliyun.com \
 && apt-get clean \
 && rm -rf /var/lib/apt/lists/*

USER jenkins

3)jenkins下安装maven插件

4)jenkins设置全局环境变量,如:dingding

5)新建"流水线"项目,这样子就可以使用流水线jenkinsfile了

6)abc项目下的jenkinsfile配置 // 自动1分钟拉取一次,检查是否有新提交

复制代码
pipeline {
    agent any

    triggers {
        pollSCM('* * * * *')
    }

    tools {
        maven 'maven3.9.14'
    }

    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }

        stage('Build') {
            steps {
                sh 'mvn clean package -DskipTests'
            }
        }

        stage('Collect Git Info') {
            steps {
                script {
                    env.GIT_BRANCH_NAME = sh(returnStdout: true, script: 'git rev-parse --abbrev-ref HEAD').trim()
                    env.GIT_LAST_COMMIT = sh(returnStdout: true, script: 'git log -1 --pretty=format:"%h %s (%an, %ad)" --date=iso').trim()
                }
            }
        }
    }

    post {
        success {
            script {
                if (env.DINGTALK_WEBHOOK?.trim()) {
                    env.BUILD_NOTIFY_MSG = """test Jenkins build succeeded
Job: ${env.JOB_NAME}
Branch: ${env.GIT_BRANCH_NAME}
Last commit: ${env.GIT_LAST_COMMIT}
Build number: #${env.BUILD_NUMBER}
Build URL: ${env.BUILD_URL}""".stripIndent().trim()

                    sh '''
payload=$(cat <<EOF
{"msgtype":"text","text":{"content":"${BUILD_NOTIFY_MSG}"}}
EOF
)
curl -sS -X POST "$DINGTALK_WEBHOOK" \
  -H "Content-Type: application/json" \
  -d "$payload" >/dev/null
'''
                } else {
                    echo 'DINGTALK_WEBHOOK is empty, skip DingTalk notification.'
                }
            }
        }
    }
}

7)打包工程ansible

8)hosts.ini

复制代码
[abc_workers]
192.168.3.204
192.168.3.205

[abc_workers:vars]
ansible_user=root
ansible_port=22
ansible_become=true
ansible_ssh_private_key_file=/var/jenkins_home/.ssh/id_ed25519

9)deploy-abc.yml // 免密登录,jdk自动安装等

复制代码
---
- name: Deploy abc Spring Boot app
  hosts: abc_workers
  become: true
  vars:
    app_name: abc
    app_user: abc
    app_group: abc
    app_dir: /opt/{{ app_name }}
    app_jar: "{{ app_dir }}/{{ app_name }}.jar"
    log_dir: /var/log/{{ app_name }}
    log_file: "{{ log_dir }}/{{ app_name }}.log"
    app_ports:
      - 8080
    app_port: "{{ app_ports[0] }}"
    java_opts: "-Xms256m -Xmx512m"
    java_package_by_family:
      RedHat: java-21-openjdk-devel
      Debian: openjdk-21-jdk-headless

  pre_tasks:
    - name: Validate artifact path argument
      delegate_to: localhost
      become: false
      run_once: true
      ansible.builtin.assert:
        that:
          - artifact_path is defined
          - artifact_path | length > 0
        fail_msg: "You must pass artifact_path, for example: -e artifact_path=/path/to/abc.jar"

    - name: Check artifact exists on Jenkins node
      delegate_to: localhost
      become: false
      run_once: true
      ansible.builtin.stat:
        path: "{{ artifact_path }}"
      register: local_artifact_stat

    - name: Fail when artifact does not exist
      delegate_to: localhost
      become: false
      run_once: true
      ansible.builtin.fail:
        msg: "Artifact not found: {{ artifact_path }}"
      when: not local_artifact_stat.stat.exists

  tasks:
    - name: Check Java runtime is installed
      ansible.builtin.command: java -version
      register: java_check
      changed_when: false
      failed_when: false

    - name: Check jps command is available
      ansible.builtin.command: jps -l
      register: jps_check
      changed_when: false
      failed_when: false

    - name: Install Java 21 JDK when java or jps is missing
      ansible.builtin.package:
        name: "{{ java_package_by_family[ansible_facts.os_family] }}"
        state: present
      when: java_check.rc != 0 or jps_check.rc != 0

    - name: Re-check Java runtime after install
      ansible.builtin.command: java -version
      register: java_check_after_install
      changed_when: false
      when: java_check.rc != 0 or jps_check.rc != 0

    - name: Re-check jps after install
      ansible.builtin.command: jps -l
      register: jps_check_after_install
      changed_when: false
      when: java_check.rc != 0 or jps_check.rc != 0

    - name: Fail when Java 21/JPS installation did not succeed
      ansible.builtin.fail:
        msg: "Java/JPS install failed on {{ inventory_hostname }}. Please check package repositories and network."
      when:
        - java_check.rc != 0 or jps_check.rc != 0
        - java_check_after_install.rc != 0 or jps_check_after_install.rc != 0

    - name: Ensure app group exists
      ansible.builtin.group:
        name: "{{ app_group }}"
        state: present

    - name: Ensure app user exists
      ansible.builtin.user:
        name: "{{ app_user }}"
        group: "{{ app_group }}"
        shell: /sbin/nologin
        create_home: false
        system: true
        state: present

    - name: Ensure app directory exists
      ansible.builtin.file:
        path: "{{ app_dir }}"
        state: directory
        owner: "{{ app_user }}"
        group: "{{ app_group }}"
        mode: "0755"

    - name: Ensure log directory exists
      ansible.builtin.file:
        path: "{{ log_dir }}"
        state: directory
        owner: "{{ app_user }}"
        group: "{{ app_group }}"
        mode: "0755"

    - name: Check firewalld CLI exists
      ansible.builtin.command: bash -lc "command -v firewall-cmd"
      register: firewall_cmd_check
      changed_when: false
      failed_when: false

    - name: Open app ports in firewalld
      ansible.builtin.command: firewall-cmd --permanent --add-port={{ item }}/tcp
      register: firewall_add_port
      changed_when: "'success' in firewall_add_port.stdout"
      loop: "{{ app_ports }}"
      when: firewall_cmd_check.rc == 0

    - name: Reload firewalld
      ansible.builtin.command: firewall-cmd --reload
      changed_when: false
      when: firewall_cmd_check.rc == 0

    - name: Upload app jar
      ansible.builtin.copy:
        src: "{{ artifact_path }}"
        dest: "{{ app_jar }}"
        owner: "{{ app_user }}"
        group: "{{ app_group }}"
        mode: "0644"
      notify: Restart app

    - name: Render systemd service
      ansible.builtin.template:
        src: ../templates/abc.service.j2
        dest: "/etc/systemd/system/{{ app_name }}.service"
        owner: root
        group: root
        mode: "0644"
      notify:
        - Reload systemd
        - Restart app

    - name: Ensure service enabled
      ansible.builtin.systemd:
        name: "{{ app_name }}"
        enabled: true

  handlers:
    - name: Reload systemd
      ansible.builtin.systemd:
        daemon_reload: true

    - name: Restart app
      ansible.builtin.systemd:
        name: "{{ app_name }}"
        state: restarted

10)abc.service.j2

复制代码
[Unit]
Description={{ app_name }} Spring Boot Service
After=network.target

[Service]
Type=simple
User={{ app_user }}
WorkingDirectory={{ app_dir }}
ExecStart=/usr/bin/java {{ java_opts }} -jar {{ app_jar }} --server.port={{ app_port }}
SuccessExitStatus=143
Restart=always
RestartSec=5
StandardOutput=append:{{ log_file }}
StandardError=append:{{ log_file }}

[Install]
WantedBy=multi-user.target

11)ansible.cfg

复制代码
[defaults]
inventory = inventory/hosts.ini
host_key_checking = False
retry_files_enabled = False
stdout_callback = yaml
interpreter_python = auto_silent

[ssh_connection]
pipelining = True

12)ansible工程下的jenkinsfile // 实现真正的打包

复制代码
pipeline {
    agent any

    tools {
        maven 'maven3.9.14'
    }

    parameters {
        string(name: 'ABC_REPO_URL', defaultValue: 'http://192.168.3.203:3000/root/abc.git', description: 'abc repository URL, for example: http://<gitea-host>:3000/<user>/abc.git')
        string(name: 'ABC_BRANCH', defaultValue: 'main', description: 'abc branch to deploy')
        string(name: 'ABC_GIT_CREDENTIALS_ID', defaultValue: 'ad6a2c23-e0fc-41cd-ae2c-40444bc1ac51', description: 'Jenkins credentialsId for abc git repo (Username with password or token)')
        booleanParam(name: 'AUTO_BOOTSTRAP_SSH', defaultValue: true, description: 'Automatically setup SSH key-based login to worker nodes before deploy')
        string(name: 'WORKER_BOOTSTRAP_CREDENTIALS_ID', defaultValue: 'root', description: 'Jenkins Username/Password credentialsId for initial worker SSH login')
        string(name: 'ANSIBLE_INVENTORY', defaultValue: 'inventory/hosts.ini', description: 'Ansible inventory path in ansible repo')
        string(name: 'APP_PORT', defaultValue: '8080', description: 'Spring Boot service port')
    }

    stages {
        stage('Validate Parameters') {
            steps {
                script {
                    if (!params.ABC_REPO_URL?.trim()) {
                        error 'ABC_REPO_URL is required.'
                    }
                }
            }
        }

        stage('Checkout abc') {
            steps {
                dir('abc-src') {
                    git branch: params.ABC_BRANCH, credentialsId: params.ABC_GIT_CREDENTIALS_ID, url: params.ABC_REPO_URL
                }
            }
        }

        stage('Build abc') {
            steps {
                dir('abc-src') {
                    sh 'mvn clean package -DskipTests'
                    script {
                        env.ARTIFACT_RELATIVE_PATH = sh(
                            returnStdout: true,
                            script: "ls -1 target/*.jar | grep -v 'original-' | head -n 1"
                        ).trim()
                        if (!env.ARTIFACT_RELATIVE_PATH) {
                            error 'No jar artifact found under target/.'
                        }
                    }
                }
            }
        }

        stage('Check Runtime') {
            steps {
                sh '''
set -e
python3 --version
ansible-playbook --version
sshpass -V
'''
            }
        }

        stage('Bootstrap SSH Trust') {
            when {
                expression { return params.AUTO_BOOTSTRAP_SSH }
            }
            steps {
                withCredentials([usernamePassword(credentialsId: params.WORKER_BOOTSTRAP_CREDENTIALS_ID, usernameVariable: 'SSH_USER', passwordVariable: 'SSH_PASS')]) {
                    sh '''
set -e
mkdir -p /var/jenkins_home/.ssh
chmod 700 /var/jenkins_home/.ssh
if [ ! -f /var/jenkins_home/.ssh/id_ed25519 ]; then
  ssh-keygen -t ed25519 -f /var/jenkins_home/.ssh/id_ed25519 -N ''
fi
chmod 600 /var/jenkins_home/.ssh/id_ed25519
chmod 644 /var/jenkins_home/.ssh/id_ed25519.pub

for host in 192.168.3.204 192.168.3.205; do
  sshpass -p "$SSH_PASS" ssh-copy-id \
    -i /var/jenkins_home/.ssh/id_ed25519.pub \
    -o StrictHostKeyChecking=no \
    -o UserKnownHostsFile=/dev/null \
    "$SSH_USER@$host"
done
'''
                }
            }
        }

        stage('Deploy to workers') {
            steps {
                script {
                    env.ARTIFACT_ABS_PATH = sh(
                        returnStdout: true,
                        script: "readlink -f abc-src/${env.ARTIFACT_RELATIVE_PATH}"
                    ).trim()
                }

                sh '''
ansible-playbook -i "${ANSIBLE_INVENTORY}" playbooks/deploy-abc.yml \
  -e "artifact_path=${ARTIFACT_ABS_PATH}" \
  -e "app_port=${APP_PORT}"
'''
            }
        }
    }

    post {
        success {
            script {
                if (env.DINGTALK_WEBHOOK?.trim()) {
                    env.DEPLOY_NOTIFY_MSG = """test Jenkins deploy succeeded
Job: ${env.JOB_NAME}
Build number: #${env.BUILD_NUMBER}
ABC branch: ${params.ABC_BRANCH}
Targets: 192.168.3.204, 192.168.3.205
Build URL: ${env.BUILD_URL}""".stripIndent().trim()

                    sh '''
payload=$(cat <<EOF
{"msgtype":"text","text":{"content":"${DEPLOY_NOTIFY_MSG}"}}
EOF
)
curl -sS -X POST "$DINGTALK_WEBHOOK" \
  -H "Content-Type: application/json" \
  -d "$payload" >/dev/null
'''
                } else {
                    echo 'DINGTALK_WEBHOOK is empty, skip DingTalk notification.'
                }
            }
        }
    }
}

13)成功

14)并且2个机器也能顺利访问

相关推荐
SPC的存折2 小时前
10、Ansible 生产级故障排查与运维最佳实践
linux·运维·ansible
Jp7gnUWcI2 小时前
.NET Win32磁盘动态卷触发“函数不正确”问题排查
运维·服务器·.net
RisunJan2 小时前
Linux命令-ncftp(增强的的FTP工具)
linux·运维
w61001046611 小时前
CKAD-2026-Ingress
运维·k8s·ckad
wb0430720113 小时前
使用 Java 开发 MCP 服务并发布到 Maven 中央仓库完整指南
java·开发语言·spring boot·ai·maven
zzzsde14 小时前
【Linux】库的制作和使用(3)ELF&&动态链接
linux·运维·服务器
CQU_JIAKE14 小时前
4.3【A]
linux·运维·服务器
AI周红伟14 小时前
OpenClaw是什么?OpenClaw能做什么?OpenClaw详细介绍及保姆级部署教程-周红伟
大数据·运维·服务器·人工智能·微信·openclaw
Elastic 中国社区官方博客14 小时前
当 TSDS 遇到 ILM:设计不会拒绝延迟数据的时间序列数据流
大数据·运维·数据库·elasticsearch·搜索引擎·logstash