云原生之 Ansible 篇(二)

2022-05-06 17:46:19 浏览数 (1)

文章目录
  • ansible gather_facts配置
    • json 文件方式
  • ansible 之 tag
  • ansible 之 block
  • ansible 之 stat
  • ansible 之 register
  • ansible 之 ignore_errors
  • ansible 之 set_fact
  • ansible 之 wait_for
  • 任务委托
  • ansible 之 lineinfile
    • 修改匹配行
    • 在匹配行前或后添加内容
      • 在匹配行前添加
      • 在匹配行后添加
    • 修改文件内容及权限
    • 删除一行内容
    • 文件存在则添加一行内容
    • 如果有匹配的行则修改该行,如果不匹配则添加
      • 参数backrefs,backup说明
    • 使用validate验证文件是否正确修改
  • ansible 之 package
  • ansible 之 滚动执行
  • ansible 之 只执行一次
  • ansible 之 设置环境变量

ansible gather_facts配置

ansible playbook 默认第一个 task 是 Gathering Facts 收集各主机的 facts 信息,以方便我们在 paybook 中直接引用 facts 里的信息。

如果不需要用到 facts 信息的话,可以设置 gather_facts: false,来省去 facts 采集这一步以提高 playbook 效率。

如果既想用 facts 信息,有希望能提高 playbook 的效率的话,可以采用 facts 缓存来实现。 facts 缓存支持多种方式:json 文件方式,redis 方式,memcache 方式等。各种方式的配置都是在 ansible.cfg 中配置。

json 文件方式
代码语言:javascript复制
gathering=smart
fact_caching_timeout=86400
fact_caching=jsonfile
fact_caching_connection=/path/to/ansible_fact_cache
1234

这里的 86400 单位为秒,表示缓存的过期时间。保存 facts 信息的 json 文件保存在 / path/to/ansible_fact_cache 下面,文件名是按照 inventory hostname 来命名的。


ansible 之 tag

你写了一个很长的playbook,其中有很多的任务,这并没有什么问题,不过在实际使用这个剧本时,你可能只是想要执行其中的一部分任务而已,或者,你只想要执行其中一类任务而已,而并非想要执行整个剧本中的全部任务,这个时候我们该怎么办呢?我们可以借助tags实现这个需求。

见名知义,tags可以帮助我们对任务进行’打标签’的操作,当任务存在标签以后,我们就可以在执行playbook时,借助标签,指定执行哪些任务,或者指定不执行哪些任务了,这样说可能不够直观,我们来看一个小示例(为了方便示例此处只写3个任务进行举例)。

代码语言:javascript复制
---
- hosts: test70
  tasks:
  - name: task1
    tags: t1

  - name: task2
    tags: t2

  - name: task3
    tags: t3

如上例所示,上例的play中有3个task,每个task都有对应的tags,假如在执行上述playbook时,我们只想执行task2,该怎样执行呢?我们可以使用如下命令

代码语言:javascript复制
ansible-playbook --tags=t2 testtag.yml

如你所见,可以使用–tags选项指定某个标签,当指定标签后,只有标签对应的任务会被执行,其他任务都不会被执行,执行上述命令后,只有task2会执行,因为task2的标签值为t2,task1和task3都不会执行,这样就达到了只执行playbook中部分任务的目的。

借助标签,除了能够指定”需要执行的任务”,还能够指定”不执行的任务”,示例命令如下。

代码语言:javascript复制
ansible-playbook --skip-tags='t2' testtag.yml

我们可以使用 –skip-tags选项指定”不执行的任务”,执行上述命令后,task1和task3会执行,task2不会执行,因为我们已经在命令中指定了’跳过’标签t2所对应的任务,相当于使用了’排除法’,t2对应的任务被排除了,其他任务都会执行。 其实,我们可以为每个任务添加多个标签

代码语言:javascript复制
tags:
 - testtag
 - t1

在调用标签时,也可以一次性指定多个标签,调用多个标签需要用逗号隔开,命令如下

代码语言:javascript复制
ansible-playbook --tags package,service testhttpd.yml

在调用标签之前,如果你想要概览一下playbook中都有哪些标签,可以使用 ‘ –list-tags’ 选项,示例如下

代码语言:javascript复制
ansible-playbook --list-tags testhttpd.yml

其实,ansible还预置了5个特殊tag,这5个特殊tag分别为

代码语言:javascript复制
always

never(2.5版本中新加入的特殊tag)

tagged

untagged

all

当我们把任务的tags的值指定为always时,那么这个任务就总是会被执行,除非你使用’–skip-tags’选项明确指定不执行对应的任务。


