Ansible 防火墙设置

知识点:

如果使用Ansible去远程配置防火墙规则,方式有好几种,比如:
(1).可以通过shell模块远程执行iptables命令;
(2).可以在Ansible本地端写好iptables规则,然后传送到目标节点上通过iptables-restore来恢复规则;
(3).可以使用Ansible提供的iptables模块来远程设置防火墙规则。

下面是使用shell模块远程执行iptables命令来定义基本的防火墙规则。

---
- name: set firewall
  hosts: new
  gather_facts: false
  tasks: 
    - name: set iptables rule
      shell: |
        # 备份已有规则
        iptables-save > /tmp/iptables.bak$(date +"%F-%T")
        # 给它三板斧
        iptables -X
        iptables -F
        iptables -Z

        # 放行lo网卡和允许ping
        iptables -A INPUT -i lo -j ACCEPT
        iptables -A INPUT -p icmp -j ACCEPT

        # 放行关联和已建立连接的包,放行22、443、80端口
        iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
        iptables -A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
        iptables -A INPUT -p tcp -m tcp --dport 443 -j ACCEPT
        iptables -A INPUT -p tcp -m tcp --dport 80 -j ACCEPT

        # 配置filter表的三链默认规则,INPUT链丢弃所有包
        iptables -P INPUT DROP
        iptables -P FORWARD DROP
        iptables -P OUTPUT ACCEPT

可以指定要禁用或放行的端口列表,然后通过循环的方式去配置相关规则,还可以指定协议,指定链的默认规则等等。

此处,我做个简单演示,只支持三种操作:
1.允许用户指定filter表中三链的默认规则
2.允许用户指定INPUT链放行的tcp端口号列表
3.允许执行用户指定的iptables

---
- name: set firewall
  hosts: new
  gather_facts: false
  vars: 
    allowed_tcp_ports: [22,80,443]
    default_policies:
      INPUT: DROP
      FORWARD: DROP
      OUTPUT: ACCEPT
    user_iptables_rule: 
      - iptables -A INPUT -p tcp -m tcp --dport 8000 -j ACCEPT
      - iptables -A INPUT -p tcp -m tcp --dport 8080 -j ACCEPT

  tasks: 
    - block: 
      - name: backup and empty rules
        shell: |
          # 备份已有规则,并清空规则等
          iptables-save > /tmp/iptables.bak$(date +"%F-%T")
          iptables -X
          iptables -F
          iptables -Z

      - name: green light for lo interface and icmp protocol
        shell: |
          # 放行lo接口、ping和已建立连接的包
          iptables -A INPUT -i lo -j ACCEPT
          iptables -A INPUT -p icmp -j ACCEPT
          iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

      # 放行用户指定的tcp端口列表
      - name: allow for given tcp port
        shell: iptables -A INPUT -p tcp -m tcp --dport {{item}} -j ACCEPT
        loop: "{{ allowed_tcp_ports | default([]) }}"

      # 执行用户自定义的iptables命令
      - name: execute user iptables command
        shell: "{{item}}"
        loop: "{{user_iptables_rule | default([]) }}"

      # 设置filter表三链的默认规则
      - name: default policies for filter table
        shell: iptables -P {{item.key}} {{item.value}}
        loop: "{{ query('dict', default_policies | default({})) }}"

上面示例中,定义了三个变量,分别用于存放用户指定要放行的tcp端口、filter表中三链的默认规则以及用户自定义的iptables命令。这没什么可解释的,但需要注意的是有些task中使用了类似{{ allowed_tcp_ports | default([]) }}的代码,这表示当变量allowed_tcp_ports未定义时,则将该变量设置为空列表作为其默认值,以免因变量未定义而loop循环出错。对于本示例来说,直接使用{{ allowed_tcp_ports }}也是健壮的代码。

Ansible lineinfile

利用lineinfile 添加dns 解析

lineinfile模块用于在源文件中插入、删除、替换行,和sed命令的功能类似,也支持正则表达式匹配和替换。

---
- name: add DNS for each
  hosts: all
  gather_facts: true
  tasks: 
    - name: add DNS
      lineinfile: 
        path: "/etc/hosts"
        line: "{{item}} {{hostvars[item].ansible_hostname}}"
      when: item != inventory_hostname
      loop: "{{ play_hosts }}"

lineinfile 字面意思为“行在文件中”

行的添加

---
- name: test inlinefile
  hosts: localhost
  gather_facts: false
  tasks: 
    - lineinfile:
        path: "a.txt"
        line: "this line must in"

如果 a.txt 中没有 this line must in 就添加到最后一行

如果再次执行,则不会再次追加此行。因为lineinfile模块的state参数默认值为present,它能保证幂等性,当要插入的行已经存在时则不会再插入。

如果要移除某行,则设置state参数值为absent即可。下面的任务会移除a.txt中所有的”this line must not in”行(如果多行则全部移除)。

- lineinfile:
    path: "a.txt"
    line: "this line must not in"
    state: absent

行前和行后插入

如果想要在某行前、某行后插入指定数据行,则结合insertbeforeinsertafter。例如:

- lineinfile:
    path: "a.txt"
    line: "LINE1"
    insertbefore: '^para.* 2'

- lineinfile:
    path: "a.txt"
    line: "LINE2"
    insertafter: '^para.* 2'

注意,insertbefore和insertafter指定的正则表达式如果匹配了多行,则默认选中最后一个匹配行,然后在被选中的行前、行后插入。如果明确要指定选中第一次匹配的行,则指定参数firstmatch=yes

- lineinfile:
    path: "a.txt"
    line: "LINE1"
    insertbefore: '^para.* 2'
    firstmatch: yes

此外,对于insertbefore,如果它的正则表达式匹配失败,则会插入到文件的尾部。

行替换

lineinfile还可以替换行,使用regexp参数指定要匹配并被替换的行即可,默认替换最后一个匹配成功的行。

- lineinfile:
    path: "a.txt"
    line: "LINE1"
    regexp: '^para.* 2'

play_hosts和hostvars变量

inventory_hostname变量已经使用过几次了,它表示当前正在执行任务的主机在inventory中定义的名称。在此示例中,inventory中的主机名都是IP地址。

play_hostshostvars是Ansible的预定义变量,执行任务时可以直接拿来使用,不过在Ansible中预定义变量有专门的称呼:魔法变量(magic variables)。Ansible支持不少魔法变量,详细信息参见官方手册:https://docs.ansible.com/ansible/latest/reference_appendices/special_variables.html#magic

首先是play_hosts变量,它存储当前play所涉及的所有主机列表,但连接失败或执行任务失败的节点不会留在此变量中。

例如,某play指定了hosts: all,那么执行这个play时,play_hosts中就以列表的方式存储了all组中的所有主机名。

hostvars变量用于保存所有和主机相关的变量,通常包括inventory中定义的主机变量和gather_facts收集到的主机信息变量hostvars是一个key/value格式的字典(即hash结构、对象),key是每个节点的主机名,value是该主机对应的变量数据。

例如,在inventory中有一行:

192.168.200.27 myvar="hello world"

如果要通过hostvars来访问该主机变量,则使用hostvars['192.168.200.27'].myvar

因为gather_facts收集的主机信息也会保存在hostvars变量中,所以也可以通过hostvars去访问收集到的信息。gather_facts中收集了非常多的信息,目前只需记住此处所涉及的ansible_hostname即可,它保存的是收集目标主机信息而来的主机名,不是定义在Ansible端inventory中的主机名(因为可能是IP地址)。

再者,由于hostvars中保存了所有目标主机的主机变量,所以任何一个节点都可以通过它去访问其它节点的主机变量。比如示例中hostvars[item].ansible_hostname访问的是某个主机的ansible_hostname变量。

再来看互相添加DNS解析记录的示例:

- name: add DNS
  lineinfile: 
    path: "/etc/hosts"
    line: "{{item}} {{hostvars[item].ansible_hostname}}"
  when: item != inventory_hostname
  loop: "{{ play_hosts }}"

Ansible 变量、循环、判断

设置主机名

配置主机名非常简单,可以使用shell模块在远程执行相关命令,也可以使用Ansible提供的hostname模块。建议使用hostname模块,它支持多种操作系统。

ansible 变量

---
- name: set hostname
  hosts: new
  gather_facts: false
  vars:
    hostnames:
      - host: 192.168.200.34
        name: new1
      - host: 192.168.200.35
        name: new2
  tasks: 
    - name: set hostname
      hostname: 
        name: "{{item.name}}"
      when: item.host == inventory_hostname
      loop: "{{hostnames}}"

vars指令可用于设置变量,可以设置一个或多个变量。下面的设置方式都是合理的:

# 设置单个变量
vars: 
  var1: value1

vars: 
  - var1: value1

# 设置多个变量:
vars: 
  var1: value1
  var2: value2

vars: 
  - var1: value1
  - var2: value2

vars可以设置在play级别,也可以设置在task级别:
(1).设置在play级别,该play范围内的task能访问这些变量,其它play范围内则无法访问
(2).设置在task级别,只有该task能访问这些变量,其它task和其它play则无法访问

这里只设置了一个变量hostnames,但这个变量的值是一个数组结构,数组的两个元素又都是对象(字典/hash)结构。

所以想要访问主机名new1和它的IP地址192.168.200.34,可以:

tasks: 
  - debug: 
      var: hostnames[0].name
  - debug: 
      var: hostnames[0].host

条件判断 when

---
- name: play1
  hosts: localhost
  gather_facts: false
  vars: 
    - myname: "kate"
  tasks:
    - name: task will skip
      debug:
        msg: "myname is: {{myname}}"
      when: myname == "liutao"

    - name: task will execute
      debug: 
        msg: "myname is: {{myname}}"
      when: myname == "kate"

需要注意的是,when指令因为已经明确是做条件判断,所以它的值必定是一个表达式,它会自动隐式地帮我们包围一层{{}},所以在写when指令的条件判断时,不要再手动加上{{}}

