阅读视图

发现新文章,点击刷新页面。
🔲 ☆

Ansible 技巧之场

Ansible 技巧之场

看到技巧之场,我就想起来被百万氪金王支配的恐惧!

拼接主机与http或其他字符串

有时候我们有一个列表,里面是IP地址。但是有时候我们要将http://拼接在每个IP地址的开头,可以利用Jinja2语法进行操作。

在模版中:

{% set http_hosts=[] %} # 定一个list变量
{% set hosts_list = discovery_seed_hosts | flatten %} # 将列表扁平化,因为这个变量列表里面存在嵌套的情况
{% for host in hosts_list %}
{% set _ = http_hosts.append("http://%s:%s" % (host,9200)) %} #使用Python语法进行操作。
{% endfor %}

在Plybook中(从网上找到的未经过测试,理论可行):

set_fact:
cluster_address: |
[
{% set pxc_hosts = groups.pxc %}
{% for host in pxc_hosts %}
{% set pxc_ip = hostvars[host].inventory_hostname | default(host) %}
{% set pxc_port = group_port %}
"{{ pxc_ip }}:{{ pxc_port }}"
{% endfor -}
]

# 使用的时候
{{ cluster_address | join(',') }}' # 使用join 过滤器可生成',' 分隔的字符串,分隔符自己定义

对字符串的操作

得益于Python特性,在Ansible中可以使用Python中处理字符串的语法。

你可以对字符串进行取单个字符等操作: {{ vars[2] }}

也可以切片: {{ vars[3:6]}}

判断某些字符是否在字符串中: when: "'haha' in vars"

使用加号连接两个字符串: {{ var1+var2 }}

使用乘号*连续输出字符串: {{ var1*3 / 'la'*nums }}

使用find函数查找:

when: {{ vars.find('test') == -1 }} # 值为

find函数的语法是find(string,begin,end)

when: {{ vars.find('test',5) == -1 }} # 从第5位开始查找

任务委派

有时候你想让某一个tasks在指定的某一个受控主机上运行而不是要在Play中指定的组或主机中全部运行这个tasks。

这种情况下可以新写一个Play,然后将hosts和tasks进行指定。但是这样要新建一个Play,有点麻烦。

可以使用delegate_to关键字来让某一个tasks指定运行在某一台主机上,任务委派时不会去看目标主机是否在inventory主机清单中。可以指定代码块进行任务委派。

- debug:
msg: "only host"
delegate_to: 192.168.1.196

---

ok: [192.168.1.39 -> 192.168.1.196] => {
"msg": "only host"
}

仅在主机上执行

如果指向让某一个任务在本主机上运行。你可以使用刚刚的任务委派,来指向Ansible控制节点。也可以尝试connection: local关键字。

connection关键字也可以用于代码块和任务。

- block:
- systemd:
name: elasticsearch
state: restarted
connection: local

仅运行一遍任务

在某些情况下,下载资源到本地时,你只需要下载一次就可以。但是任务却执行和主机清单中相同的次数。得益于Ansible模块幂等性,虽然说任务运行很多次,但只有第一次真正进行了下载,对我们的影响不大。但是能否继续减少性能开支呢。

使用run_once: true关键字让任务或代码块只运行一次。

- hosts: all
  gather_facts: no
  tasks:
  - get_url:
      url: "https://github.com/rofl0r/proxychains-ng/archive/v4.14.tar.gz"
      dest: "/tmp"
    connection: local
    run_once: true
 
  - copy:
      src: "/tmp/v4.14.tar.gz"
      dest: "/tmp"

遇到过的错误…

在when关键字中使用了Jinja2语法

[WARNING]: conditional statements should not include jinja2 templating delimiters such as {{ }} or {% %}. Found:XXX

在when关键字中不应该包含Jinja2语法。在使用循环或者变量时不小心就会顺手把花括号加上。此时会发出警告。

错误用法:
when: "'{{item.hosts}}' in group_names"

正确用法:
when: item.hosts in group_names

没有获取事实就要使用hostvars获取变量

在主机没有gether facts事实之前,是无法通过hostvars来获取它的事实变量的。这个错误在生成hosts文件时出现过一次,折腾了好久。难受的是没有指向正确的报错位置,这导致我在错误的方向研究了好久都没有解决。

错误用法:
when: "'{{item.hosts}}' in group_names"

---

正确用法:
when: item.hosts in group_names
🔲 ☆

Ansible 包含和导入文件

Ansible 包含和导入文件

如果Playbook很长或很复杂,可以尝试将其分成较小的文件以便与管理。适宜食用模块化方式将多个Playbook组合成一个主要的Playbook,或者将文件中的任务列表插入到Play中。和杨可以更轻便、更加便于管理的操作项目中的Play和任务序列。

包含与导入文件概念区别

Ansible支持两种方式将内容带入到Playbook,可以使用包含内容,也可以使用导入内容。

包含内容(include)是一个动态的过程。在playbook运行期间,Ansible会在内容到达时再去处理所包含的内容。

导入内容(import)是一个静态操作。在最开始的时候Ansible运行Playbook时就将它解析好并导入到要执行的这个Playbook中了。

导入Playbook

使用import_playbook指令可以将外部的Playbook导入到当前的Playbook中。

导入的Playbook是一个完成的playbook,因此要使用import_playbook指令需要在Play的位置导入,而不能在tasks处进行导入Playbook。如果你导入了多喝playbook,则将按照导入的顺序一次运行.

---
- name: Play1
import_playbook: web.yml

- name: Play2
hosts: localhost
tasks:
- debug:
msg: This is Play 2

- name: Play3
import_playbook: db.yml
...

在上面的例子中,会一次执行三个Play,并且导入操作可以和展开的Play穿插使用。

导入和包含任务

可以将任务文件中的任务列表导入或包含在Play中。任务文件是包含一个任务平面列表的文件:

vim tasks_firewalld.yml

- name: Install the firewall
yum:
name: firewalld
state: latest

- name: Start the firewall
service:
state: started
name: firewalld
enabled: true

导入任务文件

使用import_tasks功能将任务文件静态导入Playbook的Play中。在一开始解析Playbook时就会将该任务文件插入到Play中。

- name: Play
hosts: localhost
tasks:
- debug:
msg: This is Play
- name: I'm Import Task
import_tasks: db.yml
  • 使用import_tasks功能时,导入时设置的when等条件语句将应用于导入的每个任务。
  • 使用import_tasks功能时,无法将loop循环应用在上面。
  • 如果使用变量来指定要导入的文件的名称,则无法使用主机或组清单变量。

包含任务文件

使用include_tasks可以将任务文件动态包含进Tasks中。

- name: Play
hosts: localhost
tasks:
include_tasks: db.yml

在Play运行到达该Tasks前,Ansible不会处理该任务文件的任何内容。使用include_tasks时,包含时设置的when等条件语句将确定任务是否包含在Play中。

🔲 ☆

Ansible 学习文档

Ansible 学习文档

Ansible自动化运维学习。大部分操作及示例基于RHEL8实现。

文档未进行过任何校对和查错,目前仅用于个人学习使用。文档中可能会包含错误,请您雅正,谢谢。

最近一次的更新日期:2020年6月27日

本次更新内容:继续完成《Ansible 常用模块》 ⭕️

近期持续更新Ansible常用模块。

文档列表

更新日志

2020年6月27日

  • 继续完成《Ansible 常用模块》 ⭕️

2020年6月22日

  • 完成《Ansible 技巧之场》✅

2020年6月18日

  • 修复《Ansible 任务控制》中部分Markdown解析错误

2020年6月16日

  • Ansible角色 ✅
  • 添加部分刚刚没有同步到文档中的内容
  • 添加软件包管理模块使用
🔲 ☆

Ansible 主机模式

Ansible 主机模式

通过主机模式,可以更高效地为play或命令选择受控主机。除了常规的使用组名或清单中的IP地址以及all、ungrouped外,你还可以使用以下方式匹配受控主机。

使用通配符匹配

使用'*'匹配模式会与all主机模式具有相同的效果,它会匹配任意字符串,会使用清单中的全部主机。

- hosts: all
...
- hosts: '*'

也可以使用*与文字进行匹配,使用通配符匹配以.example.com结尾的全部主机例子如下:

- hosts: '*.example.com'

匹配192.168开头的全部主机或主机组的名称:

- hosts: '192.168*'

⚠️注意:使用匹配模式匹配时,Ansible不会区分它是主机还是组的名字。如果主机的域名和组名有被匹配到,那么不管是主机还是主机组中全部的主机都会被匹配。

使用逗号匹配多个主机和组

使用逗号可以来匹配多个主机和组,可以混合使用受控主机、主机组和通配符。

- hosts: 'server1.com,*.example.com,192.168.1.1'

也可以使用:冒号作为分隔符进行分隔,特别是将IPv6地址用作受管主机名称时。不过在较老的例子中依旧可以见到使用冒号进行分隔。

使用&!

如果在使用逗号的列表中某一项与&结合,则主机必须与该项也匹配才会匹配主机模式。

如下例子,主机模式匹配同时出现在lab组和datacenter组中的主机才能作为受控主机。

- hosts: 'lab,&datacenter'

也可以通过在主机模式前面使用感叹号!,从列表中排除匹配某以模式的主机。他的工作方式类似于逻辑NOT。

如下例子:主机模式匹配datacenter中的所有主机,但是server1.example.com主机除外!

- hosts: 'datacenter,!server1.example.com'

管理动态清单

目前为止使用静态清单很容易编写,对于管理小型的主机架构很方便。但是如果要操作一个大型的集群中许许多多的计算机,或者在机器交替非常快的环境工作,在面对维护主机静态清单想必是不太好受。

在大型IT环境下,通常会有系统来跟踪服务器资产,比如有外部目录服务通过Zabbix等监控系统维护,或者位于FreeIPA或Active Directory服务器上。Cobbler等安装服务器或红帽卫星等管理服务可能跟踪部署的逻辑系统。

Ansible支持动态清单脚本,这些清单脚本在Ansible执行时会执行脚本并检索当前信息,时清单能够得到实时的更新。这些脚本时可以执行的程序,能够从一些外部来源收集信息,并以JSON格式输出清单。

动态清单脚本的使用和静态清单一样。清单的位置可以直接在ansible.cfg中配置,也可以通过-i选型指定。如果清单文件可以执行,则会被视作动态清单程序,Ansible会尝试运行脚本来生成清单。如果文件不可执行,则它会被视作静态清单。

在使用动态清单时要确保该脚本具有执行权限并且返回值为JSON数据类型。

管理多个清单

Ansible支持在同一运行中使用多个清单,如果配置清单的地址是一个目录,那么会使用该目录下所有的清单文件,如果可执行文件会作为动态清单文件对待。

🔲 ☆

Ansible 角色

Ansible角色

使用Ansible角色可以有更多的机会去重用以前便携的Playbook中的代码。可以在标准化目录结构中打包所有任务、变量、文件、模版以及调配基础架构或部署应用其他资源。

除了自行编写、使用角色外,也可从其他来源获取角色。常用红帽企业Linux管理角色包含在rhel-system-roles软件包中可以方便的去使用。也可以从Ansible Galaxy网站获取由社区提供的其他角色。

使用ansible-galaxy list可以看到在在当前Ansible配置环境下找到了Role角色列表。

那么Ansible怎么发现这些Role呢?这些Role的路径在ansible.cfg配置文件中已经定义好了,每个目录之间通过冒号分隔:

[defaults]
inventory=./inventory
remote_user=devops
roles_path=./roles:/usr/share/ansible/roles:/etc/ansible/roles

