Ansible 中级¶
在本章中,您将继续学习如何使用 Ansible。
目标:在本章中,您将学习如何
使用变量;
使用循环;
管理状态更改并对其做出反应;
管理异步任务。
ansible、模块、剧本
知识:
复杂度:
阅读时间:30 分钟
在上一章中,您学习了如何安装 Ansible、在命令行上使用它以及编写剧本以提高代码的可重用性。
在本章中,我们将开始发现更多关于如何使用 Ansible 的高级概念以及一些您将定期使用的有趣任务。
变量¶
注意
更多信息可以在 此处找到。
在 Ansible 下,有不同类型的原始变量
- 字符串,
- 整数,
- 布尔值。
这些变量可以组织成
- 字典,
- 列表。
变量可以在不同的位置定义,例如剧本、角色或命令行。
例如,从剧本
---
- hosts: apache1
vars:
port_http: 80
service:
debian: apache2
rhel: httpd
或从命令行
ansible-playbook deploy-http.yml --extra-vars "service=httpd"
定义后,变量可以通过在双括号之间调用它来使用
{{ port_http }}
用于简单值,{{ service['rhel'] }}
或{{ service.rhel }}
用于字典。
例如
- name: make sure apache is started
ansible.builtin.systemd:
name: "{{ service['rhel'] }}"
state: started
当然,也可以访问 Ansible 的全局变量(事实)(操作系统类型、IP 地址、虚拟机名称等)。
外包变量¶
变量可以包含在剧本外部的文件中,在这种情况下,此文件必须在剧本中使用 vars_files
指令定义
---
- hosts: apache1
vars_files:
- myvariables.yml
myvariables.yml
文件
---
port_http: 80
ansible.builtin.systemd::
debian: apache2
rhel: httpd
它也可以使用 include_vars
模块动态添加
- name: Include secrets.
ansible.builtin.include_vars:
file: vault.yml
显示变量¶
要显示变量,您必须按如下方式激活 debug
模块
- ansible.builtin.debug:
var: service['debian']
您也可以在文本中使用变量
- ansible.builtin.debug:
msg: "Print a variable in a message : {{ service['debian'] }}"
保存任务的返回值¶
要保存任务的返回值并能够稍后访问它,您必须在任务本身内部使用关键字 register
。
使用存储的变量
- name: /home content
shell: ls /home
register: homes
- name: Print the first directory name
ansible.builtin.debug:
var: homes.stdout_lines[0]
- name: Print the first directory name
ansible.builtin.debug:
var: homes.stdout_lines[1]
注意
变量 homes.stdout_lines
是一个类型为字符串的变量列表,它是我们尚未遇到的组织变量的一种方式。
构成存储的变量的字符串可以通过 stdout
值访问(允许您执行 homes.stdout.find("core") != -1
之类的事情),使用循环利用它们(参见 loop
),或者只是通过它们的索引访问,如前面的示例所示。
练习:¶
编写一个使用全局变量的剧本
play-vars.yml
,打印目标的发布版名称和主要版本。编写一个使用以下字典的剧本,以显示将要安装的服务
service:
web:
name: apache
rpm: httpd
db:
name: mariadb
rpm: mariadb-server
默认类型应为“web”。
使用命令行覆盖
type
变量将变量外部化到
vars.yml
文件中
循环管理¶
循环允许您将任务迭代到列表、散列或字典上,例如。
注意
更多信息可以在 此处找到。
一个简单的使用示例,创建 4 个用户
- name: add users
user:
name: "{{ item }}"
state: present
groups: "users"
loop:
- antoine
- patrick
- steven
- xavier
在循环的每次迭代中,所使用的列表的值存储在item
变量中,可在循环代码中访问。
当然,列表可以在外部文件中定义
users:
- antoine
- patrick
- steven
- xavier
并在任务中以这种方式使用(在包含了vars文件之后)
- name: add users
user:
name: "{{ item }}"
state: present
groups: "users"
loop: "{{ users }}"
我们可以使用在学习存储变量时看到的示例来改进它。存储变量的使用
- name: /home content
shell: ls /home
register: homes
- name: Print the directories name
ansible.builtin.debug:
msg: "Directory => {{ item }}"
loop: "{{ homes.stdout_lines }}"
字典也可以在循环中使用。
在这种情况下,您必须使用jinja过滤器(jinja是Ansible使用的模板引擎)将字典转换为具有| dict2items
的项目。
在循环中,可以使用item.key
,它对应于字典键,以及item.value
,它对应于键的值。
让我们通过一个具体的示例来了解一下,展示了系统用户的管理
---
- hosts: rocky8
become: true
become_user: root
vars:
users:
antoine:
group: users
state: present
steven:
group: users
state: absent
tasks:
- name: Manage users
user:
name: "{{ item.key }}"
group: "{{ item.value.group }}"
state: "{{ item.value.state }}"
loop: "{{ users | dict2items }}"
注意
循环可以用在很多事情上。当您使用Ansible来更复杂地使用它们时,您会发现它们提供的可能性。
练习:¶
- 使用循环显示上一个练习中
service
变量的内容。
注意
您需要使用jinja过滤器list
将service
变量(它是一个字典)转换为列表,如下所示
{{ service.values() | list }}
条件语句¶
注意
更多信息可以在这里找到。
when
语句在很多情况下都非常有用,例如,不在特定类型的服务器上执行某些操作,如果文件或用户不存在等等。
注意
在when
语句的后面,变量不需要双大括号(实际上,它们是Jinja2表达式...)。
- name: "Reboot only Debian servers"
reboot:
when: ansible_os_family == "Debian"
条件可以使用括号进行分组
- name: "Reboot only CentOS version 6 and Debian version 7"
reboot:
when: (ansible_distribution == "CentOS" and ansible_distribution_major_version == "6") or
(ansible_distribution == "Debian" and ansible_distribution_major_version == "7")
与逻辑AND对应的条件可以作为列表提供
- name: "Reboot only CentOS version 6"
reboot:
when:
- ansible_distribution == "CentOS"
- ansible_distribution_major_version == "6"
您可以测试布尔值并验证它是否为真
- name: check if directory exists
stat:
path: /home/ansible
register: directory
- ansible.builtin.debug:
var: directory
- ansible.builtin.debug:
msg: The directory exists
when:
- directory.stat.exists
- directory.stat.isdir
您也可以测试它是否不为真
when:
- file.stat.exists
- not file.stat.isdir
您可能需要测试变量是否存在,以避免执行错误
when: myboolean is defined and myboolean
练习:¶
- 仅当
type
等于web
时,打印service.web
的值。
管理更改:handlers
¶
注意
更多信息可以在这里找到。
当发生更改时,允许处理程序启动操作,例如重启服务。
一个模块是幂等的,一个剧本可以检测到远程系统上发生了重大更改,从而触发一个操作来响应此更改。在剧本任务块的末尾发送一个通知,并且响应操作将只触发一次,即使多个任务发送相同的通知。
例如,多个任务可能表明httpd
服务需要由于其配置文件的更改而重新启动。但是,该服务将只重新启动一次,以避免多次不必要的启动。
- name: template configuration file
template:
src: template-site.j2
dest: /etc/httpd/sites-availables/test-site.conf
notify:
- restart memcached
- restart httpd
处理程序是一种由唯一的全局名称引用的任务
- 一个或多个通知器激活它。
- 它不会立即启动,而是在所有任务完成后等待运行。
处理程序示例
handlers:
- name: restart memcached
systemd:
name: memcached
state: restarted
- name: restart httpd
systemd:
name: httpd
state: restarted
从Ansible 2.2版本开始,处理程序也可以直接监听
handlers:
- name: restart memcached
systemd:
name: memcached
state: restarted
listen: "web services restart"
- name: restart apache
systemd:
name: apache
state: restarted
listen: "web services restart"
tasks:
- name: restart everything
command: echo "this task will restart the web services"
notify: "web services restart"
异步任务¶
注意
更多信息可以在这里找到。
默认情况下,SSH连接到主机将在所有节点上执行各种剧本任务时保持打开状态。
这会导致一些问题,特别是
- 如果任务的执行时间长于SSH连接超时
- 如果连接在操作过程中中断(例如服务器重启)
在这种情况下,您需要切换到异步模式,并指定最大执行时间和检查主机状态的频率(默认情况下为10秒)。
通过指定为0的轮询值,Ansible将执行任务并继续执行,而不必担心结果。
以下是一个使用异步任务的示例,它允许您重新启动服务器并等待端口22再次可访问
# Wait 2s and launch the reboot
- name: Reboot system
shell: sleep 2 && shutdown -r now "Ansible reboot triggered"
async: 1
poll: 0
ignore_errors: true
become: true
changed_when: False
# Wait the server is available
- name: Waiting for server to restart (10 mins max)
wait_for:
host: "{{ inventory_hostname }}"
port: 22
delay: 30
state: started
timeout: 600
delegate_to: localhost
您也可以决定启动一个长时间运行的任务并忘记它(fire and forget),因为执行对剧本来说并不重要。
练习结果¶
- 编写一个剧本
play-vars.yml
,使用全局变量,打印目标的发布名称和主要版本。
---
- hosts: ansible_clients
tasks:
- name: Print globales variables
debug:
msg: "The distribution is {{ ansible_distribution }} version {{ ansible_distribution_major_version }}"
$ ansible-playbook play-vars.yml
PLAY [ansible_clients] *********************************************************************************
TASK [Gathering Facts] *********************************************************************************
ok: [192.168.1.11]
TASK [Print globales variables] ************************************************************************
ok: [192.168.1.11] => {
"msg": "The distribution is Rocky version 8"
}
PLAY RECAP *********************************************************************************************
192.168.1.11 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
- 编写一个使用以下字典的剧本,以显示将要安装的服务
service:
web:
name: apache
rpm: httpd
db:
name: mariadb
rpm: mariadb-server
默认类型应为“web”。
---
- hosts: ansible_clients
vars:
type: web
service:
web:
name: apache
rpm: httpd
db:
name: mariadb
rpm: mariadb-server
tasks:
- name: Print a specific entry of a dictionary
debug:
msg: "The {{ service[type]['name'] }} will be installed with the packages {{ service[type].rpm }}"
$ ansible-playbook display-dict.yml
PLAY [ansible_clients] *********************************************************************************
TASK [Gathering Facts] *********************************************************************************
ok: [192.168.1.11]
TASK [Print a specific entry of a dictionnaire] ********************************************************
ok: [192.168.1.11] => {
"msg": "The apache will be installed with the packages httpd"
}
PLAY RECAP *********************************************************************************************
192.168.1.11 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
- 使用命令行覆盖
type
变量
ansible-playbook --extra-vars "type=db" display-dict.yml
PLAY [ansible_clients] *********************************************************************************
TASK [Gathering Facts] *********************************************************************************
ok: [192.168.1.11]
TASK [Print a specific entry of a dictionary] ********************************************************
ok: [192.168.1.11] => {
"msg": "The mariadb will be installed with the packages mariadb-server"
}
PLAY RECAP *********************************************************************************************
192.168.1.11 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
- 将变量外部化到
vars.yml
文件中
type: web
service:
web:
name: apache
rpm: httpd
db:
name: mariadb
rpm: mariadb-server
---
- hosts: ansible_clients
vars_files:
- vars.yml
tasks:
- name: Print a specific entry of a dictionary
debug:
msg: "The {{ service[type]['name'] }} will be installed with the packages {{ service[type].rpm }}"
- 使用循环显示上一个练习中
service
变量的内容。
注意
您需要使用jinja过滤器dict2items
或list
将service
变量(它是一个字典)转换为项目或列表,如下所示
{{ service | dict2items }}
{{ service.values() | list }}
使用dict2items
---
- hosts: ansible_clients
vars_files:
- vars.yml
tasks:
- name: Print a dictionary variable with a loop
debug:
msg: "{{item.key }} | The {{ item.value.name }} will be installed with the packages {{ item.value.rpm }}"
loop: "{{ service | dict2items }}"
$ ansible-playbook display-dict.yml
PLAY [ansible_clients] *********************************************************************************
TASK [Gathering Facts] *********************************************************************************
ok: [192.168.1.11]
TASK [Print a dictionary variable with a loop] ********************************************************
ok: [192.168.1.11] => (item={'key': 'web', 'value': {'name': 'apache', 'rpm': 'httpd'}}) => {
"msg": "web | The apache will be installed with the packages httpd"
}
ok: [192.168.1.11] => (item={'key': 'db', 'value': {'name': 'mariadb', 'rpm': 'mariadb-server'}}) => {
"msg": "db | The mariadb will be installed with the packages mariadb-server"
}
PLAY RECAP *********************************************************************************************
192.168.1.11 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
使用list
---
- hosts: ansible_clients
vars_files:
- vars.yml
tasks:
- name: Print a dictionary variable with a loop
debug:
msg: "The {{ item.name }} will be installed with the packages {{ item.rpm }}"
loop: "{{ service.values() | list}}"
~
$ ansible-playbook display-dict.yml
PLAY [ansible_clients] *********************************************************************************
TASK [Gathering Facts] *********************************************************************************
ok: [192.168.1.11]
TASK [Print a dictionary variable with a loop] ********************************************************
ok: [192.168.1.11] => (item={'name': 'apache', 'rpm': 'httpd'}) => {
"msg": "The apache will be installed with the packages httpd"
}
ok: [192.168.1.11] => (item={'name': 'mariadb', 'rpm': 'mariadb-server'}) => {
"msg": "The mariadb will be installed with the packages mariadb-server"
}
PLAY RECAP *********************************************************************************************
192.168.1.11 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
- 仅当
type
等于web
时,打印service.web
的值。
---
- hosts: ansible_clients
vars_files:
- vars.yml
tasks:
- name: Print a dictionary variable
debug:
msg: "The {{ service.web.name }} will be installed with the packages {{ service.web.rpm }}"
when: type == "web"
- name: Print a dictionary variable
debug:
msg: "The {{ service.db.name }} will be installed with the packages {{ service.db.rpm }}"
when: type == "db"
$ ansible-playbook display-dict.yml
PLAY [ansible_clients] *********************************************************************************
TASK [Gathering Facts] *********************************************************************************
ok: [192.168.1.11]
TASK [Print a dictionary variable] ********************************************************************
ok: [192.168.1.11] => {
"msg": "The apache will be installed with the packages httpd"
}
TASK [Print a dictionary variable] ********************************************************************
skipping: [192.168.1.11]
PLAY RECAP *********************************************************************************************
192.168.1.11 : ok=2 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
$ ansible-playbook --extra-vars "type=db" display-dict.yml
PLAY [ansible_clients] *********************************************************************************
TASK [Gathering Facts] *********************************************************************************
ok: [192.168.1.11]
TASK [Print a dictionary variable] ********************************************************************
skipping: [192.168.1.11]
TASK [Print a dictionary variable] ********************************************************************
ok: [192.168.1.11] => {
"msg": "The mariadb will be installed with the packages mariadb-server"
}
PLAY RECAP *********************************************************************************************
192.168.1.11 : ok=2 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0