虽然when指令的逻辑很简单:值为true则执行任务,否则不执行任务。但是,它的用法并不简单,概因when指令的值可以是Jinja2的表达式,很多内置在Jinja2中的Python的语法都可以用在when指令中,而这需要掌握Python的基本语法。如果不具备这些知识,那么想要实现某种判断功能可能会感觉到较大的局限性,而且别人写的脚本可能看不懂。

循环loop

除条件判断外,另一种分支控制结构是循环结构。

Ansible提供了很多种循环结构,一般都命名为with_xxx,例如with_itemswith_listwith_file等,使用最多的是with_items。事实上with_<lookup>结构是对应lookup插件的,关于with_xxx这些循环结构在以后的文章中再统一介绍。

在这里仅介绍loop循环,它是在Ansible 2.5版本中新添加的循环结构,等价于with_list。大多数时候,with_xxx的循环都可以通过一定的手段转换成loop循环,所以从Ansible 2.5版本之后,原来经常使用的with_items循环都可以尝试转换成loop

---
- name: play1
  hosts: localhost
  gather_facts: false
  tasks: 
    - name: create directories
      file: 
        path: "{{item}}"
        state: directory
      loop:
        - /tmp/test1
        - /tmp/test2

解释下上面的loop{{item}}

loop等价于with_list,从名字上可以知道它是遍历数组(列表)的,所以在loop指令中,每个元素都以列表的方式去定义。列表有多少个元素,就循环执行file模块多少次,每轮循环中,都会将本次迭代的列表元素保存在控制变量item

---
- name: set hostname
  hosts: new
  gather_facts: false
  vars:
    hostnames:
      - host: 192.168.200.34
        name: new1
      - host: 192.168.200.35
        name: new2
  tasks: 
    - name: set hostname
      hostname: 
        name: "{{item.name}}"
      when: item.host == inventory_hostname
      loop: "{{hostnames}}"

在这个示例中,是对{{hostnames}}进行循环遍历,hostnames中包含两个元素,每个元素都是一个key/value的对象结构。所以,第一次迭代时,item变量的值是:

{
  host: "192.168.200.34",
  name: "new1"
}

所以item.hostitem.name分别对应于192.168.200.34new1

1.loop和when结合使用时,when的条件判断是在循环内部执行的。也就是说循环指令(如loop)的解析顺序早于when指令

Ansible 禁用selinux

知识点shell 、block、ignore_errors

---
- name: disable selinux
  hosts: new
  gather_facts: false
  tasks: 
    - name: selinux diable
      shell: setenforce 0

    - name: disable forever in config
      lineinfile: 
        path: /etc/selinux/config
        line: "SELINUX=disabled"
        regexp: '^SELINUX='

在使用shell模块执行setenforce 0命令的时候,发现该命令的退出状态码不是0,所以Ansible认为这是个失败的命令,于是终止了play,后面的任务也不再执行。但其实我们自己明确地知道,这个命令是没错的,而且从上面的stderr的内容中也可以看出,setenforce命令给了我们正确的反馈。

使用Ansible去执行命令,可能经常会遇到类似问题,Ansible并没有那么聪明,它默认只认退出状态码0,其它退出状态码全认为是失败的。

所以,需要让Ansible去处理这种异常。处理异常的方式有很多种,这里只介绍最简单的一种:ignore_errors,

ignore_errors指令正如其名,表示忽略失败的任务,直接将值设置为true即可。

---
- name: disable selinux
  hosts: new
  gather_facts: false
  tasks: 
    - name: disable on the fly
      shell: setenforce 0
      ignore_errors: true

    - name: disable forever in config
      lineinfile: 
        path: /etc/selinux/config
        line: "SELINUX=disabled"
        regexp: '^SELINUX='

使用ignore_errors虽然简单,但不是很友好。各位去执行一下上面的playbook,会发现它仍然会将报错信息输出在终端或指定的日志文件中,只不过它不会因为任务失败而导致整个play的中止。但无论如何,它能达到我们的目标:在遇到错误的时候继续执行下去。

实际上,几乎所有任务级别的指令(除循环类指令外,比如loop)都可以写在block级别上,它们会拷贝到Block内部的所有任务上(也可也看作是block内部的任务继承block级别上的指令)。例如:

---
- name: disable selinux
  hosts: new
  gather_facts: false
  tasks: 
    - block: 
        - shell: ls /tmp/a.log
        - shell: ls /tmp/b.log
      ignore_errors: true

Ansible 时间同步

通常会使用ntpd时间服务器来保证时间的同步,但在这里简单点,直接使用ntpdate命令初步保证时间同步,并将同步后的时间同步到硬件。

---
- name: sync time
  hosts: new
  gather_facts: false
  tasks: 
    - name: install and sync time
      block: 
        - name: install ntpdate
          yum: 
            name: ntpdate
            state: present

        - name: ntpdate to sync time
          shell: |
            ntpdate ntp1.aliyun.com
            hwclock -w

上面使用了一个block指令来组织了两个有关联性的任务,将它们作为了一个整体。由此也可以看到,block的用法非常简单。其实block更多的用于多个关联性任务之间的异常处理