3.1 Playbooks 高级一

Handlers 触发器

这个东西类似一个触发器,比如这么一个场景,我们把nginx conf配置文件拷贝到目标机器,那么当这个配置文件更新后,需要重启nginx,类似这种需求我们就拿 Ansible handlers 来做

    - name: Copy configuration files
      copy:
        src: xtest1.conf
        dest: /etc/nginx/conf.d/xtest1.conf
      notify:
         - restart_nginx

  handlers:
    - name: restart_nginx
      service:
        name: nginx
        state: restarted

完整示例

https://gitee.com/as4k/ysansible/blob/master/handlers/main.yml

环境变量增加与修改

有多种场景,我们需要改动目标机器的一些变量(或配置),比如关闭selinux,这种操作若是写成shell脚本,可以像下面这样

sed 's#^SELINUX=.*#SELINUX=disabled#g' /etc/selinux/config

Ansible也提供了类似的用法,如下面这样

- name: Ensure SELinux is set to disabled mode
    lineinfile:
    path: /etc/selinux/config
    regexp: '^SELINUX='
    line: SELINUX=disabled

如果需要改动一个文本文件里的某一行,或者增加一行,基本用lineinfile模块就妥

比如我们在~/.bash_profile增加环境变量,类似如下

    - name: Add an environment variable to the remote user's shell. 
      lineinfile: 
        path: "~/.bash_profile" 
        regexp: "^ENV_VAR="
        line: "ENV_VAR=value_helloworld"

上面这个模块的含义是保证~/.bash_profile,有这一行ENV_VAR=value_helloworld,有保持不动,没有给增加上去

另外需要一提的是,不同的环境变量最好放到一个单独文件中去,这样最方便维护,比如在CenOS7中环境变量可以放在/etc/profile.d,这样我们简单的copy文件即可,易于阅读和维护

    - name: Copy java_env
      copy:
        src: java_env.sh
        dest: /etc/profile.d/java_env.sh

虽然Ansible有很多高级牛逼的用法,但在生产环境中使用Ansible不是为了炫技,能用简单的模块解决问题,则用简单的模块

完整示例

https://gitee.com/as4k/ysansible/blob/master/env_variables/main.yml

在剧本中使用变量

不管在编程语言中亦或是Ansible中,总有那么一些东西是需要反复被使用,此时我们应该将这些东西提出成变量,以方便维护和修改,比如我们写一个用来安装wordpress(一款PHP写的博客软件)的剧本,那么软件版本号就比较适合做成变量,这样以后我们需要升级或者是把剧本给别人用,拿过来只要更改一下版本号即可,不需要去剧本中对应的地方都手动修改一下

一般拿到一个剧本,首先就是看有哪些变量,这些变量相当于对外访问的接口,如果一些东西比如软件的版本号,软件的数据存放目录,这些明显需要根据不同的场景传递不同的变量来使用的,通常都建议提成变量使用

变量命名规则

开头是 [A-Za-z]
其它地方可以包含 下划线_  数字[0-9]

合法的变量示例:foo, foo_bar, foo_bar_5
不合法的变量示例:_foo, foo-bar, 5_foo_bar, foo.bar

内嵌在剧本中的变量

  vars:
    http_port: 80
https://gitee.com/as4k/ysansible/blob/master/variables/playbook_var_in_file.yml

单独写在YAML文件里的变量

  vars_files: 
    - vars.yml
https://gitee.com/as4k/ysansible/blob/master/variables/playbook_var_file.yml    

直接在命令行中添加的变量

# ansible-playbook playbook_var_cmd.yml  --extra-vars "foo=bar" --limit 192.168.31.100 
# ansible-playbook playbook_var_cmd.yml  --extra-vars "@vars.yml" --limit 192.168.31.100
https://gitee.com/as4k/ysansible/blob/master/variables/playbook_var_cmd.yml

这几种方式使用变量有优先级的区别,在命令行中使用的变量优先级最高,详情可以参考下面的文档

https://docs.ansible.com/ansible/latest/user_guide/playbooks_variables.html#variable-precedence-where-should-i-put-a-variable

如果仅仅是用剧本来组织Ansible(比较初级,后面我们还会介绍roles),那么变量少内嵌到playbook里,变量多独立成一个或多个文件即可,建议少用--extra-vars 形式,除非拿来测试,因为这种形式不好追踪,而写到文件里,通过git等代码管理工具即可追踪

注册变量 register 捕获命令输出

有些场景下,比如我们要在目标机器安装一个软件,但是安装之前我们需要执行一个命令,动态判断目标机器的状态,假设有A、B、C这三种状态,而每种状态要执行的东西不一样,因此有必要先把状态(也就是变量)保存下来,备用,如下面这样

    - name: Register the output of the 'uptime' command. 
      command: uptime
      register: system_uptime

    - name: Print a simple message if a command resulted in a change. 
      debug: msg="Command resulted in a change!"
      when: system_uptime.changed

https://gitee.com/as4k/ysansible/blob/master/registered_var/main1.yml

