date: 2020-03-03title: ansible批量初始化服务器 #标题
tags: 批量初始化服务器 #标签
categories: ansible # 分类

本文要实现的初始化配置目标如下:

  • ansible配置ssh免密登录;
  • ansible远程配置主机名;
  • ansible控制远程主机互相添加DNS解析记录;
  • ansible配置远程主机上的yum镜像源以及安装一些软件;
  • ansible配置远程主机上的时间同步;
  • ansible关闭远程主机上的selinux;
  • ansible配置远程主机上的防火墙;
  • ansible远程修改sshd配置文件并重启sshd,使其更安全;

1、ansible要初始化的主机

  1. [root@nginx ansible]# tail -3 /etc/ansible/hosts #要初始的主机如下
  2. [node]
  3. 192.168.20.4
  4. 192.168.20.5

2、配置ssh免密登录

playbook文件内容如下:

  1. [root@nginx ansible]# cat ssh.yaml
  2. ---
  3. - name: configure ssh connection
  4. hosts: node
  5. gather_facts: false
  6. connection: local
  7. tasks:
  8. - name: configure ssh connection
  9. shell: |
  10. ssh-keyscan {{inventory_hostname}} >>~/.ssh/known_hosts
  11. sshpass -p '123.com' ssh-copy-id root@{{inventory_hostname}}
  12. ...

注:

  • gather_facts:如果值为false,则表示不收集目标主机上的节点信息,默认为true,为收集节点信息,如果收集节点信息,则会慢很多,如果在接下来的操作中,不需要节点上的信息,可设置为false。
  • connection:local表示在ansible端本地执行任务,hosts:localhost和connection:local容易搞混,虽然两者的效果都是在本地执行任务,但是hosts:localhost是从inventory中筛选出了目标节点localhost来执行任务,而connection:local则筛选出来执行任务的目标主机是node组中的节点,但因为指定了local连接类型,使得node组中有多少个节点,就会在ansible本地执行几次该play。

3、配置主机名

配置主机名可以使用shell模块,但是对于不太专业,ansible提供了一个专用于配置主机名的模块:hostname模块。

当然,要使用ansible去设置多个主机名,要求目标主机和目标名称已经关联好,否则多个主机和多个主机名之间无法对应去设置。

例如:分别设置node组中的两个节点主机名为node01和node02,playbook内容如下:

  1. [root@ansible ansible]# cat test.yaml
  2. ---
  3. - name: set hostname
  4. hosts: node
  5. gather_facts: false
  6. vars:
  7. hostnames:
  8. - host: 192.168.20.4
  9. name: node01
  10. - host: 192.168.20.5
  11. name: node02
  12. tasks:
  13. - name: set hostname
  14. hostname:
  15. name: "{{item.name}}"
  16. when: item.host == inventory_hostname
  17. loop: "{{hostnames}}"

在上面的hostname模块中,需要详细介绍vars指令以及when、loop指令。

1)vars设置变量

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

  1. # 设置单个变量
  2. vars:
  3. var1: value1
  4. vars:
  5. - var1: value1
  6. # 设置多个变量
  7. vars:
  8. var1: value1
  9. var2: value2
  10. vars:
  11. - var1: value1
  12. - var2: value2

vars可以设置在play级别,也可以设置在task级别,设置在play级别,该play范围内的task可以访问这些变量,其他play范围内则无法访问;设置在task级别,只有该task能访问这些变量,其他task和其他play则无法访问。

例如:

  1. [root@ansible ansible]# cat test.yaml
  2. ---
  3. - name: play1
  4. hosts: localhost
  5. gather_facts: false
  6. vars:
  7. - var1: "value1"
  8. tasks:
  9. - name: access var1
  10. debug:
  11. msg: "var1's value: {{var1}}"
  12. - name: play2
  13. hosts: localhost
  14. gather_facts: false
  15. tasks:
  16. - name: cat's access vars from play1
  17. debug:
  18. var: var1
  19. - name: set and access var2 in this task
  20. debug:
  21. var: var2
  22. vars:
  23. var2: "value2"
  24. - name: cat't accesss var2
  25. debug:
  26. var: var2

