简介

Ansible的名字来源于小说《安德的游戏》中超时空的即时通信工具,可以使用它远程实时控制前线舰队的战斗。2012年3月发布0.0.1版,2015年为Redhat收购。

宗旨

  1. Keep Things Simple
  2. Stay Organized
  3. Test Often

优点

  1. 具有数千个功能丰富的模块
  2. 使用和部署简单
  3. 安全
  4. 幂等性
  5. 支持使用YAML格式进行playbook的任务编排
  6. 具有较强大的多层解决方案

缺点

  1. 不支持一般性事务回滚
  2. 对于大量主机执行效率差,如不saltstack效率高

组成

  1. Inventory:主机清单文件,提供主机信息和主机分组信息
  2. Modules:执行命令的功能模块
  3. Plugins:功能模块的补充
  4. API:供第三方程序调用的编程接口

安装

部署

# Rocklinux 需要BaseOS、APPstream、EPEL三个软件源的支持
[sujx@master ~]$ yum info ansible
Name : ansible
Version : 7.2.0
Release : 1.el8
Architecture : noarch
Size : 329 M
Source : ansible-7.2.0-1.el8.src.rpm
Repository : @System
From repo : epel
Summary : Curated set of Ansible collections included in addition to ansible-core
URL : https://ansible.com
License : GPL-3.0-or-later AND Apache-2.0 AND BSD-2-Clause AND BSD-3-Clause AND MIT AND MPL-2.0
: AND PSF-2.0
Description : Ansible is a radically simple model-driven configuration management,
: multi-node deployment, and remote task execution system. Ansible works
: over SSH and does not require any software or daemons to be installed
: on remote nodes. Extension modules can be written in any language and
: are transferred to managed machines automatically.
:
: This package provides a curated set of Ansible collections included in addition
: to ansible-core.

[sujx@master ~]$ sudo yum install -y ansible

配置文件

  1. ansible.cfg
    # 指定资产管理清单,忽略命令告警
    [defaults]
    inventory = ./hosts

    # 配置sudo用户,默认使用管理员账号登录,然后sudo为root执行操作
    [privilege_escalation]
    become = True
    become_method = sudo
    become_user = root
  2. inventory‘
    master
    node[1:2]

    [Node]
    node1
    node2
  3. 多个配置文件可以嵌套
    [sujx@master ~]$ 
    [sujx@master ~]$ ansible --version
    ansible [core 2.14.2]
    # 配置文件为用户家目录下
    config file = /home/sujx/ansible.cfg
    configured module search path = ['/home/sujx/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
    ansible python module location = /usr/lib/python3.11/site-packages/ansible
    ansible collection location = /home/sujx/.ansible/collections:/usr/share/ansible/collections
    executable location = /usr/bin/ansible
    python version = 3.11.2 (main, Feb 18 2023, 08:12:16) [GCC 8.5.0 20210514 (Red Hat 8.5.0-18)] (/usr/bin/python3.11)
    jinja version = 3.1.2
    libyaml = True

相关工具

  • /usr/bin/ansible 主程序,临时命令执行工具
  • /usr/bin/ansible-doc 查看配置文档,模块功能查看工具,相当于man
  • /usr/bin/ansible-playbook 定制自动化任务,编排剧本工具,相当于脚本
  • /usr/bin/ansible-pull 远程执行命令的工具
  • /usr/bin/ansible-vault 文件加密工具
  • /usr/bin/ansible-console 基于Console界面与用户交互的执行工具
  • /usr/bin/ansible-galaxy 下载/上传优秀代码或Roles模块的官网平台
  • 基于密钥对的批量添加脚本
#!/bin/bash
set -xn
# 设置SSH连接超时时间(秒)
TIMEOUT=5
# 待连接主机列表(IP地址或域名)
HOSTS=("192.168.1.1" "192.168.1.2" "192.168.1.3")
# SSH登录用户名和密码
USERNAME="your_username"
PASSWORD="your_password"

# 判断主机发行版是否为Redhat系
if [ -f /etc/redhat-release ]; then
# 判断sshpass是否已安装
if ! command -v sshpass &> /dev/null; then
# 安装sshpass
sudo yum install -y sshpass
fi
else
# 判断coreutils是否已安装
if ! command -v timeout &> /dev/null; then
# 安装coreutils
sudo apt-get install -y coreutils
fi
fi

for HOST in ${HOSTS[@]}
do
# 使用ssh-keygen生成密钥对
ssh-keygen -t rsa -b 2048 -f ~/.ssh/id_rsa &> /dev/null

# 把本地公钥复制到远程主机的授权文件中
sshpass -p "${PASSWORD}" ssh-copy-id -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa.pub ${USERNAME}@${HOST} &> /dev/null

# 测试是否可以无密码登录
if timeout ${TIMEOUT} ssh -o ConnectTimeout=${TIMEOUT} -o PasswordAuthentication=no ${USERNAME}@${HOST} exit &> /dev/null; then
echo "${HOST}: 登录成功!"
else
echo "${HOST}: 登录失败!"
fi
done

exit 0

基本用法

ansible 机器名 -m 模块名 -a “模块的参数”

# 以Shell模块为例
[sujx@master ~]$ ansible all -m shell -a "hostname"
# 当返回信息为绿色时,”changed”为false,表示ansible没有进行任何操作,没有”改变什么”。
# 当返回信息为黄色时,”changed”为true,表示ansible执行了操作,”当前状态”已经被ansible改变成了”目标状态”。
# 主机名 |命令执行状态|return code 为0 表示执行成功
node2 | CHANGED | rc=0 >>
node2
master | CHANGED | rc=0 >>
master
node1 | CHANGED | rc=0 >>
node1
# 执行失败的状态,返回非零值
[sujx@master ~]$ ansible all -m shell -a "hostnamexx"
master | FAILED | rc=127 >>
/bin/sh: hostnamexx: command not foundnon-zero return code
node2 | FAILED | rc=127 >>
/bin/sh: hostnamexx: command not foundnon-zero return code
node1 | FAILED | rc=127 >>
/bin/sh: hostnamexx: command not foundnon-zero return code

相关模块

file模块

# 创建文件
[sujx@master ~]$ ansible node2 -m file -a "path=/home/sujx/target owner=sujx group=sujx mode=444 state=touch"
node2 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"changed": true,
"dest": "/home/sujx/target",
"gid": 1000,
"group": "sujx",
"mode": "0444",
"owner": "sujx",
"size": 0,
"state": "file",
"uid": 1000
}
# 创建目录
[sujx@master ~]$ ansible node2 -m file -a "path=/home/sujx/targetdir owner=sujx group=sujx mode=444 state=directory"
node2 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"changed": true,
"gid": 1000,
"group": "sujx",
"mode": "0444",
"owner": "sujx",
"path": "/home/sujx/targetdir",
"size": 6,
"state": "directory",
"uid": 1000
}
# 查看文件状态
[sujx@master ~]$ ansible node2 -m shell -a "ls -l"
node2 | CHANGED | rc=0 >>
total 4
dr--r--r-- 2 sujx sujx 6 Jun 16 15:44 targetdir
-r--r--r-- 1 sujx sujx 0 Jun 16 15:44 target
-rw-r--r-- 1 root root 134 Jun 13 15:55 ansible.cfg
# 删除目录
[sujx@master ~]$ ansible node2 -m file -a "path=/home/sujx/targetdir owner=sujx group=sujx mode=444 state=absent"
node2 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"changed": true,
"path": "/home/sujx/targetdir",
"state": "absent"
}
# 删除文件
[sujx@master ~]$ ansible node2 -m file -a "path=/home/sujx/target owner=sujx group=sujx mode=444 state=absent"
node2 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"changed": true,
"path": "/home/sujx/target",
"state": "absent"
}
# 查看
[sujx@master ~]$ ansible node2 -m shell -a "ls -lr"
node2 | CHANGED | rc=0 >>
total 4
-rw-r--r-- 1 root root 134 Jun 13 15:55 ansible.cfg