某些时候,我们需要从目标配置文件中把某个变量读取出来,记录下来,以便后续使用,比如把目标机器的机房信息读出来,可以利用register实现,如下面这样

    # ignore_errors 用来防止playbook被打断
    - name: Run a shell command and register its output as a variable
      shell: cat /etc/redhat-release
      register: foo_result
      ignore_errors: true

    - name: Run a shell command using output of the previous task
      shell: echo "ok" > /tmp/tmp.txt
      when: foo_result.rc == 0

参考示例:https://gitee.com/as4k/ysansible/blob/master/env_variables/main.yml

变量的类型以及访问变量

我们知道Ansible的主要开发语言是python,很多python的数据类型在Ansible里一样有,比如数组、字典等,Ansible在模板变量替换方面使用的是jinjia语法

最基础

    - name: test1
      debug:
        msg: "{{ foo }}"

不加双引号会报错

数组

foo3_list: 
  - one
  - two
  - three
==========
    - name: test3
      debug:
        msg: "{{ foo3_list[1] }}"

    - name: test4
      debug:
        msg: "{{ foo3_list|first }}"

数组从0开始
{{ foo3_list|first }}  与 {{ foo3_list[1] }} 等价, |first 是jinjia过滤器的语法

字典

foo4_dict:
  xiaoming: 186
  xiaowang: 187
  xiaoli: 188
============
    - name: test4
      debug:
        msg: "{{ foo4_dict.xiaoming }}"

    - name: test5
      debug:
        msg: "{{ foo4_dict['xiaoli'] }}"

    - name: test6
      debug:
        msg: "{{ ansible_eth0['ipv4']['address'] }}"

{{ foo4_dict.xiaoming }} 与 {{ foo4_dict['xiaoli'] }} 等价
如果字典的key有些什么特殊符号之类,用第2种

变量的类型还可以进行复杂的嵌套,但是如无必要尽量使用简单的数据类型

完整示例

https://gitee.com/as4k/ysansible/blob/master/acccessing_var/main1.yml

主机清单里的变量 Inventory variables

形式如下面这样

cat /etc/ansible/hosts
[allservers]
192.168.31.106
192.168.31.100
192.168.31.101
192.168.31.102
192.168.31.103
192.168.31.104
192.168.31.105
192.168.31.107
192.168.31.108

[webservers]
192.168.31.100 os=centos73 admin_user=jane
192.168.31.101 os=centos77 admin_user=jack
192.168.31.102 os=centos78

[webservers:vars]
dns1=223.5.5.5
dns2=8.8.8.8
admin_user=admin

在这里定义的变量跟在剧本里定义的变量,逻辑上无太多区别,都可以在剧本中使用,一般只有和指定机器或者指定机器组强相关的变量,如一批机器大家用的NTP时间服务器都在国内,但其中一台机器特殊,使用的是国外时间服务器,那么即可在主机清单使用变量进行标识

不过变量太多都写在主机清单里则比较乱,更适合的方案是独立出来,即 “主机变量和组变量”

主机变量和组变量 Host and Group variables

观察下面的目录结构

[root@192_168_31_106 /data/ysansible]# tree host_and_group_variables/
host_and_group_variables/
├── group_vars
│   └── webservers
├── hosts
├── host_vars
│   ├── 192.168.31.100
│   ├── 192.168.31.101
│   └── 192.168.31.102
├── main1.yml
└── README.md
# 对应线上代码在 https://gitee.com/as4k/ysansible/tree/master/host_and_group_variables

hosts即主机清单文件,实际执行的时候我们移动到当前目录,使用-i参数指定这个主机清单,如

ansible-playbook main1.yml -i hosts

group_varshost_vars文件夹为固定目录,并且必须和主机清单处在同一级目录里,其中group_vars下面的文件名和主机清单里的组名对应,host_vars目录下的文件名和主机清单里的hosts对应(可能是IP也可能是域名),分别代表该组机器共用的变量,以及单个机器拥有的变量,hosts_vars优先级高于group_vars

另外还有一个特殊的组名(文件名)all,代表全部机器(组)都可以使用的变量

Facts 系统变量收集

Linux操作系统有很多基础的信息,诸如内存、CPU、操作系统版本、内网IP地址等,这些信息在我们部署服务等时候经常也能用到,Ansible称呼这些东西为facts,这个功能默认是开启的,我们可以直接使用

---
- hosts: all
  gather_facts: yes
  # gather_facts: no
  #默认是yes
  #关掉信息收集可以提升性能,但不能再使用相关变量

  tasks:

    - name: get ip address
      debug:
        msg: "{{ ansible_eth0['ipv4']['address'] }}"

如果我们想看到全部可以使用的facts,可以使用下面的命令

ansible 192.168.31.100 -m setup > ansible_setup.json

CentOS7收集的全部系统变量,参考如下:

https://gitee.com/as4k/ysansible/blob/master/facts/ansible_setup.json

参考示例:https://gitee.com/as4k/ysansible/blob/master/facts/main.yml