Ansible角色子目录

子目录描述
defaults此目录中的main.yml文件包含角色变量的默认值,使用角色时可以覆盖这些默认值。这些变量的优先级较低,应该在Play中更改和定义。
files此目录包含由角色要处理的全部静态文件
handles此目录中的main.yml文件包含角色的处理程序
metamain.yml中包含角色相关信息,如作者、许可证、平台、角色依赖项
tasksmain.yml中包含角色任务的定义
templates此目录包含角色引用的template模板
tests此目录包含清单和test.yml Playbook,用于进行测试
vars此目录的main.yml文件定义角色的变量值。这些变量通常用于角色内部用途。这些变量的优先级较高,不应在Playbook中覆盖修改

定义变量和默认值

对于角色变量可以通过在vars目录下的main.yml文件来定义。与其他变量一样,使用这些变量需要在角色文件中引入{{ VARS_NAME }}。这些变量具有较高的优先级,无法被Ansible中清单变量覆盖。

默认变量在defaults目录下的main.yml文件中定义。它们的变量优先级是最低的,任何定义变量的形式都会将其覆盖,所以更改默认变量可以使Play操作更精准、更适合受控主机。

可以在vars/main.yml或defaults/main.yml中定义具体的变量,但没必要在两者中都定义变量。

在Playbook中使用Ansible角色

可以在Play中引入roles即可使用。

- hosts: remote server
roles:
- role1
- role2

角色中使用的任何copy、script、template或include_tasks/import_tasks任务都可以引用角色中相关的文件、模版或任务文件,并且无需使用相对路径或绝对路径,因为Ansible会自动在角色的files、templates、tasks子目录下去寻找他们。

控制执行顺序

在Ansible中的每一个Play是会按照Play的顺序依次执行。在每个Play中如果定义了角色,那么会优先运行角色,之后再运行任务。在最后执行被激活的handles处理程序。

在某些情况下,可能需要在执行角色任务之前执行一些任务,你可以为Play配置pre_tasks部分,这样就可以在运行角色之前执行一部分任务。如果配置在pre_tasks中的任务出发了handles处理程序。那么也会在角色或其他普通任务之前执行处理程序。

当然也可以使用为Play配置post_tasks部分,来让任务在普通任务之后和激活的处理程序之后再运行。

---
- name: Play to illustrate order
hosts: example.com
pre_tasks:
- debug:
msg: "I am pre task"
notify: my handler
roles:
- role1
tasks:
- debug:
msg: "I am nomal task"
notify: my handler
post_tasks:
- debug:
msg: "I am posted handler"
...

上面的例子中,每个任务部分都会执行debug任务来通知my handler处理程序。my handler任务执行了三次:

  • 第一次在执行了所有的pre_tasks任务后执行处理程序
  • 第二次在执行角色结束后的普通tasks任务后执行
  • 第三次是在执行完post_tasks任务后执行处理程序

除了将角色包含在Play中的roles部分外,也可以将角色添加到普通的tasks中。使用include_role模块可以动态包含角色,使用import_role模块可以静态导入角色。

---
- name: Execute a role as a task
hosts: localhost
tasks:
- name: a simple task
debug:
msg: "Im first task"

- name: "A task to include role here"
include_role:
name: linux-system-roles.network
...

RHEL 系统角色

RHEL红帽系统中,从Linux 7.4开始,系统内随附了多个Ansible角色。它们位于rhel-system-roles软件包内。RHEL8中,需要启动AppStream仓库来安装此软件包。

角色名描述
rhel-system-roles.kdump配置kdump崩溃恢复服务
rhel-system-roles.network配置网络
rhel-system-roles.postfix配置postfix服务为每个主机配置邮件传输代理
rhel-system-roles.selinux配置和管理SELinux自定义,包括模式、文件、端口上下文、布尔值设置和SELinux用户
rhel-system-roles.timesync使用网络时间协议配置时间同步

访问RHEL系统角色文档

安装后,这些RHEL系统角色文档存放在/usr/share/doc/rhel-system-roles中,其中还包含了如何去使用以及使用的例子。

├── kdump
│   ├── COPYING
│   ├── README.html
│   └── README.md
├── network
│   ├── example-bond-with-vlan-playbook.yml
│   ├── example-bridge-with-vlan-playbook.yml
│   ├── example-down-profile-playbook.yml
│   ├── example-eth-simple-auto-playbook.yml
│   ├── example-eth-with-vlan-playbook.yml
│   ├── example-infiniband-playbook.yml
│   ├── example-inventory
│   ├── example-macvlan-playbook.yml
│   ├── example-remove-profile-playbook.yml
│   ├── LICENSE
│   ├── README.html
│   └── README.md
├── postfix
│   ├── COPYING
│   ├── README.html
│   └── README.md
├── selinux
│   ├── COPYING
│   ├── example-selinux-playbook.yml
│   ├── README.html
│   └── README.md
└── timesync
├── COPYING
├── example-timesync-playbook.yml
├── example-timesync-pool-playbook.yml
├── README.html
└── README.md

时间同步角色例子

如果为受控主机配置NTP时间同步服务,那么可以使用rhel-system-roles.timesync角色自动化配置。

通过/usr/share/doc/rhel-system-roles/timesync/README.md查看示例和需要用到的变量。

---
- name: Config NTP sync service
hosts: all
vars:
timesync_ntp_servers:
- hostname: 0.rhel.pool.ntp.org
iburst: yes
- hostname: 1.rhel.pool.ntp.org
iburst: yes
timezone: Asia/Shanghai
roles:
- rhel-system-roles.timesync

tasks:
- name: Set TimeZone
timezone:
name: "{{ timezone }}"
...

也可以把在Play中定义的变量放到变量文件中。将变量文件可以放到group_varshost_vars子目录中。

配置SELinux角色

使用linux-system-roles.selinux角色控制SELinux行为。t

通过/usr/share/doc/rhel-system-roles/selinux/README.md查看示例和需要用到的变量。

设置SELinux运行模式

变量 selinux_state: enforcing。可以设置为enforing、permissive、disabled。如果不设置则不更改。

设置SELinux布尔值

例如将httpd_enable_homedirs布尔值永久设置为no

selinux_booleans:
- name: 'http_enable_homedirs'
state: 'on'
persistent: 'yes'

设置SELinux fcontext上下文

下面的例子完成了对/srv/www目录下的所有文件的默认SELinux类型设置为httpd_sys_content_t

selinux_fcontexts:
- target: '/srv/www(/.*)?'
setype: 'httpd_sys_content_t'
state: present

使用selinux_restore_dirs变量指定要对其运行restorecon目录的列表。

设置SELinux端口

使用selinux_ports变量可以对端口进行管理。

selinux_ports: 
- ports: '82'
setype: 'http_port_t'
proto: 'tcp'
state: 'present'

创建角色框架

创建角色不需要额外的开发工具,角色是文件目录结构和文件组成的。可以使用创建目录和编辑文件命令配合完成创建一个角色框架。为了节省时间,可以使用ansible-galaxy init来创建角色框架。

ansible-galaxy init my_role

$ ls my_role/
defaults files handlers meta README.md tasks templates tests vars

默认变量的覆盖

在以下情况中,角色中defaults目录中定义的默认变量会被覆盖:

  • 在清单文件中定义,作为主机变量或组变量
  • 在playbook项目的group_vars或hosts_vars目录下的YAML文件中定义
  • 作为变量嵌套Play的vars关键字中定义
  • 在Play的定义roles角色时的所定义的变量

安装V2ray-core服务角色示例

功能如下:

  • 自动安装最新版本
  • 按照受控节点平台自动选择platform平台
  • 可以选择从Github/Jsdelivr/用户自定义V2ray-core地址下载
  • 可以控制卸载

Tasks角色任务:

---
# tasks file for v2ray
- name: Prepare to Install V2ray-core
block:
- name: V2ray download path is exist
file:
path: "{{ v2ray_download_path }}"
state: directory
mode: 0755
- set_fact:
install_error: false

when: v2ray_present

- name: Get the latest V2ray version and servers info
block:

- name: Get Server machine platform
shell: |
case "${1:-"{{ ansible_facts.machine }}"}" in
i686|i386)
echo '32'
;;
x86_64|amd64)
echo '64'
;;
*armv7*|armv6l)
echo 'arm'
;;
*armv8*|aarch64)
echo 'arm64'
;;
*mips64le*)
echo 'mips64le'
;;
*mips64*)
echo 'mips64'
;;
*mipsle*)
echo 'mipsle'
;;
*mips*)
echo 'mips'
;;
*s390x*)
echo 's390x'
;;
ppc64le)
echo 'ppc64le'
;;
ppc64)
echo 'ppc64'
;;
*)
return 1
;;
esac
register: return_machine
- name: Get the latest V2ray version and servers info
shell: >
curl -H "Accept: application/json" -H "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:74.0) Gecko/20180101 Firefox/74.0" -s "https://api.github.com/repos/v2ray/v2ray-core/releases/latest" --connect-timeout 10| grep 'tag_name' | cut -d\" -f4
register: return_version
args:
warn: false
- set_fact:
latest_v2ray: "{{ return_version.stdout_lines[0] }}"
machine: "{{ return_machine.stdout }}"
rescue:
- name: "ERROR: Get the latest V2ray version and servers info. Please check your Network Connection and V2ray-core version"
set_fact:
install_error: true
when: v2ray_present and not install_error

- name: Download V2ray-core
block:
- name: "Download V2ray-core{{ latest_v2ray }} from jsdelivr"
get_url:
url: "https://cdn.jsdelivr.net/gh/v2ray/dist/v2ray-linux-{{ machine }}.zip"
dest: "{{ v2ray_download_path }}"
when: v2ray_download_from == "jsdelivr"
- name: "Download V2ray-core{{ latest_v2ray }} from Github"
get_url:
url: "https://github.com/v2ray/v2ray-core/releases/download/{{ latest_v2ray }}/v2ray-linux-{{ machine }}.zip"
dest: "{{ v2ray_download_path }}"
when: v2ray_download_from == "Github"
- name: "Download V2ray-core{{ latest_v2ray }} from {{ v2ray_download_from }}"
get_url:
url: "{{ v2ray_download_from }}"
dest: "{{ v2ray_download_path }}"
when: |
v2ray_download_from != "jsdelivr"
and
v2ray_download_from != "Github"
rescue:
- name: "ERROR: Download V2ray-core. Please check your Network Connection and V2ray-core version"
set_fact:
install_error: true
when: v2ray_present and not install_error


- name: Unarchive V2ray-core
block:
- name: Create unarchive V2ray-core directory
file:
path: "{{ v2ray_download_path }}/v2ray-linux-{{ machine }}"
state: directory
mode: 0755
- name: Unarchive V2ray-core
unarchive:
src: "{{ v2ray_download_path }}/v2ray-linux-{{ machine }}.zip"
dest: "{{ v2ray_download_path }}/v2ray-linux-{{ machine }}"
remote_src: true
rescue:
- name: "ERROR: Unarchive V2ray-core. Please check your platform"
set_fact:
install_error: true
when: v2ray_present and not install_error

- name: Install V2ray-core and Start V2ray-core
block:
- name: Create V2ray install directory
file:
path: "{{ item }}"
state: directory
mode: 0755
loop: "{{ v2ray_installed_dir }}"
- name: Copy binary file into directory
copy:
src: "{{ v2ray_download_path }}/v2ray-linux-{{ machine }}/{{ item.src }}"
dest: "{{ item.dest }}"
remote_src: true
mode: 0755
loop: "{{ v2ray_binary }}"
- name: Copy V2ray config and Start V2ray-core
copy:
src: "{{ v2ray_config }}"
dest: /etc/v2ray/config.json
mode: 0755
notify: Start V2ray Service
rescue:
- name: "ERROR: Install V2ray-core. Please check your permissions"
set_fact:
install_error: true
when: v2ray_present and not install_error