copy和fetch模块

# 向Node1、Node2两台主机复制hosts文件
[sujx@master ~]$ ansible Node -m copy -a "src=/home/sujx/hosts dest=/home/sujx/"
node1 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"changed": true,
"checksum": "f0d83ba599f75cda5a4658de9503d0ec62954d76",
"dest": "/home/sujx/hosts",
"gid": 0,
"group": "root",
"md5sum": "f986a6c7d575b0c17440d229e7d003a5",
"mode": "0644",
"owner": "root",
"size": 40,
"src": "/home/sujx/.ansible/tmp/ansible-tmp-1686901933.678302-10766-60719225663651/source",
"state": "file",
"uid": 0
}
node2 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"changed": true,
"checksum": "f0d83ba599f75cda5a4658de9503d0ec62954d76",
"dest": "/home/sujx/hosts",
"gid": 0,
"group": "root",
"md5sum": "f986a6c7d575b0c17440d229e7d003a5",
"mode": "0644",
"owner": "root",
"size": 40,
"src": "/home/sujx/.ansible/tmp/ansible-tmp-1686901933.7078812-10767-77112866750173/source",
"state": "file",
"uid": 0
}
# 使用copy模块向某个文件写入内容
[sujx@master ~]$ ansible node2 -m copy -a 'content="Hello world!" dest=/home/sujx/target'
node2 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"changed": true,
"checksum": "d3486ae9136e7856bc42212385ea797094475802",
"dest": "/home/sujx/target",
"gid": 1000,
"group": "sujx",
"md5sum": "86fb269d190d2c85f6e0468ceca42a20",
"mode": "0444",
"owner": "sujx",
"size": 12,
"src": "/home/sujx/.ansible/tmp/ansible-tmp-1686902375.0828805-11299-205069408367467/source",
"state": "file",
"uid": 1000
}
[sujx@master ~]$ ansible node2 -m shell -a "cat /home/sujx/target"
node2 | CHANGED | rc=0 >>
Hello world!
# 使用fetch模块取回文件
[sujx@master ~]$ ansible node2 -m fetch -a "src=/home/sujx/target dest=."
node2 | CHANGED => {
"changed": true,
"checksum": "d3486ae9136e7856bc42212385ea797094475802",
"dest": "/home/sujx/node2/home/sujx/target",
"md5sum": "86fb269d190d2c85f6e0468ceca42a20",
"remote_checksum": "d3486ae9136e7856bc42212385ea797094475802",
"remote_md5sum": null
}
# 取回的文件会在本地创建一个和远端主机同名的目录用于存储文件
[sujx@master ~]$ tree node2
node2
└── home
└── sujx
└── target

2 directories, 1 file

yum_repository模块

# 一个标准的yum repository文件如下:
[nodesource]
name=Node.js Packages for Enterprise Linux 8 - $basearch
baseurl=https://rpm.nodesource.com/pub_14.x/el/8/$basearch
failovermethod=priority
enabled=0
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/NODESOURCE-GPG-SIGNING-KEY-EL
# 使用ansible创建yum_repo文件
[sujx@master ~]$ ansible node2 -m yum_repository -a "name=NodeJS description='Node.js' \
baseurl=https://rpm.nodesource.com/pub_14.x/el/8/x86_64/ gpgcheck=no enabled=yes"
node2 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"changed": true,
"repo": "NodeJS",
"state": "present"
}
# 查看结果
[sujx@node2 ~]$ cat /etc/yum.repos.d/NodeJS.repo
[NodeJS]
baseurl = https://rpm.nodesource.com/pub_14.x/el/8/x86_64/
enabled = 1
gpgcheck = 0
name = Node.js

yum模块管理软件包

# 模块参数主要为name和state,state的参数为present、absent和latest
# 在指定Node2主机上安装nmap
[sujx@master ~]$ ansible Node2 -m yum -a "name=nmap state=present"
node2 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"changed": true,
"msg": "",
"rc": 0,
"results": [
"Installed: nmap-2:7.70-8.el8.x86_64"
]
}
# 删除NMAP
[sujx@master ~]$ ansible node12 -m yum -a "name=nmap state=absent"
node2 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"changed": true,
"msg": "",
"rc": 0,
"results": [
"Removed: nmap-2:7.70-8.el8.x86_64"
]
}
# 检查系统更新
[sujx@master ~]$ ansible node2 -m yum -a "name=* state=latest"
node2 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"changed": false,
"msg": "Nothing to do",
"rc": 0,
"results": []
}

service模块管理服务

# 主要参数name enabled(yes|no) state(started|stopped|restarted)
[sujx@master ~]$ ansible Node -m service -a "name=cockpit.socket enabled=yes state=started"
# 查看服务是否启动
[sujx@master ~]$ ansible Node -m shell -a "systemctl is-active cockpit.socket"
node1 | CHANGED | rc=0 >>
active
node2 | CHANGED | rc=0 >>
active
# 查看服务是否自启动
[sujx@master ~]$ ansible Node -m shell -a "systemctl is-enabled cockpit.socket"
node2 | CHANGED | rc=0 >>
enabled
node1 | CHANGED | rc=0 >>
enabled

parted模块对硬盘分区

# 常用参数 device指那个硬盘 number第几个分区 part_start从什么位置开始,默认从头开始 part_end分区结束位置 state(present创建|absent删除)
# 在主机上创建一个2GB分区
[sujx@master ~]$ ansible Node -m parted -a "device=/dev/sda number=1 part_end=2GiB state=present"
# 在接着创建一个2GB分区
[sujx@master ~]$ ansible Node -m parted -a "device=/dev/sda number=2 part_start=2GiB part_end=4GiB state=present"
[sujx@master ~]$ ansible Node -m shell -a 'lsblk'
node1 | CHANGED | rc=0 >>
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
sda 8:0 0 10G 0 disk
├─sda1 8:1 0 2G 0 part
└─sda2 8:2 0 2G 0 part
node2 | CHANGED | rc=0 >>
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
sda 8:0 0 10G 0 disk
├─sda1 8:1 0 2G 0 part
└─sda2 8:2 0 2G 0 part
# 删除已创建分区
[sujx@master ~]$ ansible Node -m parted -a "device=/dev/sda number=2 state=absent"
[sujx@master ~]$ ansible Node -m parted -a "device=/dev/sda number=1 state=absent"
# 检查
[sujx@master ~]$ ansible Node -m shell -a 'lsblk'
node1 | CHANGED | rc=0 >>
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
sda 8:0 0 10G 0 disk
node2 | CHANGED | rc=0 >>
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
sda 8:0 0 10G 0 disk

filesystem模块格式化

# 将sda2格式化为xfs格式
[sujx@master ~]$ ansible Node -m filesystem -a "device=/dev/sda2 fstype=xfs"
# 将sda1原有的xfs格式强制格式化为ext4
[sujx@master ~]$ ansible Node -m filesystem -a "device=/dev/sda1 fstype=ext4 force=yes"
# 检查
[sujx@master ~]$ ansible Node -m shell -a 'blkid'
node1 | CHANGED | rc=0 >>
/dev/sda1: UUID="671ca843-cd6c-4184-9d97-095dde88e54b" BLOCK_SIZE="4096" TYPE="ext4" PARTUUID="4dfa6783-01"
/dev/sda2: UUID="955fb028-713f-407b-89a0-0aca4b7a6d01" BLOCK_SIZE="512" TYPE="xfs" PARTUUID="4dfa6783-02"
node2 | CHANGED | rc=0 >>
/dev/sda1: UUID="464f8844-1889-4d53-b66d-64f3b3832dfe" BLOCK_SIZE="4096" TYPE="ext4" PARTUUID="6e712d96-01"
/dev/sda2: UUID="8e040e37-a9d8-4e52-bc1e-cec548686960" BLOCK_SIZE="512" TYPE="xfs" PARTUUID="6e712d96-02"

mount模块挂载文件系统

