90DaysOfDevOps 第 68 天:Ansible 标签、变量、Inventory 与数据库服务器配置实战

原创2026-10-02 09:03:021,350 阅读
文章标签:文档/教程

90DaysOfDevOps 第 68 天:Ansible 标签、变量、Inventory 与数据库服务器配置实战

本篇文章来自 90DaysOfDevOps 学习路线图 2022 版第 68 天的内容(对应 2022/Days/day68.md),聚焦 Ansible 自动化运维中的四个关键实战主题:用标签(Tags)精细控制 playbook 的执行范围、用用户自定义变量与 Ansible Facts 让配置模板动态化、用独立 Inventory 文件管理多环境主机清单,以及最终通过一个全新的 mysql 角色把数据库服务器 db01 接入自动化体系。读完本文,你将掌握 --tags、--list-tags、setup 模块、group_vars/host_vars 目录约定与 MySQL 角色的完整落地方法,并能在自己的 Ansible 环境中直接复现整套多主机编排。

回顾:我们正在搭建的多节点环境

前几天的内容已经在本地通过 Vagrant 建起了一组虚拟机(web01、web02、loadbalancer 与 db01),其定义位于 2022/Days/Configmgmt/Vagrantfile。从该文件中可以看到:

  • db01(192.168.169.130)、web01(192.168.169.131)、web02(192.168.169.132)、loadbalancer(192.168.169.134)四台机器均使用 bento/ubuntu-21.10 镜像;
  • 每台机器把宿主机的 22 端口分别映射为 2210、2211、2212、2213,方便控制节点逐个 SSH 访问;
  • 每台虚拟机分配 2048MB 内存,并接入桥接网络。

控制节点(本地主机)通过 /etc/ansible/hosts 默认 Inventory 文件管理这些主机,将其划分为 webservers、proxy 与 database 三个组。本文将基于这个环境,逐步完成“标签化执行 → 变量外置 → 动态模板 → 数据库角色部署”的完整链路。

Tags:让大 playbook 按需分片执行

在昨天(Day 67)留下的 playbook 中,webservers 与 proxy 两组 play 会顺序执行到结束。当 playbook 变得又长又大时,这种“全量执行”并不高效——此时标签(Tags)可以把任务分组,按需只跑其中一部分。本部分使用的示例文件为 ansible-scenario5/playbook5.yml:

- hosts: webservers
  become: yes
  vars:
    http_port: 8000
    https_port: 4443
    html_welcome_msg: "Hello 90DaysOfDevOps - Welcome to Day 66!"
  roles:
    - common
    - apache2
  tags: web

- hosts: proxy
  become: yes
  roles:
    - common
    - nginx
  tags: proxy

这里的要点是:tags 既可以写在 play 级别(如上例),也可以写到任务(task)级别,从而在单个 play 内部实现更细粒度的控制。例如为安装、配置、卸载类任务分别打上不同的标签。

查看与筛选标签的常用命令

  • ansible-playbook playbook5.yml --list-tags:不实际执行,只列出该 playbook 中已定义的全部标签,便于在运行前确认标签结构;
  • ansible-playbook playbook5.yml --tags proxy:只执行带有 proxy 标签的 play 或任务,其余跳过;
  • ansible-playbook playbook5.yml --tags proxy,web:同时执行带 proxy 与 web 标签的内容(多个标签可以捆绑使用,一个 play 或任务也可以同时声明多个标签);
  • ansible-playbook playbook5.yml --skip-tags <tag>:反选,跳过指定标签。

特殊的 always 标签

文档中还强调了一个非常实用的标签值——tag: always。任何被打上 always 的 play 或任务,无论你在命令行使用哪个 --tags,它都会被执行。这非常适合放置那些每次都必须运行的公共步骤,例如基础环境检查、事实收集或安全加固任务,避免被 --tags 过滤掉。

变量体系:Ansible Facts 与用户自定义变量

Ansible 的变量主要分为两大类:

  • 用户自定义变量(User created variables):由我们自己在 playbook、vars 段或变量文件中定义;
  • Ansible Facts(事实):每次运行 playbook 时自动收集的目标主机信息,对应任务名为“Gathering facts”。

Ansible Facts:用 setup 模块洞察目标主机

每一次 ansible-playbook 执行时,默认都会先执行一个我们没有显式定义的任务——“Gathering facts”,它把目标主机的系统信息以 JSON 形式带回控制节点。我们可以单独用 setup 模块查看这些数据:

ansible proxy -m setup

输出内容非常庞大,建议重定向到文件再研究:

ansible proxy -m setup >> facts.json

仓库中保存了一份真实运行结果 ansible-scenario5/facts.json,展开后可以看到 ansible_all_ipv4_addresses(IP 地址)、ansible_architecture(架构,如 x86_64)、ansible_bios_version(BIOS 版本)等成百上千个键值。这些字段可以直接在 playbook 与模板中以 ansible_facts['xxx'] 的形式引用。

用 Facts + 循环模板实现负载均衡动态配置

在 Day 66/67 中,nginx 负载均衡模板 ansible-scenario4/roles/nginx/templates/mysite.j2 里硬编码了两个 web 服务器的 IP:

upstream webservers {
    server 192.168.169.131:8000;
    server 192.168.169.132:8000;
}

现在借助 Facts 与 Jinja2 的 for 循环,可以遍历 groups<a href="https://link.gitcode.com/i/2600d9eb49bf301468280cc412d4db98" target="_blank">'webservers'] 组并动态生成 upstream 配置(见 [ansible-scenario5/mysite.j2):

#Dynamic Config for server {{ ansible_facts['nodename'] }}
    upstream webservers {
  {% for host in groups['webservers'] %}
        server {{ hostvars[host]['ansible_facts']['nodename'] }}:8000;
    {% endfor %}
    }

    server {
        listen 80;

        location / {
                proxy_pass http://webservers;
        }
    }

这段模板的关键点:

  • groups['webservers'] 是 Inventory 中的主机组,Ansible 在模板上下文里自动提供;
  • hostvars[host] 用于从控制节点侧读取其他主机(而不是当前主机)的事实,其中 nodename 即目标主机的节点名;
  • 前提条件:目标环境必须配置好名称解析(DNS 或 /etc/hosts),因为这里使用的是主机名而非 IP 地址。

这样一来,将来新增或下线 web 服务器时,无需再手工编辑负载均衡配置——重跑 playbook 即可让 upstream 自动增减成员,这正是“配置即代码”的体现。

用户自定义变量:从 playbook 内联到 group_vars 外置

用户自定义变量最初直接写在 playbook 的 vars: 段(见上文的 playbook5.yml)。为了让 playbook 更清爽、变量可复用,Ansible 支持按目录约定把变量外置到独立文件。本部分的实战目录是 ansible-scenario6,具体做法如下:

  1. 在 ansible-scenario6 根目录创建 group_vars/all/ 文件夹(all 表示所有主机组都会获得这些变量);
  2. 在 group_vars/all/ 下创建 common_variables.yml,把 playbook 中的变量整体搬进去:
http_port: 8000
https_port: 4443
html_welcome_msg: "Hello 90DaysOfDevOps - Welcome to Day 68!"
  1. 从 playbook 中删除 vars: 段及其变量定义,得到干净的 playbook6.yml:
- hosts: webservers
  become: yes
  roles:
    - common
    - apache2
  tags: web

- hosts: proxy
  become: yes
  roles:
    - common
    - nginx
  tags: proxy

由于这些变量对所有组生效(全局性质),还可以顺手把 NTP、DNS 服务器等公共配置也放进来。变量生效范围由目录结构决定:group_vars/all/ 面向全部主机,group_vars/<组名>/ 面向某个主机组,而 host_vars/<主机名>.yml 则面向单台主机——例如可以创建 host_vars/web01.yml 让某台 web 服务器显示专属欢迎语。

外置后的变量在模板中的引用方式不变。nginx 模板 ansible-scenario7/roles/nginx/templates/mysite.j2 直接用 {{ http_port }} 替换了硬编码端口:

#Dynamic Config for server {{ ansible_facts['nodename'] }}
    upstream webservers {
  {% for host in groups['webservers'] %}
        server {{ hostvars[host]['ansible_facts']['nodename'] }}:{{ http_port }};
    {% endfor %}
    }

    server {
        listen 80;

        location / {
                proxy_pass http://webservers;
        }
    }

apache2 的首页模板 ansible-scenario6/roles/apache2/templates/index.html.j2 则同时使用用户变量与 Facts,让访问者一眼看出当前请求落到了哪台 web 服务器:

<html>

<h1>{{ html_welcome_msg }}! I'm webserver {{ ansible_facts['nodename'] }} </h1>

</html>

运行 ansible-playbook playbook6.yml 后反复刷新负载均衡地址,即可看到请求在 web01/web02 之间切换,页面标题会随之变化——这验证了动态模板与 Facts 引用的正确性。

Inventory 文件:按环境拆分主机清单

到目前为止,我们一直使用默认的 /etc/ansible/hosts 作为 Inventory。但在真实环境中,生产、预发布(staging)、开发往往需要不同的主机清单。解决方案是把 Inventory 拆成多个独立文件,并通过 -i 参数指定:

ansible-playbook -i dev playbook.yml

例如创建 dev、staging、production 三个 Inventory 文件,内容各自包含对应环境的主机与组。Inventory 文件内部同样可以定义变量,这些变量随后能在 playbook 或模板中直接引用——在文档所参考的课程示例中,就是把“环境名”变量定义在 hosts 文件里,再输出到负载均衡的网页模板,让页面上显示当前所处的环境(如 dev/staging/production)。这是“Inventory 不只是主机列表,也是变量来源”的典型用法。

实战:用 mysql 角色部署数据库服务器 db01

最后一块拼图是尚未通电配置的 db01。本部分工作目录为 ansible-scenario7。

第一步:启动虚拟机并打通 SSH

在 Vagrantfile 所在目录执行:

vagrant up db01

机器启动并可访问后,先把控制节点的 SSH 公钥拷贝过去,保证后续 Ansible 能免密连接:

ssh-copy-id db01

第二步:用 ansible-galaxy 初始化 mysql 角色

ansible-galaxy init roles/mysql

该命令会在 roles/mysql 下自动生成一套标准角色目录(tasks/、handlers/、templates/、defaults/、vars/、meta/、tests/、README.md 等)。仓库中的成品结构可见 ansible-scenario7/roles/mysql。

第三步:在 playbook 中追加 database play

在 playbook7.yml 中新增数据库组的 play,应用 common 与新建的 mysql 角色,并打上 database 标签:

- hosts: webservers
  become: yes
  roles:
    - common
    - apache2
  tags:
    web

- hosts: proxy
  become: yes
  roles:
    - common
    - nginx
  tags:
    proxy

- hosts: database
  become: yes
  roles:
    - common
    - mysql
  tags: database

database 组已存在于 /etc/ansible/hosts 中(对应 Vagrantfile 里的 db01 机器)。得益于标签机制,后续我们可以只针对数据库组部署,而不必重跑全部 play。

第四步:填充角色内容

Handlers —— handlers/main.yml 定义 MySQL 服务重启动作,供模板变更后通过 notify 触发:

---
# handlers file for roles/mysql
- name: restart mysql
  service:
    name: mysql
    state: restarted

Tasks 入口 —— tasks/main.yml 是指针文件,按顺序引入两个任务文件:

---
# tasks file for roles/mysql
- import_tasks: install_mysql.yml
- import_tasks: setup_mysql.yml

Tasks:安装与启动 —— tasks/install_mysql.yml 负责安装依赖包、MySQL 服务端与 PyMySQL 模块,并确保服务启动:

- name: "Install Common packages"
  apt: name={{ item }} state=latest
  with_items:
   - python3-pip
   - mysql-client
   - python3-mysqldb
   - libmysqlclient-dev

- name: Ensure mysql-server is installed latest version
  apt: name=mysql-server state=latest

- name: Installing python module MySQL-python
  pip:
    name: PyMySQL

- name: Ensure mysql-server is running
  service:
    name: mysql
    state: started

Tasks:建库与建用户 —— tasks/setup_mysql.yml 先渲染 MySQL 配置模板并通知重启,再通过 community.mysql.mysql_user 与 mysql_db 模块创建用户与数据库:

- name: Create my.cnf configuration file
  template: src=templates/my.cnf.j2 dest=/etc/mysql/conf.d/mysql.cnf
  notify: restart mysql

- name: Create database user with name 'devops' and password 'DevOps90' with all database privileges
  community.mysql.mysql_user:
    login_unix_socket: /var/run/mysqld/mysqld.sock
    login_user: "{{ mysql_user_name }}"
    login_password: "{{ mysql_user_password }}"
    name: "{{db_user}}"
    password: "{{db_pass}}"
    priv: '*.*:ALL,GRANT'
    host: '%'
    state: present

- name: Create a new database with name '90daysofdevops'
  mysql_db:
    login_user: "{{ db_user }}"
    login_password: "{{ db_pass }}"
    name: "{{ db_name }}"
    state: present

- name: Enable remote login to mysql
  lineinfile:
    path: /etc/mysql/mysql.conf.d/mysqld.cnf
    regexp: '^bind-address'
    line: 'bind-address = 0.0.0.0'
    backup: yes
  notify:
      - Restart mysql

几点值得注意的实践细节(均来自仓库中的真实文件):

  • login_unix_socket: /var/run/mysqld/mysqld.sock 这一行是原作者在实战中发现并补充的:当 root 账号在本机通过 Unix Socket 认证时,Ansible 需要显式指定 socket 路径才能成功连接 MySQL 实例;
  • 仓库版本中 mysql_db 任务使用 db_user/db_pass 连接(而非 root),并且额外增加了一个 lineinfile 任务把 bind-address 改为 0.0.0.0 以支持远程登录(对应模板中开放监听地址的意图);
  • 模板文件 templates/my.cnf.j2 内容如下:
[mysql]
bind-address = 0.0.0.0
[client]
user=root
password=vagrant

它被渲染到目标机器的 /etc/mysql/conf.d/mysql.cnf,其中 [client] 段提供默认连接账号,bind-address = 0.0.0.0 让 MySQL 监听所有接口。

变量集中管理 —— 上述任务中引用的用户名、密码、数据库名都来自 group_vars/all/common_variables.yml:

http_port: 8000
https_port: 4443
html_welcome_msg: "Hello 90DaysOfDevOps - Welcome to Day 68!"

mysql_user_name: root
mysql_user_password: vagrant
db_user: devops
db_pass: DevOps90
db_name: 90DaysOfDevOps

注意:把明文口令写入版本库是演示环境下的简化做法,生产环境应改用 Ansible Vault 等加密机制。

第五步:执行 playbook 并排障

完整部署全部机器:

ansible-playbook playbook7.yml

也可以只针对数据库组:

ansible-playbook playbook7.yml --tags database

文档记录了一个真实排障过程:仅对 database 标签执行时报错,提示缺少 pip3(Python)。解决办法是把 python3-pip 加进公共角色 common 的任务中安装——仓库里 common 角色的 install_tools.yml 展示了这类公共工具任务的形态(该文件安装了 neofetch、tree、figlet),而 mysql 角色的 install_mysql.yml 则已包含 python3-pip、mysql-client、python3-mysqldb、libmysqlclient-dev 等依赖。补齐依赖后重新运行,部署即成功。

第六步:在 db01 上验证结果

从控制节点登录:

ssh db01

连接 MySQL(root 使用 vagrant 密码):

sudo /usr/bin/mysql -u root -p

依次确认用户与数据库是否创建成功:

select user, host from mysql.user;
SHOW DATABASES;

预期能看到 devops 用户以及 90DaysOfDevOps 数据库。此外,还可以用应用账号验证连接方式:

sudo /usr/bin/mysql -u devops -p   # 密码为 DevOps90

至此,四台虚拟机(web01、web02、loadbalancer、db01)已全部纳入 Ansible 自动化管理,一个包含 Web 应用、负载均衡与数据库的最小化 DevOps 环境闭环完成。

小结

Day 68 的核心收获可以浓缩为四条可复用的经验:

  1. 标签让大 playbook 可分片执行:--tags / --list-tags / --skip-tags 组合使用,always 标签保障公共步骤不被过滤;
  2. Facts 是现成的机器情报:ansible <host> -m setup 导出的 JSON(见 facts.json)配合 Jinja2 循环,可把硬编码配置升级为动态配置;
  3. 变量外置符合“约定优于配置”:group_vars/all、group_vars/<组>、host_vars/<主机> 三级目录约定,配合 -i 多 Inventory 文件即可支撑多环境;
  4. 角色化是模块复用的抓手:ansible-galaxy init 生成标准骨架,install_mysql.yml 管安装、setup_mysql.yml 管建库建用户、my.cnf.j2 管配置,handlers + notify 保证配置变更后自动重启服务。

如果你希望继续沿着这条路线前进,下一步(Day 69)将在本环境基础上做进一步扩展,完整学习路径可参考仓库根目录的 2022/Days/day69.md。

登录后查看全文
90DaysOfDevOps