ansible 之 block

当我们想在满足一个条件下,执行多个任务时,就需要分组了。而不再每个任务都要用when。

代码语言:javascript复制
  tasks: 
   - block:
     - command: echo 1
     - shell: echo 2
     - raw: echo 3
     when: ansible_distribution == 'CentOS'

错误处理

代码语言:javascript复制
tasks:
  - block:
      - debug: msg='i execute normally'
      - command: /bin/false
      - debug: msg='i never execute, cause ERROR!'
    rescue:
      - debug: msg='I caught an error'
      - command: /bin/false
      - debug: msg='I also never execute :-('
    always:
      - debug: msg="this always executes"

block中的任务在执行中,如果有任何错误,将执行rescue中的任务。 无论在block和rescue中发生或没有发生错误,always部分都运行。


ansible 之 stat

检查文件或文件系统的状态,对于Windows目标,请改用win_stat模块

  1. 参数

path:文件/对象的完整路径,required

  1. 示例
代码语言:javascript复制
- tasks
  name: install_apcu | Check if apcu local file is already configured.
  stat: path={{ php_apcu_file_path }}
  connection: local
  register: php_apcu_file_result

ansible 之 register

用于注册一个变量,保存命令的结果(shell或command模块),这个变量可以在后面的task、when语句或模板文件中使用。

代码语言:javascript复制
- hosts: node3
  tasks:
    - shell: pwd
      register: pwd_status

    - name: print pwd_ststus value
      debug: msg={{ pwd_status }}

debug模块可以输出register模块中对应的变量属性。

ansible register 这个功能非常有用。当我们需要判断对执行了某个操作或者某个命令后,如何做相应的响应处理(执行其他 ansible 语句),则一般会用到register 。

举个例子:

我们需要判断sda6是否存在,如果存在了就执行一些相应的脚本,则可以为该判断注册一个register变量,并用它来判断是否存在,存在返回 succeeded, 失败就是 failed.

代码语言:javascript复制
- name: Create a register to represent the status if the /dev/sda6 exsited
  shell: df -h | grep sda6
  register: dev_sda6_result
  ignore_errors: True
  tags: docker

- name: Copy docker-thinpool.sh to all hosts
  copy: src=docker-thinpool.sh dest=/usr/bin/docker-thinpool mode=0755
  when: dev_sda6_result | succeeded
  tags: docker

注意: 1、register变量的命名不能用 -中横线,比如dev-sda6_result,则会被解析成sda6_resultdev会被丢掉,所以不要用- 2、ignore_errors这个关键字很重要,一定要配合设置成True,否则如果命令执行不成功,即 echo $?不为0,则在其语句后面的ansible语句不会被执行,导致程序中止。

那我如何去做多种条件的判断呢,比如我还需要判断是否有 docker-thinpool 存在,则还需要为它注册一个变量。

代码语言:javascript复制
- name: Create a register to represent the status if the docker-thinpool exsited
  shell: lsblk | grep docker-thinpool
  register: docker_thinpool_result
  ignore_errors: True
  tags: docker

然后在when中用and或者or来组合判断。比如当两种条件之一成功,都对docker配置文件进行修改:

代码语言:javascript复制
- name: Special config for docker-thinpool devicemapper
  lineinfile: dest=/etc/sysconfig/docker regexp=^OPTIONS= line=OPTIONS='--selinux-enabled=false --insecure-registry=10.213.42.254:10500 --log-level=warn --storage-opt dm.basesize={{ dm_base_size }} --storage-opt dm.loopdatasize={{ dm_loop_data_size }} --storage-opt dm.loopmetadatasize={{ dm_loop_meta_size }} --storage-driver=devicemapper --storage-opt=dm.thinpooldev=/dev/mapper/docker-thinpool --storage-opt=dm.use_deferred_removal=true --storage-opt=dm.use_deferred_deletion=true'
  when: (dev_sda6_result | succeeded) or (docker_thinpool_result | succeeded)
  tags: docker

ansible 之 ignore_errors

在playbook执行的过程中,难免会遇到一些错误。由于playbook遇到错误后,不会执行之后的任务,不便于调试,此时,可以使用ignore_errors来暂时忽略错误,使得playbook继续执行。


ansible 之 set_fact

set_fact是一个模块,我们可以通过set_fact模块在tasks中定义变量,先来看一个小示例,如下

代码语言:javascript复制
---
- hosts: test70
  remote_user: root
  tasks:
  - set_fact:
      testvar: "testtest"
  - debug:
      msg: "{{testvar}}"

ansible 之 wait_for

