一、前言

        为了使用Jenkins实现混合部署,经调研采用当前热门且社区强大的ansible工具,以下是环境搭建过程。下面是本人在windows10 1803操作系统上的实验记录,操作系统不同,实验步骤可能略有区别,各位小伙伴要特别留意啊。

二、实验步骤

        1.环境搭建

        重要的事情说三遍!!!网络类型必须为专用网,且关闭防火墙。
        鉴于ansible服务端(一般部署在linux发行版操作系统上)与windows操作系统进行数据传输,使用到winrm服务。因为win10中已经内置了winrm,因此只需要开启winrm服务并完成相关配置,步骤如下:

windows ansible 目录 ansible winrm_jar

图 winrm配置示意图

         传送门在此^-^         

winrm service 
winrm enumerate winrm/config/listener
winrm quickconfig 
winrm set winrm/config/service '@{AllowUnencrypted="true"}'

        2.增加配置

        CI/CD流水线作业依赖于ansible-playbook脚本,需要增加ansible主机映射,主要包括加入新的IP地址,用户凭证和传输方式等配置。Ansible该文件,默认位于控制机/etc/ansible/hosts路径下,根据该文件中其它范式配置,加入需要做CI的主机名称和变量的相关配置,如下:

#受控机主机ip
[host1]
192.168.70.51
#受控机主机相关配置
[host1:vars]
#用户名密码
ansible_ssh_user='administrator'
ansible_ssh_pass='Yanfa_1304'
#默认通信端口
ansible_ssh_port=5985
ansible_connection='winrm'
ansible_winrm_transport='ntlm'
#不使用身份有效性验证,仅使用用户名和密码
ansible_winrm_server_cert_validation=ignore

图 配置ansible受控机信息

        3.编写ansible脚本

        可以出于安全性考虑(只是建议啊),将ansible脚本内容对jenkins配置管理人员黑盒。本实验调用分为三层,第一层为调用层,第二层中间层,第三层为业务层,仅供参考。

        首先,调用层脚本放置于jenkins配置中,其中使用到了jenkins管理员定义的全局变量,如下:

/home/jenkins/ci/${ANSIBLE_HOST_NAME_VXSIP}/are-agent/exec-are-agent.sh  ${ANSIBLE_HOST_NAME_ARE}  ${JOB_NAME}

图 调用层脚本

          然后,中间层可以配置一些公共的非业务逻辑,如下:

#!/bin/sh
echo "当前项目路径:/home/jenkins/.jenkins/workspace/$2"
echo "当前ansible主机:$1"
ansible-playbook -e "host_name=$1 service_name=project1 project_name=$2" /home/jenkins/ci/$1/common-server.yml

图 中间层脚本

        最后,是业务层,是ansible-playbook脚本,主要是CI/CD相关业务逻辑。