我们也可以自己手动增加系统变量,如下是一个使用shell命令获取IP地址的示例

    - name: Get host IP address.
      shell: >
          prefix=`/sbin/ip route | awk '/default/ { print $3 }' | sed -r 's#\.[0-9]+$##g'`;
          hostname -I | egrep -o "$prefix\.[0-9]+"
      register: host_ip
      changed_when: false

    - name: Set host_ip_address variable.
      set_fact:
        host_ip_address: "{{ host_ip.stdout }}"

参考示例:https://gitee.com/as4k/ysansible/blob/master/facts/set_fact.yml

内置的主机清单变量

这方面我们介绍一个ansible_host

    - name: ansible_host
      debug:
        msg: "echo {{ ansible_host }}"

这个内置的变量专门用来获取主机清单里的IP地址(当然也可能是域名),因为在生产环境中通常机器的网卡不止一个,内网IP地址也不止一个,但一般会有一个主要使用的内网IP地址,此时如果我们使用上一小节的facts来收集IP地址则会得到多个,如何区分哪个IP是我们主要使用的IP地址,就有点麻烦。因此我们直接使用ansible_host这个内置的主机清变量,这样只需我们自己把主要使用的IP地址放到主机清单里即可

参考示例:https://gitee.com/as4k/ysansible/blob/master/centos7_init/centos7_init.yml

Vault 安全加密

有些时候我们的playbook里有些敏感信息,类似数据库root密码这种,不想给别人看到,也不想直接同步到git仓库上去,此时比较简单的办法是把相关密码等敏感信息都独立出来,比如直接放到当前项目之外,用的时候copy到目标机器。不过这种方法不太方便管理,也比较乱,破坏原有playbook的完整性,这种情况下我们可以考虑使用Ansible Vault加密功能

看一个简单的示例

cat vars.yml
my_password: hello123456

=============

#执行加密指令
ansible-vault encrypt vars.yml #系统会要求输入密码,需要记住这个密码
New Vault password: 123456
Confirm New Vault password: 123456
Encryption successful

=============

cat vars.yml
$ANSIBLE_VAULT;1.1;AES256
37613864313965393631653339356633376666386537353338613865373863316562396461303366
3161643662343963316534396365643265383562303862360a363662636437313033646333363339
30333232656361363233626436383434386234646361303130353662633566373231333934656535
6561373861346434650a303635333632616263373631393566666363373334303162363161386338
39646333353137396133363831663166633366323731646339396261623432336361303039643164
34393365383932393236653733366362303766663833376433633864343638346338646430326664
35396630663162313238396262616539376361663534623132346634613865386135643335636138
34346431636430383366323235346662653337633739366564643637313164326662633933346236
32636538646537663933373836386234366337363661323334313635346633313266643365653165
6437643437666365363830353162666163643365313366353937

可以看到,原先的文本被直接修改加密了,下面我们看下如何使用

交互式输入密码执行

[root@192-168-31-106 /data/ysansible/vault]# ansible-playbook main1.yml  --limit 192.168.31.100 --ask-vault-pass
Vault password: 123456 (这个是我们执行ansible-vault encrypt vars.yml命令要求输入的密码,不是vars.yml里面记录的内容)
....

非交互式的直接运行

touch ~/.ansible/vault_pass.txt
echo "123456" > ~/.ansible/vault_pass.txt
chmod 0600  ~/.ansible/vault_pass.txt
ansible-playbook main1.yml  --limit 192.168.31.100 --vault-password-file ~/.ansible/vault_pass.txt

其它vault常用命令

解密成原始文本,需要输入密码
ansible-vault decrypt vars.yml 

编辑原始文件,需要输入密码
ansible-vault edit vars.yml

查看原始的文本
ansible-vault view vars.yml

总结来说,使用Ansible加密信息,我们需要:

  1. 将文本进行加密(文本内容无需改动)
  2. 把加密用的密码妥善保存起来
  3. 执行命令的时候增加--vault-password-file参数

引入加密还是需要一些额外的维护成本的,加密后的文本单独看基本无意义,大家根据情况取舍

参考示例:https://gitee.com/as4k/ysansible/blob/master/vault/main1.yml

YAML 换行语法

    - name: test3
      copy:
        content: >
            这里的内容完全是在一行的
            this is really a
            single line of text
            despite appearances
            末尾有换行
        dest: /root/test3.txt

    - name: test4
      copy:
        content: |
            这里的内容保持原样,是在多行的
            this is really a
            single line of text
            despite appearances
        dest: /root/test4.txt

    - name: test5
      copy:
        content: >-
            这里的内容完全是在一行的
            this is really a
            single line of text
            despite appearances
            末尾没有有换行
        dest: /root/test5.txt

    - name: test6
      copy:
        content: |-
            这里的内容保持原样,是在多行的
            this is really a
            single line of text
            despite appearances
            末尾没有换行
        dest: /root/test6.txt

参考示例: https://gitee.com/as4k/ysansible/blob/master/yaml_syntax/main1.yml

参考资料

https://docs.ansible.com/ansible/latest/user_guide/playbooks_variables.html#registering-variables
https://docs.ansible.com/ansible/latest/user_guide/playbooks_conditionals.html#playbooks-conditionals