有些情况下,一些任务的运行需要等待一些状态的恢复,比如某一台主机或者应用刚刚重启,我们需要需要等待它上面的某个端口开启,此时就需要将正在运行的任务暂停,直到其状态满足要求。

Ansible提供了wait_for模块以实现任务暂停的需求

wait_for模块常用参数:

代码语言:javascript复制
- connect_timeout:在下一个任务执行之前等待连接的超时时间
- delay:等待一个端口或者文件或者连接到指定的状态时,默认超时时间为300秒,在这等待的300s的时间里,wait_for模块会一直轮询指定的对象是否到达指定的状态,delay即为多长时间轮询一次状态。
- host:wait_for模块等待的主机的地址,默认为127.0.0.1
- port:wait_for模块待待的主机的端口
- path:文件路径,只有当这个文件存在时,下一任务才开始执行,即等待该文件创建完成
- state:等待的状态,即等待的文件或端口或者连接状态达到指定的状态时,下一个任务开始执行。当等的对象为端口时,状态有started,stoped,即端口已经监听或者端口已经关闭;当等待的对象为文件时,状态有present或者started,absent,即文件已创建或者删除;当等待的对象为一个连接时,状态有drained,即连接已建立。默认为started
- timeout:wait_for的等待的超时时间,默认为300秒

示例:

代码语言:javascript复制
#等待8080端口已正常监听,才开始下一个任务,直到超时
- wait_for: 
    port: 8080 
    state: started  
    
#等待8000端口正常监听,每隔10s检查一次,直至等待超时
- wait_for: 
    port: 8000 
    delay: 10 
    
#等待8000端口直至有连接建立
- wait_for: 
    host: 0.0.0.0 
    port: 8000 
    delay: 10 
    state: drained
    
#等待8000端口有连接建立,如果连接来自10.2.1.2或者10.2.1.3,则忽略。
- wait_for: 
    host: 0.0.0.0 
    port: 8000 
    state: drained 
    exclude_hosts: 10.2.1.2,10.2.1.3 
    
#等待/tmp/foo文件已创建    
- wait_for: 
    path: /tmp/foo 

#等待/tmp/foo文件已创建,而且该文件中需要包含completed字符串    
- wait_for: 
    path: /tmp/foo 
    search_regex: completed 

#等待/var/lock/file.lock被删除    
- wait_for: 
    path: /var/lock/file.lock 
    state: absent 
    
#等待指定的进程被销毁
- wait_for: 
    path: /proc/3466/status 
    state: absent 
    
#等待openssh启动,10s检查一次
- wait_for: 
    port: 22 
    host: "{{ ansible_ssh_host | default(inventory_hostname) }}" search_regex: OpenSSH 
    delay: 10 

任务委托

在有些时候,我们希望运行与选定的主机或主机组相关联的task,但是这个task又不需要在选定的主机或主机组上执行,而需要在另一台服务器上执行。

这种特性适用于以下场景:

  • 在告警系统中启用基于主机的告警
  • 向负载均衡器中添加或移除一台主机
  • 在dns上添加或修改针对某个主机的解析
  • 在存储节点上创建一个存储以用于主机挂载
  • 使用一个外部程序来检测主机上的服务是否正常

可以使用delegate_to语句来在另一台主机上运行task:

代码语言:javascript复制
- name: enable alerts for web servers
  hosts: webservers
  tasks:
    - name: enable alerts
      nagios: action=enable_alerts service=web host="{{ inventory_hostname }}"
      delegate_to: nagios.example.com

如果delegate_to: 127.0.0.1的时候,等价于local_action


ansible 之 lineinfile

正则模块。

下面我们详细说一说其具体可以做的事情。

修改匹配行

下面是一个简单的task示例:

代码语言:javascript复制
# 将/etc/selinux/config中匹配到以'SELINUX='开头的行,将其替换为'SELINUX=disabled'
- name: modify selinux to disabled
  lineinfile:
    path: /etc/selinux/config
    regex: '^SELINUX='
    line: 'SELINUX=disabled'
在匹配行前或后添加内容

示例文件如下:

代码语言:javascript复制
# cat /etc/http.conf

Listen 127.0.0.1:80
Listen 80
Port
在匹配行前添加

在http.conf文件的Listen 80前面添加一行Listen 8080,task示例如下:

代码语言:javascript复制
- name: add line before Listen 80
  lineinfile:
    dest: /etc/http.conf
    insertbefore: '^Listen 80'
    line: 'Listen 8080'
在匹配行后添加

在http.conf文件的Port后面添加一行just a test,task示例如下:

