1. Ansible简介

认识Ansible

Ansible是一个为软件提供系统配置管理批量部署开源工具集,最初由Michael DeHaan于2012年编写,并于2015年被Red Hat公司收购,此后,Red Hat公司和开源社区进一步开发和改进了Ansible。

Ansible的核心组件

  • Module (模块)

    文档地址:https://docs.ansible.com/ansible/latest/collections/index_module.html

    模块涵盖领域:

    • 云计算
    • 网络
    • 服务配置和管理
    • 虚拟化
    • 容器化

    得益于Ansible可扩展的原生架构,支持自定义模块

  • ad-hoc(Ansible命令行工具)

    Ansible安装成功后即可执行Ansible指令,可使用/usr/bin/ansible命令行工具在一个或多个管理节点上执行单个任务的命令:

    • 有益于创建项目和测试Ansible配置
    • Ansible Modules的使用和测试更加方便
  • Playbook(剧本)

    Ansible安装成功后可编写剧本:

    • 使用人类易读的配置部署和编排语言编写
    • 可完成一组任务
  • Inventory(清单)

    清单是目标的集合:

    • 最常见的由主机组成,但也可以是与之相关的组件,如网络交换机、容器、存储阵列、其他物理或虚拟组件
    • 有用信息,如包含目标选择的文本文件
    • 动态清单,可通过执行程序动态获取数据

Ansible的特点

  • 基于Python开发,容易扩展
  • 功能强大,内置模块丰富,满足多样需求
  • 管理模式简单,上手容易
  • 无代理模式,通过SSH通信,跨平台支持

2. 准备环境

安装Docker,配置Ansible实验环境,配置主机间SSH免密码连接,配置Ansible课程的代码库

2.1 安装Docker

Docker是一个容器产品,利用操作系统级虚拟化,允许软件作为容器交付,容器是相互隔离的,每个容器内部都可以捆绑软件库和配置文件,Docker在Mac、Windows、Linux上都可用。

为了方便学习,我选择在Windows系统上安装Docker桌面级产品。

下载地址:https://www.docker.com/

安装过程非常简单,安装后如果出现提示,按照提示去做就ok。

测试:

# cmd控制台
C:\Users\zouxiongbin>docker run -it --rm ubuntu bash
Unable to find image 'ubuntu:latest' locally
latest: Pulling from library/ubuntu
6b851dcae6ca: Pull complete
Digest: sha256:2a357c4bd54822267339e601ae86ee3966723bdbcae640a70ace622cc9470c83
Status: Downloaded newer image for ubuntu:latest
root@1eb3165473b6:/# uname -a
Linux 1eb3165473b6 5.15.90.1-microsoft-standard-WSL2 #1 SMP Fri Jan 27 02:56:13 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux
root@1eb3165473b6:/# cat /etc/os-release
PRETTY_NAME="Ubuntu 22.04.2 LTS"
NAME="Ubuntu"
VERSION_ID="22.04"
VERSION="22.04.2 LTS (Jammy Jellyfish)"
VERSION_CODENAME=jammy
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=jammy
root@1eb3165473b6:/#

2.2 安装Ansible实验环境

Ansible实验环境代码地址: https://github.com/spurin/diveintoansible-lab,安装方法参考该地址文档,这里记录下Windows的安装方法(前提:本地Windows操作系统安装了git):

# cmd控制台
C:\Users\zouxiongbin>git version
git version 2.37.1.windows.1

# 拉取ansible实验环境代码
C:\Users\zouxiongbin>git clone https://github.com/spurin/diveintoansible-lab.git

# 对比查看文件是否缺失
C:\Users\zouxiongbin\diveintoansible-lab\.env
C:\Users\zouxiongbin\diveintoansible-lab\DiveIntoAnsible_Cover.png
C:\Users\zouxiongbin\diveintoansible-lab\README.md
C:\Users\zouxiongbin\diveintoansible-lab\docker-compose.yaml
C:\Users\zouxiongbin\diveintoansible-lab\config\guest_name
C:\Users\zouxiongbin\diveintoansible-lab\config\guest_passwd
C:\Users\zouxiongbin\diveintoansible-lab\config\guest_shell
C:\Users\zouxiongbin\diveintoansible-lab\config\root_passwd

# 运行实验环境
C:\Users\zouxiongbin\diveintoansible-lab>docker-compose up
......
Attaching to centos1, centos2, centos3, docker, portal, ubuntu-c, ubuntu1, ubuntu2, ubuntu3

# 更新实验环境
docker-compose pull

# 删除实验环境
docker-compose down

访问http://localhost:1000/即可进入Ansible实验环境,如下:

实验环境由7台主机组成,分别是ubuntu-c(ansible管理主机,其他为ubuntu系统或centos系统的被管理主机)、ubuntu1、ubuntu2、ubuntu3、centos1、centos2、centos3

登录ubuntu-c主机,账号是ansible,密码是password

# 登录Ubuntu-C主机
ubuntu-c login: ansible
Password:
Welcome to Ubuntu 22.04.1 LTS (GNU/Linux 5.15.90.1-microsoft-standard-WSL2 x86_64)

* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage

The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.

To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.

ansible@ubuntu-c:~$

除了使用本地的系统搭建Ansible实验环境外,还可以使用Google Cloud使用云服务器搭建Ansible实验环境,地址为:https://diveinto.com/p/playground

2.3 配置主机间SSH免密码连接

Ansible是一个无代理架构,意味着被管理主机无需安装Agent,只需要配置主机间的SSH免密码连接。

SSH连接建立过程如图:

配置Ansible SSH免密码连接

# ubuntu-c ansible管理主机
# 生成ssh密钥
ansible@ubuntu-c:~$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/home/ansible/.ssh/id_rsa):
Created directory '/home/ansible/.ssh'.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/ansible/.ssh/id_rsa
Your public key has been saved in /home/ansible/.ssh/id_rsa.pub
The key fingerprint is:
SHA256:p498cMfcCkbqBCie6F26a9f3vGVKJIP7nBgFP53/pmA ansible@ubuntu-c
The key's randomart image is:
+---[RSA 3072]----+
| |
| |
| . . |
| . . . + o . |
| o o oSO.* . |
|. o . *oO = . |
|. . o .=.+ E = |
| . + . o*+* * .. |
| .o+ .+=o=..o. |
+----[SHA256]-----+

# 查看公钥
ansible@ubuntu-c:~$ cat .ssh/id_rsa.pub
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC3qePtp0SWcYxUAg+05EW/s7silEEMWU1gYgdyJlbqO3miImGQKt+jit4eLcKjwcduF1CpiIJ2bR2CWFx2eCOT1esg9xwNy7J9Mu52kzkwoSgvIumlczkHMla4k03KERp7kIeX3oDae98XpRULu+b7rknKh02HGWmsPwMuhq0pX0928BWRiEa4SJmFzvNGoKoJbyXCVF8ZcAiZAJI0TaMe1aEEUuIm41dRhYZf/6AftChWFcJ7x1YgXG6EbG0xlacImgYE9/n+X1Hhjklm+MmBnXE6VMde4E8CdVP5OOkMNqg3H2s23UFj+4tPha5juLfSEKxm2mXOfE5rqtPJZQImgGwHINgiZv4qbTg6t78CsCl2YXzBHk8oAPnpE/YFZ8tc5vozdhdhphbN2a5Q9xZ9abByGV7bHB5MS+GQuuGSHSnSp9Ai87gb43QoU1V76jlbngYzwQFwhxLnfoNCwzr3FQj9OJ8KrwQS8zuKKujGzG5UIytGQDlpgJOE97OTS2s= ansible@ubuntu-c

# 使用ssh-copy-id将公钥的内容复制到被管理主机的authorized_keys文件
ansible@ubuntu-c:~$ ssh-copy-id ansible@ubuntu1
/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/home/ansible/.ssh/id_rsa.pub"
The authenticity of host 'ubuntu1 (172.18.0.3)' can't be established.
ED25519 key fingerprint is SHA256:Bq0T7Bg1OWZDjSsLlhWtp7QjqtZitWuPQCgcXX+pXas.
This host key is known by the following other names/addresses:
~/.ssh/known_hosts:1: [hashed name]
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
ansible@ubuntu1's password:

Number of key(s) added: 1

Now try logging into the machine, with: "ssh 'ansible@ubuntu1'"
and check to make sure that only the key(s) you wanted were added.

# 测试连接
ansible@ubuntu-c:~$ ssh ubuntu1
Last login: Fri Jun 16 12:28:32 2023 from 172.18.0.9
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.

ansible@ubuntu1:~$

不难发现,上面的做法存在弊端,每次将公钥的内容复制到新的被管理主机时,都需要键入yes和被管理主机的密码,被管理主机很多时设置起来比较费时,因此,可以使用sshpass脚本将这一过程自动化。

安装sshpass

# 更新apt
ansible@ubuntu-c:~$ sudo apt update
[sudo] password for ansible:
Get:1 http://security.ubuntu.com/ubuntu jammy-security InRelease [110 kB]
......
Get:18 http://archive.ubuntu.com/ubuntu jammy-backports/universe amd64 Packages [27.0 kB] Fetched 25.2 MB in 28s (904 kB/s)

Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
90 packages can be upgraded. Run 'apt list --upgradable' to see them.

# 下载sshpass
ansible@ubuntu-c:~$ sudo apt install sshpass

编写公钥分发脚本

# 编写公钥分发脚本
ansible@ubuntu-c:~$ cat ssh_key_send.sh
#!/bin/bash
rm -rf ~/.ssh/id_rsa*
ssh-keygen -f ~/.ssh/id_rsa -P "" > /dev/null 2>&1
Pass_Text=password.txt
Key_Path=~/.ssh/id_rsa.pub
for user in ansible root
do
for os in ubuntu centos
do
for instance in 1 2 3
do
sshpass -f $Pass_Text ssh-copy-id -i $Key_Path -o StrictHostKeyChecking=no ${user}@${os}${instance}
done
done
done

# 非交互式分发公钥命令,使用sshpass指定ssh密码,通过 -o StrictHostKeyChecking=no 跳过ssh连接确认信息

执行脚本

# 执行脚本
ansible@ubuntu-c:~$ sh ssh_key_send.sh

# 安全起见,删除password.txt
ansible@ubuntu-c:~$ rm password.txt

此时ubuntu-c连接任何被管理主机都不需要输入账号密码

# 测试连接
# ansible
# -i 指定清单文件,也可以接 ,和主机列表
# -m 指定模板
ansible@ubuntu-c:~$ ansible -i,ubuntu1,ubuntu2,ubuntu3,centos1,centos2,centos3 all -m ping
ubuntu1 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": false,
"ping": "pong"
}
ubuntu3 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": false,
"ping": "pong"
}
ubuntu2 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": false,
"ping": "pong"
}
centos1 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"changed": false,
"ping": "pong"
}
centos2 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"changed": false,
"ping": "pong"
}
centos3 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"changed": false,
"ping": "pong"
}

2.4 配置Ansible课程的代码库

Ansible课程代码库地址:https://github.com/spurin/diveintoansible,建议github上star

ansible@ubuntu-c:~$ git clone https://github.com/spurin/diveintoansible.git

3. Ansible架构和设计

Ansible配置文件,Ansible Inventory清单和Module模块

3.1 Ansible配置文件