- name: Remove v2ray-core
block:
- name: Stop V2ray-core service
systemd:
name: v2ray.service
daemon_reload: true
enabled: false
state: stopped
ignore_errors: true
- name: Remove V2ray-core service
file:
path: /etc/systemd/system/v2ray.service
state: absent
- name: Remove v2ray-core file
file:
path: "{{ item }}"
state: absent
loop: "{{ v2ray_installed_dir }}"

when: not v2ray_present or install_error

- name: Check remove result
debug:
msg: Removed v2ray-core
when: not v2ray_present

- name: Check install result
debug:
msg: Install V2ray-core Failure
failed_when: yes
when: v2ray_present and install_error

- name: Check install result
debug:
msg: Install V2ray-core Successful
when: v2ray_present and not install_error

Default变量:

# You can download the latest V2ray-core from Github/jsdelivr/(https://yourself)
v2ray_download_from: "Github"
v2ray_download_path: "/tmp/v2ray"
v2ray_config: "files/v2ray.conf"

# user_control_boolean
v2ray_present: true

# Ansible Role Control vars (Don't change it!!!)
v2ray_installed_dir:
- /etc/v2ray
- /var/log/v2ray
- /usr/bin/v2ray
- "{{ v2ray_download_path }}"
v2ray_binary:
- src: geoip.dat
dest: /usr/bin/v2ray
- src: geosite.dat
dest: /usr/bin/v2ray
- src: v2ctl
dest: /usr/bin/v2ray
- src: v2ray
dest: /usr/bin/v2ray
- src: systemd/v2ray.service
dest: /etc/systemd/system
🔲 ☆

Ansible 调整连接数

Ansible调整连接数量和同时运行的任务数量

调整连接数forks

Ansible在处理Playbook时会按照顺序运行每一个play。确定受管主机列表后,Ansible将按照顺序在受管主机上运行任务。通常只有会在全部受管主机完成当前任务后才会继续往下执行其他任务。

Ansible可以同时连接到play中的所有主机以执行每项任务,这种方式适用于小规模的主机列表。如果对着数百台受控主机同时进行操作的话,控制节点会面临着巨大的性能压力。

所以,为了避免这种情况发生导致控制节点压力过大或出现异常,Ansible在执行任务时默认最大连接数为5。这个默认值是由Ansible配置文件中的forks参数控制。如果你的控制节点性能强劲,你可以在配置文件中手动配置该参数调整为更大的数值,这样在执行任务时可以同时连接到更多的受控节点,进而加快任务的执行速度。

[defaults]
inventory=inventory
remote_user=devops
forks=5

当然ansibleansible-playbook命令都提供了-f--forks选项来覆盖ansible在配置文件中读取到的forks值。

分批次完成任务

通常情况下Ansible会在执行下一个任务前,要求全部的受控主机都完成当前的任务。但是要求全部的主机同时完成某一个任务可能会带来麻烦,特别是在处理Web负载均衡的机器时,如果任务要求集群内的全部主机同时重启Web服务,那么业务就会中断。所以在playbook中使用serial关键字可以解决这个问题,它能够控制play批量的完成任务,而不是让全部主机同时都完成一个任务。

下面的例子是使用serial关键字来控制一批只能2台受控主机完成这个Play,假设我们一共有6台机器,那么剩下的四台机器分别要等前面的两台受控主机执行完成全部的Play后才会完成到它们执行Play,6台节点两两一组,分批次执行任务。这样不会导致我们的业务因为重启全部的机器而终止。

---
- name: Update web server
hosts: webservers
serial: 2
tasks:
- name: Latest version of apache installed
yum:
name: httpd
state: latest
notify:
- Restart apache

handlers:
- name: Restart apache
service:
name: httpd
enabled: yes
state: restarted
...

serial关键字也可以设置为百分比。此百分比应用于该play中主机的总数,已确定每次批量执行play的主机个数。

🔲 ☆

Ansible 常用模块

Ansible模块的使用

文件模块

Files文件模块库包含的模块可以对Linux文件进行管理,如创建、删除、编辑和修改文件的权限与属性等。

模块说明
blockinfile插入、更新或删除由可定义标记线包围的多行文本块
lineinfile确保特定行位于某个文件中,或使用反向引用正则表达式来替换现有行。此模块可以在想要更改某一个行的文本时使用
copy将文件从本地或远程计算机复制到目标主机的某个位置。类似于file模块,copy模块还可以设置文件属性,包括SELinux上下文
fetch该模块和copy类似,但以相反的方式工作。fetch用来从目标主机获取文件到本机控制节点上
file设置权限、所有权、SELinux上下文以及常规文件、符号链接、硬连接、目录时间戳等。此模块还可以创建或删除常规文件、符号链接、硬连接和目录。其他过个与文件相关的模块支持与file模块相同的属性设置选项,包括copy模块
stat检索文件的状态信息,与Linux stat命令相似
synchronize对rsync命令的打包

文件模块使用示例

确保目标主机上存在文件

- name: Touch a file and set permissions
file:
path: /home/student/touch.me
owner: student
group: root
mode: 0000
state: touch

如果目标主机已存在该文件,则会进行touch操作。上面的task除了确保文件存在以外,还会保证文件的权限为设定值。

  File: touch.me
Size: 14 Blocks: 8 IO Block: 4096 regular file
Device: 801h/2049dInode: 163550 Links: 1
Access: (0000/----------) Uid: ( 1000/ student) Gid: ( 0/ root)
Context: unconfined_u:object_r:user_home_t:s0
Access: 2020-06-02 17:11:40.642364330 -0400
Modify: 2020-06-02 17:11:40.642364330 -0400
Change: 2020-06-02 17:11:40.644530998 -0400
Birth: -
------------------------------
File: touch.me
Size: 14 Blocks: 8 IO Block: 4096 regular file
Device: 801h/2049dInode: 163550 Links: 1
Access: (0000/----------) Uid: ( 1000/ student) Gid: ( 0/ root)
Context: unconfined_u:object_r:user_home_t:s0
Access: 2020-06-02 17:18:59.746867300 -0400
Modify: 2020-06-02 17:18:59.746867300 -0400
Change: 2020-06-02 17:18:59.746867300 -0400
Birth: -

修改文件属性

使用file模块,确保新的或现有文件具有正确的文件属性和SELinux类型。

- name: SELinux type is set to samba_share_t
file:
path: /home/student/touch.me
setype: samba_share_t

修改前touch.me文件的setype属性是user_home_t,使用file模块处理后setype属性已经改变为samba_share_t

$ ls -lZ
----------. 1 student root unconfined_u:object_r:user_home_t:s0 14 Jun 2 17:18 touch.me
$ ls -lZ
----------. 1 student root unconfined_u:object_r:samba_share_t:s0 14 Jun 2 17:18 touch.me

永久更改SELinux文件上下文属性

设置上下文属性时,file模块的行为和chcon类似。通过运行restorecon可能会意外的撤销使用该模块对文件上下文所做的更改。当使用file设置上下文后,可以使用System模块集合中的sefcontext来更新SELinux策略,如semanage fcontext

- name: SELinux type is persistently set to samba_share_t
sefcontext:
target: /home/student/touch.me
setype: samba_share_t
state: present

可以看得到在SELinux上下文策略中目标的默认上下文已经更改为samba_share_t

# semanage fcontext -l | grep touch.me
/home/student/touch.me all files system_u:object_r:samba_share_t:s0

注意:sefcontext模块只更新SELinux策略中目标的默认上下文,并不更改当前现有文件的上下文。

在目标主机上复制和编辑文件

使用copy模块时,模块假定设置了force: yes。这会强制copy模块覆盖远程文件(如果存在并且包含于与当前要发送的文件内容不同)。如果手动设置force: no,则它仅会在目标主机不存在要复制的这个文件时才会进行复制。

- name: Copy a file to managed hosts
copy:
src: file
dest: /home/student/touch.me
force: yes

如果要从目标主机上拉取文件到本机,则使用fetch模块。

- name: Retrieve SSH key from reference host
fetch:
src: "/home/{{ user }}/.ssh/id_rsa.pub"
dest: "files/keys/{{ user }}.pub"

要确保现有文件中存在某行文本,可以使用lineinfile模块。

- name: Add a line of text to file
lineinfile:
path: /home/student/touch.me
line: "Can you touch me?"
state: present

如果要将文本块插入到文档中,应使用blockinfile模块。

- name: Add additional lines to a file
blockinfile:
path: /home/student/touch.me
block: |
This is the block of first line.
And
This is the block of third line.
state: present

使用blockinfile模块时,注释块标记插入到块的开头和结尾,用来让Ansible识别和保持幂等性。

# BEGIN ANSIBLE MANAGED BLOCK
This is the first line.
# END ANSIBLE MANAGED BLOCK

在目标主机上删除文件

在大多数情况下,如果控制目标主机文件的删除使用file模块的state: absent参数来控制。

- name: Removed file in the server
file:
dest: /home/student/touch.me
state: absent

检索文件的详细信息

使用stat模块可以查看文件的详细信息,并返回文件的事实。你可以利用这些Facts对文件进行检索和校验。stat模块类似于Linux系统中的stat命令。

- name: Check all stat of /etc/passwd
stat:
path: /etc/passwd
register: results

- debug:
vars: results

同步控制节点和受控节点之间的文件

使用synchronize模块来操作同步主机间的文件。synchronize对rsync工具进行打包,它简化了playbook中常见文件管理任务。使用该模块要求双方主机安装rsync工具。

- name: synchronize local file to server file
synchronize:
src: /etc
dest: /home/student/

使用Jinja2模版部署自定义文件

可以利用Jinja2模版语法通过和变量与事实相配合,对固定地方的值进行覆盖和编辑。来实现定制化修改配置文件。

使用`{% EXPR %}`用于表达式或逻辑(循环、判断)
使用`{{ EXPR }}`用于输出最终表达式或变量的结果
使用`{# COMMENT #}`注释,注释的内容不会出现在最终的结果里

构建Jinja2模版

Jinja2模版由多个元素组成:数据、变量和表达式。在呈现Jinja2模版时,这些变量和表达式被替换为对应的值。模版中使用的变量可以在playbook的vars部分中指定。可以将目标主机的事实作为模版中使用的变量。

可以使用ansible all -m setup来查看目标主机中全部的Fact事实。模版文件没有固定的文件拓展名,只要是文本文件即可,但是为了方便记忆理解,通常使用.j2来代表文本文件是Jinja2的模版文件。

部署Jinja2模板

我们刚刚创建好了Jinja2模版,现在要利用这些模版。我们需要使用template模块。src参数指的是模版文件在控制节点中的路径,dest的值是在目标主机的指定目录生成文件。

tasks:
- name: template render
template:
src: motd.j2
dest: /etc/motd
backup: true

template和file模块一样支持对文件权限进行设置。

标示配置文件由Ansible管理

我们使用模版生成文件后,为了避免管理员用户手动的修改这些配置文件,我们最好在模版的开头写上声明。虽然template不会自动地帮我们完成,但是我们可以在模版文件的开头手动引入设定好的提醒文本,使用Jinja2语法将变量的内容填写到配置文件中。

可以使用ansible_managed 指令中默认设置的 Ansible managed字符串来执行此操作。这不是一个正常的变量,但是可以在模版中用作一个变量。ansible_managed指令在ansible.cfg文件中的设置:

ansible_managed = Ansible managed: modified on %Y-%m-%d %H:%M:%S

要将在ansible.cfg文件中配置的ansible_managed字符串包含在Jinja2模版内,使用下面的引用变量语法即可。

{{ ansible_managed }}

通过模版生成的配置文件开头存在了Ansible managed: modified on 2020-06-10 15:55:29。这样就能对修改此文件的人有一个提示的作用。

控制结构

可以在Jinja2模版中使用控制结构,以减少重复的输入。为Play中每个主机能够动态的生成条目,或者有条件的将文本插入到文件中。

循环

Jinja2使用for语句来提供循环功能。

{% for user in users %}
{{ user }}
{% endfor %}

下面的示例模版使用for逐一遍历users变量中的所有值,将myuser替换为各个值,但值为root时除外。

{# for statement #}
{% for myuser in users if not myuser == "root" %}
User number is {{ loop.index }} - {{ myuser }}
{% endfor %}

loop.index变量是循环到当前处的索引号。他在循环第一次执行时的值为1,每一次迭代递增1。

下面的例子是生成hosts文件。

{% for host in groups['all'] %}
{{ hostvars[host]['ansible_facts']['default_ipv4']['address'] }} {{ hostvars[host]['ansible_facts']['fqdn'] }} {{ hostvars[host]['ansible_facts']['hostname'] }}
{% endfor %}
---
- name: generate hosts
hosts: all
tasks:
- name: generate template
template:
src: hosts.j2
dest: /home/student/hosts
...

条件语句

Jinja2使用if语句来提供条件控制。如果满足某些条件,则会按照语句块内的规则继续生成。

{% if finished %}
{{ result }}
{% endif %}

Jinja2仅能用于模版,不能用于Playbook。条件语句和循环语句可以相互嵌套。

变量过滤器

可以使用Jinja2提供的过滤器改变原有变量输出的格式,例如将字符串转换为JSON或YAML。

如果要转换为json格式时,使用to_json过滤器进行输出。

如果要转换为yaml格式时,使用to_yaml过滤器进行输出。

{{ output | to_json }}
{{ output | to_yaml }}

如果要使结构更适于人类阅读,可以使用下面的过滤器来输出人类可读格式。

{{ output | to_nice_json }}
{{ output | to_nice_yaml }}

变量测试

在Ansible Playbook中与when子句一同使用的表达式是Jinja2表达式。用于测试返回值的内置Ansible测试failed、changed、succeeded、skipped四种。

tasks:
.......
- debug:
msg: "The task was aborted"
when: returnvalue is faild

软件包管理

使用dnf的Ansible模块可以在受控主机上控制dnf软件包管理器。dnf是RHEL8使用的默认包管理器用于替代yum。不过也可以使用yum模块来对RHEL8进行操作。

下面示例中使用task任务来代替原有的dnf包管理器指令:

dnf install httpd -y

- name: Install the httpd packages
dnf:
name: httpd
state: present

state关键字有如下参数:

  • present 如果软件包不存在,则安装软件包
  • absent 如果已安装,则删除软件包
  • latest 如果软件包不是最新版本,则会对软件包进行更新。要是没有安装则会安装最新版本的软件包

name关键字有以下使用方式:

  • 直接填写某一个或以列表的形式填写多个软件包名称
  • 使用'*'配合latest可以进行更新系统全部软件包
  • 若要管理模块或组需要使用@符号

安装Development Tools软件组的示例:

- name: Install the Development Tools group
dnf:
name: ‘@Development Tools’
state: present

安装postgresql数据库模块:

- name: Install the postgresql module
dnf:
name: '@postgresql:9.6/client'
state: present

如果包管理器不是yum或者dnf。可以使用package模块替代yum/dnf模块。package模块可以自动检测并使用受控主机的包管理器去安装配置的软件包。

- name: Install httpd
package:
name: httpd
state: present

收集已安装的软件包信息

使用package_facts模块就能获取到受控主机中已经安装的软件包。它会将获取到的全部软件包信息存入ansible_facts.packages变量中。

使用package_facts模块并进行输出的示例:

---
- name: Output
hosts: prod
tasks:
- name: Check packages
package_facts:
manager: auto
- debug:
var: ansible_facts.packages.httpd

package_facts模块有两个选项:

  • manager 选择软件包管理器。默认auto自动识别
  • strategy 策略

返回prod主机组中已安装的httpd软件包信息。

ok: [serverc] => {
"ansible_facts.packages.httpd": [
{
"arch": "x86_64",
"epoch": null,
"name": "httpd",
"release": "10.module+el8+2764+7127e69e",
"source": "rpm",
"version": "2.4.37"
}
]
}
ok: [serverd] => {
"ansible_facts.packages.httpd": [
{
"arch": "x86_64",
"epoch": null,
"name": "httpd",
"release": "10.module+el8+2764+7127e69e",
"source": "rpm",
"version": "2.4.37"
}
]
}

配置yum仓库

添加yum仓库

使用yum_repository模块来控制第三方yum仓库。在添加仓库时即可配置GPG密钥。

---
- name: Create yum repo
hosts: prod
tasks:
- name: Create yum repo
yum_repository:
name: rpmforge
description: RPMforge YUM repo
file: test
gpgkey: http://materials.example.com/yum/repository/RPM-GPG-KEY-example
baseurl: http://materials.example.com/yum/repository/
enabled: yes
present: yes
gpgcheck: yes

可以直接使用gpgkey选项而不需要使用其他模块进行配置。

用户管理和身份认证

如何管理用户和用户组,以及配置ssh-key。

用户模块

使用user模块可以管理主机上的用户以及它们的许多参数。也可以删除用户、设置主目录、设置UID、关联的用户组等很多参数。

如果需要创建可以登录的计算机用户,需要使用password参数和password_hash('sha512')搭配生成Hash后的密码才可以登陆系统。

---
- name: Add user
hosts: dev
tasks:
- name: User modul
user:
name: natsumi
shell: /bin/bash
groups: wheel
append: yes
state: present
password: "{{ pwd | password_hash('sha512') }}"
vars:
pwd: test123

组模块

group模块可以管理受控主机中的用户组。

---
- name: Add group
hosts: dev
tasks:
- name: change group
group:
name: devuser
state: present

group模块参数: gid,local,name,state,system(如果设置为yes,则表示创建的组是系统组)

系统调度

at一次性任务

使用at模块来创建一个一次性任务。可以安排任务在未来的某一个时间点执行一次。

at模块参数:

参数选项说明
command-计划要运行的命令
count-单位数字。必须和units一同使用
script_file-计划要执行的现有脚本文件
stateabsent/present添加或删除命令或脚本的状态
uniqueyes/no如果任务已在运行,则不会再次执行
unitsminutes/hours/days/weeks时间单位

at模块使用示例:

---
- name: Using at make task
hosts: dev
tasks:
- name: create task
at:
command: "echo 'rick' > lala.txt"
count: 1
units: minutes
unique: yes

目前使用at模块总会在文件名末尾拼接上marcinDELIMITERxxxx字符串,经过Google发现这一串文本是用来在at pool中用来标示任务的。可是在这里为什么将他们输出了出来目前还不得而知。

cron计划任务

要完成重复性的任务,也可以使用cron模块来创建计划任务。

- name: Ensure a job that runs at 2 and 5 exists. Creates an entry like "0 5,2 * * ls -alh > /dev/null"
cron:
name: "check dirs"
minute: "0"
hour: "5,2"
job: "ls -alh > /dev/null"

想要在特殊时间点,比如系统重启后运行一个任务,可以使用special_time参数来配置cron计划任务。

- name: Ensure a job that runs at system reboot. Creates an entry like "@reboot ls -alh > /dev/null"
cron:
name: "check dirs"
special_time: reboot
job: "ls -alh > /dev/null"

reboot模块

使用reboot模块重新启动比直接用shell模块发起关机更安全,使用shell模块关闭受控主机后,它会等待再次开机恢复运行,才会继续向下执行其他任务和Play。

对受控主机重启后并持续等待180s。如果受控主机恢复运行则继续执行接下来的任务。

- name: Reboot a slow machine that might have lots of updates to apply
reboot:
reboot_timeout: 180

如果超出运行时间,则执行出错。接下来的task和play都不会继续执行。

fatal: [servera]: FAILED! => {"changed": false, "elapsed": 19, "msg": "Timed out waiting for last boot time check (timeout=18)", "rebooted": true}

🔲 ☆

Ansible 变量和事实

变量和事实

如何定义变量、在Playbook中使用使用变量、加密敏感变量、Fact事实、魔法变量。

变量的定义

变量可以在很多地方定义,他们的作用域也是不同的。

1、playbook中定义:vars
2、playbook中使用变量文件
3、在当前目录的host_vars/group_vars目录下定义变量文件(针对灵活、大型的playbook适用)
4、运行playbook时,在命令行中使用-e选项来使用变量,这种方式的优先级最高,会覆盖以上所有相同名字的变量属性。

变量的使用

在playbook中,使用"{{ var_name }}"来引用变量。

register寄存器

使用寄存器可以将上一次的执行结果存储到新的变量中,用于下一次使用。

使用vault加密变量

当有一些敏感变量,比如密码等信息你应该是不想使用明文存储到变量文件。

ansible-vault [create|decrypt|edit|encrypt|encrypt_string|rekey|view] [options] [vaultfile.yml]

使用ansible-vault可以对变量文件进行加密,在执行playbook的时候通过输入密码或者使用密钥文件来对加密好的变量进行解密。

如何运行引用了加密变量的playbook

ansible-playbook --ask-vault-pass/--valut-password-file=vault-pass playbook.yml

使用Fact事实

Fact事实是在我们运行ansible-playbook时自动收集对应主机的基本信息的变量。

常用的fact事实

ansible_facts.hostname
ansible_facts.fqdn
ansible_facts.default_ipv4.address
ansible_facts.interfaces

禁用收集Fact事实

在Play中添加gather_facts:no

收集目标主机中自定义的Fact

在目标主机的/etc/ansible/facts.d/目录下创建xxxxx.fact文件,在文件中使用json格式自定义目标主机的Fact。

使用魔法变量

一些变量并非事实或通过setup模块配置,但也能由Ansible自动设置。这些魔法变量也可以用于获取与特定受管主机相关的信息。

groups

列出清单中的所有组和主机。

group_names

列出当前受控主机所属的所有组。

inventory_hostname

包含清单中配置的当前受管主机的主机名称。

hostvars

引用其他主机的变量,前提是要引用的主机已经执行过获取Fact操作。

$ ansible all -m debug -a 'var=inventory_hostname'

servera.lab.example.com | SUCCESS => {
"inventory_hostname": "servera.lab.example.com"
}
serverb.lab.example.com | SUCCESS => {
"inventory_hostname": "serverb.lab.example.com"
}

🔲 ☆

Ansible 任务控制

Ansible的循环控制及条件任务

Ansible Playbook的循环控制、条件控制、处理程序以及错误处理机制。

loop循环控制

Ansible支持使用loop关键字来执行循环任务。

简单迭代任务循环

将loop关键字添加到任务中,并用列表表示要迭代的项目的值。并使用item临时变量来保存每次循环迭代过程中使用的值。

如下是一个启动两个服务的例子,在没有了解循环控制时应这样编写playbook,分别写两个独立的任务来执行:

- name: Postfix is running
service:
name: postfix
state: started

- name: httpd is running
service:
name: httpd
state: started

使用loop循环控制,则可以简写如下:

- name : Start Postfix and httpd services
service:
name: "{{ item }}"
state: started
loop:
- postfix
- httpd

也可以把loop所使用的列表存到play的变量中,他们三个执行的效果是相同的:

vars:
start_services:
- postfix
- httpd
tasks:
- name : Start Postfix and httpd services
service:
name: "{{ item }}"
state: started
loop: "{{ start_services }}"

循环散列或字典列表

loop也可以循环散列或者是字典,下面示例中每个字典或散列有两个键,分别是name和groups。当前循环中的每个键的值可以分别通过item.name和item.group来检索。

- name: User exist and are in the corrent Groups
user:
name: "{{ item.name }}"
group: "{{ item.group }}"
loop:
- name: user1
group: wheel
- name: user2
group: root

早期的循环语法

在Ansible 2.5之前,大多数的playbook使用不同的循环愈发。提供了多个循环关键字,都以前缀with_开头,后跟Ansible查找插件的名称连用。这种循环语法在目前依旧很常见,但在未来某个时刻将会被弃用。

循环关键字描述
with_items和loop类似,但当为with_items提供了列表的列表,他们会被扁平化处理为单级列表。item作为循环变量保存每次迭代过程中的值
with_file此关键字需要控制节点文件名列表。循环变量item保存每次迭代过程中保存文件列表中相应文件的内容
wirh_sequence此关键字不需要列表,而是需要参数生成数字序列列表。循环变量item在每次迭代过程中保存生成的序列中的一个生成项的值

将Register变量与Loop一起使用

---
- name: Loop Register Test
gather_facts: no
hosts: localhost
tasks:
- name: Loop Echo Task
shell: "echo This is my item: {{ item }}"
loop:
- one
- two
register: echo_results

- name: Show echo_results variable
debug:
var: echo_results

条件任务语句

使用条件语句when可以控制该任务是否执行,根据条件配置满足一定情况下执行任务。比如在执行任务前首先要判断一下磁盘剩余空间、内存大小是否满足需求。如果不满足则直接跳过该任务。

变量也可以作为when条件语句的判断条件,如下:

---
- name: Test Boolean Task Demo
hosts: all
vars:
run_task: true
tasks:
- name: Install the web services
yum:
name: httpd
state: present
when: run_task

...

在当变量run_task为真的时候,则执行Install the web services的任务,否则跳过该任务。

下面是常用的判断条件列表。

判断条件示例
等于(字符串)ansible_machine == “x86_64”
等于(数字)max_memory == 512
常见判断<=、<、>、>=、!=
变量存在max_memory is defined
变量不存在max_memory is not defined
第一个变量在第二个变量的列表里var1 in var_all

多个条件组合

在使用when条件语句时,如果要判断组合条件,可以使用andor关键字来进行组合,并与括号分组条件。

when: ansible_facts.distribution == "RedHat" or ansible_facts.distribution == "Debian"

如果多个条件之间是and关系,也可以使用列表的形式来表示:

when:
- ansible_facts.distribution == "RedHat"
- ansible_facts.distribution == "CentOS"

使用括号能编写更复杂的条件:

when: >
(ansible_facts.distribution == "RedHat" and ansible_facts.distribution_major_version == "7")
or
(ansible_facts.distribution == "Fedora" and ansible_facts.distribution_major_version == "28")

loop循环和条件判断组合使用

例子中yum模块将要安装mariadb-server软件包,但是要求根目录满足剩余空间300MB以上才会安装,所以可以遍历目标主机上所有挂在的磁盘然后找到根目录再判断剩余空间,如果满足则安装,不满足则跳过该任务。

当对某个任务结合使用when和loop时,将对每一项都使用when语句进行判断。

tasks:
- name: Make Sure root have enough space to install mariadb
yum:
name: mariadb-server
state: present
loop: "{{ ansible_facts.mounts }}"
when: item.mount == "/" and item.size_available > 300000000

处理程序

有时候我们需要更改完配置文件后重启服务,但只想对文件有更改的情况下才重启服务。得益于Ansible的模块设计的幂等性,我们可以通过判断是否进行了更改而选择执行任务。这种方式叫处理程序。

处理程序可看作非活动任务,只有在使用notify语句激活后才会被触发,只有配置文件更新了并激活了该处理任务时,在到处理程序定义的位置时才会执行该任务。

处理任务只有在被激活的情况下才会执行,要注意的是处理程序不会按照你的激活顺序执行,而是按照激活程序的编写顺序以此判断是否被激活和执行。

- name: Enable internet services
hosts: all
become: yes
tasks:
- name: Add Web content
get_url:
url: http://materials.example.com/labs/playbook-review/index.php
dest: /var/www/html/index.php
mode: 0644
notify: restart apache
handlers:
- name: restart apache
systemd:
name: httpd
state: restarted
...

在第一次添加新页面后,激活restart apache处理程序,当开始执行handlers处理程序时,发现restart apache处理程序被激活,则执行该任务。

如果第二次执行该playbook你会发现处理程序并没有执行,因为任务Add Web content返回的状态是OK,由于没有进行任何更改所以不会执行重启httpd服务的操作。

如何处理任务失败

Ansible在执行任务的过程中,有任何一个任务执行失败,则不论接下来是否还有任务都不会执行。

但是如果你有特定的要求,即便是某一个任务执行失败,也继续往下执行其他任务的话,你需要在对应的任务上添加ignore_errors关键字。

- name: Latest version of notapkg is installed
yum:
name: notapkg
state: latest
ignore_errors: yes

任务失败后强制执行处理程序

哪怕是Play在执行任务的过程中失败了,也会强制执行已经被激活的处理程序。

---
- name: Enable internet services
force_handlers: yes
hosts: all
become: yes
tasks:
- name: Latest version of notapkg is installed
yum:
name: httpd
state: latest
notify:
- restart apache

- name: Always is False
command: /bin/false

handlers:
- name: restart apache
systemd:
name: httpd
state: restarted
...

指定任务失败的条件

使用failed_when关键字可以指定任务已失败的条件,条件语句和when的条件语句相同。

下面的例子是当有任何一个磁盘挂载点剩余的空间小于300MB则将任务结果返回为失败,即便是已经成功安装。

tasks:
- name: Make Sure root have enough space to install mariadb
yum:
name: mariadb-server
state: present
loop: "{{ ansible_facts.mounts }}"
failed_when: item.size_available < 300000000

如果failed_when关键字为true那么不管最后的执行结果如何,都会返回任务处理结果为失败。

指定是否报告任务的Changed结果

当任务对托管主机进行了更改后,如果有处理任务,则会激活处理任务。但在有些特殊模块如command模块,有时并不能按照预期来返回ok结果,所以只能返回Changed作为任务执行结果,这将会激活处理任务。

使用changed_when关键字可以控制何时返回Changed执行结果。他和failed_when关键字一样,和when的条件语句相同。你可以对某一变量是否在另一个变量中出现过进行判断。

tasks:
- shell:
cmd: /usr/local/bin/upgrade-database
register: command_result
changed_when: "'Success' in command_result.stdout"
handelers:
- name: restart database
service:
- name: mariadb
state: restarted

Ansible 块和错误自动处理

在Playbook中,块用来对任务进行逻辑分组,可用于控制任务的执行方式,例如任务块可以和when关键字连用,可以将某一条件用于多个任务。

---
- name: Enable internet services
hosts: all
become: yes
tasks:
- name: Installed Service
block:
- name: Latest version of notapkg is installed
yum:
name: httpd
state: latest

- name: Always is True
command: /bin/true
changed_when: false
when: ansible_facts.distribution == "RedHat"

- name: Always is True
command: /bin/true
changed_when: false
...

通过块,也可以与rescue和always语句连用来处理错误。如果块中包裹的任务有任何一个执行失败,则执行其rescue块中的任务来进行恢复。在block子句中的任务以及rescue运行结束后,最后运行always语句中包含的任务。

  • block:定义要运行的主要任务
  • rescue:定义在block块中有任务执行失败后要运行的任务
  • always:始终都要执行的任务,不论block和rescue子句中定义的任务执行成功还是失败都会运行always中定义的任务。
---
- name: Test Block
hosts: all
become: yes
tasks:
- name: Installed Service
block:
- name: Latest version of notapkg is installed
yum:
name: httpd
state: latest

- name: Always is True
command: /bin/true
changed_when: false
when: ansible_facts.distribution == "RedHat"
rescue:
- name: rescue block tasks
debug:
msg: "rescued block tasks"
always:
- name: Always has been exec
debug:
msg: "Always has been exec"
...

任务控制总结


vars变量


services:
- "{{ web_service }}"
- "{{ fw_service }}"

packages:
- "{{ web_package }}"
- "{{ ssl_package }}"
- "{{ fw_package }}"

ssl_cert_dir: /etc/httpd/conf.d/ssl

web_config_files:
- src: server.key
dest: "{{ ssl_cert_dir }}"
- src: server.crt
dest: "{{ ssl_cert_dir }}"
- src: ssl.conf
dest: /etc/httpd/conf.d
- src: index.html
dest: /var/www/html

PlayBook

- name: Playbook Control Lab
hosts: webservers
vars_files: vars.yml
tasks:
#Fail Fast Message
- name: check ram size
fail:
msg: "Cant install under free ram 256M"
when: ansible_facts.memtotal_mb < min_ram_mb and ansible_facts.distribution != "RedHat"

#Install all Packages

- name: Installed "{{ packages }}" Packages
yum:
name: "{{ packages }}"
state: present

#Enable and start services

- name: Enable and start "{{ services }}" services
service:
name: "{{ item }}"
state: started
enabled: true
loop: "{{ services }}"

#Block of config tasks

- name: Config Document Tasks
block:
- name: existed ssl_cret_dir
file:
path: "{{ ssl_cert_dir }}"
state: directory

- name: Config File existed
copy:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
loop: "{{ web_config_files }}"
notify:
- restart web service
rescue:
- debug:
msg: >
One or more of the configuration changes faild, but the web service is still active.
#Configure the firewall
- name: Configed Firewalld Allowed
firewalld:
service: "{{ item }}"
state: enabled
permanent: true
immediate: true
loop:
- http
- https

#Add handlers
handlers:
- name: restart web service
service:
name: "{{ web_service }}"
state: restarted

🔲 ☆

Ansible Playbook

Ansible PlayBook

如何使用Ansible Playbook来执行复杂任务。其中包括要如何检查Playbook的语法。

PlayBook和临时命令

临时命令可以作为一次性命令临时的在目标主机上执行一项简单的任务。但是要发挥ansible全部实力的话就要用playbook来完成对选定主机的一个或一组指定的有序任务。playbook是一个文本文件,是以YAML格式编写的文本文件,拓展名通常使用yml保存。

如果当vim识别到该文本文件是yaml格式的话,自动将tab键的制表符改为2个空格,以便格式正确。
修改$HOME/.vimrc文件,在其中添加规则:autocmd FileType yaml setlocal ai ts=2 sw=2 et

Playbook格式

使用---三个破折号作为playbook的开头,...三个英文句号作为playbook结尾(结尾非必须)。

在这两个标记之间,会以一个play列表的形式来定义playbook。YAML列表中的项目以一个破折号加空格开头。

- apple
- orange
- grape

play本身是一个简直对集合。确保同一个play中的键应当具有相同的缩进量。下面的例子显示了具有三个键的YAML代码片段,其中前两个键具有简单的值。第三个将含有三个项目的列表作为值。

- name: example
hosts: webservers
tasks:
- first
- second
- third

上面实例play有三个键name、hosts、tasks,他们有着相同的缩紧。第一行的name前面加了一个破折号和一个空格,表示他是第一个键,下面的两个键hosts与tasks和name这个键是同级的。

name通常作为一个play的开头,用来描述这个play是干嘛的,可以省略但是不推荐这么做。

hosts指定了要在哪些主机目标上运行这个playbook,前面了解过为如何为主机分组,所以此处可以填写在inventory中编排好的主机或组。

hosts: 可以使用通配符(*)、&与符号、!非符号进行匹配。也可以使用逗号来分割使用的不同的主机或组。⚠️注意:如果要使用这种模式来匹配主机,为了不与shell字符冲突,应使用''一对单引号来包裹你的匹配模式。

tasks是该play要运行的任务列表。下面的例子中所表达的是:在一个叫example的Play中执行两个简单的任务task1和task2的PlayBook写法:

---
- name: example
hosts: webservers
tasks:
- name: task1
service:
name: httpd
enabled: true
- name: task2
user:
name: testuser
uid: 2000
state: present
...

一个更长点的例子:

---
- name: Enable internet services
hosts: serverb.lab.example.com
become: yes
tasks:
- name: Install internet services
yum:
name:
- firewalld
- httpd
- mariadb-server
- php
- php-mysqlnd
state: latest

- name: Enable Firewalld service
systemd:
name: firewalld
state: started
enabled: yes

- name: Allow 80 tcp port in firewalld
firewalld:
service: http
state: enabled
permanent: yes
immediate: yes

- name: Enable httpd services
systemd:
name: httpd
state: started
enabled: yes

- name: Enable mariadb services
systemd:
name: mariadb
state: started
enabled: yes

- name: Add Web content
get_url:
url: http://materials.example.com/labs/playbook-review/index.php
dest: /var/www/html/index.php
mode: 0644


- name: Check services working well
become: false
hosts: localhost
tasks:
- name: check services correct
uri:
url: http://serverb.lab.example.com
status_code: 200
...

执行playbook

ansible-playbook playbook.yml

检查编写的playbook中的语法错误

ansible-playbook –syntax-check playbook.yml

使用自动检查有时并不能精确的定位到问题位置所在,但是几乎都在问题的附近,也能提供一些指导。

以检查模式运行playbook

ansible-playbook -C playbook.yml

使用检查模式运行playbook并不会对目标主机进行任何更改。可以用来在真正运行前进行一次测试。

🔲 ☆

Ansible 基础概念

Ansible 基础概念

Ansible的基本环境、受控主机清单、Ansible的配置文件。

查看Ansible环境信息

absible --version

查看ansible控制端的环境信息,其中包括默认模块和自定义模块的文件目录以及ansible和python的版本。

[student@workstation ~]$ ansible --version
ansible 2.8.0
config file = /etc/ansible/ansible.cfg
configured module search path = ['/home/student/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
ansible python module location = /usr/lib/python3.6/site-packages/ansible
executable location = /usr/bin/ansible
python version = 3.6.8 (default, Apr 3 2019, 17:26:03) [GCC 8.2.1 20180905 (Red Hat 8.2.1-3)]

构建ansible清单

如果要用Ansible管理其他主机,需要将受控主机添加到清单中。

sudo vim /etc/ansible/hosts

使用静态清单配置

静态清单可以使用多种文件格式,包括INI和YAML。以下使用INI样式格式做例子。

在INI文件开头直接添加主机IP/域名

servera.yeefire.com
serverb.yeefire.com
192.168.1.211
192.168.1.232

将主机进行分组

[china-server]
blog.yeefire.com
server2.yeefire.com

[us-server]
us1.yeefire.com
us2.yeefire.com

分组嵌套

分组嵌套需要在组名处使用:children后缀。分组嵌套可以写在分组之前,他们之间没有绝对顺序。也可以编写与分组嵌套相同的组名,在使用这个名字的时候会将分组嵌套中所有的主机和该名字所对应的组名的主机包括在内。

[china-server]
blog.yeefire.com
server2.yeefire.com

[us-server]
us1.yeefire.com
us2.yeefire.com

[webservers:children]
china-server
us-server

[webservers]
test.app.yeefire.com

简化主机配置

如果要配置范围主机,一个个添加会很麻烦,通常他们都会有固定的格式。
使用[START:END]就可以范围。

[us-server]
us[001:009].yeefire.com

[test]
test[a:d].yeefire.com

localhost是一种特殊的存在,ansible知道他是本机,如果对localhost操作,ansible不使用ssh进行连接,这样可能导致执行结果和其他主机不一致。解决方案也很简单就是将localhost添加到inventory

ansible模块

模块是ansible的核心功能,近乎一些的操作都和模块相关。

使用ansible-doc可以阅读模块帮助手册

常用模块列表

模块类别模块描述
文件模块copy将本地文件复制到受管主机
-file设置文件的权限和其他属性
-lineinfile确保特定行是否在文件中
-synchronize使用rsync同步内容
软件包模块package自动检测操作系统的软件包管理器管理软件包
-yumyum管理软件包
-aptapt管理软件包
-dnfdnf管理软件包
-pip从PyPI管理Python软件包
系统模块firewalld使用firewalld管理任意端口和服务
-reboot重新启动计算机
-service管理服务
-user添加、删除、管理用户账户
NetTools模块get_url通过HTTP、HTTPS、FTP下载文件
-nmcli管理网络
-uri与Web服务交互

ansible -m setup

setup 模块用来收集主机详尽信息。

ansible -m setup localhost

在本地运行ansible的setup模块,收集本地的详尽信息。

ansible配置文件

有四种方式来让ansible读取配置文件,优先级依次由高到低:

使用环境变量

使用ANSIBLE_CONFIG环境变量,使用环境变量的优先级是最高的,也就是说如果Ansible运行时发现该变量存在,那么会直接使用它而不会理会其他三个位置的配置文件。

使用./ansible.cfg

如果在当前执行ansible的目录下存在ansible.cfg配置文件,那么在当没有配置环境变量时,就会使用此配置文件。(可以配置目录结构来方便管理不同的群组)

使用~/.ansible.cfg

使用当前用户Home目录下的ansible.cfg隐藏文件,在当前两个都没有配置的情况下,如果在用户家目录下存在配置文件则会使用此处的配置文件运行ansible。

使用/etc/ansible/ansible.cfg

使用/etc/ansible/ansible.cfg全局配置文件是优先级最低的选择,在上面三个地点都没有找到ansible配置文件时则会使用此处的配置文件。如果这个文件也不存在的话……那就完全按照ansible的默认来操作了。

配置文件参数

inventory主机清单

在配置文件中配置好了主机清单后,在运行临时命令或者Playbook时就可以不用手动指定主机清单。

参数值这里inventory可以是文件,也可以是文件夹。当参数是文件夹时,会使用文件夹下全部的inventory主机清单。

[defaults]
inventory = inventory
🔲 ☆

使用脚本、工具批量操作服务器做免密登录及修改密码

  • 生产实践:

    使用脚本、工具批量操作服务器做免密及修改密码

  • 学习技巧:

    Shell 脚本、sshpass、parallel-ssh及ansible工具使用

  • 脚本内容:      

     使用脚本、工具批量操作服务器做免密及修改密码,常用于服务器初始化及批量操作。

一、sshpass使用

1、确保本地机器已安装sshpass工具

apt install sshpass  # Ubuntu/Debian
yum install sshpass  # CentOS/RHEL

2、准备服务器列表文件(如servers.txt),格式为:用户名@IP地址:端口 密码

例如:

root@192.168.1.100:22 password1
admin@192.168.1.101:2222 password2

批量配置SSH免密登录

#!/bin/bash

# 生成SSH密钥对(如果尚未生成)
if [ ! -f ~/.ssh/id_rsa ]; then
    ssh-keygen -t rsa -N "" -f ~/.ssh/id_rsa
fi

# 读取服务器列表
while read line; do
    # 提取信息
    server=$(echo $line | cut -d' ' -f1)
    password=$(echo $line | cut -d' ' -f2)
    
    # 分离用户名、IP和端口
    user=$(echo $server | cut -d'@' -f1)
    ip_port=$(echo $server | cut -d'@' -f2)
    ip=$(echo $ip_port | cut -d':' -f1)
    port=$(echo $ip_port | cut -d':' -f2)
    port=${port:-22}  # 默认SSH端口22
    
    echo "正在配置 $user@$ip:$port ..."
    
    # 使用sshpass复制公钥到远程服务器
    sshpass -p "$password" ssh-copy-id -o StrictHostKeyChecking=no -p $port $user@$ip
    
    # 测试免密登录
    ssh -p $port $user@$ip "echo 'SSH免密登录配置成功!'"
    
    echo "$user@$ip:$port 配置完成"
    echo "------------------------------------"
done < servers.txt

批量修改服务器密码

#!/bin/bash

# 读取新密码
read -s -p "请输入新密码: " new_password
echo
read -s -p "再次确认新密码: " new_password_confirm
echo

if [ "$new_password" != "$new_password_confirm" ]; then
    echo "两次输入的密码不一致,请重试!"
    exit 1
fi

# 读取服务器列表
while read line; do
    # 提取信息
    server=$(echo $line | cut -d' ' -f1)
    old_password=$(echo $line | cut -d' ' -f2)
    
    # 分离用户名、IP和端口
    user=$(echo $server | cut -d'@' -f1)
    ip_port=$(echo $server | cut -d'@' -f2)
    ip=$(echo $ip_port | cut -d':' -f1)
    port=$(echo $ip_port | cut -d':' -f2)
    port=${port:-22}  # 默认SSH端口22
    
    echo "正在修改 $user@$ip:$port 的密码..."
    
    # 使用sshpass登录并修改密码
    sshpass -p "$old_password" ssh -o StrictHostKeyChecking=no -p $port $user@$ip \
    "echo -e '$old_password\n$new_password\n$new_password' | passwd"
    
    # 测试新密码
    sshpass -p "$new_password" ssh -o StrictHostKeyChecking=no -p $port $user@$ip \
    "echo '密码修改成功!'"
    
    echo "$user@$ip:$port 密码修改完成"
    echo "------------------------------------"
done < servers.txt


二、parallel-ssh使用

1、确保本地机器已安装parallel-ssh工具

apt install pssh  # Ubuntu/Debian
yum install pssh  # CentOS/RHEL

# 手动编译安装
# 下载源码
wget https://github.com/lilydjwg/pssh/archive/refs/tags/v2.3.1.tar.gz
tar xvf v2.3.1.tar.gz
cd pssh-2.3.1

# 安装依赖和编译
apt install python3-setuptools
python3 setup.py install

2、parallel-ssh基本语法及使用

parallel-ssh [选项] -H "主机1 主机2 ..." -i "要执行的命令"

核心参数说明

参数作用
-H指定主机列表(用空格分隔),如 -H "user@host1 user@host2"
-h从文件读取主机列表(每行一个主机),如 -h hosts.txt
-i实时显示命令输出(交互模式)
-l指定远程主机的用户名,如 -l root
-A手动输入SSH密码(默认使用密钥认证)
-p并发连接数(默认32),如 -p 10
-tSSH超时时间(秒),如 -t 15
-o将输出保存到指定目录(每主机一个文件),如 -o /tmp/output
-O指定SSH配置选项(如 -O StrictHostKeyChecking=no 跳过主机密钥检查)
-x传递额外的SSH参数,如 -x "-o ConnectTimeout=5","-p 2222"
-v显示详细错误信息

相关工具

  • parallel-scp:批量上传文件到多台主机。

  • parallel-rsync:使用rsync协议批量同步文件。

  • parallel-slurp:从多台主机下载文件。

  • parallel-nuke:批量终止远程进程。

批量配置SSH免密登录

步骤1:生成SSH密钥对(控制机)

若尚未生成密钥,执行:

ssh-keygen -t rsa -b 4096 -N "" -f ~/.ssh/id_rsa
  • -N "":设置空密码(根据需求可选是否加密密钥)。

  • 生成的密钥对:~/.ssh/id_rsa(私钥)和 ~/.ssh/id_rsa.pub(公钥)。

步骤2:创建主机列表文件

将目标主机 IP 或域名写入文件 hosts.txt,每行一个:

echo -e "192.168.1.101\n192.168.1.102\n192.168.1.103" > hosts.txt
或者
seq -f "192.168.1.%01g" 101 103 > hosts.txt

步骤3:批量推送公钥

使用 parallel-ssh 或 parallel-scp 结合命令完成公钥分发:

方法1:直接追加公钥到目标主机
# 使用 pssh 执行远程命令(需输入密码)
parallel-ssh -h hosts.txt -l <用户名> -A -i \
"mkdir -p ~/.ssh && chmod 700 ~/.ssh && echo $(cat ~/.ssh/id_rsa.pub) >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"
  • -l <用户名>:替换为目标主机的 SSH 用户名。

  • -A:提示手动输入密码(所有主机密码需相同)。

  • 命令分解:

    • mkdir -p ~/.ssh:创建目录(若不存在)。

    • chmod 700 ~/.ssh:设置目录权限。

    • echo 公钥内容 >> ~/.ssh/authorized_keys:追加公钥。

    • chmod 600 ~/.ssh/authorized_keys:设置文件权限。

方法2:通过临时文件上传(更安全)
# 1. 将公钥复制到临时文件
cat ~/.ssh/id_rsa.pub > /tmp/my_key.pub

# 2. 上传公钥文件到目标主机
parallel-scp -h hosts.txt -l <用户名> -A /tmp/my_key.pub /tmp/

# 3. 追加公钥到 authorized_keys 并修复权限
parallel-ssh -h hosts.txt -l <用户名> -A -i \
"mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat /tmp/my_key.pub >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys && rm /tmp/my_key.pub"

步骤4:验证免密登录

# 随机挑选一台主机测试
parallel-ssh -h hosts.txt -l <用户名> -i "hostname"
ln -s /usr/bin/paraller-ssh /usr/local/bin/pssh
pssh -h hosts.txt -i "ifconfig eth0 | grep netmask | awk '{print \$2;system(\"cat /etc/hostname\")}' | xargs"

若无需输入密码且返回主机名,则配置成功!

批量配置SSH免密登录

将以下脚本保存为 auto_ssh_keys.sh,替换 <用户名> 并赋予执行权限:

#!/bin/bash
USER="<用户名>"  # 替换为目标主机的用户名
HOSTS_FILE="hosts.txt"

# 生成密钥对(若不存在)
if [ ! -f ~/.ssh/id_rsa.pub ]; then
  ssh-keygen -t rsa -b 4096 -N "" -f ~/.ssh/id_rsa
fi

# 分发公钥
cat ~/.ssh/id_rsa.pub > /tmp/ssh_temp.pub
parallel-scp -h $HOSTS_FILE -l $USER -A /tmp/ssh_temp.pub /tmp/
parallel-ssh -h $HOSTS_FILE -l $USER -A -i \
  "mkdir -p ~/.ssh && chmod 700 ~/.ssh && \
   cat /tmp/ssh_temp.pub >> ~/.ssh/authorized_keys && \
   chmod 600 ~/.ssh/authorized_keys && \
   rm /tmp/ssh_temp.pub"

# 清理临时文件
rm /tmp/ssh_temp.pub

# 验证
echo "验证免密登录:"
parallel-ssh -h $HOSTS_FILE -l $USER -i "echo 'SSH免密配置成功!'"

批量修改服务器密码

#!/bin/bash
HOSTS_FILE="hosts.txt"
GPG_PASSPHRASE="YourGPGPassphrase"  # 替换为你的GPG密码

# 生成随机密码函数
generate_password() {
  tr -dc 'A-Za-z0-9!@#$%^&*' < /dev/urandom | head -c 16
}

# 修改密码并记录
while read host; do
  USER=$(whoami)  # 或从hosts.txt解析用户名
  NEW_PASS=$(generate_password)
  echo "修改 $host 的密码..."
  
  # 执行修改(需sudo权限)
  parallel-ssh -H "$host" -i \
    "echo '$USER:$NEW_PASS' | sudo chpasswd" && \
    echo "$host:$USER:$NEW_PASS" >> /tmp/temp_passwords.txt
  
  # 检查是否成功
  if [ $? -eq 0 ]; then
    echo "[SUCCESS] $host 密码已更新"
  else
    echo "[FAILED] $host 密码修改失败"
  fi
done < "$HOSTS_FILE"

# 加密存储密码文件
gpg -c --batch --passphrase "$GPG_PASSPHRASE" /tmp/temp_passwords.txt && \
mv /tmp/temp_passwords.txt.gpg ./server_passwords.gpg && \
rm /tmp/temp_passwords.txt

echo "所有主机密码已更新,加密文件:server_passwords.gpg"

gpg加解密使用:

  • -d:解密模式。

  • -o:指定输出文件名。

  • --batch:非交互式(避免提示)。

  • --passphrase:直接提供密码(安全性较低,仅用于测试)。

# 加解密
echo "新密码: 111111" | gpg -c --batch --passphrase "123456" > passwords.gpg
gpg -o decrypted_file.txt --decrypt passwords.gpg


# 解密后自动处理内容
gpg --batch --passphrase "YourGPGPassphrase" -d passwords.gpg | while read -r line; do
  host=$(echo "$line" | cut -d':' -f1)
  user=$(echo "$line" | cut -d':' -f2)
  pass=$(echo "$line" | cut -d':' -f3)
  echo "主机 $host 的用户 $user 密码已提取"
done

三、ansible使用

批量配置SSH免密登录及修改服务器密码

1. 准备工作

首先确保:

  • 已安装Ansible

  • 所有目标机器可以通过SSH访问(使用密码或现有密钥)

  • 有一个inventory文件列出所有目标机器

示例inventory文件 (hosts.ini):

[servers]
server1 ansible_host=192.168.1.101
server2 ansible_host=192.168.1.102
server3 ansible_host=192.168.1.103

[servers:vars]
ansible_user=admin
ansible_ssh_pass=oldpassword  # 或者使用现有的SSH密钥

2. 创建Ansible playbook

创建 setup_ssh_trust.yml 文件:

---
- name: Setup SSH mutual trust and change user password
  hosts: servers
  become: yes
  vars:
    # 定义变量
    new_password: "NewSecurePassword123!"  # 要设置的新密码
    ssh_key_path: "/home/{{ ansible_user }}/.ssh/id_rsa"
    ssh_pubkey_path: "{{ ssh_key_path }}.pub"
    authorized_keys_path: "/home/{{ ansible_user }}/.ssh/authorized_keys"

  tasks:
    # 任务1: 确保openssh-client已安装
    - name: Ensure openssh-client is installed
      apt:
        name: openssh-client
        state: present
      when: ansible_os_family == 'Debian'

    - name: Ensure openssh-clients is installed (RedHat)
      yum:
        name: openssh-clients
        state: present
      when: ansible_os_family == 'RedHat'

    # 任务2: 生成SSH密钥对(如果不存在)
    - name: Generate SSH key pair if not exists
      become_user: "{{ ansible_user }}"
      community.crypto.openssh_keypair:
        path: "{{ ssh_key_path }}"
        type: rsa
        size: 2048
        force: no  # 不覆盖现有密钥

    # 任务3: 收集所有主机的公钥
    - name: Gather public keys from all hosts
      run_once: yes
      delegate_to: localhost
      block:
        - name: Create temp directory for keys
          tempfile:
            state: directory
            suffix: ansible_ssh_keys
          register: keys_tempdir

        - name: Fetch public keys from all hosts
          fetch:
            src: "{{ ssh_pubkey_path }}"
            dest: "{{ keys_tempdir.path }}/{{ inventory_hostname }}.pub"
            flat: yes
          delegate_to: "{{ item }}"
          loop: "{{ groups['servers'] }}"

    # 任务4: 创建包含所有主机公钥的authorized_keys文件
    - name: Create combined authorized_keys file
      run_once: yes
      delegate_to: localhost
      vars:
        combined_keys_path: "{{ keys_tempdir.path }}/authorized_keys_combined"
      block:
        - name: Combine all public keys
          assemble:
            src: "{{ keys_tempdir.path }}"
            dest: "{{ combined_keys_path }}"
            regexp: ".*\.pub$"

    # 任务5: 分发组合的authorized_keys文件到所有主机
    - name: Distribute authorized_keys to all hosts
      copy:
        src: "{{ combined_keys_path }}"
        dest: "{{ authorized_keys_path }}"
        owner: "{{ ansible_user }}"
        group: "{{ ansible_user }}"
        mode: '0600'
      delegate_to: localhost
      run_once: yes
      become: no

    - name: Push authorized_keys to each host
      copy:
        content: "{{ lookup('file', combined_keys_path) }}"
        dest: "{{ authorized_keys_path }}"
        owner: "{{ ansible_user }}"
        group: "{{ ansible_user }}"
        mode: '0600'
      become: yes
      become_user: "{{ ansible_user }}"

    # 任务6: 确保SSH目录权限正确
    - name: Ensure .ssh directory permissions
      file:
        path: "/home/{{ ansible_user }}/.ssh"
        state: directory
        owner: "{{ ansible_user }}"
        group: "{{ ansible_user }}"
        mode: '0700'

    # 任务7: 修改用户密码
    - name: Change user password
      user:
        name: "{{ ansible_user }}"
        password: "{{ new_password | password_hash('sha512') }}"
        update_password: always

3. 执行Playbook

运行以下命令执行配置:

ansible-playbook -i hosts.ini setup_ssh_trust.yml

4. 验证SSH互信

执行完成后,可以验证任意两台机器之间是否可以实现SSH免密登录:

ansible all -i hosts.ini -m ping

或者手动测试:

ssh admin@server1 hostname

5. 可选:更安全的密码管理

为了更安全地管理密码,建议:

  1. 使用Ansible Vault加密密码

  2. 创建vault加密的变量文件

创建加密的变量文件:

ansible-vault create vars/secrets.yml

内容示例:

new_password: "NewSecurePassword123!"

然后修改playbook,移除vars部分,改为:

vars_files:
  - vars/secrets.yml

然后使用以下命令运行playbook:

ansible-playbook -i hosts.ini setup_ssh_trust.yml --ask-vault-pass

其它操作

# ssh互信
ansible ssh-host -m authorized_key -a "user=root state=present key=\"{{ lookup('file', '/root/.ssh/id_rsa.pub') }} \"" -k

---
  - hosts: ssh-host
    user: root
    tasks:
     - name: sshkey-copy
       authorized_key: user=root key="{{ lookup('file', '/root/.ssh/id_rsa.pub') }}"
       
- hosts: group_name
  tasks:
    - name: Enable SSH passwordless login
      authorized_key:
        user: user
        state: present
        key: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"
        
# 修改密码并配置sudoer
- hosts: '{{ host_list }}'
  gather_facts: false
  vars:
    user: anzhihe
    password: "an39DWP6ZDwC."  #python -c 'import crypt;print(crypt.crypt("anzhihe@2025","anzhihe"))'
  tasks:
    - name: adduser for anzhihe
      user: name={{ user }} comment="add debug user:anzhihe" password={{ password }}
    - name: Config /etc/sudoers
      lineinfile: dest=/etc/sudoers state=present line='{{ item }}' validate='visudo -cf %s'
      with_items:
             - "{{ user }} ALL=(ALL) NOPASSWD: ALL"
             - "Defaults: {{ user }}  !requiretty"

      - name: check user
        shell: id anzhihe && grep anzhihe /etc/sudoers
      #ignore_errors: True
      
    - name: copy sudo file to remote host
      copy: src=/scripts/sudofile dest=/tmp/sudofile
    - name: attach sudofile to /etc/sudoers
      shell: cat /tmp/sudofile >> /etc/sudoers 
      
# 批量修改不同用户密码
---
  - hosts: ssh-host
    gather_facts: false
    tasks:
    - name: change user passwd
      user: name={{ item.name }} password={{ item.chpass | password_hash('sha512') }}  update_password=always
      with_items:
           - { name: 'root', chpass: 'kevin@123' }
           - { name: 'app', chpass: 'bjop123' }
           
# 指定主机修改用户密码
# 需要注意脚本中"ens192"是客户机ip所在的网卡设备名称, 这个要根据自己实际环境去配置, 比如eth0, eth1等
- hosts: test-host
  remote_user: root
  tasks:
  - name: change password for root
    shell: echo '{{ item.password }}' |passwd --stdin root
    when: ansible_ens192.ipv4.address  == '{{ item.ip }}'
    with_items:
     - { ip: "172.16.60.220", password: 'haha@123' }
     - { ip: "172.16.60.221", password: 'kevin@123' }
     - { ip: "172.16.60.222", password: 'bobo@123' }

注意事项

  1. 敏感信息及密码策略

  • 永远不要将密码明文存储在脚本或历史记录中

  • 使用密码管理器或加密配置文件

  • 考虑使用ansible-vault等工具加密敏感数据

  • 服务器列表文件包含明文密码,处理完毕后应立即删除

  • 确保新密码符合安全策略(如包含特殊字符、长度≥12)

  • 定期更换密码

  • 最小权限原则要求

    • .ssh 目录权限必须为 700

    • authorized_keys 文件权限必须为 600

    • 不要使用root账户,除非绝对必要,为服务创建专用账户

    • 记录所有批量操作,定期检查服务器登录日志

    • 目标主机用户必须有 sudo 权限且配置了免密码sudo:

      # 在目标主机上执行:
      echo "$(whoami) ALL=(ALL) NOPASSWD: /usr/bin/chpasswd" | sudo tee /etc/sudoers.d/password_change
  • 网络隔离

    • 操作期间确保网络环境安全(防止密码被截获)

    • 确保所有目标主机 SSH 端口开放(默认22)

    • 首次连接需接受主机密钥时,添加 -O StrictHostKeyChecking=no


    参考:

    🔲 ⭐

    2020 年第 36 周

    期末结束了。新学期也快要开始了。新增了这周看了什么板块。

    这周做了什么

    PM2

    PM2 是用来管理,监控 Node.js 服务和进程的,当然它也支持监控 bash 程序。

    在这之前我们使用node app.js来启动一个 Node.js 服务。如果想要保证这个服务在 SSH 窗口退出后保持运行,我们需要将其放在 Screen 或者 Tmux 中。

    有了 PM2 后,执行pm2 app.js就可以启动 Node.js 程序了。退出 ssh 窗口也不会中止app.js。PM2 还提供了查看app.js的日志,重启,中止程序的功能。我们也可执行pm2 plus以登录 PM2 账号,这样就可以在 PM 官网的仪表盘上管理和监控我们的服务。

    如果想要传入参数,可以执行以下命令。pm2 start app.js --name my-api -- --port 4000。这样就传入了port参数给 Node,并且将app.js这个进程命名为 my-api。

    Steam 挂卡&卖卡

    Steam 挂卡通过使用 Steam 机器人 24 小时不间断玩游戏来获取集换卡片。卖卡就是将这些集换卡片在市场卖出。如果用 Steam 令牌添加了新设备,集换卡片七天内不能交易。

    ASF 是比较热门的挂卡程序。我们可以选择在本地挂卡或者在 VPS 上进行挂卡。本地不可能 24 小时开机,所以我选择在 VPS 上进行挂卡。教程 1教程 2

    在教程 1 当中,作者将 ASF 程序放在另一个 Screen 中运行。像上一节描述的那样,我么可以用 PM2 来达到更好的效果。pm2 ./ArchiSteamFarm

    Pixiv

    写了一个脚本把点赞的图收集起来,每周都可以回顾一下过去一周点赞的图,好好的欣赏,评价一下。至于怎么评价可以看怎么判断画的是神作,还是被无脑吹捧的狗屎。由于点赞列表数据里不含时间,所以就增量更新。以前下载到哪里,我们就把在那之后的点赞的图下载一遍。图片相关信息就直接写进图片元数据中,这样移动起来比较方便。

    图片元数据的格式分为 EXIF,IPTC,XMP 这几种格式。每一种格式有着自己的数据规范。比如 EXIF 是没有 Title 这一栏的,但是 XMP 却有。对比了他们所提供的格式和我自己所获取的图片数据,我选择 XMP。

    虽然将所有图片信息写入元数据方便图片的移动,但我比较担心读取和写入数据时的性能开销。如果影响不大,这个方法比把数据单独写入数据库要简洁很多。即便图片改了名字,元数据也不会丢。

    这个 工具 可以在线查看元数据。

    Star Renegade

    像素风,回合制,Roguelike,RPG 游戏。虽然是像素风格,光影却是正常的。打击特效和音效做的不错,但是看上那么多遍还是会疲劳。

    作为一个 Roguelike 游戏,随机的成分并不是特别多,地图虽然是随机,但是 Boss 和资源变化并不大。在每次旅途中会有装备,等级,和队友互相之间的好感系统。这个养成系统感觉是很 RPG 的。但这个 RPG 是在 Roguelike 的框架下,也就是每次死亡,装备,等级,好感都会重置,需要重新获取。由于回合制的节奏,以及 RPG 的机制,每一把的时间都特别长,大概在三个小时左右,这还是打到一大半的位置。

    地图与其他简洁明了的 Roguelike 不同,这款游戏并没有地图,取而代之的是 2D RPG 游戏的那种设计。玩家可以在区域内走来走去。整个大区域被划分成更加细小的区划,通过限制玩家进入新的区划的次数来限制玩家的行动点。整个大区域的设计更像是图,而不是 Slay the Spire 那样的树。Star Renegade 通过这种方式更好的展示了游戏的美术设定,玩家也可以与区域中的一些物品进行互动,从而触发一些关于背景文化的对话。这样的坏处就是玩家不容易看清图的结构,游戏的节奏也会被拉慢,因为玩家在游戏战斗之间需要探索。

    游戏中独特的扎营机制也放缓了游戏节奏。消耗完三个行动点,玩家就需要扎营,等待新的一天。在扎营期间,玩家可以培养人物之间的好感,使用各种 Buff 的卡片。

    相比 Slay the Spire,选择节点,打怪,选择节点,打怪,这款游戏的节奏慢了很多。打一把需要更长的时间。那么每一把之后,玩家会变强从而在下一次走的更远吗?我个人觉得不怎么会。每一把之后的结算所能获取的东西并不多,我至今还卡在第三关。每一次花费两三个小时来到第三关,然后死亡,然后重来,最后放弃。我想这是游戏节奏的问题,又或者是去掉 Roguelike 可以解决的问题。

    Ansible

    Ansible 是一个来自 Red Hat 的运维工具。它可以自动化,批量进行许多操作。它适合用来在已有的服务器上安装和管理软件。

    我日常会用到 VPS,并在上面安装或者管理软件。常用的软件也就那几个。那么我们可以把这几个软件的安装和管理写成 Ansible Playbook。以后我们就不再需要重复安装和管理软件的步骤,只需要执行 Playbook 就可以了。用自动化解放了我们的双手。

    Ansible 的 Playbook 编写采用渐进式,用户需要写下每一步需要做什么。Ansible 有丰富的 Role(库,插件,比如 安装 Node.js),组件支持,大部分操作都可以用现成的 Role 或者组件实现,不需要自己额外进行编写。

    Ansible 没有 Master 的概念,任何一个节点都可以推送命令。Ansible 通过 SSH 连接上节点后,执行对应的命令。由于采用 SSH 进行命令推送,接受命令推送的节点不需要安装 Ansible 客户端。

    因此对于我来说最好的实践便是在本地安装好 Ansible,注意它不支持 Windows。将常用的软件配置写成 Ansible Playbook。介于丰富的组件和 Role,这并不难。每当有新的 VPS 时,我们执行相应的 Ansible Playbook 就可以完成所有工作。比如我写了 Tiny Tiny Rss,Unblock Netease Music 的 Playbook,如果以后更换 VPS 就可以很方便的安装这些软件。

    我们可以把用到的 Role 放到 Requirement.yml 中,这可以保证换个环境,安装完依赖后,我们之前写的 Playbook 还可以正常运行。

    最后推荐一个 视频教程,视频教程虽然看起来慢但是比较容易理解使用者的思路,易于模仿。

    这周看了什么

    • 回到夏天: 作曲:爱写歌的小田,作词:雷壮、爱写歌的小田,编曲:罗洋(卡其漠)。然而这个旋律从头到尾就没怎么变过。
    • Trasfer: 这个工具整合了所有主流的文件分享工具,比如 airpotal,奶牛快传等。文件的分享和下载可以通过命令行完成。传输方法不是点对点,因此借助第三方服务器需要消耗更多的时间。如果想要点对点,节省时间的传输方式可以用 WinSCP 等软件采用 SFTP 的传输方式。
    • Batch Git Pull: 批量更新 Git 仓库。我想很少有人一次性更新所有仓库吧。对于那些不怎么维护的 Git 仓库,更新了之后只会徒增 bug。那些正在维护的仓库,应该会一个个的更新,慢慢解决 Bug。
    • Tabnine: 非常好用的 AI 自动补全。当别人只能一个词一个词补全的时候,它可以补全整句话。连for语句也可以帮你补全,list 的 index 也可以帮我补全。
    • Applied ML: 大公司发的人工智能文章。
    • Hero Pattern: 可以自定义的 SVG 几何背景图。
    • autoscrapper: 一个智能的爬虫库。用户将目标告诉它,这个库会返回类似的结果。比如要抓取一个博客中所有文章的标题,用户可以将一个文章的标题告诉它,它便输出所有标题。
    • 怎么判断画的是神作,还是被无脑吹捧的狗屎: 这个视频讲述了作者自己如何判断一幅画好看不好看。
      • 艺术元素:
        • 线条
        • 明暗和颜色
        • 主体形状
        • 材质
        • 空间
      • 艺术原理:
        • 平衡
        • 引导线(视线运动)
        • 对比
          • 颜色明暗
          • 颜色冷暖
          • 颜色饱和
        • 风格统一
        • 规则变化(不重复和呆板)
          • 软的线条和硬的线条组合
          • 直和弯的组合
      • 数据指标:
        • 初次观看时长
        • 重复观看次数
        • 观看后的思考
    • Thoughts on the market: Morgan Stanley 对于市场的分析。
    • MyFonts: 识别图片中的字体。
    • 《乔布斯传》作者沃尔特,艾萨克森:“一个具有强烈个性的人身上集合了人文和科学的天赋后所产生的创造力,是在 21 世纪建立创新型经济的关键因素。”

    下周做什么

    • GRE 备考
    • 极简欧洲史
    • Pixiv 完善
    • 图片展示网站 UI 设计
    • 整理手机浏览器标签页
    ❌