# 常用参数 src待挂载设备 path挂载点 fstype文件系统 opts挂载选项 state(mounted挂载的同时写入fstab|present只写入fstab|unmounted卸载但是不从fstab中删除|absent卸载并清理fstab)
# 建立挂载点
[sujx@master ~]$ ansible Node -m file -a 'path=/data state=directory owner=root group=root mode=444'
[sujx@master ~]$ ansible Node -m file -a 'path=/data/sda1 state=directory owner=root group=root mode=444'
[sujx@master ~]$ ansible Node -m file -a 'path=/data/sda2 state=directory owner=root group=root mode=444'
[sujx@master ~]$ ansible Node -m shell -a 'tree /data'
node2 | CHANGED | rc=0 >>
/data
├── sda1
└── sda2
2 directories, 0 files
node1 | CHANGED | rc=0 >>
/data
├── sda1
└── sda2
2 directories, 0 files
# sda1 sda2分别挂载
[sujx@master ~]$ ansible Node -m mount -a 'src=/dev/sda1 path=/data/sda1 fstype=ext4 state=mounted'
[sujx@master ~]$ ansible Node -m mount -a 'src=/dev/sda2 path=/data/sda2 fstype=xfs state=present'
# 检查Node2主机的fstab,可见fstab均以写入
[sujx@node2 ~]$ cat /etc/fstab
/dev/sda1 /data/sda1 ext4 defaults 0 0
/dev/sda2 /data/sda2 xfs defaults 0 0
# 检查实际挂载,可见只挂载了sda1
[sujx@node2 ~]$ sudo df -Th
Filesystem Type Size Used Avail Use% Mounted on
/dev/sda1 ext4 2.0G 24K 1.9G 1% /data/sda1
# 卸载挂载,可以只写挂载点和状态参数
[sujx@master ~]$ ansible Node -m mount -a 'path=/data/sda1 state=absent'
[sujx@master ~]$ ansible Node -m mount -a 'path=/data/sda2 state=absent'

lvg模块管理卷组

# 常用参数 pvs指定逻辑卷,不需要提前创建会自动创建 vg指定卷组名称 pesize指定PE大小 state(present创建|absent删除)
# 检查现有主机状态
[sujx@master ~]$ ansible Node -m shell -a 'vgs'
node2 | CHANGED | rc=0 >>
VG #PV #LV #SN Attr VSize VFree
rl_rocky 1 2 0 wz--n- 18.41g 0
node1 | CHANGED | rc=0 >>
VG #PV #LV #SN Attr VSize VFree
rl_rocky 1 2 0 wz--n- 18.41g 0
# 将/dev/sda1 /dev/sda2建立PV并加入vg_data卷组
[sujx@master ~]$ ansible Node -m lvg -a "pvs=/dev/sda1,/dev/sda2 vg=vg_data state=present"
node1 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"changed": true
}
node2 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"changed": true
}
# 检查结果,可见创建了名为vg_data,数量为2个PV,容量为4G的卷组
[sujx@master ~]$ ansible Node -m shell -a 'vgs'
node2 | CHANGED | rc=0 >>
VG #PV #LV #SN Attr VSize VFree
rl_rocky 1 2 0 wz--n- 18.41g 0
vg_data 2 0 0 wz--n- 3.99g 3.99g
node1 | CHANGED | rc=0 >>
VG #PV #LV #SN Attr VSize VFree
rl_rocky 1 2 0 wz--n- 18.41g 0
vg_data 2 0 0 wz--n- 3.99g 3.99g

lvol模块管理逻辑卷

# 常用参数 vg指定卷组创建逻辑卷 lv指定逻辑卷名称 size指定逻辑卷大小 state(present创建|absent删除)
# 在vg_data上创建1GB大小的逻辑卷lv_data
[sujx@master ~]$ ansible Node -m lvol -a "vg=vg_data lv=lv_data size=1G state=present"
node2 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"changed": true,
"msg": ""
}
node1 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"changed": true,
"msg": ""
}
[sujx@master ~]$ ansible Node -m shell -a "lvs"
node2 | CHANGED | rc=0 >>
LV VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert
root rl_rocky -wi-ao---- 16.41g
swap rl_rocky -wi-ao---- 2.00g
lv_data vg_data -wi-a----- 1.00g
node1 | CHANGED | rc=0 >>
LV VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert
root rl_rocky -wi-ao---- 16.41g
swap rl_rocky -wi-ao---- 2.00g
lv_data vg_data -wi-a----- 1.00g
# 将lv_data扩容1GB
[sujx@master ~]$ ansible Node -m lvol -a "vg=vg_data lv=lv_data size=+1G"
node2 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"changed": true,
"lv": "lv_data",
"size": 1.0,
"vg": "vg_data"
}
node1 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"changed": true,
"lv": "lv_data",
"size": 1.0,
"vg": "vg_data"
}
[sujx@master ~]$ ansible Node -m shell -a "lvs"
node2 | CHANGED | rc=0 >>
LV VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert
root rl_rocky -wi-ao---- 16.41g
swap rl_rocky -wi-ao---- 2.00g
lv_data vg_data -wi-a----- 2.00g
node1 | CHANGED | rc=0 >>
LV VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert
root rl_rocky -wi-ao---- 16.41g
swap rl_rocky -wi-ao---- 2.00g
lv_data vg_data -wi-a----- 2.00g
# 将VG的所有空间分配给lv_data,同时还有100%PVS将所有的PV分配给逻辑卷,100%FREE将剩余空间都分配给逻辑卷
[sujx@master ~]$ ansible Node -m lvol -a "vg=vg_data lv=lv_data size=100%VG"
node1 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"changed": true,
"lv": "lv_data",
"size": 2048.0,
"vg": "vg_data"
}
node2 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"changed": true,
"lv": "lv_data",
"size": 2048.0,
"vg": "vg_data"
}
[sujx@master ~]$ ansible Node -m shell -a "lvs"
node2 | CHANGED | rc=0 >>
LV VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert
root rl_rocky -wi-ao---- 16.41g
swap rl_rocky -wi-ao---- 2.00g
lv_data vg_data -wi-a----- 3.99g
node1 | CHANGED | rc=0 >>
LV VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert
root rl_rocky -wi-ao---- 16.41g
swap rl_rocky -wi-ao---- 2.00g
lv_data vg_data -wi-a----- 3.99g

firewad模块管理防火墙

# 常用参数 service开放服务 ports开放端口 premanent(yes|no)设置永久生效 immediate(yes|no)是否立即生效 state(enabled|disabled) rich_rule富规则
[sujx@master ~]$ ansible Node -m firewalld -a "service=https immediate=yes permanent=yes state=enabled"
[sujx@master ~]$ ansible Node -m firewalld -a "port=3306/tcp immediate=yes permanent=yes state=enabled"
[sujx@master ~]$ ansible Node -m shell -a "firewall-cmd --list-all"
node1 | CHANGED | rc=0 >>
trusted (active)
target: ACCEPT
icmp-block-inversion: no
interfaces: ens160
sources:
services: https
ports: 3306/tcp
node2 | CHANGED | rc=0 >>
trusted (active)
target: ACCEPT
icmp-block-inversion: no
interfaces: ens160
sources:
services: https
ports: 3306/tcp

替换模块replace

# 常用参数 path待编辑文件 regexp正则表达式 replace替换后的字符
# 准备文件
[sujx@master ~]$ cat target
aa = 111
bb = 222
cc = 333
# 复制文件
[sujx@master ~]$ ansible Node -m copy -a "src=/home/sujx/target dest=/home/sujx/"
[sujx@master ~]$ ansible Node -m replace -a 'path=/home/sujx/target regexp=^aa replace="XX = 666"'
# 将aa替换为 XX = 666
[sujx@node2 ~]$ cat target
XX = 666 = 111
bb = 222
cc = 333
# 将bb之后的整行进行替换
[sujx@master ~]$ ansible Node -m replace -a 'path=/home/sujx/target regexp=^bb.+ replace="YY = 999"'
node1 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"changed": true,
"msg": "1 replacements made",
"rc": 0
}
node2 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"changed": true,
"msg": "1 replacements made",
"rc": 0
}
[sujx@master ~]$ ansible Node -m shell -a "cat /home/sujx/target"
node1 | CHANGED | rc=0 >>
XX = 666
YY = 999
cc = 333
node2 | CHANGED | rc=0 >>
XX = 666
YY = 999
cc = 333