# 查看ansible版本信息
ansible@ubuntu-c:~$ ansible --version
ansible [core 2.14.2]
config file = None
configured module search path = ['/home/ansible/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
ansible python module location = /usr/local/lib/python3.10/dist-packages/ansible
ansible collection location = /home/ansible/.ansible/collections:/usr/share/ansible/collections
executable location = /usr/local/bin/ansible
python version = 3.10.6 (main, Nov 14 2022, 16:10:14) [GCC 11.3.0] (/usr/bin/python3)
jinja version = 3.1.2
libyaml = True

配置文件优先级

可以发现Ansible config file还未配置,Ansible配置文件位置和环境变量可以用于影响config file,优先级从高到低依次是:

  1. ANSIBLE_CONFIG 环境变量,带ansible配置文件路径
  2. ./ansible.cfg 当前目录,当前目录可以有自己单独的配置文件,推荐
  3. ~/.ansible.cfg 隐藏文件,在用户的主目录中
  4. /etc/ansible/ansible.cfg 通常由Ansible通过包或系统安装提供,如apt install ansible

3.2 Ansible清单

停止Ansible实验环境

# cmd控制台
C:\Users\zouxiongbin\diveintoansible-lab>docker-compose down

修改docker-compose.yml文件为:

version: '3.8' # if no version is specificed then v1 is assumed. Recommend v2 minimum

services:
ubuntu-c:
hostname: ubuntu-c
container_name: ubuntu-c
image: spurin/diveintoansible:ansible
ports:
- ${UBUNTUC_PORT_SSHD}:22
- ${UBUNTUC_PORT_TTYD}:7681
privileged: true
volumes:
- ${CONFIG}:/config
- ${ANSIBLE_HOME}/shared:/shared
- ${ANSIBLE_HOME}/ubuntu-c/ansible:/home/ansible
- ${ANSIBLE_HOME}/ubuntu-c/root:/root
networks:
- diveinto.io

ubuntu1:
hostname: ubuntu1
container_name: ubuntu1
image: spurin/diveintoansible:ubuntu
ports:
- ${UBUNTU1_PORT_SSHD}:22
- ${UBUNTU1_PORT_TTYD}:7681
privileged: true
volumes:
- ${CONFIG}:/config
- ${ANSIBLE_HOME}/shared:/shared
- ${ANSIBLE_HOME}/ubuntu1/ansible:/home/ansible
- ${ANSIBLE_HOME}/ubuntu1/root:/root
networks:
- diveinto.io

ubuntu2:
hostname: ubuntu2
container_name: ubuntu2
image: spurin/diveintoansible:ubuntu
ports:
- ${UBUNTU2_PORT_SSHD}:22
- ${UBUNTU2_PORT_TTYD}:7681
privileged: true
volumes:
- ${CONFIG}:/config
- ${ANSIBLE_HOME}/shared:/shared
- ${ANSIBLE_HOME}/ubuntu2/ansible:/home/ansible
- ${ANSIBLE_HOME}/ubuntu2/root:/root
networks:
- diveinto.io

ubuntu3:
hostname: ubuntu3
container_name: ubuntu3
image: spurin/diveintoansible:ubuntu
ports:
- ${UBUNTU3_PORT_SSHD}:22
- ${UBUNTU3_PORT_TTYD}:7681
privileged: true
volumes:
- ${CONFIG}:/config
- ${ANSIBLE_HOME}/shared:/shared
- ${ANSIBLE_HOME}/ubuntu3/ansible:/home/ansible
- ${ANSIBLE_HOME}/ubuntu3/root:/root
networks:
- diveinto.io

centos1:
hostname: centos1
container_name: centos1
#image: spurin/diveintoansible:centos
image: spurin/diveintoansible:centos-sshd-2222
ports:
#- ${CENTOS1_PORT_SSHD}:22
- ${CENTOS1_PORT_SSHD}:2222
- ${CENTOS1_PORT_TTYD}:7681
privileged: true
volumes:
- ${CONFIG}:/config
- ${ANSIBLE_HOME}/shared:/shared
- ${ANSIBLE_HOME}/centos1/ansible:/home/ansible
- ${ANSIBLE_HOME}/centos1/root:/root
networks:
- diveinto.io

centos2:
hostname: centos2
container_name: centos2
image: spurin/diveintoansible:centos
ports:
- ${CENTOS2_PORT_SSHD}:22
- ${CENTOS2_PORT_TTYD}:7681
privileged: true
volumes:
- ${CONFIG}:/config
- ${ANSIBLE_HOME}/shared:/shared
- ${ANSIBLE_HOME}/centos2/ansible:/home/ansible
- ${ANSIBLE_HOME}/centos2/root:/root
networks:
- diveinto.io

centos3:
hostname: centos3
container_name: centos3
image: spurin/diveintoansible:centos
ports:
- ${CENTOS3_PORT_SSHD}:22
- ${CENTOS3_PORT_TTYD}:7681
privileged: true
volumes:
- ${CONFIG}:/config
- ${ANSIBLE_HOME}/shared:/shared
- ${ANSIBLE_HOME}/centos3/ansible:/home/ansible
- ${ANSIBLE_HOME}/centos3/root:/root
networks:
- diveinto.io

# Docker in Docker
#
# Usage: on host that wishes to use docker
#
# sudo apt-get update
# sudo apt -y install docker.io
# export DOCKER_HOST=tcp://docker:2375
# docker ps -a
#
docker:
hostname: docker
container_name: docker
image: spurin/diveintoansible:dind
privileged: true
volumes:
- ${ANSIBLE_HOME}/shared:/shared
networks:
- diveinto.io

portal:
hostname: portal
container_name: portal
image: spurin/diveintoansible:portal
environment:
- NGINX_ENTRYPOINT_QUIET_LOGS=1
depends_on:
- centos1
- centos2
- centos3
- ubuntu1
- ubuntu2
- ubuntu3
ports:
- "1000:80"
networks:
- diveinto.io

networks:
diveinto.io:
name: diveinto.io
# Canonical bridge interface name
#
# The setting below provides a friendly name for the bridge interface
# as seen in the likes of the ip command. Use at your own discretion
#
#driver_opts:
# com.docker.network.bridge.name: "diveinto.io"

重新启动Ansible实验环境

# cmd控制台
C:\Users\zouxiongbin\diveintoansible-lab>docker-compose up
......
Attaching to centos1, centos2, centos3, docker, portal, ubuntu-c, ubuntu1, ubuntu2, ubuntu3

访问http://localhost:1000/进入ubuntu-c环境

ini清单文件

ansible@ubuntu-c:~$ ls
diveintoansible ssh_key_send.sh this_is_example_ansible.cfg
# 新建文件夹并进入
ansible@ubuntu-c:~$ mkdir demo01 && cd demo01
# 创建配置文件和清单文件
ansible@ubuntu-c:~/demo01$ touch ansible.cfg hosts

配置文件 ansible.cfg :

[defaults]
inventory = hosts
host_key_checking = False

清单文件 hosts :

[control]
ubuntu-c ansible_connection=local

[centos]
centos1 ansible_port=2222
centos[2:3]

[centos:vars]
ansible_user=root

[ubuntu]
ubuntu[1:3]

[ubuntu:vars]
ansible_become=true
ansible_become_pass=password

[linux:children]
centos
ubuntu

[linux:vars]
ansible_port=1234

上面的清单文件包括了:

  • 清单主机变量,如ansible_port

  • 简单的范围清单,如ubuntu[1:3]

  • 清单组变量,如[ubuntu:vars]、[linux:vars]

  • 清单子组,如[linux:children]

  • ansible通过root连接centos机器

  • ansible通过2222端口连接centos1

  • ansible通过sudo连接ubuntu机器

测试:

ansible@ubuntu-c:~/demo01$ ansible all -m ping -o
centos2 | UNREACHABLE!: Failed to connect to the host via ssh: ssh: connect to host centos2 port 1234: Connection refused
centos3 | UNREACHABLE!: Failed to connect to the host via ssh: ssh: connect to host centos3 port 1234: Connection refused
ubuntu1 | UNREACHABLE!: Failed to connect to the host via ssh: ssh: connect to host ubuntu1 port 1234: Connection refused
ubuntu2 | UNREACHABLE!: Failed to connect to the host via ssh: ssh: connect to host ubuntu2 port 1234: Connection refused
ubuntu3 | UNREACHABLE!: Failed to connect to the host via ssh: ssh: connect to host ubuntu3 port 1234: Connection refused
ubuntu-c | SUCCESS => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python3"},"changed": false,"ping": "pong"}
centos1 | SUCCESS => {"ansible_facts": {"discovered_interpreter_python": "/usr/libexec/platform-python"},"changed": false,"ping": "pong"}

注释掉hosts文件里最后两行,再进行测试:

ansible@ubuntu-c:~/demo01$ ansible all -m ping -o
ubuntu-c | SUCCESS => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python3"},"changed": false,"ping": "pong"}
centos2 | SUCCESS => {"ansible_facts": {"discovered_interpreter_python": "/usr/libexec/platform-python"},"changed": false,"ping": "pong"}
centos1 | SUCCESS => {"ansible_facts": {"discovered_interpreter_python": "/usr/libexec/platform-python"},"changed": false,"ping": "pong"}
centos3 | SUCCESS => {"ansible_facts": {"discovered_interpreter_python": "/usr/libexec/platform-python"},"changed": false,"ping": "pong"}
ubuntu1 | SUCCESS => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python3"},"changed": false,"ping": "pong"}
ubuntu2 | SUCCESS => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python3"},"changed": false,"ping": "pong"}
ubuntu3 | SUCCESS => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python3"},"changed": false,"ping": "pong"}

yaml清单文件

新增清单文件 hosts.yaml :

---
control:
hosts:
ubuntu-c:
ansible_connection: local
centos:
hosts:
centos1:
ansible_port: 2222
centos2:
centos3:
vars:
ansible_user: root
ubuntu:
hosts:
ubuntu1:
ubuntu2:
ubuntu3:
vars:
ansible_become: true
ansible_become_pass: password
linux:
children:
centos:
ubuntu:
...

将清单改为yaml类型,只需修改配置文件 ansible.cfg :

[defaults]
inventory = hosts.yaml
host_key_checking = False

再次进行测试:

ansible@ubuntu-c:~/demo01$ ansible linux -m ping -o
centos1 | SUCCESS => {"ansible_facts": {"discovered_interpreter_python": "/usr/libexec/platform-python"},"changed": false,"ping": "pong"}
centos2 | SUCCESS => {"ansible_facts": {"discovered_interpreter_python": "/usr/libexec/platform-python"},"changed": false,"ping": "pong"}
centos3 | SUCCESS => {"ansible_facts": {"discovered_interpreter_python": "/usr/libexec/platform-python"},"changed": false,"ping": "pong"}
ubuntu1 | SUCCESS => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python3"},"changed": false,"ping": "pong"}
ubuntu2 | SUCCESS => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python3"},"changed": false,"ping": "pong"}
ubuntu3 | SUCCESS => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python3"},"changed": false,"ping": "pong"}

json清单文件

使用python根据hosts.yaml生成清单文件 hosts.json (前提是已安装python3):

ansible@ubuntu-c:~/demo01$ python3 --version
Python 3.10.6
ansible@ubuntu-c:~/demo01$ python3 -c 'import sys, yaml, json; json.dump(yaml.load(sys.stdin, Loader=yaml.FullLoader), sys.stdout, indent=4)' < hosts.yaml > hosts.json
ansible@ubuntu-c:~/demo01$ cat hosts.json
{
"control": {
"hosts": {
"ubuntu-c": {
"ansible_connection": "local"
}
}
},
"centos": {
"hosts": {
"centos1": {
"ansible_port": 2222
},
"centos2": null,
"centos3": null
},
"vars": {
"ansible_user": "root"
}
},
"ubuntu": {
"hosts": {
"ubuntu1": null,
"ubuntu2": null,
"ubuntu3": null
},
"vars": {
"ansible_become": true,
"ansible_become_pass": "password"
}
},
"linux": {
"children": {
"centos": null,
"ubuntu": null
}
}
}

将清单改为json类型,只需修改配置文件 ansible.cfg :

[defaults]
inventory = hosts.json
host_key_checking = False

再次进行测试:

ansible@ubuntu-c:~/demo01$ ansible centos -m ping -o
centos1 | SUCCESS => {"ansible_facts": {"discovered_interpreter_python": "/usr/libexec/platform-python"},"changed": false,"ping": "pong"}
centos3 | SUCCESS => {"ansible_facts": {"discovered_interpreter_python": "/usr/libexec/platform-python"},"changed": false,"ping": "pong"}
centos2 | SUCCESS => {"ansible_facts": {"discovered_interpreter_python": "/usr/libexec/platform-python"},"changed": false,"ping": "pong"}

命令行工具参数

也可以使用ansible命令行工具参数,指定清单文件运行,如:

# ansible
# -i 指定清单文件,会覆盖配置文件的inventory参数
# -e 指定清单命令,会覆盖清单文件对应的变量
# -m 指定模块,默认使用command模块
# -a 指定在被管理主机上执行的命令,命令需要使用引号
ansible@ubuntu-c:~/demo01$ ansible all -i hosts.yaml --list-hosts
hosts (7):
ubuntu-c
centos1
centos2
centos3
ubuntu1
ubuntu2
ubuntu3

ansible@ubuntu-c:~/demo01$ ansible linux -m ping -e 'ansible_port=22' -o
centos1 | UNREACHABLE!: Failed to connect to the host via ssh: ssh: connect to host centos1 port 22: Connection refused
centos2 | SUCCESS => {"ansible_facts": {"discovered_interpreter_python": "/usr/libexec/platform-python"},"changed": false,"ping": "pong"}
centos3 | SUCCESS => {"ansible_facts": {"discovered_interpreter_python": "/usr/libexec/platform-python"},"changed": false,"ping": "pong"}
ubuntu1 | SUCCESS => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python3"},"changed": false,"ping": "pong"}
ubuntu2 | SUCCESS => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python3"},"changed": false,"ping": "pong"}
ubuntu3 | SUCCESS => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python3"},"changed": false,"ping": "pong"}

ansible@ubuntu-c:~/demo01$ ansible ubuntu -a 'id' -o
ubuntu1 | CHANGED | rc=0 | (stdout) uid=0(root) gid=0(root) groups=0(root)
ubuntu3 | CHANGED | rc=0 | (stdout) uid=0(root) gid=0(root) groups=0(root)
ubuntu2 | CHANGED | rc=0 | (stdout) uid=0(root) gid=0(root) groups=0(root)

3.3 Ansible模块

执行期间颜色含义

  • 红色 —— 代表失败
  • 黄色 —— 代表成功,有改变
  • 绿色 —— 代表成功,无改变

幂等性

幂等性指多次执行产生的结果不会发生改变,ansible的大部分模块都能够保持操作的幂等性,即相关操作的多次执行能够达到相同结果,但也有不满足幂等性原则的模块,比如shell模块和raw模块。

setup模块

setup模块自动被剧本调用,收集远程主机的基本信息,随后可以在剧本中使用,可以使用ansible命令直接调用该模块查看目标的信息

ansible@ubuntu-c:~/demo01$ ansible centos1 -m setup | more

file模块

file模块用于文件管理操作,包括文件/文件夹/链接文件的增删改查,其他模块如copy、template、assemble也可进行文件管理操作,对于windows目标系统,则需要使用win_file模块。

# 创建文件
ansible@ubuntu-c:~/demo01$ ansible all -m file -a 'path=/tmp/test state=touch'
# 修改文件权限
ansible@ubuntu-c:~/demo01$ ansible all -m file -a 'path=/tmp/test state=file mode=600'

copy模块

copy模块作用是复制文件,通常用于将ansible管理主机上的文件拷贝到远程主机中,也可以将远程主机的文件拷贝到远程主机

# 拷贝管理主机上的文件到远程主机中
ansible@ubuntu-c:~/demo01$ ansible all -m copy -a 'src=/tmp/x dest=/tmp/x'
# 拷贝远程主机上的文件到远程主机中
ansible@ubuntu-c:~/demo01$ ansible all -m copy -a 'remote_src=yes src=/tmp/x dest=/tmp/y'

fetch模块

fetch模块作用也是复制文件,通常用于将到远程主机上的文件拷贝到ansible管理主机上

# 创建文件
ansible@ubuntu-c:~/demo01$ ansible all -m file -a 'path=/tmp/test_modules.txt state=touch mode=600' -o

# 远程文件拷贝到本地
ansible@ubuntu-c:~/demo01$ ansible all -m fetch -a 'src=/tmp/test_modules.txt dest=/tmp/' -o

command模块

command模块是ansible的默认基本模块,也可以省略不写,但是使用command模块,不得出现shell变量,如$name,也不得出现特殊符号> < | ; &,如果需要使用,请使用shell模块

# 所有主机执行 hostname 命令
ansible@ubuntu-c:~/demo01$ ansible all -a 'hostname' -o
ubuntu-c | CHANGED | rc=0 | (stdout) ubuntu-c
centos1 | CHANGED | rc=0 | (stdout) centos1
centos2 | CHANGED | rc=0 | (stdout) centos2
ubuntu1 | CHANGED | rc=0 | (stdout) ubuntu1
centos3 | CHANGED | rc=0 | (stdout) centos3
ubuntu2 | CHANGED | rc=0 | (stdout) ubuntu2
ubuntu3 | CHANGED | rc=0 | (stdout) ubuntu3

shell模块

shell模块作用是在远程主机上执行命令

# 远程主机执行 ps -ef | grep vim 命令
ansible@ubuntu-c:~/demo01$ ansible linux -m shell -a 'ps -ef | grep vim' -o
centos1 | CHANGED | rc=0 | (stdout) root 864 863 0 07:33 pts/0 00:00:00 /bin/sh -c ps -ef | grep vim\nroot 866 864 0 07:33 pts/0 00:00:00 grep vim
centos2 | CHANGED | rc=0 | (stdout) root 769 768 0 07:33 pts/0 00:00:00 /bin/sh -c ps -ef | grep vim\nroot 771 769 0 07:33 pts/0 00:00:00 grep vim
centos3 | CHANGED | rc=0 | (stdout) root 682 681 0 07:33 pts/0 00:00:00 /bin/sh -c ps -ef | grep vim\nroot 684 682 0 07:33 pts/0 00:00:00 grep vim
ubuntu2 | CHANGED | rc=0 | (stdout) root 1338 1337 0 07:33 pts/1 00:00:00 /bin/sh -c ps -ef | grep vim\nroot 1340 1338 0 07:33 pts/1 00:00:00 grep vim
ubuntu1 | CHANGED | rc=0 | (stdout) root 1345 1344 0 07:33 pts/1 00:00:00 /bin/sh -c ps -ef | grep vim\nroot 1347 1345 0 07:33 pts/1 00:00:00 grep vim
ubuntu3 | CHANGED | rc=0 | (stdout) root 1335 1334 0 07:33 pts/1 00:00:00 /bin/sh -c ps -ef | grep vim\nroot 1337 1335 0 07:33 pts/1 00:00:00 grep vim

script模块

script模块的作用是在远程主机上执行ansible管理主机上的脚本,脚本存在ansible管理主机上,不需要手动拷贝到远程主机后再执行

# 创建脚本
ansible@ubuntu-c:~/demo01$ cat show_pwd.sh
#!/bin/bash
pwd

# 去/tmp目录下 批量执行脚本
ansible@ubuntu-c:~/demo01$ ansible all -m script -a 'chdir=/tmp /home/ansible/demo01/show_pwd.sh' -o
ubuntu-c | CHANGED => {"changed": true,"rc": 0,"stderr": "","stderr_lines": [],"stdout": "/tmp\n","stdout_lines": ["/tmp"]}
centos1 | CHANGED => {"changed": true,"rc": 0,"stderr": "Shared connection to centos1 closed.\r\n","stderr_lines": ["Shared connection to centos1 closed."],"stdout": "/tmp\r\n","stdout_lines": ["/tmp"]}
centos2 | CHANGED => {"changed": true,"rc": 0,"stderr": "Shared connection to centos2 closed.\r\n","stderr_lines": ["Shared connection to centos2 closed."],"stdout": "/tmp\r\n","stdout_lines": ["/tmp"]}
centos3 | CHANGED => {"changed": true,"rc": 0,"stderr": "Shared connection to centos3 closed.\r\n","stderr_lines": ["Shared connection to centos3 closed."],"stdout": "/tmp\r\n","stdout_lines": ["/tmp"]}
ubuntu1 | CHANGED => {"changed": true,"rc": 0,"stderr": "Shared connection to ubuntu1 closed.\r\n","stderr_lines": ["Shared connection to ubuntu1 closed."],"stdout": "\r\n/tmp\r\n","stdout_lines": ["","/tmp"]}
ubuntu2 | CHANGED => {"changed": true,"rc": 0,"stderr": "Shared connection to ubuntu2 closed.\r\n","stderr_lines": ["Shared connection to ubuntu2 closed."],"stdout": "\r\n/tmp\r\n","stdout_lines": ["","/tmp"]}
ubuntu3 | CHANGED => {"changed": true,"rc": 0,"stderr": "Shared connection to ubuntu3 closed.\r\n","stderr_lines": ["Shared connection to ubuntu3 closed."],"stdout": "\r\n/tmp\r\n","stdout_lines": ["","/tmp"]}

Ansible-doc

适合用于查看指定模块的变量和语法

ansible@ubuntu-c:~/demo01$ ansible-doc shell

4. Ansible Playbook介绍

YAML,Playbook剧本,变量,Facts 变量, Jinja2模板,Playbook的编写和执行

4.1 YAML

YAML是一种面向数据的语言,ansible剧本可以使用YAML和json编写,使用YAML编写的剧本具有易读易写的特性,便于分享合作。

编写yaml文件test.yaml

# YAML文件开始于3个 - (短横线)
---

# 字符串
example_key_1: this is a string
example_key_2: this is another string

# 单引号和双引号
no_quotes: this is a string example
double_quotes: "this is a string example"
single_quotes: 'this is a string example'

# 转义字符
escape_no_quotes: this is a string example\n
escape_double_quotes: "this is a string example\n"
escape_single_quotes: 'this is a string example\n'

# 多行
multilines_example_key_1: |
this is a string
that goes over
multiple lines

multilines_example_key_2: >
this is a string
that goes over
multiple lines

multilines_example_key_3: >-
this is a string
that goes over
multiple lines

# 数字
example_integer_1: 1

example_integer_2: "1"

# 真与假
# false, False, FALSE, no, No, NO, off, Off, OFF
# true, True, TRUE, yes, Yes, YES, on, On, ON
# n不等于假,y不等于真

is_false_01: false
is_false_02: False
is_false_03: FALSE
is_false_04: no
is_false_05: No
is_false_06: NO
is_false_07: off
is_false_08: Off
is_false_09: OFF
is_false_10: n
is_true_01: true
is_true_02: True
is_true_03: TRUE
is_true_04: yes
is_true_05: Yes
is_true_06: YES
is_true_07: on
is_true_08: On
is_true_09: ON
is_true_10: y

# 列表,内联列表和内联字典不能同时存在,所以此处注释掉
# - item 1
# - item 2
# - item 3
# - item 4
# - item 5

example_key_4: [item 1, item 2, item 3, item 4, item 5]

# 字典
example_key_5: example_key_5
example_key_6: example_key_6

# 下面这种字典表示方法不常用
# {example_key_7: example_key_7, example_key_8: example_key_8}
# 嵌套字典
example_key_9:
example_key_10: sub_example_value1
example_key_11:
example_key_12: sub_example_value2

# 嵌套列表
example_key_13:
- item_1
- item_2
- item_3
exmaple_key_14:
- item_4
- item_5
- item_6

# 多级嵌套
example_dictionary_1:
- example_dictioanry_2:
- 1
- 2
- 3
- example_dictionary_3:
- 4
- 5
- 6
- example_dictioanry_4:
- 7
- 8
- 9

# YAML文件结束语3个 . (点)
...

编写展示脚本show_yaml_python.sh

python3 -c 'import yaml,pprint;pprint.pprint(yaml.load(open("test.yaml").read(), Loader=yaml.FullLoader))'

测试:

ansible@ubuntu-c:~/demo02$ chmod +x show_yaml_python.sh 
ansible@ubuntu-c:~/demo02$ ./show_yaml_python.sh
{'double_quotes': 'this is a string example',
'escape_double_quotes': 'this is a string example\n',
'escape_no_quotes': 'this is a string example\\n',
'escape_single_quotes': 'this is a string example\\n',
'example_dictionary_1': [{'example_dictioanry_2': [1, 2, 3]},
{'example_dictionary_3': [4, 5, 6]},
{'example_dictioanry_4': [7, 8, 9]}],
'example_integer_1': 1,
'example_integer_2': '1',
'example_key_1': 'this is a string',
'example_key_11': {'example_key_12': 'sub_example_value2'},
'example_key_13': ['item_1', 'item_2', 'item_3'],
'example_key_2': 'this is another string',
'example_key_4': ['item 1', 'item 2', 'item 3', 'item 4', 'item 5'],
'example_key_5': 'example_key_5',
'example_key_6': 'example_key_6',
'example_key_9': {'example_key_10': 'sub_example_value1'},
'exmaple_key_14': ['item_4', 'item_5', 'item_6'],
'is_false_01': False,
'is_false_02': False,
'is_false_03': False,
'is_false_04': False,
'is_false_05': False,
'is_false_06': False,
'is_false_07': False,
'is_false_08': False,
'is_false_09': False,
'is_false_10': 'n',
'is_true_01': True,
'is_true_02': True,
'is_true_03': True,
'is_true_04': True,
'is_true_05': True,
'is_true_06': True,
'is_true_07': True,
'is_true_08': True,
'is_true_09': True,
'is_true_10': 'y',
'multilines_example_key_1': 'this is a string\n'
'that goes over\n'
'multiple lines\n',
'multilines_example_key_2': 'this is a string that goes over multiple lines\n',
'multilines_example_key_3': 'this is a string that goes over multiple lines',
'no_quotes': 'this is a string example',
'single_quotes': 'this is a string example'}

YAML学习资源

4.2 初识剧本

先通过例子来熟悉下Ansible Playbook剧本。

# 新建文件夹并进入
ansible@ubuntu-c:~$ mkdir demo03 && cd demo03
# 创建配置文件和清单文件
ansible@ubuntu-c:~/demo03$ touch ansible.cfg hosts motd_playbook.yaml

配置文件 ansible.cfg:

[defaults]
inventory = hosts
host_key_checking = False

清单文件 hosts:

[control]
ubuntu-c ansible_connection=local

[centos]
centos1 ansible_port=2222
centos[2:3]

[centos:vars]
ansible_user=root

[ubuntu]
ubuntu[1:3]

[ubuntu:vars]
ansible_become=true
ansible_become_pass=password

[linux:children]
centos
ubuntu

剧本文件motd_playbook.yaml:

# YAML文件开始于3个 - (短横线)
---

# YAML中的减号表示列表项,剧本(playbook)包含戏剧(play)列表,每个戏剧都是字典数据类型

-
# Hosts: 戏剧将在哪运行以及其他运行选项配置
hosts: centos
user: root

# Vars: 将在所有目标系统上应用的戏剧变量
vars:
motd: "Welcome to CentOS Linux - Ansible Rocks\n"

# Tasks: 将在戏剧中执行的任务列表,此部分也可用于前置和后置任务
tasks:
- name: Configure a MOTD (message of day)
# 将motd变量的内容复制到远程cento主机的/etc/motd文件
copy:
content: "{{ motd }}"
dest: /etc/motd

# Handlers: 作为任务的通知键处理程序列表

# Roles: 导入戏剧中的角色列表

# YAML文件结束语3个 . (点)
...

测试:

ansible@ubuntu-c:~/demo03$ ansible-playbook  motd_playbook.yaml 

PLAY [centos] *******************************************************************************************************************

TASK [Gathering Facts] **********************************************************************************************************
ok: [centos2]
ok: [centos3]
ok: [centos1]

TASK [Configure a MOTD (message of the day)] ************************************************************************************
changed: [centos1]
changed: [centos2]
changed: [centos3]

PLAY RECAP **********************************************************************************************************************
centos1 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
centos2 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
centos3 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

其中,TASK [Gathering Facts] 是默认执行的任务,通过setup模块收集目标系统的Facts变量。所以,虽然只定义了1个任务,但是每个目标主机都执行了2个任务。

上面定义了变量motd,在执行时也可以通过-e 'motd="Testing the motd playbook\n"'重写

登录任意centos系统,可以看到:

centos1 login: ansible
Password:
Last login: Tue Jun 20 03:15:36 from 172.19.0.3
Welcome to CentOS Linux - Ansible Rocks
ansible@centos1:~$

修改剧本文件,添加任务的通知键及处理程序:

# YAML文件开始于3个 - (短横线)
---

# YAML中的减号表示列表项,剧本(playbook)包含戏剧(play)列表,每个戏剧都是字典数据类型

-
# Hosts: 戏剧将在哪运行以及其他执行选项配置
hosts: centos
user: root
gather_facts: False

# Vars: 将在所有目标系统上应用的戏剧变量
vars:
motd: "Welcome to CentOS Linux - Ansible Rocks\n"

# Tasks: 将在戏剧中执行的任务列表,此部分也可用于前置和后置任务
tasks:
- name: Configure a MOTD (message of day)
# 将motd变量的内容复制到远程cento主机的/etc/motd路径
copy:
content: "{{ motd }}"
dest: /etc/motd
# 设置任务的通知键
notify: MOTD changed

# Handlers: 作为任务的通知键处理程序列表
handlers:
- name: MOTD changed
debug:
msg: The MOTD was changed
# Roles: 导入剧本中的角色列表

# YAML文件结束语3个 . (点)
...

gather_facts —— 默认为True,设置为False时不执行默认的TASK [Gathering Facts] 任务

测试:

ansible@ubuntu-c:~/demo03$ ansible-playbook motd_playbook.yaml

PLAY [centos] *******************************************************************************************************************

TASK [Configure a MOTD (message of day)] ****************************************************************************************
changed: [centos2]
changed: [centos1]
changed: [centos3]

RUNNING HANDLER [MOTD changed] **************************************************************************************************
ok: [centos1] => {
"msg": "The MOTD was changed"
}
ok: [centos2] => {
"msg": "The MOTD was changed"
}
ok: [centos3] => {
"msg": "The MOTD was changed"
}

PLAY RECAP **********************************************************************************************************************
centos1 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
centos2 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
centos3 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

因为MOTD任务更改了,通知键的处理程序 RUNNING HANDLER [MOTD changed] 任务也执行了,调试输出msg: The MOTD was changed,并且 ok=2

利用系统信息的facts变量ansible_distribution,配合when指令来区分不同系统发行版,从而在不同系统上执行不同内容,修改剧本文件:

# YAML文件开始于3个 - (短横线)
---

# YAML中的减号表示列表项,剧本(playbook)包含戏剧(play)列表,每个戏剧都是字典数据类型

-
# Hosts: 戏剧将在哪运行以及其他执行选项配置
hosts: linux

# Vars: 将在所有目标系统上应用的戏剧变量
vars:
motd_centos: "Welcome to CentOS Linux - Ansible Rocks\n"
motd_ubuntu: "Welcome to Ubuntu Linux - Ansible Rocks\n"

# Tasks: 将在戏剧中执行的任务列表,此部分也可用于前置和后置任务
tasks:
- name: Configure a MOTD (message of day)
# 将motd变量的内容复制到远程cento主机的/etc/motd路径
copy:
content: "{{ motd_centos }}"
dest: /etc/motd
# 设置任务的通知键
notify: MOTD changed
when: ansible_distribution == "CentOS"

- name: Configure a MOTD (message of day)
# 将motd变量的内容复制到远程cento主机的/etc/motd路径
copy:
content: "{{ motd_ubuntu }}"
dest: /etc/motd
# 设置任务的通知键
notify: MOTD changed
when: ansible_distribution == "Ubuntu"

# Handlers: 作为任务的通知键处理程序列表
handlers:
- name: MOTD changed
debug:
msg: The MOTD was changed
# Roles: 导入剧本中的角色列表

# YAML文件结束语3个 . (点)
...

测试:

ansible@ubuntu-c:~/demo03$ ansible-playbook motd_playbook.yaml 

PLAY [linux] ********************************************************************************************************************

TASK [Gathering Facts] **********************************************************************************************************
ok: [centos3]
ok: [centos1]
ok: [centos2]
ok: [ubuntu1]
ok: [ubuntu2]
ok: [ubuntu3]

TASK [Configure a MOTD (message of day)] ****************************************************************************************
skipping: [ubuntu1]
skipping: [ubuntu2]
skipping: [ubuntu3]
ok: [centos3]
ok: [centos1]
ok: [centos2]

TASK [Configure a MOTD (message of day)] ****************************************************************************************
skipping: [centos1]
skipping: [centos2]
skipping: [centos3]
changed: [ubuntu1]
changed: [ubuntu3]
changed: [ubuntu2]

RUNNING HANDLER [MOTD changed] **************************************************************************************************
ok: [ubuntu1] => {
"msg": "The MOTD was changed"
}
ok: [ubuntu2] => {
"msg": "The MOTD was changed"
}
ok: [ubuntu3] => {
"msg": "The MOTD was changed"
}

PLAY RECAP **********************************************************************************************************************
centos1 : ok=2 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
centos2 : ok=2 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
centos3 : ok=2 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
ubuntu1 : ok=3 changed=1 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
ubuntu2 : ok=3 changed=1 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
ubuntu3 : ok=3 changed=1 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0

点击查看更多的剧本关键字:http://docs.ansible.com/ansible/devel/playbooks_keywords.html

4.3 剧本变量

新建变量文件external_vars.yaml

---
external_example_key: example value

external_dict:
dict_key: This is a dictionary value

external_inline_dict:
{inline_dict_key: This is an inline dictionary value}

external_named_list:
- item1
- item2
- item3
- item4

external_inline_named_list:
[ item1, item2, item3, item4 ]
...

新建剧本variables_playbook.yaml

---

-

hosts: centos1
gather_facts: False

# 通过文件引入变量
vars_files:
- external_vars.yaml

tasks:
- name: Test external dictionary key value
debug:
msg: "{{ external_example_key }}"

- name: Test external named dictionary dictionary
debug:
msg: "{{ external_dict }}"

- name: Test external named dictionary dictionary key value with dictionary dot notation
debug:
msg: "{{ external_dict.dict_key }}"

- name: Test external named dictionary dictionary key value with brackets notation
debug:
msg: "{{ external_dict['dict_key'] }}"

- name: Test external named inline dictionary dictionary
debug:
msg: "{{ external_inline_dict }}"

- name: Test external named inline dictionary dictionary key value with dictionary dot notation
debug:
msg: "{{ external_inline_dict.inline_dict_key }}"

- name: Test external named inline dictionary dictionary key value with brackets notation
debug:
msg: "{{ external_inline_dict['inline_dict_key'] }}"

- name: Test external named list
debug:
msg: "{{ external_named_list }}"

- name: Test external named list first item dot notation
debug:
msg: "{{ external_named_list.0 }}"

- name: Test external named list first item brackets notation
debug:
msg: "{{ external_named_list[0] }}"

- name: Test external inline named list
debug:
msg: "{{ external_inline_named_list }}"

- name: Test external inline named list first item dot notation
debug:
msg: "{{ external_inline_named_list.0 }}"

- name: Test external inline named list first item brackets notation
debug:
msg: "{{ external_inline_named_list[0] }}"

...

vars_files —— 通过文件引入变量,如:

vars_files: 
- external_vars.yaml

vars_prompt —— 提示用户输入变量值,如:

vars_promt:
- name: username
private: False
- name: password
private: True

hostvars[ansible_hostname]['ansible_port'] | default('22') —— 获取主机的变量,如:

tasks:
- name: Test hostvars with an ansible fact and collect ansible_port, dict notation
debug:
msg: "{{ hostvars[ansible_hostname]['ansible_port'] | default('22') }}"

ansible-playbook通过-e传入变量值时有5种方式:

# 1.通过ini格式传入
ansible-playbook variable_playbook.yaml -e extra_vars_key="extra vars value"

# 2.通过json格式传入
ansible-playbook variable_playbook.yaml -e {"extra_vars_key": "extra vars value"}

# 3.通过yaml格式传入
ansible-playbook variable_playbook.yaml -e {extra_vars_key: extra vars value}

# 4.通过yaml文件传入
ansible-playbook variable_playbook.yaml -e @extra_vars_file.yaml

# 5.通过json文件传入
ansible-playbook variable_playbook.yaml -e @extra_vars_file.json

另外,主机变量和组变量可以分布使用单独的yaml文件host_vars/centos1、host_vars/ubuntu-c和group_vars/centos、group_vars/ubuntu来保存和区分,而不是写在一个host文件里

hosts文件内容变为:

[control]
ubuntu-c

[centos]
centos[1:3]

[ubuntu]
ubuntu[1:3]

[linux:children]
centos
ubuntu

新建的host_vars/centos1内容为:

---
ansible_port: 2222
...

新建的host_vars/ubuntu-c内容为:

---
ansible_connection: local
...

新建的group_vars/centos内容为:

---
ansible_user: root
...

新建的group_vars/ubuntu内容为:

---
ansible_become: true
ansible_become_pass: password
...

4.4 使用和自定义Facts变量

**Ansible Facts 是 Ansible 在被托管主机上自动收集的变量。**它是通过在执行 ad-hoc 以及 Playbook 时使用 setup 模块进行收集的,并且这个操作是默认的。

setup模块官方文档地址:https://docs.ansible.com/ansible/latest/collections/ansible/builtin/setup_module.html

ad-hoc使用setup模块示例:

# 显示来自centos1主机所有关于network子集的facts变量
ansible@ubuntu-c:~/demo03$ ansible centos1 -m setup -a 'gather_subset=network'

# 显示来自centos1主机特定network子集的facts变量
ansible@ubuntu-c:~/demo03$ ansible centos1 -m setup -a 'gather_subset=!all,!min,network'

# 使用过滤器选项显示来自centos1主机facts变量 - ansible_memfree_mb
ansible@ubuntu-c:~/demo03$ ansible centos1 -m setup -a 'filter=ansible_memfree_mb'

# 使用过滤器选项+通配符显示来自centos1主机facts变量 - 包含ansible_mem 字段的变量
ansible@ubuntu-c:~/demo03$ ansible centos1 -m setup -a 'filter=ansible_mem*'

ad-hoc命令模式中,setup模块收集的返回数据是字典结构,其中键为ansible_facts包含着收集到的facts变量,如:

ansible@ubuntu-c:~/demo03$ ansible centos1 -m setup -a 'filter=ansible_mem*'
centos1 | SUCCESS => {
"ansible_facts": {
"ansible_memfree_mb": 10619,
"ansible_memory_mb": {
"nocache": {
"free": 13976,
"used": 1882
},
"real": {
"free": 10619,
"total": 15858,
"used": 5239
},
"swap": {
"cached": 0,
"free": 4096,
"total": 4096,
"used": 0
}
},
"ansible_memtotal_mb": 15858,
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"changed": false
}

playbook剧本模式中,setup模块收集的返回数据是字典结构,没有ansible_facts键,可以直接用facts变量,如:

ansible@ubuntu-c:~$ mkdir demo04 && cd demo04
ansible@ubuntu-c:~/demo04$ cp ../demo03/hosts .
ansible@ubuntu-c:~/demo04$ cp ../demo03/ansible.cfg .
ansible@ubuntu-c:~/demo04$ cat facts_playbook.yaml
---

-

hosts: all

tasks:
- name: Show Ip Address
debug:
msg: "{{ ansible_default_ipv4.address }}"

...
ansible@ubuntu-c:~/demo04$ ansible-playbook facts_playbook.yaml

PLAY [all] **********************************************************************************************************************

TASK [Gathering Facts] **********************************************************************************************************
ok: [ubuntu-c]
ok: [centos2]
ok: [centos1]
ok: [centos3]
ok: [ubuntu1]
ok: [ubuntu2]
ok: [ubuntu3]

TASK [Show Ip Address] **********************************************************************************************************
ok: [ubuntu-c] => {
"msg": "172.19.0.2"
}
ok: [centos1] => {
"msg": "172.19.0.7"
}
ok: [centos2] => {
"msg": "172.19.0.5"
}
ok: [centos3] => {
"msg": "172.19.0.8"
}
ok: [ubuntu1] => {
"msg": "172.19.0.9"
}
ok: [ubuntu2] => {
"msg": "172.19.0.4"
}
ok: [ubuntu3] => {
"msg": "172.19.0.3"
}

PLAY RECAP **********************************************************************************************************************
centos1 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
centos2 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
centos3 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
ubuntu-c : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
ubuntu1 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
ubuntu2 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
ubuntu3 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

创造自定义facts变量

  • 能够使用任何语言编写
  • 返回JSON结构或ini结构
  • 默认放置在/etc/ansible/facts.d

创建一个自定义facts变量用于收集系统日期信息:

# 自定义facts变量(返回JSON结构)
ansible@ubuntu-c:~/demo04$ cat getdate1.fact
#!/bin/bash
echo {\""date\"": \""`date`""}
ansible@ubuntu-c:~/demo04$ chmod +x getdate1.fact
ansible@ubuntu-c:~/demo04$ ./getdate1.fact
{"date" : "Mon Jun 26 09:45:11 UTC 2023"}

# 自定义facts变量(返回ini结构)
ansible@ubuntu-c:~/demo04$ cat getdate2.fact
#!/bin/bash
echo [date]
echo date=`date`
ansible@ubuntu-c:~/demo04$ chmod +x getdate2.fact
ansible@ubuntu-c:~/demo04$ ./getdate2.fact
[date]
date=Mon Jun 26 09:47:31 UTC 2023

ansible@ubuntu-c:~/demo04$ sudo mkdir -p /etc/ansible/facts.d
ansible@ubuntu-c:~/demo04$ sudo cp getdate* /etc/ansible/facts.d/
ansible@ubuntu-c:~/demo04$ ansible ubuntu-c -m setup -a 'filter=ansible_local'
ubuntu-c | SUCCESS => {
"ansible_facts": {
"ansible_local": {
"getdate1": {
"date": "Mon Jun 26 09:53:15 UTC 2023"
},
"getdate2": {
"date": {
"date": "Mon Jun 26 09:53:15 UTC 2023"
}
}
},
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": false
}

修改剧本facts_playbook.yaml,使用自定义的facts变量

ansible@ubuntu-c:~/demo04$ cat facts_playbook.yaml 
---

-

hosts: all

tasks:
- name: Show Ip Address
debug:
msg: "{{ ansible_default_ipv4.address }}"

- name: Show Custom Fact 1
debug:
msg: "{{ ansible_local.getdate1.date }}"

- name: Show Custom Fact 2
debug:
msg: "{{ ansible_local.getdate2.date.date }}"

...
ansible@ubuntu-c:~/demo04$ ansible-playbook facts_playbook.yaml -l ubuntu-c

PLAY [all] **********************************************************************************************************************

TASK [Gathering Facts] **********************************************************************************************************
ok: [ubuntu-c]

TASK [Show Ip Address] **********************************************************************************************************
ok: [ubuntu-c] => {
"msg": "172.19.0.2"
}

TASK [Show Custom Fact 1] *******************************************************************************************************
ok: [ubuntu-c] => {
"msg": "Mon Jun 26 09:58:49 UTC 2023"
}

TASK [Show Custom Fact 2] *******************************************************************************************************
ok: [ubuntu-c] => {
"msg": "Mon Jun 26 09:58:49 UTC 2023"
}

PLAY RECAP **********************************************************************************************************************
ubuntu-c : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

目前自定义的facts变量只在ubuntu-c管理主机上生效,要想其他被管理主机也能使用自定义的facts变量,则可将自定义facts变量的文件拷贝到各自主机的/etc/ansible/facts.d目录下。

修改剧本facts_playbook.yaml:

---

-

hosts: linux

tasks:
- name: Make Facts Dir
file:
path: /etc/ansible/facts.d
recurse: yes
state: directory

- name: Copy Fact 1
copy:
src: /etc/ansible/facts.d/getdate1.fact
dest: /etc/ansible/facts.d/getdate1.fact
mode: 0755

- name: Copy Fact 2
copy:
src: /etc/ansible/facts.d/getdate2.fact
dest: /etc/ansible/facts.d/getdate2.fact
mode: 0755

- name: Refresh Facts
setup:

- name: Show Ip Address
debug:
msg: "{{ ansible_default_ipv4.address }}"

- name: Show Custom Fact 1
debug:
msg: "{{ ansible_local.getdate1.date }}"

- name: Show Custom Fact 2
debug:
msg: "{{ ansible_local.getdate2.date.date }}"
- name: Show Custom Fact 1 in hostvars
debug:
msg: "{{ hostvars[ansible_hostname].ansible_local.getdate1.date }}"
- name: Show Custom Fact 2 in hostvars
debug:
msg: "{{ hostvars[ansible_hostname].ansible_local.getdate2.date.date }}"

...

默认情况下,Ansible期望自定义facts变量文件位于需要root访问权限的位置/etc/ansible/facts.d,如何在没有root访问权限的环境中使用自定义facts变量。

# 首先删除被管理主机上的自定义facts变量文件
ansible@ubuntu-c:~/demo04$ ansible linux -m file -a 'path=/etc/ansible/facts.d/getdate1.fact state=absent'
ansible@ubuntu-c:~/demo04$ ansible linux -m file -a 'path=/etc/ansible/facts.d/getdate2.fact state=absent'

# 然后将控制主机上的自定义facts变量文件移动
ansible@ubuntu-c:~/demo04$ ls
ansible.cfg facts_playbook.yaml getdate1.fact getdate2.fact hosts
ansible@ubuntu-c:~/demo04$ mkdir facts.d
ansible@ubuntu-c:~/demo04$ mv getdate* facts.d
ansible@ubuntu-c:~/demo04$ ls
ansible.cfg facts.d facts_playbook.yaml hosts

修改剧本facts_playbook.yaml,使用fact_path制定自定义facts变量文件位置:

---

-

hosts: linux

tasks:
- name: Make Facts Dir
file:
path: /home/ansible/facts.d
recurse: yes
state: directory
owner: ansible

- name: Copy Fact 1
copy:
src: facts.d/getdate1.fact
dest: /home/ansible/facts.d/getdate1.fact
owner: ansible
mode: 0755

- name: Copy Fact 2
copy:
src: /facts.d/getdate2.fact
dest: /home/ansible/facts.d/getdate2.fact
owner: ansible
mode: 0755

- name: Refresh Facts
setup:
fact_path: /home/ansible/facts.d

- name: Show Ip Address
debug:
msg: "{{ ansible_default_ipv4.address }}"

- name: Show Custom Fact 1
debug:
msg: "{{ ansible_local.getdate1.date }}"

- name: Show Custom Fact 2
debug:
msg: "{{ ansible_local.getdate2.date.date }}"
- name: Show Custom Fact 1 in hostvars
debug:
msg: "{{ hostvars[ansible_hostname].ansible_local.getdate1.date }}"
- name: Show Custom Fact 2 in hostvars
debug:
msg: "{{ hostvars[ansible_hostname].ansible_local.getdate2.date.date }}"

...

测试:

ansible@ubuntu-c:~/demo04$ ansible-playbook facts_playbook.yaml 

PLAY [linux] ********************************************************************************************************************

TASK [Gathering Facts] **********************************************************************************************************
ok: [centos3]
ok: [centos1]
ok: [centos2]
ok: [ubuntu2]
ok: [ubuntu1]
ok: [ubuntu3]

TASK [Make Facts Dir] ***********************************************************************************************************
changed: [centos1]
changed: [centos3]
changed: [centos2]
changed: [ubuntu1]
changed: [ubuntu2]
changed: [ubuntu3]

TASK [Copy Fact 1] **************************************************************************************************************
changed: [ubuntu1]
changed: [centos3]
changed: [ubuntu2]
changed: [centos1]
changed: [centos2]
changed: [ubuntu3]

TASK [Copy Fact 2] **************************************************************************************************************
changed: [centos1]
changed: [centos2]
changed: [ubuntu1]
changed: [ubuntu2]
changed: [centos3]
changed: [ubuntu3]

TASK [Refresh Facts] ************************************************************************************************************
ok: [centos1]
ok: [centos3]
ok: [centos2]
ok: [ubuntu1]
ok: [ubuntu2]
ok: [ubuntu3]

TASK [Show Ip Address] **********************************************************************************************************
ok: [centos1] => {
"msg": "172.19.0.7"
}
ok: [centos2] => {
"msg": "172.19.0.5"
}
ok: [centos3] => {
"msg": "172.19.0.8"
}
ok: [ubuntu1] => {
"msg": "172.19.0.9"
}
ok: [ubuntu2] => {
"msg": "172.19.0.4"
}
ok: [ubuntu3] => {
"msg": "172.19.0.3"
}

TASK [Show Custom Fact 1] *******************************************************************************************************
ok: [centos1] => {
"msg": "Mon Jun 26 10:23:47 UTC 2023"
}
ok: [centos2] => {
"msg": "Mon Jun 26 10:23:47 UTC 2023"
}
ok: [centos3] => {
"msg": "Mon Jun 26 10:23:47 UTC 2023"
}
ok: [ubuntu1] => {
"msg": "Mon Jun 26 10:23:47 UTC 2023"
}
ok: [ubuntu2] => {
"msg": "Mon Jun 26 10:23:47 UTC 2023"
}
ok: [ubuntu3] => {
"msg": "Mon Jun 26 10:23:48 UTC 2023"
}

TASK [Show Custom Fact 2] *******************************************************************************************************
ok: [centos1] => {
"msg": "Mon Jun 26 10:23:47 UTC 2023"
}
ok: [centos2] => {
"msg": "Mon Jun 26 10:23:47 UTC 2023"
}
ok: [centos3] => {
"msg": "Mon Jun 26 10:23:47 UTC 2023"
}
ok: [ubuntu1] => {
"msg": "Mon Jun 26 10:23:47 UTC 2023"
}
ok: [ubuntu2] => {
"msg": "Mon Jun 26 10:23:47 UTC 2023"
}
ok: [ubuntu3] => {
"msg": "Mon Jun 26 10:23:48 UTC 2023"
}

TASK [Show Custom Fact 1 in hostvars] *******************************************************************************************
ok: [centos1] => {
"msg": "Mon Jun 26 10:23:47 UTC 2023"
}
ok: [centos2] => {
"msg": "Mon Jun 26 10:23:47 UTC 2023"
}
ok: [centos3] => {
"msg": "Mon Jun 26 10:23:47 UTC 2023"
}
ok: [ubuntu1] => {
"msg": "Mon Jun 26 10:23:47 UTC 2023"
}
ok: [ubuntu2] => {
"msg": "Mon Jun 26 10:23:47 UTC 2023"
}
ok: [ubuntu3] => {
"msg": "Mon Jun 26 10:23:48 UTC 2023"
}

TASK [Show Custom Fact2 in hostvars] ********************************************************************************************
ok: [centos1] => {
"msg": "Mon Jun 26 10:23:47 UTC 2023"
}
ok: [centos2] => {
"msg": "Mon Jun 26 10:23:47 UTC 2023"
}
ok: [centos3] => {
"msg": "Mon Jun 26 10:23:47 UTC 2023"
}
ok: [ubuntu1] => {
"msg": "Mon Jun 26 10:23:47 UTC 2023"
}
ok: [ubuntu2] => {
"msg": "Mon Jun 26 10:23:47 UTC 2023"
}
ok: [ubuntu3] => {
"msg": "Mon Jun 26 10:23:48 UTC 2023"
}

PLAY RECAP **********************************************************************************************************************
centos1 : ok=10 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
centos2 : ok=10 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
centos3 : ok=10 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
ubuntu1 : ok=10 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
ubuntu2 : ok=10 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
ubuntu3 : ok=10 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

# 测试完毕后删除自定义的facts变量
ansible@ubuntu-c:~/demo04$ ansible linux -m file -a 'path=/home/ansible/facts.d state=absent'
centos1 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"changed": true,
"path": "/home/ansible/facts.d",
"state": "absent"
}
centos3 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"changed": true,
"path": "/home/ansible/facts.d",
"state": "absent"
}
centos2 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"changed": true,
"path": "/home/ansible/facts.d",
"state": "absent"
}
ubuntu1 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": true,
"path": "/home/ansible/facts.d",
"state": "absent"
}
ubuntu2 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": true,
"path": "/home/ansible/facts.d",
"state": "absent"
}
ubuntu3 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": true,
"path": "/home/ansible/facts.d",
"state": "absent"
}