执行结果如下:

  1. [root@ansible ansible]# ansible-playbook test.yaml
  2. PLAY [play1] **************************************************************************
  3. TASK [access var1] ********************************************************************
  4. ok: [localhost] => {
  5. "msg": "var1's value: value1"
  6. }
  7. PLAY [play2] **************************************************************************
  8. TASK [cat's access vars from play1] ***************************************************
  9. ok: [localhost] => {
  10. "var1": "VARIABLE IS NOT DEFINED!"
  11. }
  12. TASK [set and access var2 in this task] ***********************************************
  13. ok: [localhost] => {
  14. "var2": "value2"
  15. }
  16. TASK [cat't accesss var2] *************************************************************
  17. ok: [localhost] => {
  18. "var2": "VARIABLE IS NOT DEFINED!"
  19. }
  20. PLAY RECAP ****************************************************************************
  21. localhost : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

回到我们更改主机名的配置vars指令中:

  1. vars:
  2. hostnames:
  3. - host: 192.168.20.4
  4. name: node01
  5. - host: 192.168.20.5
  6. name: node02

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

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

  1. tasks:
  2. - debug:
  3. var: hostnames[0].name
  4. - debug:
  5. var: hostnames[0].host

2)when条件判断

在ansible中,提供的唯一一个通用的条件判断是when指令,当when指令的值为true时,则执行该任务,否则不执行该任务。

例如:

  1. [root@ansible ansible]# cat test.yaml
  2. ---
  3. - name: play1
  4. hosts: localhost
  5. gather_facts: false
  6. vars:
  7. - myname: "Ray"
  8. tasks:
  9. - name: task will skip
  10. debug:
  11. msg: "myname is : {{myname}}"
  12. when: myname == "lv"
  13. - name: task will execute
  14. debug:
  15. msg: "myname is : {{myname}}"
  16. when: myname == "Ray"

在上面的myname值设置为Ray,第一个任务因为when的判断条件是myname==“lv”,所以判断结果为false,该任务不执行,同理,第二个任务因为when的值为true,所以执行了。

该playbook的执行结果:

  1. PLAY [play1] **************************************************************************
  2. TASK [task will skip] *****************************************************************
  3. skipping: [localhost]
  4. TASK [task will execute] **************************************************************
  5. ok: [localhost] => {
  6. "msg": "myname is : Ray"
  7. }
  8. PLAY RECAP ****************************************************************************
  9. localhost : ok=1 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0

4、互相添加DNS解析记录

  1. [root@ansible ansible]# cat add_dns.yaml
  2. ---
  3. - name: play1
  4. hosts: node
  5. gather_facts: true
  6. tasks:
  7. - name: add DNS
  8. lineinfile:
  9. path: "/etc/hosts"
  10. line: "{{item}} {{hostvars[item].ansible_hostname}}"
  11. when: item != inventory_hostname
  12. loop: "{{ play_hosts }}"

执行结果如下:

  1. TASK [Gathering Facts] ****************************************************************
  2. ok: [192.168.20.4]
  3. ok: [192.168.20.5]
  4. TASK [add DNS] ************************************************************************
  5. skipping: [192.168.20.4] => (item=192.168.20.4)
  6. changed: [192.168.20.4] => (item=192.168.20.5)
  7. changed: [192.168.20.5] => (item=192.168.20.4)
  8. skipping: [192.168.20.5] => (item=192.168.20.5)

5、配置yum镜像源并安装软件

需求如下:

  • 备份原有yum镜像源文件,并配置清华大学的yum镜像源:os源和epel源
  • 安装常用软件,包括lrzsz、dos2unix、wget、curl、vim等;

playbook如下:

  1. [root@ansible ansible]# cat config_yum.yaml
  2. - name: config yum repo add install software
  3. hosts: node
  4. gather_facts: false
  5. tasks:
  6. - name: backup origin yum repos
  7. shell:
  8. cmd: "mkdir bak; mv *.repo bak"
  9. chdir: /etc/yum.repos.d
  10. creates: /etc/yum.repos.d/bak
  11. - name: add os repo and epel repo
  12. yum_repository:
  13. name: "{{item.name}}"
  14. description: "{{item.name}} repo"
  15. baseurl: "{{item.baseurl}}"
  16. file: "{{item.name}}"
  17. enabled: 1
  18. gpgcheck: 0
  19. reposdir: /etc/yum.repos.d
  20. loop:
  21. - name: os
  22. baseurl: "https://mirrors.tuna.tsinghua.edu.cn/centos/7/os/$basearch"
  23. - name: epel
  24. baseurl: "https://mirrors.tuna.tsinghua.edu.cn/epel/7/$basearch"
  25. - name: install pkgs
  26. yum:
  27. name: lrzsz,vim,dos2unix,wget,curl
  28. state: present

在上面的yaml文件中,第一个任务是将所有系统默认的repo文件备份到bak目录中,chdir参数表示在执行shell模块的命令前先切换到/etc/yum.repos.d目录下,creates参数表示bak目录存在时则不执行shell模块。

第二个任务是使用yum_repository模块配置yum源,该模块可添加或移除yum源。

相关参数如下:

  • name:指定repo的名称,对应于repo文件中的[name];
  • description:repo的描述信息,对应repo文件中的name:xxx;
  • baseurl:指定该repo的路径;
  • file:指定repo的文件名,不需要加.repo后缀,会自动加上;
  • reposdir:repo文件所在的目录,默认为/etc/yum.repos.d目录;
  • enabled:是否启用该repo,对应于repo文件中的enabled;
  • gpgcheck:该repo是否启用gpgcheck,对应于repo文件中的gpgcheck;
  • state:present表示保证该repo存在,absent表示移除该repo。

在上面的配置中使用了一个loop循环来添加两个repo:os和epel。

第三个任务是使用yum模块安装一些rpm包,yum模块可以更新、安装、移除、下载包。

yum常用参数说明:

  • name:指定要操作的包名

    • 可以带版本号;
    • 可以是单个包名,也可以是包名列表,或者逗号分隔多个包名;
    • 可以是url;
    • 可以是本地rpm包
  • state:

    • present和installed:保证包已安装,它们是等价的别名;
    • latest:保证包已安装了最新版本,如果不是则更新;
    • absent和removed:移除包,它们是等价的别名;
  • download_only:仅下载不安装包(ansible 2.7才支持)

  • download_dir:下载包存放在哪个目录下(ansible 2.8才支持)

yum模块是RHEL系列的包管理器,如果是ubuntu则无法使用,可以使用另一个更为通用的包管理器模块:package,它可以自动探测目标节点的包管理器类型并使用它们去管理软件。大多数时候使用package来代替yum或代替apt-install等不会有什么问题,但是有些包名在不同的操作系统上是不一样的,这是需要注意的。

6、时间同步

保证时间同步可以避免很多玄学性的问题,特别是对集群中的节点。

通常会使用ntpd时间服务器来保证时间的同步,这里使用aliyun提供的时间服务器来保证时间同步,并将同步后的时间同步到硬件。

playbook文件如下:

  1. ---
  2. - name: sync time
  3. hosts: node
  4. gather_facts: false
  5. tasks:
  6. - name: install and sync time
  7. block:
  8. - name: install ntpdate
  9. yum:
  10. name: ntpdate
  11. state: present
  12. - name: ntpdate to sync time
  13. shell: |
  14. ntpdate ntp1.aliyun.com
  15. hwclock -w

上面使用了一个block指令来组织了两个有关联性的任务,将他们作为了一个整体。block更多的用于多个关联性任务之间的异常处理。

7、关闭selinux

关闭selinux的playbook如下:

  1. [root@ansible roles]# cat disable_selinux.yaml
  2. ---
  3. - name: disable selinux
  4. hosts: node
  5. gather_facts: false
  6. tasks:
  7. - name: disable on the fly
  8. shell: setenforce 0
  9. ignore_errors: true #由于上条命令执行后的返回状态码不一定为0,所以为了防止非0报错并停止palsybook接下来的任务,所以使用ignore_errors忽略错误
  10. - name: disable forever in config
  11. lineinfile:
  12. path: /etc/selinux/config
  13. line: "SELINUX=disabled" #修改配置文件中的值,以便永久关闭
  14. regexp: '^SELINUX=' #要修改的内容

注:ignore_errors也经常结合block使用,因为在block级别上设置异常处理,可以处理block内部的所有错误。

8、配置iptables规则

playbook文件如下:

  1. - name: Set Firewall
  2. hosts: node
  3. gather_facts: false
  4. tasks:
  5. - name: set iptables rule
  6. shell: |
  7. # 备份已有规则
  8. iptables-save > /tmp/iptables.bak$(date +"%F-%T")
  9. # 给它三板斧
  10. iptables -X
  11. iptables -F
  12. iptables -Z
  13. # 放行lo网卡和允许ping
  14. iptables -A INPUT -i lo -j ACCEPT
  15. iptables -A INPUT -p icmp -j ACCEPT
  16. # 放行关联和已建立连接的包,放行22、443、80端口
  17. iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
  18. iptables -A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
  19. iptables -A INPUT -p tcp -m tcp --dport 443 -j ACCEPT
  20. iptables -A INPUT -p tcp -m tcp --dport 80 -j ACCEPT
  21. # 配置filter表的三链默认规则,INPUT链丢弃所有包
  22. iptables -P INPUT DROP
  23. iptables -P FORWARD DROP
  24. iptables -P OUTPUT ACCEPT

9、远程修改sshd配置文件并重启

有时候为了服务器的安全,可能会去修改目标节点上sshd服务的默认配置,比如禁止root用户登录、禁止密码认证登录而只允许使用ssh密码认证等。

在修改服务的配置文件时,一般有几种方法:

  • 通过远程执行sed等命令进行修改配置文件;
  • 通过lineinfile模块去修改配置文件;
  • 在ansible本地段写好配置文件,然后使用copy模块或者template模块传输到目标节点上。

相对来说,第三种方案是最统一、最易维护的方案。

此外,对于服务进程来说,修改了配置文件往往意味着要重启服务,使其加载新的配置文件,对于sshd也一样如此,但是sshd要比其他服务特殊一些,因为ansible默认基于ssh连接,重启sshd服务会使ansible连接断开,好在ansible默认会重试建立连接,无非是多等待几秒。但重建连接有可能会失败,比如修改了配置文件不允许重试、修改了sshd的监听端口等,这可能会使得ansible因连接失败而无法再继续执行后续任务。

所以,在修改sshd配置文件时,有如下建议:

  • 将此任务作为初始化服务器的最后一个任务,即使连接失败也无所谓;
  • 在playbook中加入连接失败的异常处理;
  • 如果目标节点修改了sshd端口号,建议通过ansible自动或者我们手动去修改inventory文件中的ssh连接端口号。

这里为了简单,我准备使用lineinfile模块去修改配置文件,要修改的内容只有两项:

  • 将PermitRootLogin指令设置为no,禁止root用户直接登录;
  • 将PasswordAuthentication指令设置为no,不允许使用密码认证的方式登录

playbook内容如下:

  1. [root@ansible roles]# cat sshd_config.yaml
  2. ---
  3. - name: modify sshd_config
  4. hosts: node
  5. gather_facts: false
  6. tasks:
  7. # 1.备份/etc/ssh/sshd_config文件
  8. - name: backup sshd config
  9. shell:
  10. /usr/bin/cp -f {{path}} {{path}}.bak
  11. vars:
  12. - path: /etc/ssh/sshd_config
  13. # 2.设置PermitRootLogin no
  14. - name: disable root login
  15. lineinfile:
  16. path: "/etc/ssh/sshd_config"
  17. line: "PermitRootLogin no"
  18. insertafter: "^#PermitRootLogin"
  19. regexp: "^PermitRootLogin"
  20. notify: "restart sshd"
  21. # 3.设置PasswordAuthentication no
  22. - name: disable password auth
  23. lineinfile:
  24. path: "/etc/ssh/sshd_config"
  25. line: "PasswordAuthentication no"
  26. regexp: "^PasswordAuthentication yes"
  27. notify: "restart sshd"
  28. handlers:
  29. - name: "restart sshd"
  30. service:
  31. name: sshd
  32. state: restarted

关于notify和handlers的作用如下:

ansible会监控playbook执行后的changed的状态,如果changed=1,则表示关注的状态发生了改变,即本次任务的执行不具备幂等性,如果changed=0,则表示本次任务要么没执行,要么执行了也没有影响,即本次任务具备幂等性。

ansible提供了notify指令和handlers功能,如果在某个task中定义notify指令,当ansible在监控到该任务changed=1时,会触发该notify指令所定义的handler,然后去执行handler。所谓handler,其实就是task,无论是在写法上还是作用上它和task都没有什么区别,唯一的区别在于handler是被触发而被动执行的,不像普通task一样会按流程正常执行。

唯一需要注意的是,notify和handler中任务的名称必须一致。比如: notify: “restart sshd”,那么handlers中必须得有一个任务设置了 name: “restart sshd”。

此外,在上面的playbook中,两个lineinfile任务都设置了相同的notify,但ansible不会多次去重启sshd,而是在最后重启一次。实际上,ansible在执行完某个任务之后,并不会立即去执行对应的handler,而是在当前play中所有普通任务都执行完成后再去执行handler,这样的好处是可以多次触发notify,但最后只执行一次对应的handler,从而避免多次重启。

10、整合所有任务到单个playbook中

这里将前面所有的playbook集合到单个playbook文件中去,这样就可以一次性执行所有任务。

整合后的playbook如下:

  1. ---
  2. - name: Configure ssh Connection
  3. hosts: node
  4. gather_facts: false
  5. connection: local
  6. tasks:
  7. - name: configure ssh connection
  8. shell: |
  9. ssh-keyscan {{inventory_hostname}} >>~/.ssh/known_hosts
  10. sshpass -p'123.com' ssh-copy-id root@{{inventory_hostname}}
  11. - name: Set Hostname
  12. hosts: node
  13. gather_facts: false
  14. vars:
  15. hostnames:
  16. - host: 192.168.20.4
  17. name: node01
  18. - host: 192.168.20.5
  19. name: node02
  20. tasks:
  21. - name: set hostname
  22. hostname:
  23. name: "{{item.name}}"
  24. when: item.host == inventory_hostname
  25. loop: "{{hostnames}}"
  26. - name: Add DNS For Each
  27. hosts: node
  28. gather_facts: true
  29. tasks:
  30. - name: add DNS
  31. lineinfile:
  32. path: "/etc/hosts"
  33. line: "{{item}} {{hostvars[item].ansible_hostname}}"
  34. when: item != inventory_hostname
  35. loop: "{{ play_hosts }}"
  36. - name: Config Yum Repo And Install Software
  37. hosts: node
  38. gather_facts: false
  39. tasks:
  40. - name: backup origin yum repos
  41. shell:
  42. cmd: "mkdir bak; mv *.repo bak"
  43. chdir: /etc/yum.repos.d
  44. creates: /etc/yum.repos.d/bak
  45. - name: add os repo and epel repo
  46. yum_repository:
  47. name: "{{item.name}}"
  48. description: "{{item.name}} repo"
  49. baseurl: "{{item.baseurl}}"
  50. file: "{{item.name}}"
  51. enabled: 1
  52. gpgcheck: 0
  53. reposdir: /etc/yum.repos.d
  54. loop:
  55. - name: os
  56. baseurl: "https://mirrors.tuna.tsinghua.edu.cn/centos/7/os/$basearch"
  57. - name: epel
  58. baseurl: "https://mirrors.tuna.tsinghua.edu.cn/epel/7/$basearch"
  59. - name: install pkgs
  60. yum:
  61. name: lrzsz,vim,dos2unix,wget,curl
  62. state: present
  63. - name: Sync Time
  64. hosts: node
  65. gather_facts: false
  66. tasks:
  67. - name: install and sync time
  68. block:
  69. - name: install ntpdate
  70. yum:
  71. name: ntpdate
  72. state: present
  73. - name: ntpdate to sync time
  74. shell: |
  75. ntpdate ntp1.aliyun.com
  76. hwclock -w
  77. - name: Disable Selinux
  78. hosts: node
  79. gather_facts: false
  80. tasks:
  81. - block:
  82. - name: disable on the fly
  83. shell: setenforce 0
  84. - name: disable forever in config
  85. lineinfile:
  86. path: /etc/selinux/config
  87. line: "SELINUX=disabled"
  88. regexp: '^SELINUX='
  89. ignore_errors: true
  90. - name: Set Firewall
  91. hosts: node
  92. gather_facts: false
  93. tasks:
  94. - name: set iptables rule
  95. shell: |
  96. # 备份已有规则
  97. iptables-save > /tmp/iptables.bak$(date +"%F-%T")
  98. # 给它三板斧
  99. iptables -X
  100. iptables -F
  101. iptables -Z
  102. # 放行lo网卡和允许ping
  103. iptables -A INPUT -i lo -j ACCEPT
  104. iptables -A INPUT -p icmp -j ACCEPT
  105. # 放行关联和已建立连接的包,放行22、443、80端口
  106. iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
  107. iptables -A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
  108. iptables -A INPUT -p tcp -m tcp --dport 443 -j ACCEPT
  109. iptables -A INPUT -p tcp -m tcp --dport 80 -j ACCEPT
  110. # 配置filter表的三链默认规则,INPUT链丢弃所有包
  111. iptables -P INPUT DROP
  112. iptables -P FORWARD DROP
  113. iptables -P OUTPUT ACCEPT
  114. - name: Modify sshd_config
  115. hosts: node
  116. gather_facts: false
  117. tasks:
  118. - name: backup sshd config
  119. shell:
  120. /usr/bin/cp -f {{path}} {{path}}.bak
  121. vars:
  122. - path: /etc/ssh/sshd_config
  123. - name: disable root login
  124. lineinfile:
  125. path: "/etc/ssh/sshd_config"
  126. line: "PermitRootLogin no"
  127. insertafter: "^#PermitRootLogin"
  128. regexp: "^PermitRootLogin"
  129. notify: "restart sshd"
  130. - name: disable password auth
  131. lineinfile:
  132. path: "/etc/ssh/sshd_config"
  133. line: "PasswordAuthentication no"
  134. regexp: "^PasswordAuthentication yes"
  135. notify: "restart sshd"
  136. handlers:
  137. - name: "restart sshd"
  138. service:
  139. name: sshd
  140. state: restarted