替换模块lineinfile

# 与replace类似功能,但replace进行字符替换,lineinfile进行整行替换
[sujx@master ~]$ ansible Node -m lineinfile -a 'path=/home/sujx/target regexp=^cc line="ZZ = 000"'
node2 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"backup": "",
"changed": true,
"msg": "line replaced"
}
node1 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"backup": "",
"changed": true,
"msg": "line replaced"
}
[sujx@master ~]$ ansible Node -m shell -a "cat /home/sujx/target"
node1 | CHANGED | rc=0 >>
XX = 666
YY = 999
ZZ = 000
node2 | CHANGED | rc=0 >>
XX = 666
YY = 999
ZZ = 000

打印模块debug

# 用于打印提示信息,类似echo,常用参数为msg表示具体信息,var表示变量
[sujx@master ~]$ ansible Node -m debug -a "msg='Hello World.'"
node1 | SUCCESS => {
"msg": "Hello World."
}
node2 | SUCCESS => {
"msg": "Hello World."
}

script模块在远端执行脚本

# 远程主机直接调用本地脚本,而无须先复制到远程再执行
[sujx@master ~]$ vim hello.sh
#!/bin/bash
echo "Hello World"
[sujx@master ~]$ chmod +x hello.sh
[sujx@master ~]$ ./hello.sh
Hello World
[sujx@master ~]$ ansible Node -m script -a "./hello.sh"
node1 | CHANGED => {
"changed": true,
"rc": 0,
"stderr": "Shared connection to node1 closed.\r\n",
"stderr_lines": [
"Shared connection to node1 closed."
],
"stdout": "Hello World\r\n",
"stdout_lines": [
"Hello World"
]
}
node2 | CHANGED => {
"changed": true,
"rc": 0,
"stderr": "Shared connection to node2 closed.\r\n",
"stderr_lines": [
"Shared connection to node2 closed."
],
"stdout": "Hello World\r\n",
"stdout_lines": [
"Hello World"
]
}

group模块对用户组进行管理

# 常用参数有 name组名 state(present|absent)
# 创建node组
[sujx@master ~]$ ansible Node -m group -a "name=node state=present"
node2 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"changed": true,
"gid": 1001,
"name": "node",
"state": "present",
"system": false
}
node1 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"changed": true,
"gid": 1001,
"name": "node",
"state": "present",
"system": false
}
# 检查组的存续
[sujx@master ~]$ ansible Node -m shell -a "grep node /etc/group"
node1 | CHANGED | rc=0 >>
node:x:1001:
node2 | CHANGED | rc=0 >>
node:x:1001:

user模块对用户进行管理

# 常用参数 name用户名 comment备注信息 group用户组 groups附属组 password用户密码 state(present|absent)
# 执行加密需要先安装加密套件
[sujx@master ~]$ sudo yum install -y python3-passlib
[sujx@master ~]$ ansible Node -m user -a "name=node group=node groups=wheel comment='extra admin user' password={{ 'haha001' | password_hash('sha512') }} state=present "
[sujx@master ~]$ ssh node@node2
node@node2's password:
[node@node2 ~]$ whoami
node
# 删除用户,同时也会删除用户的组
[sujx@master ~]$ ansible Node -m user -a "name=node state=absent "

get_url模块下载文件

# 常用参数 url文件的链接 dest目标路径
[sujx@master ~]$ ansible Node -m get_url -a "url=https://repo.huaweicloud.com/repository/conf/CentOS-8-reg.repo dest=/etc/yum.repos.d/"

setup模块获取被管理主机