4.5 剧本使用Jinja2模板语言

ansible@ubuntu-c:~$ mkdir demo05 && cd demo05
ansible@ubuntu-c:~/demo05$ cp ../demo04/ansible.cfg .
ansible@ubuntu-c:~/demo05$ cp ../demo04/hosts .
ansible@ubuntu-c:~/demo05$ touch jinja2_playbook.yaml
ansible@ubuntu-c:~/demo05$ echo jinja2_extensions = jinja2.ext.loopcontrols >> ansible.cfg

jinja2_playbook.yaml文件内容如下:

---

-
hosts: all
tasks:
- name: Ansible Jinja2 if elif else statement
debug:
msg: >
--== Ansible Jinja2 if elif else statement ==--

{# If the hostname is ubuntu-c, include a message -#}
{% if ansible_hostname == "ubuntu-c" -%}
This is ubuntu-c
{% elif ansible_hostname == "centos1" -%}
This is centos1 with it's modified SSH Port
{% else -%}
This is good old {{ ansible_hostname }}
{% endif %}

- name: Ansible Jinja2 if variable is defined ( where variable is defined )
debug:
msg: >
--== Ansible Jinja2 if variable is defined ( where variable is defined ) ==--

{% set example_variable = 'defined' -%}
{% if example_variable is defined -%}
example_variable is defined
{% else -%}
example_variable is not defiend
{% endif %}

- name: Ansible Jinja2 for statement
debug:
msg: >
--== Ansible Jinja2 for statement ==--

{% for entry in ansible_interfaces -%}
Interface entry {{ loop.index }} = {{ entry }}
{% endfor %}


- name: Ansible Jinja2 for range
debug:
msg: >
--== Ansible Jinja2 for range ==--

{% for entry in range(1, 11) -%}
{{ entry }}
{% endfor %}

- name: Ansible Jinja2 for range, reversed (simulate while greater 5)
debug:
msg: >
--== Ansible Jinja2 for range, reversed (simulate while greater 5) ==--

{% for entry in range(10, 0, -1) -%}
{% if entry == 5 -%}
{% break %}
{% endif -%}
{{ entry }}
{% endfor %}

- name: Ansible Jinja2 for range, reversed(continue if odd)
debug:
msg: >
--== Ansible Jinja2 for range, reversed (continue if odd)
{% for entry in range(10, 0, -1) -%}
{% if entry is odd -%}
{% continue %}
{% endif -%}
{{ entry }}
{% endfor %}

- name: Ansible Jinja2 filters
debug:
msg: >
--=== Ansible Jinja2 filters ===--

--== min [1, 2, 3, 4, 5] ==--
{{ [1, 2, 3, 4, 5] | min }}

--== max [1, 2, 3, 4, 5] ==--
{{ [1, 2, 3, 4, 5] | max }}

--== unique [1, 1, 2, 2, 3, 3, 4, 4, 5, 5] ==--
{{ [1, 1, 2, 2, 3, 3, 4, 4, 5, 5] | unique }}

--== difference [1, 2, 3, 4, 5] vs [2, 3, 4] ==--
{{ [1, 2, 3, 4, 5] | difference([2, 3, 4]) }}

--== random ['rod', 'jane', 'freddy'] ==--
{{ ['rod', 'jane', 'freddy'] | random }}

--== urlsplit hostname ==--
{{ "http://docs.ansible.com/ansible/latest/playbook_filters.html" | urlsplit('hostname') }}

...

剧本过滤器官方文档地址:https://docs.ansible.com/ansible/latest/user_guide/playbooks_filters.html

jinjia2相关内容可以写成单独的文件,然后在Ansible通过template模块引入。如:

新建template.j2文件,内容如下:

--== Ansible Jinja2 if statement ==--

{# If the hostname is ubuntu-c, include a message -#}
{% if ansible_hostname == "ubuntu-c" -%}
This is ubuntu-c
{% endif %}

--== Ansible Jinja2 if elif statement ==--

{% if ansible_hostname == "ubuntu-c" -%}
This is ubuntu-c
{% elif ansible_hostname == "centos1" -%}
This is centos1 with it's modified SSH Port
{% endif %}

--== Ansible Jinja2 if elif else statement ==--

{% if ansible_hostname == "ubuntu-c" -%}
This is ubuntu-c
{% elif ansible_hostname == "centos1" -%}
This is centos1 with it's modified SSH Port
{% else -%}
This is good old {{ ansible_hostname }}
{% endif %}

--== Ansible Jinja2 if variable is defined ( where variable is not defined ) ==--

{% if example_variable is defined -%}
example_variable is defined
{% else -%}
example_variable is not defined
{% endif %}

--== Ansible Jinja2 if varible is defined ( where variable is defined ) ==--

{% set example_variable = 'defined' -%}
{% if example_variable is defined -%}
example_variable is defined
{% else -%}
example_variable is not defined
{% endif %}

--== Ansible Jinja2 for statement ==--

{% for entry in ansible_all_ipv4_addresses -%}
IP Address entry {{ loop.index }} = {{ entry }}
{% endfor %}

--== Ansible Jinja2 for range

{% for entry in range(1, 11) -%}
{{ entry }}
{% endfor %}

--== Ansible Jinja2 for range, reversed (simulate while greater 5) ==--

{% for entry in range(10, 0, -1) -%}
{% if entry == 5 -%}
{% break %}
{% endif -%}
{{ entry }}
{% endfor %}

--== Ansible Jinja2 for range, reversed (continue if odd) ==--

{% for entry in range(10, 0, -1) -%}
{% if entry is odd -%}
{% continue %}
{% endif -%}
{{ entry }}
{% endfor %}

---=== Ansible Jinja2 filters ===---

--== min [1, 2, 3, 4, 5] ==--

{{ [1, 2, 3, 4, 5] | min }}

--== max [1, 2, 3, 4, 5] ==--

{{ [1, 2, 3, 4, 5] | max }}

--== unique [1, 1, 2, 2, 3, 3, 4, 4, 5, 5] ==--

{{ [1, 1, 2, 2, 3, 3, 4, 4, 5, 5] | unique }}

--== difference [1, 2, 3, 4, 5] vs [2, 3, 4] ==--

{{ [1, 2, 3, 4, 5] | difference([2, 3, 4]) }}

--== random ['rod', 'jane', 'freddy'] ==--

{{ ['rod', 'jane', 'freddy'] | random }}

--== urlsplit hostname ==--

{{ "http://docs.ansible.com/ansible/latest/playbooks_filters.html" | urlsplit('hostname') }}

修改jinja2_playbook.yaml:

---

-
hosts: all

tasks:
- name: Jinja2 template
template:
src: template.j2
dest: "/tmp/{{ ansible_hostname }}_template.out"
trim_blocks: true
mode: 0644
...

4.6 剧本实际应用

在不同的linux系统上安装nginx,并配置index.html,引用变量

  • 配置hosts指向目标linux组
  • 创建任务 Install EPEL:CentOS使用yum安装epel-release
  • 分别创建任务 Install Nginx CentOS、Install Nginx Ubuntu:CentOS使用yum安装nginx,Ubuntu使用apt安装包安装nginx
  • 删除任务 Install Nginx CentOS、Install Nginx Ubuntu,新增任务 Install Nginx:改为使用package模块安装nginx,不需要区分CentOS和Ubuntu
  • 创建任务 Restart Nginx:使用service模块重启目标linux系统的nginx服务器
  • 创建处理程序 Check HTTP Service:使用uri模块测试HTTP服务
  • 修改任务 Restart Nginx:添加notify指向Check HTTP Service处理程序
  • 创建组变量 nginx_root_location:CentOS组的nginx_root_location=/usr/share/nginx/html、Ubuntu组的nginx_root_location=/var/www/html
  • 创建任务 Template index.html-base.j2 to index.html on target: 使用template模块,将自定义的index.html-base.j2 Jinja2模板指向nginx的/index.html
  • 修改ansible.cfg:echo ‘ansible_managed = Managed by Ansible - file: {file} - host: {host} - uid:{uid}’ >> ansible.cfg
  • 更新任务 Template index.html-base.j2 to index.html on target: 改为Template index.html-ansible_managed.j2 to index.html on target,更新自定义的模板文件为index.html-ansible_managed.j2
  • 更新剧本,引入变量文件vars/logos.yaml
  • 更新任务 Template index.html-ansible_managed.j2 to index.html on target:改为 Template index.html-logos.j2 to index.html on target,更新自定义的模板文件为index.html-logos.j2
  • 使用package模块安装unzip包,创建任务 Unarchive playbook stacker game,使用unarchive模块解压缩
  • 更新任务 Template index.html-ansible_managed.j2 to index.html on target:改为 Template index.html-logos.j2 to index.html on target,更新自定义的模板文件为index.html-logos.j2
  • 更新任务 Template index.html-logos.j2 to index.html on target,改为 Template index.html-easter_egg.j2 to index.html on target,更新自定义的模板文件为index.html-easter_egg.j2

最终,nginx_playbook.yaml内容为:

---

-

hosts: linux

vars_files:
- vars/logos.yaml

tasks:
- name: Install EPEL
yum:
name: epel-release
update_cache: yes
state: latest
when: ansible_distribution == 'CentOS'

- name: Install Nginx
package:
name: nginx
state: latest

- name: Restart nginx
service:
name: nginx
state: restarted
notify: Check HTTP Service

- name: Template index.html-easter_egg.j2 to index.html on target
template:
src: index.html-easter_egg.j2
dest: "{{ nginx_root_location }}/index.html"
mode: 0644

- name: Install unzip
package:
name: unzip
state: latest

- name: Unarchive playbook stacker game
unarchive:
src: playbook_stacker.zip
dest: "{{ nginx_root_location }}"
mode: 0755

handlers:
- name: Check HTTP Service
uri:
url: http://{{ ansible_default_ipv4.address }}
status_code: 200
...

详细资源可以参见Ansible课程代码

5. 深入Ansible Playbooks

剧本 Module模块,动态Inventory清单,Register,When和Loops循环的使用,异步、串行、并行的性能,任务委派,Ansible魔法变量,Ansible Blocks块,Ansible Vault信息保护

5.1 剧本常用模块

Ansible内置了数以千计的模块,涵盖了众多领域和技术方向,接下来将介绍一些剧本常用模块

set_fact模块

作用:允许执行期间动态添加或改变facts变量

官方文档地址:https://docs.ansible.com/ansible/latest/collections/ansible/builtin/set_fact_module.html

示例:

---

-
hosts: ubuntu3,centos3

tasks:
- name: Set a fact
set_fact:
our_fact: Ansible Rocks!
ansible_distribution: "{{ ansible_distribution | upper }}"

- name: Set our installation variables for CentOS
set_fact:
webserver_application_port: 80
webserver_application_path: /usr/share/nginx/html
webserver_application_user: root
when: ansible_distribution == 'CENTOS'

- name: Set our installation variables for Ubuntu
set_fact:
webserver_application_port: 8080
webserver_application_path: /var/www/html
webserver_application_user: nginx
when: ansible_distribution == 'UBUNTU'

- name: Show our_fact
debug:
msg: "{{ our_fact }}"

- name: Show ansible_distribution
debug:
msg: "{{ ansible_distribution }}"

- name: Show pre-set distribution based facts
debug:
msg: "webserver_application_port: {{ webserver_application_port }} webserver_application_path: {{ webserver_application_path }} webserver_application_user: {{ webserver_application_user }}"

...

pause模块

作用:允许暂停给定时间的剧本执行,或者暂停直到确认特定提示

官方文档地址:https://docs.ansible.com/ansible/latest/collections/ansible/builtin/pause_module.html

示例:

---

-
hosts: ubuntu3,centos3

tasks:
- name: Pause our playbook for 10 seconds
pause:
seconds: 10

- name: Prompt user to verify before continue
pause:
prompt: Please check that the webserver is running, press enter to continue

- name: Wait for the webserver to be running on port 80
wait_for:
port: 80
...

wait_for模块

作用:允许等待指定的时间后、等待指定的端口可用时、等待指定模块启动并准备好时、等待正则匹配文件中字符串存在时,继续执行剧本

官方文档地址:https://docs.ansible.com/ansible/latest/collections/ansible/builtin/wait_for_module.html

示例:

---

-
hosts: ubuntu3,centos3

tasks:
- name: Wait for the webserver to be running on port 80
wait_for:
port: 80
...

assemble模块

作用:允许将文件的集合组装成一个文件,可将获取本地或被管理主机的文件目录,并将他们连接在一起生成目标文件

官方文档地址:https://docs.ansible.com/ansible/latest/collections/ansible/builtin/assemble_module.html

示例:

---

-
hosts: ubuntu-c

tasks:
- name: Assemble conf.d to sshd_config
assemble:
src: conf.d
dest: sshd_config
...

add_host模块

作用:允许动态添加目标主机到正在执行的剧本中,已备后续使用

官方文档地址:https://docs.ansible.com/ansible/latest/collections/ansible/builtin/add_host_module.html

示例:

---

-
hosts: ubuntu-c

tasks:
- name: Add centos1 to adhoc_group
add_host:
name: centos1
groups: adhoc_group1, adhoc_group2

-
hosts: adhoc_group1

tasks:
- name: Ping all in adhoc_group
ping:

...

group_by模块

作用:允许使用facts变量创建临时组,以便稍后在剧本中使用

官方文档地址:https://docs.ansible.com/ansible/latest/collections/ansible/builtin/group_by_module.html

示例:

---

-
host: all

tasks:
- name: Create group based on ansible_distribution
group_by:
key: "custom_{{ ansible_distribution | lower }}"

- hosts: custom_centos

tasks:
- name: Ping all in custom_centos
ping:

...

fetch模块

作用:允许从远程计算机获取文件并将它们存储在本地文件树中,按主机名组织

官方文档地址:https://docs.ansible.com/ansible/latest/collections/ansible/builtin/fetch_module.html

示例:

---

-
hosts: centos

tasks:
- name: Fetch /etc/redhat-release
fetch:
src: /etc/redhat-release
dest: /tmp/redhat-release

...

5.2 动态清单

目前,我们都是在ansible.cfg文件中配置清单hosts文件,然后在hosts文件内或host_vars、group_vars文件夹内指定清单变量,在命令行工具中也可以使用-i选项指定或覆盖之前配置的清单文件,如果指定的清单文件为可执行文件,则Ansible将执行此文件并将结果作为清单。

Ansible可以使用可执行文件作为动态清单,并使用文件执行结果作为清单

动态清单需要满足的条件:

  • 必须是可执行文件,能够从 命令行执行,语言不限
  • 接受 --list--host命令行选项
  • 使用--list选项,返回JSON编码字典,包含清单内容
  • 使用--host选项,返回基本的JSON编码字典,包含主机内容

示例:

ansible.cfg

[defaults]
host_key_checking = False

inventory.py

#!/usr/bin/env python3

'''
Dynamic inventory for Ansible in Python
'''

# Use print functionality from Python 3 for compatibility
from __future__ import print_function

import argparse
import logging

# Attempt to import json, if it fails, import simplejson
try:
import json
except ImportError:
import simplejson as json

# Inherit from object for Python 2/3 compatibility
class Inventory(object):

# Constructor
def __init__(self, include_hostvars_in_list):

# Configure logger
#self.configure_logger()

# Capture and store include_hostvars_in_list
self.include_hostvars_in_list = include_hostvars_in_list

# Capture the script command line arguments
parser = argparse.ArgumentParser()
parser.add_argument('--list', action='store_true',
help='list inventory')
parser.add_argument('--host', action='store',
help='show HOST variables')
self.args = parser.parse_args()

# If not called with --host or --list, show usage and exit
if not (self.args.list or self.args.host):
parser.print_usage()
raise SystemExit

# Capture and store the inventory
self.define_inventory()

# When called with --list, print the inventory
if self.args.list:
self.print_json(self.list())

# If called with --host, print host information
elif self.args.host:
self.print_json(self.host())

def define_inventory(self):
self.groups = {
"centos": {
"hosts": ["centos1", "centos2", "centos3"],
"vars": {
"ansible_user": 'root'
}
},
"control": {
"hosts": ["ubuntu-c"],
},
"ubuntu": {
"hosts": ["ubuntu1", "ubuntu2", "ubuntu3"],
"vars": {
"ansible_become": True,
"ansible_become_pass": 'password'
}
},
"linux": {
"children": ["centos", "ubuntu"],
}}

self.hostvars = {
'centos1': {
'ansible_port': 2222
},
'ubuntu-c': {
'ansible_connection': 'local'
}
}

# Pretty print JSON
def print_json(self, content):
print(json.dumps(content, indent=4, sort_keys=True))

# Return inventory dictionary
def list(self):

self.logger.info('list executed')

# If include_hostvars_in_list is True, merge the hostvars
# as _meta data
if self.include_hostvars_in_list:
merged = self.groups
merged['_meta'] = {}
merged['_meta']['hostvars'] = self.hostvars
return merged

# Otherwise, return the groups without hostvars
else:
return self.groups

# Return host dictionary
def host(self):

self.logger.info('host executed for {}'.format(self.args.host))

# If the requested hosts exists in hostvars, return it
if self.args.host in self.hostvars:
return self.hostvars[self.args.host]

# Otherwise, return an empty list
else:
return {}

# Logger, for debugging as stdout is used by the script
def configure_logger(self):
self.logger = logging.getLogger('ansible_dynamic_inventory')
self.hdlr = logging.FileHandler('/var/tmp/ansible_dynamic_inventory.log')
self.formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
self.hdlr.setFormatter(self.formatter)
self.logger.addHandler(self.hdlr)
self.logger.setLevel(logging.DEBUG)

# Call the Inventory class constructor (__init__)
# Pass include_hostsvars_in_list as True to include hostvars
# as _meta data in list output
Inventory(include_hostvars_in_list=False)

测试:

ansible@ubuntu-c:~/diveintoansible/Ansible Playbooks, Deep Dive/Dynamic Inventories/01$ ansible all -i inventory.py --list-hosts
hosts (7):
ubuntu-c
centos1
centos2
centos3
ubuntu1
ubuntu2
ubuntu3

ansible@ubuntu-c:~/diveintoansible/Ansible Playbooks, Deep Dive/Dynamic Inventories/01$ ansible all -i inventory.py -m ping -o
ubuntu-c | SUCCESS => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python3"},"changed": false,"ping": "pong"}
centos1 | SUCCESS => {"ansible_facts": {"discovered_interpreter_python": "/usr/libexec/platform-python"},"changed": false,"ping": "pong"}
centos2 | SUCCESS => {"ansible_facts": {"discovered_interpreter_python": "/usr/libexec/platform-python"},"changed": false,"ping": "pong"}
centos3 | SUCCESS => {"ansible_facts": {"discovered_interpreter_python": "/usr/libexec/platform-python"},"changed": false,"ping": "pong"}
ubuntu1 | SUCCESS => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python3"},"changed": false,"ping": "pong"}
ubuntu2 | SUCCESS => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python3"},"changed": false,"ping": "pong"}
ubuntu3 | SUCCESS => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python3"},"changed": false,"ping": "pong"}

将脚本inventory.py中的最后一行改为Inventory(include_hostvars_in_list=True),对比发现,使用_meta可提高性能表现

5.3 Register和When

Register

使用Register指令可以存储输出为一个变量,后续可以直接使用:

---

-
hosts: linux

tasks:
- name: Exploring register
command: hostname -c
register: hostname_output

- name: Show hostname_output
debug:
var: hostname_output.stdout

...

register与when结合使用可以针对命令的执行结果进行而额外的处理,从而覆盖多种情况,如:

---

-
hosts: linux

tasks:
- name: Exploring register
command: hostname -s
when:
- ansible_distribution == "CentOS"
- ansible_distribution_major_version | int >= 8
register: command_register

- name: Install patch when changed
yum:
name: patch
state: present
when: command_register is changed

- name: Install patch when skipped
apt:
name: patch
state: present
when: command_register is skipped

...

5.4 循环的使用

用好循环能够使剧本的编写更加简洁美观,提高编写效率。Ansible的剧本可以使用多种方式的循环,下面简单了解下:

with_items循环

遍历列表

---

-
hosts: linux

tasks:
- name: Configure a MOTD (message of the day)
copy:
content: "Welcome to {{ item }} Linux - Ansible Rocks!\n"
dest: /ect/motd
notify: MOTD changed
with_items: [ 'CentOS', 'Ubuntu' ]
when: ansible_distribution == item

- name: Creating user
user:
name: "{{ item }}"
with_items:
- jane
- michael
- tom
- danel

- name: Removing user
user:
name: "{{ item }}"
state: absent
with_items:
- jane
- michael
- tom
- danel

handlers:
- name: MOTD changed
debug:
msg: The MOTD was changed

...

with_dict循环

遍历字典

---

-
hosts: linux

tasks:
- name: Creating user
user:
name: "{{ item.key }}"
comment: "{{ item.value.full_name }}"
with_dict:
jane:
full_name: Jane Smith
michael:
full_name: Michael Smith
tom:
full_name: Tom Smith
danel:
full_name: Danel Smith

- name: Removing user
user:
name: "{{ item.key }}"
comment: "{{ item.value.full_name }}"
state: absent
with_dict:
jane:
full_name: Jane Smith
michael:
full_name: Michael Smith
tom:
full_name: Tom Smith
danel:
full_name: Danel Smith

...

with_subelements循环

组合子元素后,然后遍历

---

-
hosts: linux

tasks:
- name: Creating user
user:
name: "{{ item.1 }}"
comment: "{{ item.1 | title }} {{ item.0.surname }}"
# https://docs.ansible.com/ansible/latest/plugins/lookup/password.html
password: "{{ lookup('password', 'dev/null length=15 chars=ascii_letters,digits,hexdigits,punctuation') | password_hash('sha512') }}"
with_subelements:
- family:
surname: Smith
members:
- jane
- michael
- tom
- danel
- members

- name: Creating user
user:
name: "{{ item.1 }}"
comment: "{{ item.1 | title }} {{ item.0.surname }}"
password: "{{ lookup('password', 'dev/null length=15 chars=ascii_letters,digits,hexdigits,punctuation') | password_hash('sha512') }}"
with_subelements:
-
- surname: Smith
members:
- jane
- michael
- tom
- danel
- surname: James
members:
- Kangkang
- Lihua
- surname: Angne
members:
- Richu
- members

...

with_nested循环

排列组合后,遍历

---

-
hosts: linux

tasks:
- name: Creating user directories
file:
dest: "/home/{{ item.0 }}/{{ item.1 }}"
owner: "{{ item.0 }}"
group: "{{ item.0 }}"
state: directory
with_nested:
- [ jane, michael, tom, danel ]
- [ photos, movies, documents ]

...

with_together循环

一一对应后,遍历

---

-
hosts: linux

tasks:
- name: Creating user directories
file:
dest: "/home/{{ item.0 }}/{{ item.1 }}"
owner: "{{ item.0 }}"
group: "{{ item.0 }}"
state: directory
with_together:
- [ jane, michael, tom, danel ]
- [ photos, movies, documents, music ]

...

with_file循环

遍历读取文件内容

---

-
hosts: linux

tasks:
- name: Create authorized key
authorized_key:
user: jane
key: "{{ item }}"
with_file:
- /home/ansible/.ssh/id_rsa.pub
- custom_key.pub

...

with_sequence循环

遍历序列

---

-
hosts: linux

tasks:
- name: Create sequence directories
file:
dest: "/home/jane/sequence_{{ item }}"
state: directory
with_sequence: start=0 end=100 stride=10

- name: Create sequence directories
file:
dest: "{{ item }}"
state: directory
with_sequence: start=110 end=120 stride=2 format=/home/jane/hex_sequence_%d

- name: Create hex sequence directories
file:
dest: "{{ item }}"
state: directory
with_sequence: start=0 end=16 stride=1 format=/home/jane/hex_sequence_%x

- name: Create hex sequence directories
file:
dest: "{{ item }}"
state: directory
with_sequence: count=5 format=/home/jane/count_sequence_%x

...

with_random_choice循环

遍历选项,随机选择其一

---

-
hosts: linux

tasks:
- name: Create random directory
file:
dest: "/home/jane/{{ item }}"
state: directory
with_random_choice:
- "google"
- "apple"
- "microsoft"
- "tencent"

...

util循环

直到达到条件,循环停止

---

-
hosts: linux

tasks:
- name: Run a script until we hit 10
script: random.sh
register: result
retries: 100
util: result.stdout.find("10") != -1
delay: 1

...
# random.sh
# !/bin/bash
echo $((1 + RANDOM % 10))

5.5 异步、串行和并行

剧本执行也需要关注其执行的性能和速度

先看一个执行效率低下的剧本:

---

-
hosts: linux
gather_facts: False
tasks:
- name: Task 1
command: /bin/sleep 5

- name: Task 2
command: /bin/sleep 5

- name: Task 3
command: /bin/sleep 5

- name: Task 4
command: /bin/sleep 5

- name: Task 5
command: /bin/sleep 5

- name: Task 6
command: /bin/sleep 5

...

ansible的剧本执行默认使用线性策略:执行该任务的所有主机都执行完毕后,才开始进入下一个任务的执行。如果执行该任务的某一个主机因为性能或别的原因,需要很长时间才能执行完该任务,其他所有主机都需要等待。

ansible的剧本支持异步执行策略,有益于需要长执行时间的任务。

异步指的是程序或任务可以并发执行,当前任务不必等待前一个任务的完成。在异步方式下,任务可以提交给其他线程、进程或服务进行处理,而当前任务可以继续执行其他操作。

应用场景:异步通常用于需要提高系统的并发性和响应性能的情况,比如处理大量的并发请求或执行耗时操作。

修改上面剧本为异步剧本:

---

-
hosts: linux
gather_facts: False
tasks:
- name: Task 1
command: /bin/sleep 5
async: 10
poll: 0
register: result1

- name: Task 2
command: /bin/sleep 5
async: 10
poll: 0
register: result2

- name: Task 3
command: /bin/sleep 5
async: 10
poll: 0
register: result3

- name: Task 4
command: /bin/sleep 30
async: 60
poll: 0
register: result4

- name: Task 5
command: /bin/sleep 5
async: 10
poll: 0
register: result5

- name: Task 6
command: /bin/sleep 5
async: 10
poll: 0
register: result6

- name: Capture Job IDs
set_fact:
jobids: >
{% if item.ansible_job_id is defined -%}
{{ jobids + [item.ansible_job_id] }}
{% else -%}
{{ jobids }}
{% endif %}
with_items: "{{ [ result1, result2, result3, result4, result5, result6 ] }}"

- name: Show Job IDs
debug:
var: jobids

- name: 'Wait for Job IDs'
aysnc_status:
jid: "{{ item }}"
with_items: "{{ jobids }}"
register: jobs_result
util: jobs_result.finished
retries: 30

...

设置async参数为10,代表等待至少10秒,设置poll参数为0,代表每秒轮询状态,不等待上个任务所有主机都执行完毕就开始下一个任务,但是最终会等待所有任务执行完成。

上面的方法需要用到很多剧本的知识,还有其他简单的方法设置异步剧本,如执行echo forks=6 >> ansible.cfg,不修改原剧本的情况下就能实现执行的异步。

ansible的剧本支持串行批量执行策略。

串行是一种任务执行方式,指的是任务按照顺序依次执行,每个任务在前一个任务完成后才能开始执行。在串行执行中,任务之间没有并发或并行的特性。

应用场景:串行通常用于必须按照严格的顺序执行任务的情况,比如单线程的程序或依赖关系严格的任务流。

修改原剧本为串行批量执行剧本:

---

-
hosts: linux
gather_facts: False
serial: 2

tasks:
- name: Task 1
command: /bin/sleep 5

- name: Task 2
command: /bin/sleep 5

- name: Task 3
command: /bin/sleep 5

- name: Task 4
command: /bin/sleep 5

- name: Task 5
command: /bin/sleep 5

- name: Task 6
command: /bin/sleep 5

...

serial设置分批次执行,2个执行主机为1批,比如说,centos1和centos2为第1批次,执行任务1,然后执行任务2,。。。,直到执行完成任务6。然后centos3和ubuntu1为第2个批次,执行任务1,然后执行任务2,。。。,直到执行完成任务6。最后ubuntu2和ubuntu3为第3个批次,执行任务1,然后执行任务2,。。。,直到执行完成任务6。

serial也可以指定为数字列表或百分比列表,即每个批次的执行主机数可以不一样。

ansible的剧本支持自由随机执行策略,适合没有执行顺序和主机要求的任务。

修改原剧本为自由随机执行剧本:

---

-
hosts: linux
gather_facts: False
strategy: free

tasks:
- name: Task 1
command: /bin/sleep {{ 10 | random }}

- name: Task 2
command: /bin/sleep {{ 10 | random }}

- name: Task 3
command: /bin/sleep {{ 10 | random }}

- name: Task 4
command: /bin/sleep {{ 10 | random }}

- name: Task 5
command: /bin/sleep {{ 10 | random }}

- name: Task 6
command: /bin/sleep {{ 10 | random }}

...

任务执行的顺序和每次任务执行的主机数目都是随机的。

5.6 任务委派

委派特定的任务在特定的目标上执行,因为存在在管理主机或其他被管理主机运行特定的命令或任务的需求。

如在主机ubuntu-c上设置ubuntu3的tcpwrappers规则,约束SSH连接只有来自ubuntu-c、centos1、ubuntu1主机才能成功。

---

-
hosts: ubuntu-c
gather_facts: False

tasks:
- name: Generate an OpenSSH keypair for ubuntu3
openssh_keypair:
path: ~/.ssh/ubuntu3_id_rsa

-
hosts: linux
gather_facts: False

tasks:
- name: Copy ubuntu3 OpenSSH keypair with permissions
copy:
owner: root
src: "{{ item.0 }}"
dest: "{{ item.0 }}"
mode: "{{ item.1 }}"
with_together:
- [ ~/.ssh/ubuntu3_id_rsa, ~/.ssh/ubuntu3_id_rsa.pub ]
- [ "0600", "0644" ]

-
hosts: ubuntu3
gather_facts: False

tasks:
- name: Add public key to the ubuntu3 authorized_keys file
authorized_key:
user: root
state: present
key: "{{ lookup('file', '~/.ssh/ubuntu3_id_rsa.pub') }}"

-
hosts: all
gather_facts: False

tasks:
- name: Check that ssh can connect to ubuntu3 using the ssh tool
command: ssh -i ~/.ssh/ubuntu3_id_rsa -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null root@ubuntu3 date
changed_when: False
ignore_errors: True

-
hosts: ubuntu-c, centos1, ubuntu1
serial: 1

tasks:
- name: Add host to /etc/hosts.allow for sshd
lineinfile:
path: /etc/hosts.allow
line: "sshd: {{ ansible_hostname }}.diveinto.io"
create: True
delegate_to: ubuntu3

-
hosts: all
gather_facts: False

tasks:
- name: Check that ssh can connect to ubuntu3 using the ssh tool
command: ssh -i ~/.ssh/ubuntu3_id_rsa -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null root@ubuntu3 date
changed_when: False
ignore_errors: True

-
hosts: ubuntu3
gather_facts: False

tasks:
- name: Drop SSH connectivity from everywhere else
lineinfile:
path: /etc/hosts.deny
line: "sshd: ALL"
create: True

-
hosts: all
gather_facts: False

tasks:
- name: Check that ssh can connect to ubuntu3 using the ssh tool
command: ssh -i ~/.ssh/ubuntu3_id_rsa -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null root@ubuntu3 date
changed_when: False
ignore_errors: True

-
hosts: ubuntu-c, centos1, ubuntu1
serial: 1

tasks:
- name: Remove specific host entries in /etc/hosts.allow for sshd
lineinfile:
path: /etc/hosts.allow
line: "sshd: {{ ansible_hostname }}.diveinto.io"
state: absent
delegate_to: ubuntu3

-
hosts: ubuntu3
gather_facts: False

tasks:
- name: Allow SSH connectivity from everywhere
lineinfile:
path: /etc/hosts.deny
line: "sshd: ALL"
state: absent

...

5.7 魔法变量

Ansible默认会提供一些内置的变量以实现一些特定的功能,这些变量不能由用户直接设置,我们称之为魔法变量,如:

  • hostvars
  • inventory_hostname
  • inventory_hostname_short
  • groups
  • group_names

参考文档:https://docs.ansible.com/ansible/latest/reference_appendices/special_variables.html

示例:

---

-
hosts: all

tasks:
- name: Using template, create a remote file that contains all variables available to the play
template:
src: templates/dump_variables
dest: /tmp/ansible_variables

- name: Fetch the templated file with all variables, back to the control host
fetch:
src: /tmp/ansible_variables
dest: "captured_variables/{{ ansible_hostname }}"
flat: yes

- name: Clean up left over files
file:
name: /tmp/ansible_variables
state: absent

...

其中,templates/dump_variables的内容为:

PLAYBOOK VARS (Ansible vars):

{{ vars | to_nice_yaml }}

5.8 块的使用

块可以将任务进行分组,并且可以块级别上应用任务变量,同时支持在块内进行异常处理

常用语法:

- block: 定义块

rescue: 当出现异常时,执行的语句

always: 无论结果如何都要执行的语句

示例如下:

---

-
hosts: linux

tasks:
- name: A block of modules being executed
block:
- name: Example 1 CentOS only
debug:
msg: Example 1 CentOS only
when: ansible_distribution == 'CentOS'

- name: Example 2 Ubuntu only
debug:
msg: Example 2 Ubuntu only
when: ansible_distribution == 'Ubuntu'

- name: Example 3 with items
debug:
msg: "Example 3 with items - {{ item }}"
with_items: ['x', 'y', 'z']
- name: Install patch and python-dns
block:
- name: Install patch
package:
name: patch

- name: Install python-dnspython
package:
name: python-dnspython

rescue:
- name: Rollback python
package:
name: patch
state: absent
- name: Rollback python-dnspython
package:
name: python-dnspython
state: absent

always:
- debug:
msg: This always runs, regardless
...

5.9 Vault信息保护

Ansible Vault是一项安全功能,用于加密或保护剧本或文件中的敏感信息,而不是明文保存

加密和解密变量

# 加密变量
ansible@ubuntu-c:~/diveintoansible/Ansible Playbooks, Deep Dive/Vault/01$ ansible-vault encrypt_string --ask-vault-pass --name 'ansible_become_pass' 'password'
New Vault password:
Confirm New Vault password:
Encryption successful
ansible_become_pass: !vault |
$ANSIBLE_VAULT;1.1;AES256
34396561636439353966346563616432643335646135656133313163613862383439656565363334
3263326331356230396662656636323365663830346461350a396436373862383237643739643134
32303234386534323634313635303163346466356361656238356530393734306665383737656264
6163376237306532630a393531323666626262363538616566626136356462353430336361653864
3239

然后修改group_vars/ubuntu为:

ansible_become: true
ansible_become_pass: !vault |
$ANSIBLE_VAULT;1.1;AES256
34396561636439353966346563616432643335646135656133313163613862383439656565363334
3263326331356230396662656636323365663830346461350a396436373862383237643739643134
32303234386534323634313635303163346466356361656238356530393734306665383737656264
6163376237306532630a393531323666626262363538616566626136356462353430336361653864
3239

然后执行下面命令:

# 解密变量
ansible@ubuntu-c:~/diveintoansible/Ansible Playbooks, Deep Dive/Vault/01$ ansible --ask-vault-pass ubuntu -m ping -o
Vault password:
ubuntu1 | SUCCESS => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python3"},"changed": false,"ping": "pong"}
ubuntu2 | SUCCESS => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python3"},"changed": false,"ping": "pong"}
ubuntu3 | SUCCESS => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python3"},"changed": false,"ping": "pong"}

加密和解密文件

# 加密文件
ansible@ubuntu-c:~/diveintoansible/Ansible Playbooks, Deep Dive/Vault/02$ ansible-vault encrypt external_vault_vars.yaml
New Vault password:
Confirm New Vault password:
Encryption successful

剧本内容:

---

-
hosts: linux

vars_files:
- external_vault_vars.yaml

tasks:
- name: Show external_vault_var
debug:
var: external_vault_var

...

执行:

ansible@ubuntu-c:~/diveintoansible/Ansible Playbooks, Deep Dive/Vault/02$ ansible-playbook --ask-vault-pass vault_playbook.yaml
Vault password:

解密:

ansible@ubuntu-c:~/diveintoansible/Ansible Playbooks, Deep Dive/Vault/02$ ansible-vault decrypt external_vault_vars.yaml
Vault password:
Decryption successful

重新加密数据

# 加密文件
ansible@ubuntu-c:~/diveintoansible/Ansible Playbooks, Deep Dive/Vault/02$ ansible-vault encrypt external_vault_vars.yaml
New Vault password:
Confirm New vault password:
Encryption successful

# 重新加密文件
ansible@ubuntu-c:~/diveintoansible/Ansible Playbooks, Deep Dive/Vault/02$ ansible-vault rekey external_vault_vars.yaml
New Vault password:
Confirm New Vault password:
Rekey successful

# 查看加密文件
ansible@ubuntu-c:~/diveintoansible/Ansible Playbooks, Deep Dive/Vault/02$ ansible-vault view external_vault_vars.yaml
Vault password:
external_vault_var: Example External Vault Var


# 查看加密文件(使用密码文件)
ansible@ubuntu-c:~/diveintoansible/Ansible Playbooks, Deep Dive/Vault/02$ ansible-vault view --vault-password-file password_file external_vault_vars.yaml
external_vault_var: Example External Vault Var

# 查看加密文件(使用密码文件)
ansible@ubuntu-c:~/diveintoansible/Ansible Playbooks, Deep Dive/Vault/02$ ansible-vault view --vault-id @password_file external_vault_vars.yaml
external_vault_var: Example External Vault Var

# 加密文件至命名valut变量 —— vars
ansible@ubuntu-c:~/diveintoansible/Ansible Playbooks, Deep Dive/Vault/02$ ansible-vault encrypt --vault-id vars@prompt external_vault_vars.yaml
New vault passowrd (vars):
Confirm New vault password (vars):
Encryption successful

# 加密变量至命名valut变量 —— ssh
ansible@ubuntu-c:~/diveintoansible/Ansible Playbooks, Deep Dive/Vault/02$ ansible-vault encrypt_string --vault-id ssh@prompt --name 'ansible_become_pass' 'password'
New vault password (ssh):
Confirm new vault password (ssh):
Encryption successful
ansible_become_pass: !vault |
$ANSIBLE_VAULT;1.2;AES256;ssh
30373934396234613766353262633936373238643366326131653735393237663830326362623432
6564663637656537366163323763316139616238633433340a633436323664643635383465383064
35633963306665626237306566376666383130396333326366663661653666663535316638303839
6131396362313266300a386331336665376562663631316564306138333534383131643439663364
6432

使用加密的变量和文件

ansible@ubuntu-c:~/diveintoansible/Ansible Playbooks, Deep Dive/Vault/02$ ansible-vault encrypt_string --vault-id ssh@prompt --name 'ansible_become_pass' 'password'
New vault password (ssh):
Confirm new vault password (ssh):
Encryption successful
ansible_become_pass: !vault |
$ANSIBLE_VAULT;1.2;AES256;ssh
30373934396234613766353262633936373238643366326131653735393237663830326362623432
6564663637656537366163323763316139616238633433340a633436323664643635383465383064
35633963306665626237306566376666383130396333326366663661653666663535316638303839
6131396362313266300a386331336665376562663631316564306138333534383131643439663364
6432ansible@ubuntu-c:~/diveintoansible/Ansible Playbooks, Deep Dive/Vault/02$ ^C
ansible@ubuntu-c:~/diveintoansible/Ansible Playbooks, Deep Dive/Vault/02$ ansible-playbook --vault-id vars@prompt --vault-id ssh@prompt vault_playbook.yaml
Vault password (vars):
Vault password (ssh):

PLAY [linux] *******************************************************************************************

TASK [Gathering Facts] *********************************************************************************
ok: [centos2]
ok: [centos1]
ok: [centos3]
ok: [ubuntu1]
ok: [ubuntu2]
ok: [ubuntu3]

TASK [Show external_vault_var] *************************************************************************
ok: [centos1] => {
"external_vault_var": "Example External Vault Var"
}
ok: [centos2] => {
"external_vault_var": "Example External Vault Var"
}
ok: [centos3] => {
"external_vault_var": "Example External Vault Var"
}
ok: [ubuntu1] => {
"external_vault_var": "Example External Vault Var"
}
ok: [ubuntu2] => {
"external_vault_var": "Example External Vault Var"
}
ok: [ubuntu3] => {
"external_vault_var": "Example External Vault Var"
}

PLAY RECAP *********************************************************************************************
centos1 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
centos2 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
centos3 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
ubuntu1 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
ubuntu2 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
ubuntu3 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

还可以加密剧本到命名vault变量,类似上述操作,在此不再赘述。

6. 构建Ansible Playbooks

使用includes和imports、Tags标签和Roles角色

6.1 使用includes和imports

先准备tasks文件,如play1_task2.yaml:

---

- name: Play 1 - Task 2
debug:
msg: Play 1 - Task 2

...

include_tasks指令

动态导入

使用include_tasks指令在下面的剧本文件中将play1_task2.yaml文件导入:

---

-
hosts: all

tasks:
- name: Play 1 - Task 1
debug:
msg: Play 1 - Task 1

- include_tasks: play1_task2.yaml

...

测试:

ansible@ubuntu-c:~/diveintoansible/Structuring Ansible Playbooks/Using Include and Import/01$  ansible-playbook include_tasks_playbook.yaml 

PLAY [all] ***********************************************************************************************************************

TASK [Gathering Facts] ***********************************************************************************************************
ok: [ubuntu-c]
ok: [centos1]
ok: [centos2]
ok: [centos3]
ok: [ubuntu1]
ok: [ubuntu2]
ok: [ubuntu3]

TASK [Play 1 - Task 1] ***********************************************************************************************************
ok: [ubuntu-c] => {
"msg": "Play 1 - Task 1"
}
ok: [centos1] => {
"msg": "Play 1 - Task 1"
}
ok: [centos2] => {
"msg": "Play 1 - Task 1"
}
ok: [centos3] => {
"msg": "Play 1 - Task 1"
}
ok: [ubuntu1] => {
"msg": "Play 1 - Task 1"
}
ok: [ubuntu2] => {
"msg": "Play 1 - Task 1"
}
ok: [ubuntu3] => {
"msg": "Play 1 - Task 1"
}

TASK [include_tasks] *************************************************************************************************************
included: /home/ansible/diveintoansible/Structuring Ansible Playbooks/Using Include and Import/01/play1_task2.yaml for ubuntu-c, centos1, centos2, centos3, ubuntu1, ubuntu2, ubuntu3

TASK [Play 1 - Task 2] ***********************************************************************************************************
ok: [ubuntu-c] => {
"msg": "Play 1 - Task 2"
}
ok: [centos1] => {
"msg": "Play 1 - Task 2"
}
ok: [centos2] => {
"msg": "Play 1 - Task 2"
}
ok: [centos3] => {
"msg": "Play 1 - Task 2"
}
ok: [ubuntu1] => {
"msg": "Play 1 - Task 2"
}
ok: [ubuntu2] => {
"msg": "Play 1 - Task 2"
}
ok: [ubuntu3] => {
"msg": "Play 1 - Task 2"
}

PLAY RECAP ***********************************************************************************************************************
centos1 : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
centos2 : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
centos3 : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
ubuntu-c : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
ubuntu1 : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
ubuntu2 : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
ubuntu3 : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

import_tasks指令

静态导入

使用import_tasks指令在下面的剧本文件中将play1_task2.yaml文件导入:

---

-
hosts: all

tasks:
- name: Play 1 - Task 1
debug:
msg: Play 1 - Task 1

- import_tasks: play1_task2.yaml

...

测试:

ansible@ubuntu-c:~/diveintoansible/Structuring Ansible Playbooks/Using Include and Import/02$ ansible-playbook import_tasks_playbook.yaml 

PLAY [all] ***********************************************************************************************************************

TASK [Gathering Facts] ***********************************************************************************************************
ok: [ubuntu-c]
ok: [centos1]
ok: [centos2]
ok: [centos3]
ok: [ubuntu1]
ok: [ubuntu2]
ok: [ubuntu3]

TASK [Play 1 - Task 1] ***********************************************************************************************************
ok: [ubuntu-c] => {
"msg": "Play 1 - Task 1"
}
ok: [centos1] => {
"msg": "Play 1 - Task 1"
}
ok: [centos2] => {
"msg": "Play 1 - Task 1"
}
ok: [centos3] => {
"msg": "Play 1 - Task 1"
}
ok: [ubuntu1] => {
"msg": "Play 1 - Task 1"
}
ok: [ubuntu2] => {
"msg": "Play 1 - Task 1"
}
ok: [ubuntu3] => {
"msg": "Play 1 - Task 1"
}

TASK [Play 1 - Task 2] ***********************************************************************************************************
ok: [ubuntu-c] => {
"msg": "Play 1 - Task 2"
}
ok: [centos1] => {
"msg": "Play 1 - Task 2"
}
ok: [centos2] => {
"msg": "Play 1 - Task 2"
}
ok: [centos3] => {
"msg": "Play 1 - Task 2"
}
ok: [ubuntu1] => {
"msg": "Play 1 - Task 2"
}
ok: [ubuntu2] => {
"msg": "Play 1 - Task 2"
}
ok: [ubuntu3] => {
"msg": "Play 1 - Task 2"
}

PLAY RECAP ***********************************************************************************************************************
centos1 : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
centos2 : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
centos3 : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
ubuntu-c : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
ubuntu1 : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
ubuntu2 : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
ubuntu3 : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

静态导入vs动态导入

静态导入动态导入
在解析剧本时处理在剧本执行时处理
每个任务将独立针对when条件执行when 语句执行一次,如果满足条件,则执行所有任务
import指令include指令

为了对比,分别创建import_tasks.yaml和include_tasks.yaml,其中:

import_task.yaml

---

- set_fact:
import_tasks_var: foo

- name: 2nd Task
debug:
msg: 2nd Task

- name: 3rd Task
debug:
msg: 3rd Task

...

include_tasks.yaml

---

- set_fact:
include_tasks_var: foo

- name: 2nd Task
debug:
msg: 2nd Task

- name: 3rd Task
debug:
msg: 3rd Task

...

比较import指令和include指令的剧本include_import_tasks_playbook.yaml:

---

-
hosts: centos1

tasks:

- debug:
msg: ===================== Testing include_tasks =====================
# include_tasks is dynamic
# The when statement is executed once, if the condition is met, all tasks are executed
- include_tasks: include_tasks.yaml
when: include_tasks_var is not defined

- debug:
msg: ===================== Testing import_tasks ======================

# import_tasks is static
# Each task that in the include will be independently executed against the when condition
- import_tasks: import_tasks.yaml
when: import_tasks_var is not defined

...

测试:

ansible@ubuntu-c:~/diveintoansible/Structuring Ansible Playbooks/Using Include and Import/03$ ansible-playbook include_import_tasks_playbook.yaml 

PLAY [centos1] *******************************************************************************************************************

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

TASK [debug] *********************************************************************************************************************
ok: [centos1] => {
"msg": "===================== Testing include_tasks ====================="
}

TASK [include_tasks] *************************************************************************************************************
included: /home/ansible/diveintoansible/Structuring Ansible Playbooks/Using Include and Import/03/include_tasks.yaml for centos1

TASK [set_fact] ******************************************************************************************************************
ok: [centos1]

TASK [2nd Task] ******************************************************************************************************************
ok: [centos1] => {
"msg": "2nd Task"
}

TASK [3rd Task] ******************************************************************************************************************
ok: [centos1] => {
"msg": "3rd Task"
}

TASK [debug] *********************************************************************************************************************
ok: [centos1] => {
"msg": "===================== Testing import_tasks ======================"
}

TASK [set_fact] ******************************************************************************************************************
ok: [centos1]

TASK [2nd Task] ******************************************************************************************************************
skipping: [centos1]

TASK [3rd Task] ******************************************************************************************************************
skipping: [centos1]

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

import_playbook指令

先准备剧本文件,如imported_playbook.yaml:

---

-
hosts: centos1

tasks:
- set_fact:
import_playbook_var: true

- debug:
msg: Playbook executed

...

使用import_playbook指令在下面的剧本文件中将imported_playbook.yaml文件静态导入

---

- import_playbook: imported_playbook.yaml
when: import_playbook_var is not defined

...

每个任务都会进行一次when条件的判断。

6.2 使用tags

标签在处理大型剧本或剧本中包含其他剧本时非常有用,你可以运行部分配置而无需运行整个剧本

如:

---

-
hosts: linux

-
hosts: linux
tags:
- webapp
vars_files:
- vars/logos.yaml

tasks:
- name: Install EPEL
yum:
name: epel-release
update_cache: yes
state: latest
when: ansible_distribution == 'CentOS'
tags:
- install-epel

- name: Install Nginx
package:
name: nginx
state: latest
tags:
- install-nginx

- name: Restart nginx
service:
name: nginx
state: restarted
notify: Check HTTP Service
tags:
- always

- name: Template index.html-easter_egg.j2 to index.html on target
template:
src: index.html-easter_egg.j2
dest: "{{ nginx_root_location }}/index.html"
mode: 0644
tags:
- deploy-app

- name: Install unzip
package:
name: unzip
state: latest

- name: Unarchive playbook stacker game
unarchive:
src: playbook_stacker.zip
dest: "{{ nginx_root_location }}"
mode: 0755
tags:
- deploy-app

handlers:
- name: Check HTTP Service
uri:
url: http://{{ ansible_default_ipv4.address }}
status_code: 200

...

执行时添加--tags可只运行部分任务,添加--skip-tags可不执行部分命令,always标签的任务总是会指向,可通过--skip-tags "always"指令跳过

ansible@ubuntu-c:~/diveintoansible/Structuring Ansible Playbooks/Using Tags/03$ ansible-playbook nginx_playbook.yaml --tags "install-epel"

ansible@ubuntu-c:~/diveintoansible/Structuring Ansible Playbooks/Using Tags/03$ ansible-playbook nginx_playbook.yaml --tags "install-nginx,restart-nginx"

ansible@ubuntu-c:~/diveintoansible/Structuring Ansible Playbooks/Using Tags/03$ ansible-playbook nginx_playbook.yaml --skip-tags "deploy-app"

除了给任务打tag外,也可以给剧本打tag,但是可能会影响facts变量的值,因为默认情况下facts变量指向default标签。

特殊标签:

  • tagged: 只有打了tag的任务才会执行
  • untagged: 打了tag的任务不会执行
  • all:运行所有任务,默认ansible 使用 --tags all运行

如:

# 运行所有任务
ansible@ubuntu-c:~/diveintoansible/Structuring Ansible Playbooks/Using Tags/04$ ansible-playbook nginx_playbook.yaml --tags "all"

# 只运行打了tag的任务
ansible@ubuntu-c:~/diveintoansible/Structuring Ansible Playbooks/Using Tags/04$ ansible-playbook nginx_playbook.yaml --tags "tagged"

# 只运行没打tag的任务
ansible@ubuntu-c:~/diveintoansible/Structuring Ansible Playbooks/Using Tags/04$ ansible-playbook nginx_playbook.yaml --tags "untagged"

除了任务、剧本可以打标签外,导入也可以打标签,如下:

---

-
hosts: ubuntu3

tasks:
- include_tasks: include_tasks.yaml
tags:
- include_tasks
- import_tasks: import_tasks.yaml
tags:
- import_tasks

- import_playbook: import_playbook.yaml
tags:
- import_playbook

...

6.3 使用roles

roles(角色)用于层次性,结构化地组织剧本,角色分别将变量、文件、任务、模块及处理器放置于单独的目录中,而在剧本中使用include指令导入。

使用角色的好处:

  • 代码复用

  • 角色使大项目更加容易管理

  • 角色按逻辑分组的结构,更易于分享

  • 可以根据特定需求编写角色,如web server的角色,DNS角色或补丁角色

  • 角色可以独立开发,由不同实体并行

  • 模板、变量、文件、已指定目录和include被简化

  • 角色可以依赖其他角色,因此提供自动包含

简单的角色结构示例:

roles/ \ansible所有的信息都放到此目录下面对应的目录中
└── example-role \角色名称
├── default \为当前角色设定默认变量时使用此目录,应当包含一个main.yml文件;
├── files \存放有copy或script等模块调用的文件,或压缩安装包等
├── handlers \此目录总应当包含一个main.yml文件,用于定义各角色用到的各handler
├── meta \应当包含一个main.yml,用于定义角色的特殊设定及其依赖关系
├── tasks \至少包含一个名为main.yml的文件,定义了此角色的任务列表,可使用include指令
├── templates \template模块会自动在此目录中寻找Jinja2模板文件
└── vars \应当包含一个main.yml文件,用于定义此角色用到的变量

将已有剧本改成角色结构

有一个批量部署nginx应用的剧本,./nginx_playbook.yaml内容为:

---

-
hosts: linux

-
hosts: linux
tags:
- webapp

vars_files:
- vars/logos.yaml

tasks:
- name: Install EPEL
yum:
name: epel-release
update_cache: yes
state: latest
when: ansible_distribution == 'CentOS'
tags:
- install-epel

- name: Install Nginx
package:
name: nginx
state: latest
tags:
- install-nginx

- name: Restart nginx
service:
name: nginx
state: restarted
notify: Check HTTP Service
tags:
- always

- name: Template index.html-easter_egg.j2 to index.html on target
template:
src: templates/index.html-easter_egg.j2
dest: "{{ nginx_root_location }}/index.html"
mode: 0644
tags:
- deploy-app

- name: Install unzip
package:
name: unzip
state: latest

- name: Unarchive playbook stacker game
unarchive:
src: playbook_stacker.zip
dest: "{{ nginx_root_location }}"
mode: 0755
tags:
- deploy-app

handlers:
- name: Check HTTP Service
uri:
url: http://{{ ansible_default_ipv4.address }}
status_code: 200

...

另外,模板内容可参见Ansible课程代码

使用ansible-galaxy指令创建roles

ansible@ubuntu-c:~/diveintoansible/Structuring Ansible Playbooks/Using Roles/02$ ansible-galaxy init nginx
- Role nginx was created successfully
ansible@ubuntu-c:~/diveintoansible/Structuring Ansible Playbooks/Using Roles/02$ find .
.
./ansible.cfg
./files
./files/playbook_stacker.zip
./group_vars
./group_vars/centos
./group_vars/ubuntu
./hosts
./host_vars
./host_vars/centos1
./host_vars/ubuntu-c
./nginx
./nginx/defaults
./nginx/defaults/main.yml
./nginx/files
./nginx/handlers
./nginx/handlers/main.yml
./nginx/meta
./nginx/meta/main.yml
./nginx/README.md
./nginx/tasks
./nginx/tasks/main.yml
./nginx/templates
./nginx/tests
./nginx/tests/inventory
./nginx/tests/test.yml
./nginx/vars
./nginx/vars/main.yml
./nginx_playbook.yaml
./templates
./templates/index.html-ansible_managed.j2
./templates/index.html-base.j2
./templates/index.html-easter_egg.j2
./templates/index.html-logos.j2
./templates/index.html.j2
./vars
./vars/logos.yaml

nginx_playbook.yaml文件中handlers部分的内容写入nginx/handlers/main.yml,如下:

---
- name: Check HTTP Service
uri:
url: http://{{ ansible_default_ipv4.address }}
status_code: 200

...

移动现有的模板进入nginx/templates文件夹

ansible@ubuntu-c:~/diveintoansible/Structuring Ansible Playbooks/Using Roles/02$ mv templates/* nginx/templates/ && rm -rf templates/

移动现有文件进入nginx/files目录

ansible@ubuntu-c:~/diveintoansible/Structuring Ansible Playbooks/Using Roles/02$ mv files/* nginx/files/ && rm -rf files/

复制现有变量内容到nginx/var/main.yml,进入文件删除多余的破折号和点

ansible@ubuntu-c:~/diveintoansible/Structuring Ansible Playbooks/Using Roles/02$ cat vars/logos.yaml >> nginx/vars/main.yml
ansible@ubuntu-c:~/diveintoansible/Structuring Ansible Playbooks/Using Roles/02$ rm -rf vars/

nginx_playbook.yaml文件中tasks部分的内容写入nginx/tasks/main.yml,并修改template部分,如下:

---
- name: Install EPEL
yum:
name: epel-release
update_cache: yes
state: latest
when: ansible_distribution == 'CentOS'
tags:
- install-epel

- name: Install Nginx
package:
name: nginx
state: latest
tags:
- install-nginx

- name: Restart nginx
service:
name: nginx
state: restarted
notify: Check HTTP Service
tags:
- always

- name: Template index.html-easter_egg.j2 to index.html on target
template:
src: index.html-easter_egg.j2
dest: "{{ nginx_root_location }}/index.html"
mode: 0644
tags:
- deploy-app

- name: Install unzip
package:
name: unzip
state: latest

- name: Unarchive playbook stacker game
unarchive:
src: playbook_stacker.zip
dest: "{{ nginx_root_location }}"
mode: 0755
tags:
- deploy-app

...

修改nginx_playbook.yaml为:

---

-
hosts: linux

roles:
- nginx

...

此时,文件结构变为:

ansible@ubuntu-c:~/diveintoansible/Structuring Ansible Playbooks/Using Roles/02$ find .
.
./ansible.cfg
./group_vars
./group_vars/centos
./group_vars/ubuntu
./hosts
./host_vars
./host_vars/centos1
./host_vars/ubuntu-c
./nginx
./nginx/.travis.yml
./nginx/defaults
./nginx/defaults/main.yml
./nginx/files
./nginx/files/playbook_stacker.zip
./nginx/handlers
./nginx/handlers/main.yml
./nginx/meta
./nginx/meta/main.yml
./nginx/README.md
./nginx/tasks
./nginx/tasks/main.yml
./nginx/templates
./nginx/templates/index.html-ansible_managed.j2
./nginx/templates/index.html-base.j2
./nginx/templates/index.html-easter_egg.j2
./nginx/templates/index.html-logos.j2
./nginx/templates/index.html.j2
./nginx/tests
./nginx/tests/inventory
./nginx/tests/test.yml
./nginx/vars
./nginx/vars/main.yml
./nginx_playbook.yaml

经过测试,发现可以正常运行。

上面的nginx角色还可以继续把webapp部分拆分出来做为独立的角色。

角色也可以覆盖参数,如下:

---

-
hosts: linux

roles:
- nginx
- { role: webapp, target_dir: "{%- if ansible_distribution == 'CentOS' -%}/usr/share/nginx/html{%- elif ansible_distribution == 'Ubuntu' -%}/var/www/html{%- endif %}" }

...

角色依赖

可以在webapp/meta/main.yml文件最后为webapp角色添加对nginx角色的依赖:

...... 省略大量内容
dependencies:
- nginx

剧本内容变为:

---

-
hosts: linux

roles:
- { role: webapp, target_dir: "{%- if ansible_distribution == 'CentOS' -%}/usr/share/nginx/html{%- elif ansible_distribution == 'Ubuntu' -%}/var/www/html{%- endif %}" }

...

7. 云服务、容器、Ansible

AWS、Docker与Ansible

7.1 AWS与Ansible

使用Ansible在AWS中自动化部署实例

首先在AWS的EC2控制台中创建密钥对,然后去个人账号创建访问密钥,进入VPCs创建默认的VPC

然后在AWS的EC2控制台中创建实例,选定系统镜像

配置AWS模块

ansible@ubuntu-c:~/diveintoansible/Using Ansible with Cloud Services and Containers/AWS with Ansible/01$ export AWS_ACCESS_KEY_ID="your_accesskey"

ansible@ubuntu-c:~/diveintoansible/Using Ansible with Cloud Services and Containers/AWS with Ansible/01$ export AWS_SECRET_ACCESS_KEY="your_accesskey_secret"

ansible@ubuntu-c:~/diveintoansible/Using Ansible with Cloud Services and Containers/AWS with Ansible/01$ sudo pip install boto boto3

ansible.cfg文件

[defaults]
inventory=hosts
host_key_checking=False
forks=6

hosts文件

[control]
ubuntu-c

[centos]
centos[1:3]

[ubuntu]
ubuntu[1:3]

[linux:children]
centos
ubuntu

ec2_playbook.yaml文件

  • 在 AWS 中创建用于 SSH 访问和 HTTP 的安全组
  • 预置一组实例
  • 将所有实例公共 IP 添加到主机组
---

-
hosts: localhost
connection: local
gather_facts: false

tasks:
- name: Create a security group in AWS for SSH access and HTTP
ec2_group:
name: ansible
description: Ansible Security Group
region: us-east-1
rules:
- proto: tcp
from_port: 80
to_port: 80
cidr_ip: 0.0.0.0/0
- proto: tcp
from_port: 22
to_port: 22
cidr_ip: 0.0.0.0/0

- name: Provision a set of instances
ec2:
key_name: ansible
group: ansible
instance_type: t2.micro
image: ami-096fda3c22c1c990a
region: us-east-1
wait: true
exact_count: 20
count_tag:
Name: AnsibleNginxWebservers
instance_tags:
Name: Ansible
register: ec2
ignore_errors: true

- name: Add all instance public IPs to host group
add_host:
hostname: "{{ item.public_ip }}"
groups: ansiblehosts
with_items: "{{ ec2.instances }}"

- name: Show group
debug:
var: groups.ansiblehosts

...

使用AWS动态清单

ansible@ubuntu-c:~/diveintoansible/Using Ansible with Cloud Services and Containers/AWS with Ansible/04$ mkdir inventory && cd inventory

ansible@ubuntu-c:~/diveintoansible/Using Ansible with Cloud Services and Containers/AWS with Ansible/04$ wget https://raw.githubusercontent.com/ansible/ansible/stable-2.9/contrib/inventory/ec2.py

ansible@ubuntu-c:~/diveintoansible/Using Ansible with Cloud Services and Containers/AWS with Ansible/04$ wget https://raw.githubusercontent.com/ansible/ansible/stable-2.9/contrib/inventory/ec2.ini

ansible@ubuntu-c:~/diveintoansible/Using Ansible with Cloud Services and Containers/AWS with Ansible/04$ chmod u+x ec2.py

修改ec2.py文件,第1行的python变为python3,注释掉第172行内容,即# from ansible.module_utils import ec2 as ec2_utils

修改ec2.ini文件:cache_max_go=0

配置:

ansible@ubuntu-c:~/diveintoansible/Using Ansible with Cloud Services and Containers/AWS with Ansible/04$ export EC2_INI_PATH=inventory/ec2.ini

修改ansible.cfg为:

[defaults]
inventory=inventory/ec2
host_key_checking=False
forks=20
ansible_managed=Managed by Ansible - file:{file} - host:{host} - uid:{uid}

创建~./ssh/ansible.pem,将密钥复制到此处,并且设置权限为600

创建group_vars/tag_Name_Ansible:

---
ansible_ssh_private_key_file:~./ssh/ansible.pem
ansible_user: ec2-user
ansible_become: true
...

修改剧本内容为:

---

-
hosts: localhost
connection: local
gather_facts: false

tasks:
- name: Create a security group in AWS for SSH access and HTTP
ec2_group:
name: ansible
description: Ansible Security Group
region: us-east-1
rules:
- proto: tcp
from_port: 80
to_port: 80
cidr_ip: 0.0.0.0/0
- proto: tcp
from_port: 22
to_port: 22
cidr_ip: 0.0.0.0/0

- name: Provision a set of instances
ec2:
key_name: ansible
group: ansible
instance_type: t2.micro
image: ami-096fda3c22c1c990a
region: us-east-1
wait: true
exact_count: 20
count_tag:
Name: AnsibleNginxWebservers
instance_tags:
Name: Ansible
register: ec2
ignore_errors: true

- name: Refresh inventory to ensure new instances exist in inventory
meta: refresh_inventory

-
hosts: tag_Name_Ansible

roles:
- { role: webapp, target_dir: /usr/share/nginx/html }

...

并将其改为角色结构,详细代码可查看提供的教程代码

测试:

ansible@ubuntu-c:~/diveintoansible/Using Ansible with Cloud Services and Containers/AWS with Ansible/04$ ansible tag_Name_Ansible -m ping -o

最终剧本为:

---

-
hosts: localhost
connection: local
gather_facts: false

tasks:
- name: Create a security group in AWS for SSH access and HTTP
ec2_group:
name: ansible
description: Ansible Security Group
region: us-east-1
rules:
- proto: tcp
from_port: 80
to_port: 80
cidr_ip: 0.0.0.0/0
- proto: tcp
from_port: 22
to_port: 22
cidr_ip: 0.0.0.0/0

- name: Provision a set of instances
ec2:
key_name: ansible
group: ansible
instance_type: t2.micro
image: ami-096fda3c22c1c990a
region: us-east-1
wait: true
exact_count: 20
count_tag:
Name: AnsibleNginxWebservers
instance_tags:
Name: Ansible
register: ec2
ignore_errors: true

- name: Refresh inventory to ensure new instances exist in inventory
meta: refresh_inventory

-
hosts: tag_Name_Ansible

roles:
- { role: webapp, target_dir: /usr/share/nginx/html }

-
hosts: tag_Name_Ansible

tasks:
- debug:
msg: "Check http://{{ ansible_host }}"

- pause:
prompt: "Verify service availability and continue to terminate"

- name: Remove tagged EC2 instances from security group by setting an empty group
ec2:
state: running
region: "{{ ec2_region }}"
instance_ids: "{{ ec2_id }}"
group_id: ""
delegate_to: localhost

- name: Terminate EC2 instances
ec2:
state: absent
region: "{{ ec2_region }}"
instance_ids: "{{ ec2_id }}"
wait: true
delegate_to: localhost

-
hosts: localhost
connection: local
gather_facts: false

tasks:
- name: Remove ansible security group
ec2_group:
name: ansible
region: us-east-1
state: absent

...

7.2 Docker与Ansible

配置Docker实验室

install_docker.sh脚本:

install_docker.sh sudo apt update
sudo apt install -y docker.io
pip3 install docker

执行:

ansible@ubuntu-c:~/diveintoansible/Using Ansible with Cloud Services and Containers/Docker with Ansible/01$ bash -x install_docker.sh

设置环境变量envdocker

export DOCKER_HOST=tcp://docker:2375

使之生效

ansible@ubuntu-c:~/diveintoansible/Using Ansible with Cloud Services and Containers/Docker with Ansible/01$ source envdocker

编写剧本 docker_playbook.yaml:

---

-
hosts: ubuntu-c

tasks:
- name: Pull images
docker_host: tcp://docker:2375
name: "{{ item }}"
source: pull
with_items:
- centos
- ubuntu
- redis
- nginx
- wernight/funbox

- name: Create a customised index.html
copy:
dest: /shared/index.html
mode: 0644
content:
Customised page for nginxcustomised

- name: Create a customised Dockerfile
copy:
dest: /shared/Dockerfile
mode: 0644
content:
FROM nginx
COPY index.html /usr/share/nginx/html/index.html

- name: Build a customised image
docker_image:
docker_host: tcp://docker:2375
name: nginxcustomised:latest
source: build
build:
path: /shared
pull: yes
state: present
force_source: yes

- name: Create an nginxcustomised container
docker_container:
docker_host: tcp://docker:2375
name: containerwebserver
image: nginxcustomised:latest
ports:
- 80:80
container_default_behavior: no_defaults
recreate: yes


...

其他例子:

---

-
hosts: ubuntu-c

tasks:
- name: Pull python image
docker_image:
docker_host: tcp://docker:2375
name: python:3.8.5
source: pull

- name: Create 3 python containers
docker_container:
docker_host: tcp://docker:2375
name: "python{{ item }}"
image: python:3.8.5
container_default_behavior: no_defaults
command: sleep infinity
with_sequence: 1-3

-
hosts: containers
gather_facts: False

tasks:
- name: Ping containers
ping:

...

其中,containers的配置为:

[containers]
python[1:3] ansible_connection=docker ansible_python_interpreter=/usr/bin/python3

终止并移除容器

---

-
hosts: ubuntu-c

tasks:
- name: Remove old containers
docker_container:
docker_host: tcp://docker:2375
name: "{{ item }}"
state: absent
container_default_behavior: no_defaults
with_items:
- containerwebserver
- python1
- python2
- python3

- name: Remove images
docker_image:
docker_host: tcp://docker:2375
name: "{{ item }}"
state: absent
with_items:
- centos
- ubuntu
- redis
- nginx
- wernight/funbox
- nginxcustomised
- python:3.8.5

- name: Remove files
file:
path: "{{ item }}"
state: absent
with_items:
- /shared/Dockerfile
- /shared/index.html

...

8. 创建模块和插件

创建自己的模块和插件

8.1 创建模块

下载ansible源码

git clone https://github.com/ansible/ansible.git

使用开发工具Hacking去调试模块,如:

~/ansible/hacking/test_module -m ~/ansible/lib/ansible/modules/command.py -a hostname

生成模块测试成功和失败报告

脚本icmp.sh

#!/bin/bash

source $1>/dev/null 2>&1

TARGET=${target:-127.0.0.1}

ping -c 1 ${TARGET} >/dev/null 2>/dev/null

if [ $? == 0 ];
then
echo "{\"changed\": true, \"rc\": 0}"
else
echo "{\"failed\": true, \"msg\": \"failed to ping\", \"rc\": 1}"
fi

测试:

~ansible/hacking/test-module -m icmp.sh
~ansible/hacking/test-module -m icmp.sh -a 'target=centos1'

模块测试结果保存在 /home/ansible/.ansible_module_generated

模块输入的参数保存在**/home/ansible/.ansible_test_module_arguments** 中

两个文件中有数据的前提是使用了source $1>/dev/null 2>&1捕获输入

创建一个简单的ping模块

在ansible工作目录下新建一个library文件夹,将icmp.sh放入library文件夹并去掉.sh文件后缀,就算创建了一个简单的ping模块

剧本内容为:

---
-
hosts: linux

tasks:
- name: Test icmp module
icmp:
target: 127.0.0.1

。。。

如果想要将自定义模块发布到ansible源,需要遵循规范,可以参考:http://docs.ansible.com/ansible/latest/dev_guide/developing_modules_general.html

官方给的模块模板内容如下:

#!/usr/bin/python

# Copyright: (c) 2018, Terry Jones <terry.jones@example.org>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

DOCUMENTATION = r'''
---
module: my_test

short_description: This is my test module

# If this is part of a collection, you need to use semantic versioning,
# i.e. the version is of the form "2.5.0" and not "2.4".
version_added: "1.0.0"

description: This is my longer description explaining my test module.

options:
name:
description: This is the message to send to the test module.
required: true
type: str
new:
description:
- Control to demo if the result of this module is changed or not.
- Parameter description can be a list as well.
required: false
type: bool
# Specify this value according to your collection
# in format of namespace.collection.doc_fragment_name
extends_documentation_fragment:
- my_namespace.my_collection.my_doc_fragment_name

author:
- Your Name (@yourGitHubHandle)
'''

EXAMPLES = r'''
# Pass in a message
- name: Test with a message
my_namespace.my_collection.my_test:
name: hello world

# pass in a message and have changed true
- name: Test with a message and changed output
my_namespace.my_collection.my_test:
name: hello world
new: true

# fail the module
- name: Test failure of the module
my_namespace.my_collection.my_test:
name: fail me
'''

RETURN = r'''
# These are examples of possible return values, and in general should use other names for return values.
original_message:
description: The original name param that was passed in.
type: str
returned: always
sample: 'hello world'
message:
description: The output message that the test module generates.
type: str
returned: always
sample: 'goodbye'
'''

from ansible.module_utils.basic import AnsibleModule


def run_module():
# define available arguments/parameters a user can pass to the module
module_args = dict(
name=dict(type='str', required=True),
new=dict(type='bool', required=False, default=False)
)

# seed the result dict in the object
# we primarily care about changed and state
# changed is if this module effectively modified the target
# state will include any data that you want your module to pass back
# for consumption, for example, in a subsequent task
result = dict(
changed=False,
original_message='',
message=''
)

# the AnsibleModule object will be our abstraction working with Ansible
# this includes instantiation, a couple of common attr would be the
# args/params passed to the execution, as well as if the module
# supports check mode
module = AnsibleModule(
argument_spec=module_args,
supports_check_mode=True
)

# if the user is working with this module in only check mode we do not
# want to make any changes to the environment, just return the current
# state with no modifications
if module.check_mode:
module.exit_json(**result)

# manipulate or modify the state as needed (this is going to be the
# part where your module will do what it needs to do)
result['original_message'] = module.params['name']
result['message'] = 'goodbye'

# use whatever logic you need to determine whether or not this module
# made any modifications to your target
if module.params['new']:
result['changed'] = True

# during the execution of the module, if there is an exception or a
# conditional state that effectively causes a failure, run
# AnsibleModule.fail_json() to pass in the message and the result
if module.params['name'] == 'fail me':
module.fail_json(msg='You requested this to fail', **result)

# in the event of a successful module execution, you will want to
# simple AnsibleModule.exit_json(), passing the key/value results
module.exit_json(**result)


def main():
run_module()


if __name__ == '__main__':
main()

因此,将library文件夹下的icmp替换为icmp.py,简单的ping模块可以改为:

#!/usr/bin/python3

ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}

DOCUMENTATION = '''
---
module: icmp

short_description: simple module for icmp ping

version_added: "2.10"

description:
- "simple module for icmp ping"

options:
target:
description:
- The target to ping
required: true

author:
- James Spurin (@spurin)
'''

EXAMPLES = '''
# Ping an IP
- name: Ping an IP
icmp:
target: 127.0.0.1

# Ping a host
- name: Ping a host
icmp:
target: centos1
'''

RETURN = '''
'''

from ansible.module_utils.basic import AnsibleModule

def run_module():
# define the available arguments/parameters that a user can pass to
# the module
module_args = dict(
target=dict(type='str', required=True)
)

# seed the result dict in the object
# we primarily care about changed and state
# change is if this module effectively modified the target
# state will include any data that you want your module to pass back
# for consumption, for example, in a subsequent task
result = dict(
changed=False
)

# the AnsibleModule object will be our abstraction working with Ansible
# this includes instantiation, a couple of common attr would be the
# args/params passed to the execution, as well as if the module
# supports check mode
module = AnsibleModule(
argument_spec=module_args,
supports_check_mode=True
)

# if the user is working with this module in only check mode we do not
# want to make any changes to the environment, just return the current
# state with no modifications
if module.check_mode:
return result

# manipulate or modify the state as needed (this is going to be the
# part where your module will do what it needs to do)
ping_result = module.run_command('ping -c 1 {}'.format(module.params['target']))

# use whatever logic you need to determine whether or not this module
# made any modifications to your target
if module.params['target']:
result['debug'] = ping_result
result['rc'] = ping_result[0]
if result['rc']:
result['failed'] = True
module.fail_json(msg='failed to ping', **result)
else:
result['changed'] = True
module.exit_json(**result)

def main():
run_module()

if __name__ == '__main__':
main()

执行下面指令可以查看模板使用说明:

ansible-doc -M library icmp

8.2 创建插件

ansible插件是增强ansible的核心功能的代码片段,ansible使用插件架构来实现丰富,灵活和可扩展的功能集。

Ansible提供了许多方便的插件,也轻松自定义的插件。

官方lookup插件的源码:

https://github.com/ansible/ansible/blob/devel/lib/ansible/plugins/lookup/items.py

官方vars插件的源码:

https://github.com/ansible/ansible/blob/devel/lib/ansible/plugins/vars/host_group_vars.py

开发插件的官方文档:

http://docs.ansible.com/ansible/latest/dev_guide/developing_plugins.html

现有插件:

  • Action插件
  • Cache插件
  • Callback插件
  • Connection插件
  • Filters插件
  • Lookup插件
  • Strategy插件
  • Shell插件
  • Test插件
  • Vars插件
  • inventory插件

自定义lookup插件实现排序遍历

mkdir lookup_plugins && cd lookup_plugins
wget https://raw.githubusercontent.com/ansible/ansible/devel/lib/ansible/plugins/lookup/items.py
mv items.py sorted_items.py

sorted_items.py内容为:

# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
# (c) 2017 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

DOCUMENTATION = """
name: items
author: Michael DeHaan
version_added: historical
short_description: list of items
description:
- this lookup returns a list of items given to it, if any of the top level items is also a list it will flatten it, but it will not recurse
notes:
- this is the standard lookup used for loops in most examples
- check out the 'flattened' lookup for recursive flattening
- if you do not want flattening nor any other transformation look at the 'list' lookup.
options:
_terms:
description: list of items
required: True
"""

EXAMPLES = """
- name: "loop through list"
ansible.builtin.debug:
msg: "An item: {{ item }}"
with_items:
- 1
- 2
- 3

- name: add several users
ansible.builtin.user:
name: "{{ item }}"
groups: "wheel"
state: present
with_items:
- testuser1
- testuser2

- name: "loop through list from a variable"
ansible.builtin.debug:
msg: "An item: {{ item }}"
with_items: "{{ somelist }}"

- name: more complex items to add several users
ansible.builtin.user:
name: "{{ item.name }}"
uid: "{{ item.uid }}"
groups: "{{ item.groups }}"
state: present
with_items:
- { name: testuser1, uid: 1002, groups: "wheel, staff" }
- { name: testuser2, uid: 1003, groups: staff }

"""

RETURN = """
_raw:
description:
- once flattened list
type: list
"""

from ansible.plugins.lookup import LookupBase


class LookupModule(LookupBase):

def run(self, terms, **kwargs):

return self._flatten(terms)

lookup插件引用了下面地址的类方法

https://github.com/ansible/ansible/blob/devel/lib/ansible/plugins/lookup/init.py

其内容为:

# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.

# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

from abc import abstractmethod

from ansible.errors import AnsibleFileNotFound
from ansible.plugins import AnsiblePlugin
from ansible.utils.display import Display

display = Display()

__all__ = ['LookupBase']


class LookupBase(AnsiblePlugin):

def __init__(self, loader=None, templar=None, **kwargs):

super(LookupBase, self).__init__()

self._loader = loader
self._templar = templar

# Backwards compat: self._display isn't really needed, just import the global display and use that.
self._display = display

def get_basedir(self, variables):
if 'role_path' in variables:
return variables['role_path']
else:
return self._loader.get_basedir()

@staticmethod
def _flatten(terms):
ret = []
for term in terms:
if isinstance(term, (list, tuple)):
ret.extend(term)
else:
ret.append(term)
return ret

@staticmethod
def _combine(a, b):
results = []
for x in a:
for y in b:
results.append(LookupBase._flatten([x, y]))
return results

@staticmethod
def _flatten_hash_to_list(terms):
ret = []
for key in terms:
ret.append({'key': key, 'value': terms[key]})
return ret

@abstractmethod
def run(self, terms, variables=None, **kwargs):
"""
When the playbook specifies a lookup, this method is run. The
arguments to the lookup become the arguments to this method. One
additional keyword argument named ``variables`` is added to the method
call. It contains the variables available to ansible at the time the
lookup is templated. For instance::

"{{ lookup('url', 'https://toshio.fedorapeople.org/one.txt', validate_certs=True) }}"

would end up calling the lookup plugin named url's run method like this::
run(['https://toshio.fedorapeople.org/one.txt'], variables=available_variables, validate_certs=True)

Lookup plugins can be used within playbooks for looping. When this
happens, the first argument is a list containing the terms. Lookup
plugins can also be called from within playbooks to return their
values into a variable or parameter. If the user passes a string in
this case, it is converted into a list.

Errors encountered during execution should be returned by raising
AnsibleError() with a message describing the error.

Any strings returned by this method that could ever contain non-ascii
must be converted into python's unicode type as the strings will be run
through jinja2 which has this requirement. You can use::

from ansible.module_utils.common.text.converters import to_text
result_string = to_text(result_string)
"""
pass

def find_file_in_search_path(self, myvars, subdir, needle, ignore_missing=False):
'''
Return a file (needle) in the task's expected search path.
'''

if 'ansible_search_path' in myvars:
paths = myvars['ansible_search_path']
else:
paths = [self.get_basedir(myvars)]

result = None
try:
result = self._loader.path_dwim_relative_stack(paths, subdir, needle)
except AnsibleFileNotFound:
if not ignore_missing:
self._display.warning("Unable to find '%s' in expected paths (use -vvvvv to see paths)" % needle)

return result

def _deprecate_inline_kv(self):
# TODO: place holder to deprecate in future version allowing for long transition period
# self._display.deprecated('Passing inline k=v values embedded in a string to this lookup. Use direct ,k=v, k2=v2 syntax instead.', version='2.18')
pass

修改sorted_items.py最后一行返回有序列表:return self._flatten(sorted(terms, key=str)),并将所有的with_items修改为with_sorted_items。

创建一个剧本用于测试:

---

hosts: centos1

tasks:
- name: loop through list
debug:
msg: "An item: {{item}}"
with_sorted_items:
- 3
- 2
- 1
- Z
- A
- M

...

自定义filter插件实现字符串逆序+大写

mkdir filter_plugins && cd filter_plugins
wget https://raw.githubusercontent.com/ansible/ansible/devel/lib/ansible/plugins/filter/core.py
mv core.py reverse_upper.py

修改reverse_upper.py内容为:

# (c) 2012, Jeroen Hoekx <jeroen@hoekx.be>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

import base64
import glob
import hashlib
import json
import ntpath
import os.path
import re
import shlex
import sys
import time
import uuid
import yaml
import datetime

from collections.abc import Mapping
from functools import partial
from random import Random, SystemRandom, shuffle

from jinja2.filters import pass_environment

from ansible.errors import AnsibleError, AnsibleFilterError, AnsibleFilterTypeError
from ansible.module_utils.six import string_types, integer_types, reraise, text_type
from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
from ansible.module_utils.common.collections import is_sequence
from ansible.module_utils.common.yaml import yaml_load, yaml_load_all
from ansible.parsing.ajson import AnsibleJSONEncoder
from ansible.parsing.yaml.dumper import AnsibleDumper
from ansible.template import recursive_check_defined
from ansible.utils.display import Display
from ansible.utils.encrypt import passlib_or_crypt, PASSLIB_AVAILABLE
from ansible.utils.hashing import md5s, checksum_s
from ansible.utils.unicode import unicode_wrap
from ansible.utils.vars import merge_hash

display = Display()

UUID_NAMESPACE_ANSIBLE = uuid.UUID('361E6D51-FAEC-444A-9079-341386DA8E2E')

def reverse_upper(string):
"""Reverse and upper string """
return string[::-1].upper()

class FilterModule(object):
''' Ansible core jinja2 filters '''

def filters(self):
return {
'reverse_upper': reverse_upper,
}

创建一个剧本用于测试:

---

-
hosts: all

tasks:
- name: Reverse and upper ansible_distribution
debug:
msg: "Reverse and upper of ansible_distribution: {{ ansible_distribution | reverse_upper }}"

...

9. 故障排除和最佳实践

故障排除和最佳实践

9.1 故障排除

SSH连接错误

模拟:从ubuntu-c连接ubuntu1后,修改ubuntu1主机上~/.ssh/authorized_keys的权限为777

ssh ubuntu1

chmod 777 ~/.ssh/authorized_keys

exit

此时,重新登录时SSH免密码登录失效,提示输入密码

解决:

  1. 使用ssh -v ubuntu1查看有用信息,但还是提示输入登录ubuntu1的密码

  2. 在新窗口连接ubuntu1,输入root用户名和密码,登录ubuntu后,执行/usr/sbin/sshd -d -p 1234

  3. ubuntu-c主机执行ssh ubuntu1 -p 1234,然后ssh -v ubuntu1信息窗口会出现因为所有权模式身份验证拒绝错误,由此确认了问题所在。

剧本语法检查

如:

ansible-playbook xxx_playbook.yaml --syntax-check

每个任务都选择是否执行

如:

ansible-playbook xxx_playbook.yaml --step

指定开始执行的任务

如:

ansible-playbook xxx_playbook.yaml --start-at-task="Install python-dnspython"

日志路径

在ansible.cfg配置log_path,指定保存执行日志的文件,如:

[default]
inventory=hosts
host_key_checking=False
forks=6
log_path=log.txt

详细程度

详细程度有4级

  • -v 1级 输出数据显示
  • -vv 2级 输入输出显示
  • -vvv 3级 获取提供的附加信息,用于连接到托管主机
  • -vvvv 4级 提供额外的详细信息,包括连接插件和脚本以及用户上下文

如:

ansible-playbook -vvvv xxx_playbook.yaml

9.2 最佳实践

官方最佳实践:https://docs.ansible.com/ansible/latest/tips_tricks/index.html