代码语言:javascript复制
- name: add line before Listen 80
  lineinfile:
    dest: /etc/http.conf
    insertafter: '^Port'
    line: 'just a test'
修改文件内容及权限

示例文件:

代码语言:javascript复制
#cat /etc/hosts

127.0.0.1       localhost.localdomain localhost ::1       localhost6.localdomain6 localhost6
192.168.0.130     hub.breezey.top

修改/etc/hosts,将以127.0.0.1开头的行替换为 127.0.0.1 localhost,并将/etc/hosts的属主和属组都修改为root,权限改为644,如下:

代码语言:javascript复制
- name: modify hosts
  lineinfile:
    dest: /etc/hosts
    regex: '^127.0.0.1'
    line: '127.0.0.1 localhost'
    owner: root
    group: root
    mode: 0644
删除一行内容

示例原文件:

代码语言:javascript复制
#cat /etc/hosts

127.0.0.1       localhost.localdomain localhost ::1       localhost6.localdomain6 localhost6
192.168.0.130     hub.breezey.top

删除以192.168.0.130开头的行:

代码语言:javascript复制
- name: delete a line
  lineinfile:
    dest: /etc/hosts
    regex: '^192.168.0'
    state: absent
文件存在则添加一行内容

往/etc/hosts里添加一行192.168.0.131 test.breezey.top(多次执行,不会重复添加),示例如下:

代码语言:javascript复制
- name: add a line
  lineinfile:
    dest: /etc/hosts
    line: '192.168.0.131 test.breezey.top'
如果有匹配的行则修改该行,如果不匹配则添加

示例原文件/tmp/test.txt内容如下:

代码语言:javascript复制
# %wheel   ALL=(ALL)   ALL

下面的示例task中,匹配以%wheel开头的行,匹配到,则执行替换,未匹配,则添加。因为原文件中,没有以%wheel开头的行,所以会添加一行:

代码语言:javascript复制
- name: add or modify a line
  lineinfile: 
    dest: /tmp/test.txt
    regex: '^%wheel'
    line: '%wheel  ALL=(ALL)       NOPASSWD: ALL'

修改后的文件如下:

代码语言:javascript复制
#cat /tmp/text.txt

# %wheel   ALL=(ALL)   ALL
%wheel  ALL=(ALL)       NOPASSWD: ALL
参数backrefs,backup说明
  • backup: 是否备份原文件,默认为no
  • backrefs:
    • 当backrefs为no时,如果regex没有匹配到行,则添加一行,如果Regx匹配到行,则修改该行
    • 当backrefs为yes时,如果regex没有匹配到行,则保持原文件不变,如果regex匹配到行,则修改该行
    • backrefs默认为no,所以上面那个示例中,我们没有配置backrefs,而默认没有匹配,则修改。

下面我们看一看backrefs为yes时匹配到行的示例:

示例原文件:

代码语言:javascript复制
# cat /tmp/testfile

# %wheel   ALL=(ALL)   ALL
%wheel  ALL=(ALL)       NOPASSWD: ALL
#?bar

task示例:

代码语言:javascript复制
 - name: test backrefs
  lineinfile:
      backup: yes
      state: present
      dest: /tmp/testfile
      regexp: '^#?bar'
      backrefs: yes
      line: 'bar'

修改后的文件:

代码语言:javascript复制
# cat /tmp/testfile

# %wheel   ALL=(ALL)   ALL
%wheel  ALL=(ALL)       NOPASSWD: ALL
bar
使用validate验证文件是否正确修改

在一些场景下,我们修改完文件后,需要对文件做一下测试,用以检查文件修改之后,是否能正常运行。如http.conf、nginx.conf等,一旦改错,而不加以测试,可能会直接导致http服务挂掉。

可以使用validate关键字,在修改完成以后,对文件执行检测:

代码语言:javascript复制
- name: test validate
  lineinfile:
      dest: /etc/sudoers
      state: present
      regexp: '^�MIN ALL='
      line: '�MIN ALL=(ALL)'
      validate: 'visudo -cf %s'
  tags:
    - testsudo

ansible 之 package

包管理器,通过包管理器安装软件

  1. 参数

name :指定要安装的软件包名 state: present 安装 absent 卸载

  1. 示例

示例1

代码语言:javascript复制
[admin@node1 ~]$ ansible webserver -m package -a "name=openssl-devel state=present" -b --ask-sudo-pass

示例2

代码语言:javascript复制
- name: install_from_source | Ensure dependencies for building from source are installed.
  package: "name={{ item }} state=installed"
  with_items: "{{ php_packages }}"

ansible 之 滚动执行