# 用来获取主机信息
# 获取主机网卡地址
[sujx@master ~]$ ansible Node -m setup -a "filter=ansible_default_ipv4"
node2 | SUCCESS => {
"ansible_facts": {
"ansible_default_ipv4": {
"address": "192.168.10.102",
"alias": "ens160",
"broadcast": "192.168.10.255",
"gateway": "192.168.10.2",
"interface": "ens160",
"macaddress": "00:0c:29:b6:0a:a1",
"mtu": 1500,
"netmask": "255.255.255.0",
"network": "192.168.10.0",
"prefix": "24",
"type": "ether"
},
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"changed": false
}
node1 | SUCCESS => {
"ansible_facts": {
"ansible_default_ipv4": {
"address": "192.168.10.101",
"alias": "ens160",
"broadcast": "192.168.10.255",
"gateway": "192.168.10.2",
"interface": "ens160",
"macaddress": "00:0c:29:1d:aa:5f",
"mtu": 1500,
"netmask": "255.255.255.0",
"network": "192.168.10.0",
"prefix": "24",
"type": "ether"
},
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"changed": false
}
# 获取主机BIOS版本
[sujx@master ~]$ ansible Node -m setup -a "filter=ansible_bios_version"
node1 | SUCCESS => {
"ansible_facts": {
"ansible_bios_version": "VMW71.00V.18452719.B64.2108091906",
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"changed": false
}
node2 | SUCCESS => {
"ansible_facts": {
"ansible_bios_version": "VMW71.00V.18452719.B64.2108091906",
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"changed": false
}
# 如果filter包含子健,在ad-hoc命令中不显示,只有写入playbook才会显示。

剧本

简介

Ansible的模块都是在命令中执行,每次只能执行一个模块。如果需要 执行多个模块,且要对执行是否成功进行判断以及后续如何处理。这就需要编写脚本,Ansible中的脚本叫做playbook,即英文剧本的意思。其中,一个playbook可以包含多个playbook。

YAML语言

YAML 的语法比较简洁直观,特点是使用空格来表达层次结构,其最大优势在于数据结构方面的表达,所以 YAML 更多应用于编写配置文件,其文件一般以 .yml 为后缀。

YAML 目前的官方全称为 “YAML Ain’t Markup Language(YAML 不是标记语言)”,但有意思的是,其实 YAML 最初的含义是 “Yet Another Markup Language(还是一种标记语言)”。

基本语法:一文看懂YAML

写法

- name: play的名称
hosts: 主机组1, 主机组2, 主机组3, ……
tasks:
- name: 提示信息1
模块1: argx1=vx1 argx2=vx2 #=号两边不要有空格
- name: 提示信息2
模块2: rgx1=vx1 argx2=vx2

- name: 第二个play的名称
hosts: 主机组4, 主机组5, ……
gather_facts: false #执行过程中不需要手机主机组信息可以提高执行速度
tasks:
- name: 提示信息3
模块1: argx1=vx1 argx2=vx2
- name: 提示信息x
模块x: rgx1=vx1 rgx2=vx2
  1. 参数写在模块之后
  2. 一定要先写好框架,然后再往框架里面写内容
  3. 多个主机执行相同内容,可以放到同一个playbook之中;
  4. gather_facts: false 避免使用setup模块收集主机信息,以提高执行速度

命令

  • 编写一个playbook,在node1和node2上打印主机名和IP
---
- hosts: node1,node2
tasks:
- name: Print Host Name
debug: msg={{ ansible_fqdn }}
- name: Print IP Address
debug: msg={{ ansible_default_ipv4.address }}

执行结果

[sujx@master ~]$ ansible-playbook playbook/printhostname.yaml 
PLAY [node1,node2] **************************************************************************************************
TASK [Gathering Facts] **************************************************************************************************
ok: [node1]
ok: [node2]

TASK [Print Host Name] **************************************************************************************************
ok: [node1] => {
"msg": "node1"
}
ok: [node2] => {
"msg": "node2"
}

TASK [Print IP Address] **************************************************************************************************
ok: [node1] => {
"msg": "192.168.10.101"
}
ok: [node2] => {
"msg": "192.168.10.102"
}

PLAY RECAP **************************************************************************************************
node1 : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
node2 : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
  • 写一个palybook,完成下面的任务

    • 在node2上安装nfs-utils包,启动nfs-server服务,并设置开机启动
    • 在node2上配置防火墙,要求开放nfs、rpc-bind、mountd三个服务,重启系统也能生效
    • 在node2上创建目录/share
    • 在node2上的/etc/exports中写入 /share *(rw,no_root_squash)
    • 在node2上执行系统命令 exportfs -arv
    • 在node3上将node2:/share挂载到/nfs目录上,文件系统为nfs
---
- name: Node2开启NFS服务
hosts: node2
gather_facts: false
tasks:
- name: 安装NFS软件包
yum: name=nfs-utils state=installed
- name: 启动NFS服务
service: name=nfs-server state=started enabled=yes
- name: 开启防火墙
firewalld: service=nfs service=mountd service=rpc-bind immediate=yes permanent=yes state=enabled
- name: 创建目录
file: path=/share state=directory
- name: 修改配置
copy: content="/share *(rw,no_root_squash)" dest=/etc/exports
- name: 执行配置
shell: exportfs -arv

- name: Node1挂载NFS共享磁盘
hosts: node1
gather_facts: false
tasks:
- name: 安装NFS软件包
yum: name=nfs-utils state=installed
- name: 开启防火墙
firewalld: service=nfs service=mountd service=rpc-bind immediate=yes permanent=yes state=enabled
- name: 创建目录
file: path=/nfs state=directory
- name: 挂载目录
mount: src=node2:/share path=/nfs fstype=nfs state=mounted opts=_netdev,rw

执行结果如下

[sujx@master ~]$ ansible-playbook playbook/nfsmount.yaml 

PLAY [Node2开启NFS服务] ***********************************************************

TASK [安装NFS软件包] **************************************************************
changed: [node2]
TASK [启动NFS服务] ****************************************************************
changed: [node2]
TASK [开启防火墙] *****************************************************************
changed: [node2]
TASK [创建目录] *******************************************************************
changed: [node2]
TASK [修改配置] *******************************************************************
changed: [node2]
TASK [执行配置] *******************************************************************
changed: [node2]
PLAY [Node1挂载NFS共享磁盘] *********************************************************************************

TASK [安装NFS软件包] **************************************************************************************************
changed: [node1]
TASK [开启防火墙] **************************************************************************************************
changed: [node1]
TASK [创建目录] **************************************************************************************************
changed: [node1]
TASK [挂载目录] **************************************************************************************************
changed: [node1]
PLAY RECAP **************************************************************************************************
node1 : ok=4 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
node2 : ok=6 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

[root@node1 ~]# df -Th
Filesystem Type Size Used Avail Use% Mounted on
node2:/share nfs4 17G 2.7G 14G 17% /nfs
[root@node1 ~]# vim /etc/fstab
/dev/mapper/rl_rocky-root / xfs defaults 0 0
UUID=51ea77c3-fcaa-4985-98d6-1d80de9fdbe9 /boot xfs defaults 0 0
UUID=F2E1-A827 /boot/efi vfat umask=0077,shortname=winnt 0 2
/dev/mapper/rl_rocky-swap none swap defaults 0 0
node2:/share /nfs nfs _netdev,rw 0 0

变量

为了能够写出更实用的playbook,就需要在playbook中加入变量。

手动变量

  1. 通过vars来定义变量,不可以有重复的变量,否则后面定义的变量会覆盖前面定义的变量值。
  2. 引用变量时用, 大括号内侧两边是否有空格时无所谓。
  • 定义三个变量,测试变量的输出
---
- name: 打印变量名
hosts: node1
gather_facts: false
vars:
myname1: 北京
myname2: 天津
myname3: 南京
myname3: 上海
tasks:
- name: 打印myname1变量
debug: msg="变量 myname1 的值是 {{ myname1}}"
- name: 打印myname2变量
debug: msg="变量 myname2 的值是 {{ myname2}}"
- name: 打印myname3变量
debug: msg="变量 myname3 的值是 {{ myname3}}"

执行结果

[sujx@master ~]$ ansible-playbook playbook/printvars.yaml 
[WARNING]: While constructing a mapping from /home/sujx/playbook/printvars.yaml, line 6, column 5, found a duplicate dict key (myname3). Using last defined value only.

PLAY [打印变量名] *******************************************************************************************

TASK [打印myname1变量] *************************************************************************************
ok: [node1] => {
"msg": "变量 myname1 的值是 北京"
}

TASK [打印myname2变量] **************************************************************************************
ok: [node1] => {
"msg": "变量 myname2 的值是 天津"
}

TASK [打印myname3变量] **************************************************************************************
ok: [node1] => {
"msg": "变量 myname3 的值是 上海"
}

PLAY RECAP **************************************************************************************************
node1 : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

变量myname3定义两次,输出最后一次定义的值。

变量文件

如果定义变量太多,则可以将其单独存放到一个文件之中,然后在playbook中通过vars_files参数调用。
编写vars.yaml

myname1: 北京
myname2: 天津
myname3: 南京

playbook调用变量文件

---
- name: 打印变量名
hosts: node1
gather_facts: false
vars_files:
- vars.yaml
tasks:
- name: 打印myname1变量
debug: msg="变量 myname1 的值是 {{ myname1}}"
- name: 打印myname2变量
debug: msg="变量 myname2 的值是 {{ myname2}}"
- name: 打印myname3变量
debug: msg="变量 myname3 的值是 {{ myname3}}"

执行结果

[sujx@master ~]$ ansible-playbook playbook/printvars.yaml 

PLAY [打印变量名] *******************************************************************************************
TASK [打印myname1变量] **************************************************************************************
ok: [node1] => {
"msg": "变量 myname1 的值是 北京"
}
TASK [打印myname2变量] **************************************************************************************
ok: [node1] => {
"msg": "变量 myname2 的值是 天津"
}
TASK [打印myname3变量] **************************************************************************************
ok: [node1] => {
"msg": "变量 myname3 的值是 南京"
}
PLAY RECAP **************************************************************************************************
node1 : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

字典变量

字典就是存储变量的容器,一个字典中可以有多个变量。同一个字典中定义的多个变量不可有重复值。要引用字典中的变量,必须指定是那个字典。

vars:
字典1:
var1: value1
var2: value2
字典2:
var1: value1
var2: value2

示例

---
- name: 打印变量名
hosts: node1
gather_facts: false
#vars_files:
#- vars.yaml
vars:
dict1:
city1: 北京
city2: 广州
city3: 上海
dict2:
city1: 乌鲁木齐
city2: 呼和浩特
city3: 巴彦淖尔
tasks:
- name: 打印城市名称
debug: msg="有个城市,她的名字是 {{ dict1.city1 }}"
- name: 打印城市名称
debug: msg="某个城市,名字是四个字 {{dict2.city1}}"

执行结果

[sujx@master ~]$ ansible-playbook playbook/printvars.yaml 
PLAY [打印变量名] *******************************************************************************************
TASK [打印城市名称] *****************************************************************************************
ok: [node1] => {
"msg": "有个城市,她的名字是 北京"
}
TASK [打印城市名称] *****************************************************************************************
ok: [node1] => {
"msg": "某个城市,名字是四个字 乌鲁木齐"
}
PLAY RECAP *************************************************************************************************
node1 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

列表变量

列表变量与字典变量类似,都可以输出容器中的元素。区别是列表变量可以输出整个列表,并且列表变量前面有-,而字典列表没有。列表用角标[]来确定元素位置,并从0开始排序。
示例

---
- name: 打印变量名
hosts: node1
gather_facts: false
vars:
city:
- name: 北京
location: 北方
population: 2150
- name: 上海
location: 中部
population: 2480
- name: 广州
location: 南方
population: 1530
tasks:
- name: 打印城市信息
debug: msg="{{ city[2] }}"
- name: 打印城市地理
debug: msg="{{ city[1].location }}"
- name: 打印城市名称
debug: msg="{{ city[0].name }}"

执行结果

[sujx@master ~]$ ansible-playbook playbook/printlistvar.yaml 
PLAY [打印变量名] *************************************************************************************************
TASK [打印城市信息] *************************************************************************************************
ok: [node1] => {
"msg": {
"location": "南方",
"name": "广州",
"population": "1530万"
}
}
TASK [打印城市地理] *************************************************************************************************
ok: [node1] => {
"msg": "中部"
}
TASK [打印城市名称] *************************************************************************************************
ok: [node1] => {
"msg": "北京"
}
PLAY RECAP *************************************************************************************************
node1 : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

数字变量

在YAML文件中的变量,其值如果是数字,可以进行数字运算,包括加减乘除和次方。

---
- name: 使用Ansible进行数学计算
hosts: node1
gather_facts: false
vars:
num: 256
tasks:
- name: 乘法计算
debug: msg="{{num*3}}"
- name: 次方计算
debug: msg="{{num**3}}"

执行结果

[sujx@master ~]$ ansible-playbook playbook/printnum.yaml 

PLAY [使用Ansible进行数学计算] **************************************************************************************************

TASK [乘法计算] **************************************************************************************************
ok: [node1] => {
"msg": "768"
}

TASK [次方计算] **************************************************************************************************
ok: [node1] => {
"msg": "16777216"
}

PLAY RECAP **************************************************************************************************
node1 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

注册变量

playbook用shell模块执行系统命令在结果中不显示命令结果。如果需要查看命令结果,就需要临时将结果保存在一个变量中,这个变量就是注册变量。

---
- name: 注册变量
hosts: node1
gather_facts: false
tasks:
- name: 将shell命令结果注册为变量
shell: "hostname"
register: cmd
- name: 打印注册变量
debug: msg={{cmd}}

执行结果

[sujx@master ~]$ ansible-playbook playbook/printregister.yaml 
PLAY [注册变量] **************************************************************************************************
TASK [将shell命令结果注册为变量] **************************************************************************************************
changed: [node1]
TASK [打印注册变量] **************************************************************************************************
ok: [node1] => {
"msg": {
"ansible_facts": {
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"changed": true,
"cmd": "hostname",
"delta": "0:00:00.005883",
"end": "2023-06-20 19:57:15.575235",
"failed": false,
"msg": "",
"rc": 0,
"start": "2023-06-20 19:57:15.569352",
"stderr": "",
"stderr_lines": [],
"stdout": "node1",
"stdout_lines": [
"node1"
]
}
}

PLAY RECAP **************************************************************************************************
node1 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

stdout 标准输出,表示命令的执行结果。rc表示执行命令的状态返回值。如果只需要命令结果,就可以将运行结果当作字典,debug=

TASK [打印注册变量] 
****
ok: [node1] => {
"msg": "node1"
}

facts 变量

Ansible通过setup模块收集被管理主机的信息,这些信息以变量的形式存在,统称为facts。

---
- name: Facts变量
hosts: node1
tasks:
- name: 打印IP地址
debug: msg="主机IP地址为:{{ansible_default_ipv4.address}}"
- name: 主机运行时间
debug: msg="主机运行时长为:{{ansible_uptime_seconds / 3600 }}小时"
[sujx@master ~]$ ansible-playbook playbook/printfacts.yaml 

PLAY [Facts变量] **************************************************************************************************

TASK [Gathering Facts] **************************************************************************************************
ok: [node1]

TASK [打印IP地址] **************************************************************************************************
ok: [node1] => {
"msg": "主机IP地址为:192.168.10.101"
}

TASK [主机运行时间] **************************************************************************************************
ok: [node1] => {
"msg": "主机运行时长为:5.427222222222222小时"
}

PLAY RECAP **************************************************************************************************
node1 : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

内置变量 groups

用于列出inventory文件中定义的主机组和里面的主机

---
- name: Group变量
hosts: node1
tasks:
- name: 打印主机组
debug: msg="{{groups}}"
[sujx@master ~]$ ansible-playbook playbook/printgroups.yaml 
TASK [打印主机组] *******************************************************************************************
ok: [node1] => {
"msg": {
"Node": [
"node1",
"node2"
],
"all": [
"master",
"node1",
"node2"
],
"ungrouped": [
"master"
]
}
}

变量的过滤器

变量的过滤器,实际上就是对变量的值进行操作,例如类型转化、截取、加密等。

  • 大小写转化
---
- name: 大小写转换
hosts: node1
vars:
bb: BOB
tasks:
- name: 执行转换
debug: msg={{bb|lower}}

执行结果

PLAY [大小写转换] ************************************************************************************************************************************************************************************************

TASK [Gathering Facts] *******************************************************************************************************************************************************************************************
ok: [node1]

TASK [执行转换] **************************************************************************************************************************************************************************************************
ok: [node1] => {
"msg": "bob"
}
  • 列表
---
- name: 列表
hosts: node1
gather_facts: false
vars:
list1: [0,1,2,3,4,5,6,7,10]
tasks:
- name: 求列表的长度
debug: msg="{{ list1 | length}}"
- name: 求列表中元素的最大值
debug: msg="{{ list1 | max}}"
- name: 求列表中元素的最小值
debug: msg="{{ list1 | min}}"
TASK [求列表的长度] **********************************************************************************************************************************************************************************************
ok: [node1] => {
"msg": "9"
}

TASK [求列表中元素的最大值] **************************************************************************************************************************************************************************************
ok: [node1] => {
"msg": "10"
}

TASK [求列表中元素的最小值] **************************************************************************************************************************************************************************************
ok: [node1] => {
"msg": "0"
}
  • 加密
---
- name: 新建用户并修改密码
hosts: node1
gather_facts: false
vars:
passcontent: qwe123!!
tasks:
- name: 创建用户Test
user: user=test comment="Test User,Don't Remove." group=sujx password={{passcontent|password_hash('sha512')}}
PLAY [新建用户并修改密码] **********************************************************************************
TASK [创建用户Test] ******************************************************************************************************
changed: [node1]
PLAY RECAP **********************************************************************************
node1 : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

控制语句

一个playbook中可以包含多个tasks,可以设置满足某个条件时执行某个tasks。

判断语句 when

---
- name: 条件判断
hosts: node1
tasks:
- name: 使用>判断
debug: msg="1大于2"
when: 1>2
- name: 使用<判断
debug: msg="1小于2"
when: 1<2
- name: 使用!=判断
debug: msg="1不等于2"
when: 1!=2
- name: 使用==判断
debug: msg="1等于2"
when: 1==2
- name: 使用or连接
debug: msg="或判断"
when: 1>2 or 2<3
- name: 使用and连接
debug: msg="与判断"
when: 1>2 and 2<3
- name: 变量判断
debug: msg="操作系统的版本是8"
when: ansible_distribution_major_version == "8"

执行结果

PLAY [条件判断] *********************************************************************************************
TASK [Gathering Facts] *********************************************************************************************
ok: [node1]

TASK [使用>判断] *********************************************************************************************
skipping: [node1]

TASK [使用<判断] *********************************************************************************************
ok: [node1] => {
"msg": "1小于2"
}

TASK [使用!=判断] *********************************************************************************************
ok: [node1] => {
"msg": "1不等于2"
}
TASK [使用==判断] *********************************************************************************************
skipping: [node1]

TASK [使用or连接] *********************************************************************************************
ok: [node1] => {
"msg": "或判断"
}
TASK [使用and连接] *********************************************************************************************
skipping: [node1]

TASK [变量判断] *********************************************************************************************
ok: [node1] => {
"msg": "操作系统的版本是8"
}

判断语句 block-rescue

对于when来说,只能做一个判断,成立就执行,反之则不执行。block和rescue一般同用,类似if-else语句。

---
- name: 条件判断
hosts: node1
tasks:
- name: ifelse判断
block:
- name: 使用>判断
debug: msg="1大于2不成立"
when: 2>1
- name: 使用<判断
debug: msg="1小于2"
when: 1>2
- name: 使用!=判断
debug: msg="1不等于2"
when: 1==2

rescue:
- name: 使用==判断
debug: msg="1等于2"
when: 1==2
- name: 使用or连接
debug: msg="或判断"
when: 1>2 or 2<3
- name: 使用and连接
debug: msg="与判断"
when: 1>2 and 2<3

- name: 变量判断
debug: msg="操作系统的版本是8"
when: ansible_distribution_major_version == "8"

执行结果

PLAY [条件判断] **************************************************************************************************
TASK [Gathering Facts] **************************************************************************************************
ok: [node1]

TASK [使用>判断] **************************************************************************************************
ok: [node1] => {
"msg": "1大于2不成立"
}

TASK [使用<判断] **************************************************************************************************
skipping: [node1]

TASK [使用!=判断] **************************************************************************************************
skipping: [node1]

TASK [变量判断] **************************************************************************************************
ok: [node1] => {
"msg": "操作系统的版本是8"
}

PLAY RECAP **************************************************************************************************
node1 : ok=3 changed=0 unreachable=0 failed=0 skipped=2 rescued=0 ignored=0

循环语句

---
- name: 循环列表
hosts: node1
gather_facts: false
vars:
city:
- name: 北京
location: 北方
population: 2150
provincelevel: 直辖市
- name: 上海
location: 中部
population: 2480
provincelevel: 直辖市
- name: 广州
location: 南方
population: 1530
provincelevel: 省会

tasks:
- name: 打印城市信息
debug: msg={{ item.name }}
when: item.provincelevel == "直辖市"
loop: "{{ city }}"

循环打印城市列表内容,并输出所有直辖市

PLAY [循环列表] *********************************************************************************************
TASK [打印城市信息] *********************************************************************************************
ok: [node1] => (item={'name': '北京', 'location': '北方', 'population': '2150万', 'provincelevel': '直辖市'}) => {
"msg": "北京"
}
ok: [node1] => (item={'name': '上海', 'location': '中部', 'population': '2480万', 'provincelevel': '直辖市'}) => {
"msg": "上海"
}
skipping: [node1] => (item={'name': '广州', 'location': '南方', 'population': '1530万', 'provincelevel': '省会'})

PLAY RECAP *********************************************************************************************
node1 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

Template模板

通过playbook中的template模块复制含有变量的文件,在复制到远端主机之后,文件中包含的变量会打印为该主机的实际值。这个通过template模块复制、包含变量的文件称为jinja2模板,其后缀为j2.

示例

---
- name: Copy JinJa2
hosts: Node
tasks:
- name: Copy IPaddress
template: src=hostinfo.j2 dest=/opt/hostinfo
- name: Print HostInfo
shell: cat /opt/hostinfo
register: shellresult
- name: PrintShell
debug: msg={{shellresult.stdout}}

模板

+-------------------------------------------+
主机IP地址为:{{ ansible_default_ipv4.address }}
主机IP名为:{{ ansible_fqdn }}
+-------------------------------------------+

执行结果

[sujx@master ~]$ ansible-playbook playbook/copy.yaml 

PLAY [Copy JinJa2] ************************************************************************************************

TASK [Gathering Facts] ************************************************************************************************
ok: [node1]
ok: [node2]

TASK [Copy IPaddress] ************************************************************************************************
changed: [node1]
changed: [node2]

TASK [Print HostInfo] ************************************************************************************************
changed: [node2]
changed: [node1]

TASK [PrintShell] ************************************************************************************************
ok: [node1] => {
"msg": "+-------------------------------------------+\n 主机IP地址为:192.168.10.101\n 主机IP名为:node1\n+-------------------------------------------+"
}
ok: [node2] => {
"msg": "+-------------------------------------------+\n 主机IP地址为:192.168.10.102\n 主机IP名为:node2\n+-------------------------------------------+"
}

PLAY RECAP ************************************************************************************************
node1 : ok=4 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
node2 : ok=4 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

[sujx@master ~]$ ansible Node -m shell -a 'cat /opt/hostinfo'
node2 | CHANGED | rc=0 >>
+-------------------------------------------+
主机IP地址为:192.168.10.102
主机IP名为:node2
+-------------------------------------------+
node1 | CHANGED | rc=0 >>
+-------------------------------------------+
主机IP地址为:192.168.10.101
主机IP名为:node1
+-------------------------------------------+

在jinja2模板中也可以使用if判断语句,语法格式如下:

{% if 判断1 %}
内容1
{% elif 判断2 %}
内容2
{% else %}
内容3
{% endif %}

示例

  1. 模板
+-------------------------------------------+
主机IP地址为:{{ ansible_default_ipv4.address }}

{% if ansible_fqdn=="master" %}
{{ ansible_fqdn }}为控制节点
{% else %}
{{ ansible_fqdn }}为计算节点
{% endif %}
+-------------------------------------------+
  1. playbook
---
- name: Copy JinJa2
hosts: all
tasks:
- name: Copy IPaddress
template: src=hostinfo.j2 dest=/opt/hostinfo
- name: Print HostInfo
shell: cat /opt/hostinfo
register: shellresult
- name: PrintShell
debug: msg={{shellresult.stdout}}

执行

[sujx@master ~]$ ansible-playbook playbook/copy.yaml 

[sujx@master ~]$ ansible all -m shell -a 'cat /opt/hostinfo'
node1 | CHANGED | rc=0 >>
+-------------------------------------------+
主机IP地址为:192.168.10.101

node1为计算节点
+-------------------------------------------+
node2 | CHANGED | rc=0 >>
+-------------------------------------------+
主机IP地址为:192.168.10.102

node2为计算节点
+-------------------------------------------+
master | CHANGED | rc=0 >>
+-------------------------------------------+
主机IP地址为:192.168.10.100

master为控制节点
+-------------------------------------------+

角色

配置linux主机服务时,需要进行一系列的操作,比如安装、配置、启动服务等,为了避免重复劳动,将所有操作打包成为一个整体,这个整体就是角色。角色本质上就是一个文件夹,文件夹名就是角色名,其中包含多个文件,比如执行各个模块的文件、jinja模板、变量文件等等。为保持配置的整洁,对于不同的文件需要放置到不同的文件夹下。

名称 作用
tasks 执行的任务
vars 定义变量
files 需要拷贝的文件
templates 需要拷贝的jinja2模板
defaults 默认变量
handlers Handler操作
mate 注释信息

所有的角色都放在一个目录中等待调用,默认目录为ansible.cfg所在目录的roles目录,如要修改路径可通过ansible.cfg中roles_path来指定。

# 在配置文件中指定roles位置
[sujx@master demo]$ cat ansible.cfg
[defaults]
inventory = ./hosts
roles_path = ./roles
# 使用ansible-galaxy 创建role文件夹
[sujx@master demo]$ ansible-galaxy init roles/apache
- Role roles/apache was created successfully
[sujx@master roles]$ tree apache/
apache/
├── defaults
│   └── main.yml
├── files
├── handlers
│   └── main.yml
├── meta
│   └── main.yml
├── README.md
├── tasks
│   └── main.yml
├── templates
├── tests
│   ├── inventory
│   └── test.yml
└── vars
└── main.yml

8 directories, 8 files
# 将httpd.conf文件复制为jinja2模板,并将80端口修改为{{myport}}
[sujx@master ~]$ egrep -V '#|^#' /etc/httpd/conf/httpd.conf > /home/sujx/deamo/roles/apache/templates/httpd.conf.j2

原始的配置文件

---
- name: 安装HTTP服务
hosts: master
vars:
myport: 80
tasks:
- name: 软件安装
yum: name=httpd state=installed
- name: 开启防火墙
firewalld: port=8080/tcp immediate=yes permanent=yes state=enabled
- name: 复制配置文件
template: src=httpd.conf.j2 dest=/etc/httpd/conf/httpd.conf
notify: restart httpd
- name: 启动服务
service: name=httpd state=started enabled=yes
handlers:
- name: restart httpd
service: name=httpd state=restarted

将任务文件写入roles/apache/tasks/main.yml中

[sujx@master demo]$ cat roles/apache/tasks/main.yml
---
# tasks file for roles/apache
- name: 软件安装
yum: name=httpd state=installed
- name: 开启防火墙
firewalld: port=8080/tcp immediate=yes permanent=yes state=enabled
- name: 复制配置文件
template: src=httpd.conf.j2 dest=/etc/httpd/conf/httpd.conf
notify: restart httpd
- name: 启动服务
service: name=httpd state=started enabled=yes

将handles文件内容写入roles/apache/handles/main.yml中

[sujx@master demo]$ cat roles/apache/handlers/main.yml 
---
# handlers file for roles/apache
- name: restart httpd
service: name=httpd state=restarted

将变量名写入roles/apache/vars/main.yml中

[sujx@master demo]$ cat roles/apache/vars/main.yml 
---
# vars file for roles/apache
myport: 8080

编写调用role的playbook

[sujx@master demo]$ cat rolehttpd.yaml
---
- name: Ansible Roles安装httpd
hosts: master
roles:
- role: apache

执行部署

[sujx@master demo]$ ansible-playbook rolehttpd.yaml 
PLAY [Ansible Roles安装httpd] *************************************************************************
TASK [Gathering Facts] ********************************************************************************
ok: [master]
TASK [apache : 软件安装] ******************************************************************************
ok: [master]
TASK [apache : 开启防火墙] ****************************************************************************
ok: [master]
TASK [apache : 复制配置文件] **************************************************************************
changed: [master]
TASK [apache : 启动服务] ******************************************************************************
ok: [master]
RUNNING HANDLER [apache : restart httpd] **************************************************************
changed: [master]

PLAY RECAP ********************************************************************************************
master : ok=6 changed=2 unreachable=0 failed=0 skipped=0 rescued=0

[sujx@master demo]$ netstat -ltnp
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 127.0.0.1:44321 0.0.0.0:* LISTEN -
tcp 0 0 127.0.0.1:4330 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN -
tcp6 0 0 :::8080 :::* LISTEN -
tcp6 0 0 :::22 :::* LISTEN -

加密

Ansilbe playbook是以明文形式存在,可以使用ansible-vault命令来实现。

  • 对脚本进行加密
# 执行加密
[sujx@master ~]$ ansible-vault encrypt playbook/printnum.yaml
New Vault password:
Confirm New Vault password:
Encryption successful
# 直接查看文本内容
[sujx@master ~]$ cat playbook/printnum.yaml
$ANSIBLE_VAULT;1.1;AES256
62353932373763343836636637366664623362353632646234343662643531316662333661333730
6630333464373263633630306137343761666533626536650a303331343461626662373866346330
36346335303866366637306139646264373363393465633763313133626266306639396231623335
3863356334383732650a653332363239616236356332346632363730383066376336316234616134
66313738333636376663303766626333653938313863633935336364373164323832646536363334
30343461343064646538316463636537633832316665613164333435346437646434356339653365
31323730336464353866323030306233343665336439346535343464326536313431656561653531
30393230396332336538616461306663393434623164376330366336626661643231383233663135
61373430353736396239323566653861393961653432626432386634663139633864343865623964
65336234653339373261623931356132326362653361356561343731326361656537303562663563
37373366336239636662373632613762313835393664383263653631386162636363336438316261
61316165376334313239303931383838643031343064643939646166333836356361353735646466
31346365633236343937353462653163626536343662613666323865346430653065333431336166
35363861636561313537383364613932343561343937373634653565313762396662653234633830
353432626638653466303366396332323738
# 解密查看
[sujx@master ~]$ ansible-vault view playbook/printnum.yaml
Vault password:
---
- name: 使用Ansible进行数学计算
hosts: node1
gather_facts: false
vars:
num: 256
tasks:
- name: 乘法计算
debug: msg="{{num*3}}"
- name: 次方计算
debug: msg="{{num**3}}"
# 直接执行报错
[sujx@master ~]$ ansible-playbook playbook/printnum.yaml
ERROR! Attempting to decrypt but no vault secrets found
# 输入密码执行
[sujx@master ~]$ ansible-playbook --ask-vault-pass playbook/printnum.yaml
Vault password:

PLAY [使用Ansible进行数学计算] ************************************************************************

TASK [乘法计算] ***************************************************************************************
ok: [node1] => {
"msg": "768"
}

TASK [次方计算] ***************************************************************************************
ok: [node1] => {
"msg": "16777216"
}
# 执行解密
[sujx@master ~]$ ansible-vault decrypt playbook/printnum.yaml
Vault password:
Decryption successful
# 直接执行
[sujx@master ~]$ ansible-playbook playbook/printnum.yaml

PLAY [使用Ansible进行数学计算] ************************************************************************

TASK [乘法计算] ***************************************************************************************
ok: [node1] => {
"msg": "768"
}

TASK [次方计算] ***************************************************************************************
ok: [node1] => {
"msg": "16777216"
  • 使用密码文件
# 查看密码文件
[sujx@master ~]$ cat pass
qwe123!!
# 以密码文件加密
[sujx@master ~]$ ansible-vault encrypt --vault-id pass playbook/printnum.yaml
Encryption successful
# 使用密码查看文件
[sujx@master ~]$ ansible-vault view --vault-id pass playbook/printnum.yaml
---
- name: 使用Ansible进行数学计算
hosts: node1
gather_facts: false
vars:
num: 256
tasks:
- name: 乘法计算
debug: msg="{{num*3}}"
- name: 次方计算
debug: msg="{{num**3}}"
# 使用密码文件执行剧本
[sujx@master ~]$ ansible-playbook --vault-id pass playbook/printnum.yaml

PLAY [使用Ansible进行数学计算] ************************************************************************

TASK [乘法计算] ***************************************************************************************
ok: [node1] => {
"msg": "768"
}

TASK [次方计算] ***************************************************************************************
ok: [node1] => {
"msg": "16777216"
}

PLAY RECAP ********************************************************************************************
node1 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
# 解密剧本
[sujx@master ~]$ ansible-vault decrypt --vault-id pass playbook/printnum.yaml
Decryption successful
  • 对字符串进行单独加密
# 原文
[sujx@master ~]$ cat playbook/cat.yaml
---
- hosts: node1
gather_facts: false
vars:
printf: test
tasks:
- name: 打印变量
debug: msg="{{printf}}"
# 使用前面的pass文本对字符串test进行加密,生成密文
[sujx@master ~]$ ansible-vault encrypt_string --vault-id pass test
Encryption successful
!vault |
$ANSIBLE_VAULT;1.1;AES256
37613637323338326563306562653533646330336365366566613838326337323637613065373465
3765373233376331613661316663616530663239366633320a323562653631373931613861613037
30363263636366346232316566653032373564643337383761363432363330646162646436303465
3638653064376464330a646135336661623637656530353436636666653433643935666531613239
3330
# 替换文本
[sujx@master ~]$ cat playbook/cat.yaml
---
- hosts: node1
gather_facts: false
vars:
printf: !vault |
$ANSIBLE_VAULT;1.1;AES256
37613637323338326563306562653533646330336365366566613838326337323637613065373465
3765373233376331613661316663616530663239366633320a323562653631373931613861613037
30363263636366346232316566653032373564643337383761363432363330646162646436303465
3638653064376464330a646135336661623637656530353436636666653433643935666531613239
3330
tasks:
- name: 打印变量
debug: msg="{{printf}}"
# 使用密码文件解密并执行
[sujx@master ~]$ ansible-playbook --vault-id pass playbook/cat.yaml

PLAY [node1] ******************************************************************************************

TASK [打印变量] ***************************************************************************************
ok: [node1] => {
"msg": "test"
}