---
- hosts: "{{ host_name }}"   #目标机器的主机名
  gather_facts: no  #不收集facts
  
  # 变量引用
  vars_files: 
       - ./common_vars.yml
  vars:
       - exec_path: "{{ remote_server_exec_path }}/{{ service_name }}"
       - src_path: "{{ compile_file_path }}/{{ project_name }}/target"
       - src_conf_path: "{{ compile_file_path }}/{{ project_name }}/deploy/windows-undocker"
  tasks:
    
    #获取当前时间
  - name: get current timestamp 
    win_shell: "Get-Date -Format 'yyyyMMddHHmmssfff'"
    register: current_date 
    
   
    #停止和卸载远程服务(如果存在*.exe并且存在该windows服务)
  - name: stop and uninstall remote {{ service_name }} server
    win_shell: 'if (( test-path {{ exec_path }}/{{ service_name }}.exe ) -and (Get-Service {{ service_name }} -ErrorAction SilentlyContinue))  { (sc.exe query {{ service_name }} |findstr /i  "state" |findstr /i "running") -and (net stop {{ service_name }}); {{ exec_path }}/{{ service_name }}.exe uninstall; }'
    #ignore_errors: True

    
    #判断是否存在dependency,存在则操作
  - name: justify if exists {{ src_path }}/dependency folder 
    shell: 'ls {{ src_path }}/dependency'
    register: dependency_folder_exists_flag
    delegate_to: localhost
    ignore_errors: True
  #- debug: var=dependency_folder_exists_flag
    
    #拷贝target/dependency中的依赖到临时交换目录下面
  - name: copy dependency folder from path {{ src_path }}/dependency/ to exec path {{ remote_server_exec_path }}/dependency_{{ current_date.stdout_lines[0] }}
    win_copy: "src='{{ src_path }}/dependency/' dest='{{ remote_server_exec_path }}/dependency_{{ current_date.stdout_lines[0] }}/'"
    ignore_errors: True
    #succeeded 和 failed
    #忽略错误因为dependency可能不存在
    when: dependency_folder_exists_flag.failed  == false

    #如果控制机有dependency目录,而且受控机没有dependency目录,此时需要在受控机新建dependency目录
  - name: create dependency folder in path {{ remote_server_exec_path }}/ if it is not exists
    win_shell: 'if (( test-path {{ remote_server_exec_path }}/dependency ) -ne "True" )  {new-item -path "{{ remote_server_exec_path }}/" -name "dependency" -type directory }'
    ignore_errors: True
    #succeeded 和 failed
    #忽略错误因为dependency可能不存在
    when: dependency_folder_exists_flag.failed  == false


    #中转文件夹到dependency的文件合并
  - name: combine dependency folder from path {{ remote_server_exec_path }}/dependency_{{ current_date.stdout_lines[0] }}/ to  path {{ remote_server_exec_path }}/dependency/
    #win_shell: "if (Test-Path {{ remote_server_exec_path }}/dependency_{{ current_date.stdout_lines[0] }}) {copy-item -Force '{{ remote_server_exec_path }}/dependency_{{ current_date.stdout_lines[0] }}/*' '{{ remote_server_exec_path }}/dependency/'}"
    win_shell: 'if (Test-Path {{ remote_server_exec_path }}/dependency_{{ current_date.stdout_lines[0] }}) { $source_path="{{ remote_server_exec_path }}/dependency_{{ current_date.stdout_lines[0] }}/";$dest_path="{{ remote_server_exec_path }}/dependency/";$filelist=Get-ChildItem $source_path;$filelist | foreach {  Copy-Item -Path $source_path$_ -Destination $dest_path -Force };}'
   # win_shell: 'if (Test-Path {{ remote_server_exec_path }}/dependency_{{ current_date.stdout_lines[0] }}) { $source_path={{ remote_server_exec_path }}/dependency_{{ current_date.stdout_lines[0] }};$dest_path={{ remote_server_exec_path }}/dependency/;$filelist=Get-ChildItem $source_path;$filelist | foreach {  try{Copy-Item -Path $source_path$_ -Destination $dest_path -Force }catch{}};}'
    #当复制时,遇到被占用的jar包将会报错,但是该错误并不会影响后续其它文件的传输(本质为相同文件的略过)
    ignore_errors: True
    #register: combine_result
  #- debug: var=combine_result


    #删除用于中转的dependency临时文件夹
  - name: delete the temp exchange folder with {{ remote_server_exec_path }}/dependency_{{ current_date.stdout_lines[0] }} 
    win_shell: "if (Test-Path {{ remote_server_exec_path }}/dependency_{{ current_date.stdout_lines[0] }}) {Remove-Item '{{ remote_server_exec_path }}/dependency_{{ current_date.stdout_lines[0] }}' -recurse }"    
    
    
    
    #清空项目文件夹
  - name: clear {{ exec_path }} folder exclude {{ service_name }}.jar and {{ service_name }}_backup_*.jar
    win_shell: "if (Test-Path {{ exec_path }}) { Remove-Item '{{ exec_path }}/*' -Exclude ('{{ service_name }}.jar','{{ service_name }}_backup_*.jar') -recurse -ErrorAction SilentlyContinue; if (!$?) {break;}} "
    ignore_errors: True
    
    #检查是否存在当前服务执行文件,如果存在将会备份,如果不存在则会跳过备份task
  - name: backup flie {{ service_name }}.jar if it exists
    win_shell: "if (Test-Path {{ exec_path }}/{{ service_name }}.jar) {ren  {{ exec_path }}/{{ service_name }}.jar  {{ exec_path }}/{{ service_name }}_backup_{{ current_date.stdout_lines[0] }}.jar}"    


    #判断是否存在bootstrap.yml,存在则操作
  - name: justify if exists file {{ src_path }}/bootstrap.yml  
    shell: 'ls {{ src_conf_path }}/bootstrap.yml'
    register: bootstrap_yml_exists_flag
    delegate_to: localhost
    ignore_errors: True
 # - debug: var=bootstrap_yml_exists_flag
    
    #获取远程主机的ip地址
  - name: get remote host ipv4 address if it is a dual network it may failed 
    win_shell: '(ipconfig|select-string "IPv4"|out-string).Split(":")[-1].trim("")'
    register: remote_host_ip_address
    ignore_errors: True
    when: bootstrap_yml_exists_flag.failed  == false

    #更改bootstrap.yml中的127.0.0.1为远程主机的网卡地址
  #- debug: 'msg="replace 127.0.0.1 to remote network address {{ remote_host_ip_address.stdout_lines[0] }} with flie {{ exec_path }}/bootstrap.yml"'
  - name: replace 127.0.0.1 to remote network address {{ remote_host_ip_address.stdout_lines[0] }} with flie {{ src_conf_path }}/bootstrap.yml
    replace:
      path: '{{ src_conf_path }}/bootstrap.yml'
      regexp: '127.0.0.1'
      replace: '{{ remote_host_ip_address.stdout_lines[0] }}'
    delegate_to: localhost
    ignore_errors: True
    when: bootstrap_yml_exists_flag.failed  == false
    

    #拷贝项目目录下的所有配置文件到远程目录下
  - name: copy configuration flies from path {{ src_conf_path }} to exec path {{ exec_path }}/
    win_copy: "src={{ src_conf_path }}/  dest={{ exec_path }}/"
    

    #拷贝可执行文件到远程目录下
  - name: copy {{ service_name }}.jar flie from path {{ src_path }}/{{ service_name }}-{{ version }}.jar to exec path {{ exec_path }}/
    win_copy: "src={{ src_path }}/{{ service_name }}-{{ version }}.jar dest={{ exec_path }}/"
    

    #去掉执行文件当前服务版本号
  - name: remove server exec flie version {{ exec_path }}/{{ service_name }}-{{ version }}.jar to {{ exec_path }}/{{ service_name }}.jar
    win_shell: "ren {{ exec_path }}/{{ service_name }}-{{ version }}.jar  {{ exec_path }}/{{ service_name }}.jar"
     
     
    #安装和启动服务
  - name: install and startup remote {{ service_name }} server 
    win_shell: 'if (( test-path {{ exec_path }}/{{ service_name }}.exe ) -eq "True")  { Get-Service {{ service_name }} -ErrorAction SilentlyContinue; if ( !$? ) { {{ exec_path }}/{{ service_name }}.exe install;} else {(sc.exe query {{ service_name }} |findstr /i  "state" |findstr /i "running") -and (net stop {{ service_name }}); ({{ exec_path }}/{{ service_name }}.exe uninstall) -and ({{ exec_path }}/{{ service_name }}.exe install);} net start {{ service_name }};  }'

三、实验验证

        环境和脚本搭建了这么久,到底行不行,验证一下。执行jenkins job,一方面,我们可以观察构建日志,如图,即可证明脚本执行成功。另一方面,我们可以去受控机查看脚本执行结果。

windows ansible 目录 ansible winrm_ci_02

        以上即是本人在jenkins中集成ansible的方案和实操步骤,使用不当还请各位朋友指正!