默认情况下,ansible会并行的在所有选定的主机或主机组上执行每一个task,但有的时候,我们会希望能够逐台运行。最典型的例子就是对负载均衡器后面的应用服务器进行更新时。通常来讲,我们会将应用服务器逐台从负载均衡器上摘除,更新,然后再添加回去。我们可以在play中使用serial语句来告诉ansible限制并行执行play的主机数量。

下面是一个在amazon EC2的负载均衡器中移除主机,更新软件包,再添加回负载均衡的配置示例:

代码语言:javascript复制
- name: upgrade pkgs on servers behind load balancer
  hosts: myhosts
  serial: 1
  tasks:
    - name: get the ec2 instance id and elastic load balancer id
      ec2_facts:

    - name: take the host out of the elastic load balancer id
      local_action: ec2_elb
      args:
        instance_id: "{{ ansible_ec2_instance_id }}"
        state: absent

    - name: upgrade pkgs
      apt: 
          update_cache: yes 
          upgrade: yes

    - name: put the host back n the elastic load balancer
      local_action: ec2_elb
      args:
        instance_id: "{{ ansible_ec2_instance_id }}"
        state: present
        ec2_elbs: "{{ items }}"
      with_items: ec2_elbs

在上述示例中,serial的值为1,即表示在某一个时间段内,play只在一台主机上执行。如果为2,则同时有2台主机运行play。

一般来讲,当task失败时,ansible会停止执行失败的那台主机上的任务,但是继续对其他 主机执行。在负载均衡的场景中,我们会更希望ansible在所有主机执行失败之前就让play停止,否则很可能会面临所有主机都从负载均衡器上摘除并且都执行失败导致服务不可用的场景。这个时候,我们可以使用serial语句配合max_fail_percentage语句使用。max_fail_percentage表示当最大失败主机的比例达到多少时,ansible就让整个play失败。示例如下:

代码语言:javascript复制
- name: upgrade pkgs on fservers behind load balancer
  hosts: myhosts
  serial: 1
  max_fail_percentage: 25
  tasks:
    ......

假如负载均衡后面有4台主机,并且有一台主机执行失败,这时ansible还会继续运行,要让Play停止运行,则必须超过25%,所以如果想一台失败就停止执行,我们可以将max_fail_percentage的值设为24。如果我们希望只要有执行失败,就放弃执行,我们可以将max_fail_percentage的值设为0。


ansible 之 只执行一次

某些时候,我们希望某个task只执行一次,即使它被绑定到了多个主机上。例如在一个负载均衡器后面有多台应用服务器,我们希望执行一个数据库迁移,只需要在一个应用服务器上执行操作即可。

可以使用run_once语句来处理:

代码语言:javascript复制
- name: run the database migrateions
  command: /opt/run_migrateions
  run_once: true

还可以与local_action配合使用,如下:

代码语言:javascript复制
- name: run the task locally, only once
  command: /opt/my-custom-command
  connection: local
  run_once: true

还可以与delegate_to配合使用,让这个只执行一次的任务在指定的机器上运行:

代码语言:javascript复制
- name: run the task locally, only once
  command: /opt/my-custom-command
  run_once: true
  delegate_to: app.a1-61-105.dev.unp

ansible 之 设置环境变量

我们在命令行下执行某些命令的时候,这些命令可能会需要依赖环境变量。比如在安装某些包的时候,可能需要通过代理才能完成完装。或者某个脚本可能需要调用某个环境变量才能完成运行。

ansible 支持通过environment关键字来定义一些环境变量。

在如下场景中可能需要用到环境变量:

  • 运行shell的时候,需要设置path变量
  • 需要加载一些库,这些库不在系统的标准库路径当中

下面是一个简单示例:

代码语言:javascript复制
---
- name: upload a remote file to aws s3
  hosts: test
  tasks:
    - name: install pip
      yum:
        name: python-pip
        state: installed
    
    - name: install the aws tools
      pip:
        name: awscli
        state: present
    
    - name upload file to s3
      shell aws s3 put-object --bucket=my-test-bucket --key={{ ansible_hostname }}/fstab --body=/etc/fstab --region=eu-west-1
      environment:
        AWS_ACCESS_KEY_ID: xxxxxx
        AWS_SECRET_ACCESS_KEY: xxxxxx

事实上,environment也可以存储在变量当中:

代码语言:javascript复制
- hosts: all
  remote_user: root
  vars:
    proxy_env:
      http_proxy: http://proxy.example.com:8080
      https_proxy: http://proxy.bos.example.com:8080
  tasks:
    - apt: name=cobbler state=installed
      environment: proxy_env

0 人点赞