前两天有同学来问我如何获得 ansible 远程执行的结果。在这里我整理一下思路。

首先我们要认识一点:你不能单一地依赖 ansible 完成所有事情,配合 shell 脚本一起用会更有效率更灵活。

以下就是我们的一个 role 做例子。它的作用是在 builder 机器上编译 jar 包,然后推送到 3 个 node 上,再逐个更新 node 上的 java 程序。只提 3 个关键文件的内容。

目录结构:
roles/build.console
├── defaults
│   └── main.yml
├── tasks
│   ├── build.yml
│   ├── main.yml
│   └── startup.yml
└── templates
    ├── clean.sh.j2
    ├── rollback.sh.j2
    ├── send.sh.j2
    └── startup.sh.j2

send.sh.j2 模板内容,在 builder 上创建并执行。
#!/usr/bin/env bash
MD5=`md5sum ./target/{{ prj_name }}-0.0.1-SNAPSHOT.jar | awk $'{ print $1 }'`
DATE=`date +%F`

{% for item in groups[region_name] %}
{% if 'node' in item %}
rsync -avzP ./target/{{ prj_name }}-0.0.1-SNAPSHOT.jar {{ item }}:~/dist/{{ region_name }}/{{ region_name }}-{{ prj_name }}-${DATE}-${MD5}.jar
ssh {{ item }} touch /tmp/{{ prj_name }}_sended
{% endif %}
{% endfor %}

注意:发送成功后,我还会在目标机器上 touch 一个 {{ prj_name }}_sended 的文件,后续的 startup.yml 就可以用来判断是否可以执行了。毕竟每次要更新 24 台服务器,步骤和时间能省则省。


startup.sh.j2 模板内容,在 node 上创建并执行
#!/usr/bin/env bash

# 检查 logs 目录
if [[ ! -d logs ]]; then
  mkdir logs
fi

# 获取最新的 jar 文件名
PKG=`ls -t {{ region_name }}/*{{ prj_name }}*jar | head -n1`

# 杀掉当前运行的进程
kill -9 $(jps | grep {{ prj_name }} | awk $'{ print $1 }')
sleep 5

# 启动新进程
nohup java -Djava.security.egd=file:/dev/./urandom -Dlogging.file=/var/logs/{{ prj_name }}.log -Xms2048m -Xmx2048m -jar {{ ansible_user_dir }}/${PKG} >/dev/null &
sleep 5

# 设置定时器, 时间到了自动中止 tail 命令
sleep 15 & timerPid=$!

# 读取日志
tail -f --pid=$timerPid -n0 ${PWD}/logs/{{ prj_name }}.log

startup.yml 内容
- name: Checking send statu
  stat:
    path: '/tmp/{{ prj_name }}_sended'
  register: sended

- block:
    - name: Create '{{ prj_name }}' scripts
      template:
        src: '../templates/{{ item }}.sh.j2'
        dest: '~/dist/{{ prj_name }}_{{ item }}.sh'
        mode: 0700
      with_items:
        - startup
        - rollback
        - clean
    - name: Run '{{ prj_name }}' startup.sh
      shell: '~/dist/{{ prj_name }}_startup.sh'
      args:
        chdir: '~/dist/'
      register: startup
    - name: Get '{{ prj_name }}' log file
      fetch:
        src: '/var/logs/{{ prj_name }}.log'
        dest: 'logs/{{ prj_name }}-{{ region_name }}.log'
        flat: yes
    - name: Roll back when '{{ prj_name }}' failed
      shell: '~/dist/{{ prj_name }}_{{ item }}.sh'
      args:
        chdir: '~/dist'
      with_items:
        - rollback
        - startup
      when: startup.stdout.find('ERROR') != -1
    - name: Clean '{{ prj_name }}' old files
      shell: '~/dist/{{ prj_name }}_clean.sh'
      args:
        chdir: '~/dist'
    - name: Remove '/tmp/{{ prj_name }}_sended'
      file:
        path: '/tmp/{{ prj_name }}_sended'
        state: absent
  when: sended.stat.exists == True

这里有 2 种输出结果:

  1. 一个是 startup.sh 里,java 启动命令里有 logging.file 参数,日志文件写入到磁盘。所以,启动完成后就可以通过 fetch 模块把日志文件取回到本地,后续查看。
  2. 另一个是执行 startup.sh 时,输出到终端的内容,通过 register 模块获取。只为一件事,回滚。

    一旦终端输出里捕捉到 ERROR 关键词,就触发 Roll back... 的 task。rollback.sh 里的内容很简单,就是把最新的文件删掉,然后再执行一次 startup.sh 就恢复原来的版本了。

获取到本地的 log 文件
logs/
├── openstack-develop-xn2.log
├── openstack-finance-xn1.log
├── openstack-finance-xn2.log
├── openstack-hd1.log
├── openstack-hd2.log
├── openstack-hd3.log
├── openstack-hd4.log
├── openstack-sh1.log