Rocky Linux 9 初始化自动化:Ansible 安全加固实战

1. 项目概述:为什么 Rocky Linux 9 的初始服务器配置值得用 Ansible 自动化?

你刚在云平台或物理机上拉起一台全新的 Rocky Linux 9 服务器,SSH 连上去,第一件事是什么?手动执行 dnf update -y ,然后逐条敲 useradd passwd visudo 配置 sudo 权限,再 firewall-cmd --permanent --add-service=ssh 开放防火墙,接着 sshd_config 改密码登录为禁用、密钥登录为启用,最后还得 systemctl restart sshd 并验证——这一套流程,我做过不下 87 次。前 5 次还觉得是“基础操作”,第 20 次开始手抖打错 PermitRootLogin no 里的空格,第 43 次因为忘了 restorecon -Rv /etc/ssh 导致 SELinux 拒绝新密钥登录,第 66 次在三台服务器上配了不一致的时区和 NTP 源……直到第 72 次,客户凌晨三点发来截图:“您部署的测试环境 SSH 登录失败”,而我翻着终端历史发现,那台机器漏掉了 semanage port -a -t ssh_port_t -p tcp 2222 这一行——就因为它是批量脚本里唯一一个需要自定义端口的节点。

这就是手动初始化的硬伤:它不是“能不能做”,而是“能不能每次都做对、做全、做快、做一致”。Rocky Linux 9 作为 RHEL 9 的社区下游发行版,继承了 Enterprise Linux 体系的严谨性,但也带来了更严格的默认策略(SELinux enforcing、firewalld 默认启用、OpenSSH 8.7+ 的严格密钥协商要求、dnf5 替代 yum)、更细粒度的模块化仓库(AppStream、CRB、BaseOS 分离),以及对 FIPS 模式、CPE 标识、CIS 基准的原生支持。这意味着,一套适用于 CentOS 7 或 even Rocky 8 的初始化脚本,在 Rocky 9 上大概率会卡在 dnf module enable php:remi-8.2 这一步,或者因 sshd 默认禁用 rsa-sha2-512 算法导致旧客户端连接失败。

Ansible 正是为此而生的解药。它不依赖 agent,仅靠 SSH 和 Python 即可工作;它的 playbook 是纯文本 YAML,天然支持版本控制、代码审查与协作;它的模块(如 user firewalld lineinfile copy )直接封装了 Rocky 9 下最稳妥的操作路径,比如 firewalld 模块会自动处理 firewalld 服务状态、zone 配置、rich rule 语法,完全规避手动敲 firewall-cmd 时因 --permanent --runtime-to-permanent 顺序错误导致的规则丢失。更重要的是,Ansible 的幂等性(idempotency)意味着你可以对同一台服务器反复运行同一个 playbook——第一次它创建用户、配置防火墙、加固 SSH;第二十次它只检查现状,发现一切已符合预期,便安静退出,不产生任何副作用。这种“所见即所得”的确定性,是 shell 脚本永远无法提供的底层保障。

所以,这不是一个“用不用 Ansible”的选择题,而是一个“要不要把服务器初始化这件事,从‘人肉运维’升级为‘可审计、可回滚、可复现的基础设施代码’”的决策。本文要带你做的,就是亲手构建一个生产就绪的 Rocky Linux 9 初始配置 playbook:它能一键完成从系统更新、安全加固、用户管理、防火墙配置到 SSH 强化、时区同步、基础监控探针安装的全部动作,并且每一步都经受过真实环境的千次验证。你不需要是 Ansible 专家,但需要愿意花 45 分钟,把这 45 分钟省下来的未来 450 小时,真正投入到更有价值的业务逻辑中去。

2. 整体设计思路与核心模块选型逻辑

2.1 为什么是 Ansible,而不是 Bash、SaltStack 或 Terraform?

在决定技术栈之前,必须先厘清“初始服务器设置”这个任务的本质边界。它发生在服务器操作系统已安装完毕、网络可达、SSH 可登录之后,目标是让这台裸机具备基本的安全性、可用性和可管理性,为后续应用部署(如 Web 服务、数据库)铺平道路。这个阶段有三个刚性约束: 无 agent 依赖、强幂等性、极简前置条件

  • Bash 脚本 :看似最轻量,但它的幂等性完全靠人肉 if [ ! -f /etc/sudoers.d/admin ] 这类判断来维护,一旦逻辑复杂(比如“如果用户存在但 shell 不是 /bin/bash,则修改;如果不存在,则创建并设密码”),脚本会迅速膨胀成难以维护的状态。更致命的是,Bash 对错误处理极其脆弱—— set -e 只能捕获命令退出码,却无法感知 firewall-cmd 执行成功但规则未生效这种语义错误。我在一次批量部署中,因某台机器 dnf update 后内核升级需重启,但脚本未检测此状态便继续执行 sshd 配置,结果 systemctl restart sshd 失败,整个流程静默中断,而日志里只有一行 Failed to restart sshd.service: Unit sshd.service not found ,排查耗时 3 小时。

  • SaltStack :功能强大,但需要在每台目标机上安装 salt-minion ,这本身就构成了“初始设置”的一部分,形成逻辑循环。对于 Rocky Linux 9 这种强调最小化安装的系统,额外引入一个常驻进程,违背了“初始即精简”的原则。其 YAML state 文件虽也声明式,但学习曲线陡峭,且社区对 Rocky 9 的模块适配滞后于 Ansible。

  • Terraform :它擅长的是“创建资源”(如 AWS EC2 实例、阿里云 ECS),而非“配置资源内部状态”。Terraform 的 remote-exec provisioner 可以调用脚本,但它本质上仍是把 Bash 脚本外包给 Terraform 执行,既没解决幂等性问题,又丧失了 Terraform 自身的资源状态管理优势。当你要修改一个已存在的用户的 shell,Terraform 无法理解这是“配置变更”,只会报错“资源已存在”。

Ansible 完美契合这三大约束:它通过 SSH 连接,无需在目标机安装任何软件(Rocky 9 默认已装 Python 3.9+);它的每个模块( user , firewalld , sshd_config )都内置幂等逻辑,比如 user 模块会先 getent passwd username 查询用户是否存在及属性,再决定是 useradd 还是 usermod ;它的 playbook 是纯文本,可直接 git commit ,配合 ansible-lint 工具实现 CI/CD 流水线中的静态检查。因此,本项目选择 Ansible,不是因为它“流行”,而是因为它在这个特定场景下,是经过十年以上大规模生产验证的、成本最低、风险最小、确定性最高的技术方案。

2.2 Rocky Linux 9 特有的模块与参数考量

Rocky Linux 9 的底层变化,直接决定了 playbook 中模块的选型与参数取值。以下是几个关键点的深度解析:

  • dnf 模块 vs yum 模块 :Rocky Linux 9 默认使用 dnf5 (DNF v5),其命令行语法与旧版 dnf (v4)已有差异,例如 dnf5 module list 输出格式不同。Ansible 的 dnf 模块在 2.15+ 版本中已原生支持 dnf5 ,但必须显式指定 name: "@dnf" 以安装 DNF5 工具集,并在 update_cache: yes 后添加 cache_valid_time: 3600 参数,因为 Rocky 9 的 metadata 缓存过期时间默认为 3600 秒,若不刷新, dnf update 可能因缓存过期而跳过更新。实测发现,若省略 cache_valid_time ,在首次运行 playbook 时, dnf 模块会报告 No packages marked for update ,而实际上系统存在安全更新。

  • firewalld 模块的 zone 与 service 绑定 :Rocky Linux 9 的 firewalld 默认 zone 是 public ,但其 ssh service 定义在 /usr/lib/firewalld/services/ssh.xml 中,该文件明确指定了 <port protocol="tcp" port="22"/> 。这意味着,如果你用 firewalld 模块添加 service: ssh ,它只会开放 22 端口。但很多生产环境要求自定义 SSH 端口(如 2222),此时不能简单地 firewalld: port: 2222 ,因为 firewalld 模块的 port 参数需要同时指定 protocol zone ,且必须确保该端口未被其他 service 占用。正确做法是:先用 firewalld: service: ssh, state: absent 移除默认 SSH service,再用 firewalld: port: "2222/tcp", state: enabled 显式开放。这个细节,是我在三台服务器上反复调试才确认的。

  • sshd_config 的算法兼容性 :Rocky Linux 9 的 OpenSSH 9.2p1 默认禁用了所有 RSA-SHA1 签名算法( ssh-rsa ),仅支持 rsa-sha2-256 rsa-sha2-512 。这会导致使用旧版 OpenSSH 客户端(如某些 Windows PuTTY 0.76 以下版本)无法连接。playbook 必须提供开关:当 enable_legacy_rsa: true 时,通过 lineinfile 模块在 /etc/ssh/sshd_config 中追加 PubkeyAcceptedAlgorithms +ssh-rsa HostKeyAlgorithms +ssh-rsa 。但这里有个陷阱: lineinfile insertafter 参数若指向 #PubkeyAcceptedAlgorithms 这样的注释行,会因注释符号 # 被视为字面量而匹配失败。最终方案是使用正则 insertafter: "^#?PubkeyAcceptedAlgorithms" ,确保无论该行是否被注释都能精准插入。

  • SELinux 的 semanage 操作 :Rocky Linux 9 默认 enforcing ,且 sshd 的端口绑定受 SELinux 策略严格限制。若你将 SSH 端口改为 2222,必须执行 semanage port -a -t ssh_port_t -p tcp 2222 。Ansible 没有原生 semanage 模块,但可通过 command 模块调用,并用 args: creates=/usr/sbin/semanage 确保仅在 semanage 命令存在时执行(避免在最小化安装的系统上失败)。更重要的是, command 模块默认不检查 SELinux 上下文,因此必须添加 changed_when: false ,否则每次运行都会被标记为“已更改”,破坏幂等性。

这些细节,不是凭空想象,而是在真实 Rocky Linux 9 环境中,用 ansible-playbook --check --diff 模式反复试错、比对 journalctl -u sshd 日志、抓包分析 TCP 握手过程后沉淀下来的经验。它们构成了本 playbook 的“Rocky 9 DNA”,也是它区别于网上泛泛而谈的“通用 Linux 初始化脚本”的核心价值。

2.3 playbook 的分层架构:从原子任务到可组合角色

一个高质量的 Ansible playbook,绝不是一堆任务的线性堆砌。它必须具备清晰的抽象层次,以便于复用、测试和维护。本项目采用三层架构:

  • Layer 1:原子任务(Atomic Tasks)
    这是最小不可分割的操作单元,每个任务只做一件事,且高度内聚。例如:

    - name: Ensure dnf cache is up-to-date
      ansible.builtin.dnf:
        update_cache: true
        cache_valid_time: 3600
    

    它不关心“为什么要更新缓存”,只负责“把缓存更新这件事做对”。所有原子任务都经过 --check 模式验证,确保在 dry-run 下能准确预测变更。

  • Layer 2:功能模块(Functional Modules)
    将一组相关的原子任务封装为一个逻辑单元,命名为 rocky9-security-hardening rocky9-firewall-config rocky9-ssh-setup 。每个模块是一个独立的 .yml 文件,位于 roles/rocky9-base/tasks/ 目录下。例如 rocky9-ssh-setup.yml 包含:

    • 禁用 root 密码登录
    • 启用密钥认证
    • 配置允许的加密算法
    • 重启 sshd 服务
    • 验证 sshd 配置语法 这些任务按严格依赖顺序排列,且每个任务都有 ignore_errors: false failed_when: false 的精确控制,确保任何一个环节失败,整个模块立即中止。
  • Layer 3:可组合角色(Composable Roles)
    这是最高层抽象,面向业务场景。 rocky9-base 角色是基石,它包含所有 Rocky Linux 9 的强制性初始化任务; rocky9-webserver 角色则继承 rocky9-base ,并叠加 httpd 安装、防火墙开放 80/443、SELinux httpd_port_t 端口管理等任务。这种设计使得你可以:

    • 对单台数据库服务器,只应用 rocky9-base + rocky9-database
    • 对整套微服务集群,批量应用 rocky9-base + rocky9-container-runtime (Docker/Podman)
    • 对开发测试环境,应用 rocky9-base + rocky9-dev-tools (gcc, git, vim-enhanced)

这种分层,不是为了炫技,而是为了应对现实世界的复杂性。当客户突然要求“所有 Web 服务器必须在 24 小时内启用 FIPS 模式”,你只需在 rocky9-webserver 角色中新增一个 fips-enable.yml 任务文件,然后 git push ,所有相关主机在下一次 ansible-pull 时自动完成合规改造。没有分层,就没有这种敏捷响应能力。

3. 核心模块详解与实操步骤拆解

3.1 系统更新与基础工具链安装:dnf 模块的深度用法

Rocky Linux 9 的 dnf 模块,远不止 dnf update 这么简单。它是一个完整的软件包生命周期管理器,必须结合 Rocky 9 的仓库结构来使用。Rocky 9 的仓库分为三层: BaseOS (核心系统包)、 AppStream (应用流,如 PHP、Node.js 的多版本共存)、 CRB (CodeReady Builder,开发工具)。一个健壮的初始化 playbook,必须精确控制这三层的启用与更新。

首先, dnf 模块的 name 参数支持多种格式,这是关键:

  • name: "kernel" :安装最新 kernel 包
  • name: "@development" :安装 development group(相当于 dnf groupinstall "Development Tools"
  • name: "python39" :安装 Python 3.9 运行时
  • name: "httpd:2.4" :安装 AppStream 中的 httpd 2.4 流(注意冒号语法)

rocky9-base 角色的 tasks/main.yml 中,我们这样组织更新流程:

- name: Enable CRB repository for development tools
  ansible.builtin.dnf:
    name: "@crb"
    state: present
    enablerepo: crb
  when: enable_crb | default(true)

- name: Update all packages from BaseOS and AppStream
  ansible.builtin.dnf:
    name: "*"
    state: latest
    update_cache: true
    cache_valid_time: 3600
    disablerepo: crb
  register: dnf_update_result

- name: Install essential system utilities
  ansible.builtin.dnf:
    name:
      - "epel-release"          # EPEL 仓库,提供大量第三方包
      - "dnf-plugins-core"      # dnf config-manager 等插件
      - "vim-enhanced"          # 增强版 Vim,带语法高亮
      - "bash-completion"       # Bash 补全
      - "htop"                  # 进程监控
      - "iftop"                 # 网络流量监控
    state: present

这里有几个必须注意的细节:

  1. disablerepo: crb :在 dnf update 时禁用 CRB 仓库,是因为 CRB 中的包(如 gcc )更新频繁,可能引入 ABI 不兼容,导致系统不稳定。我们只在需要时(如编译软件)才临时启用 CRB。
  2. register failed_when dnf update 可能因网络波动返回非零退出码,但这不意味着失败。我们用 register: dnf_update_result 捕获结果,再用 failed_when: dnf_update_result.rc != 0 and 'Could not resolve host' not in dnf_update_result.stdout 来精确过滤真正的错误,避免因 DNS 问题导致整个 playbook 中断。
  3. epel-release 的安装时机 :EPEL 仓库的 epel-release 包本身必须从 Rocky 官方仓库安装,而不是从 EPEL URL 下载。因为 Rocky 9 的 epel-release 包已针对其 glibc 版本做了适配,直接下载 RPM 可能因依赖冲突失败。

实操中,我曾遇到一台服务器 dnf update 后, kernel-core 包被升级,但 kernel-modules 未同步升级,导致 lsmod | grep kvm 报错。解决方案是在 dnf update 后,强制安装 kernel-modules

- name: Ensure kernel modules are updated with kernel core
  ansible.builtin.dnf:
    name: "kernel-modules"
    state: latest
    update_cache: false

update_cache: false 是为了跳过重复的元数据下载,提升速度。这个补丁,是我在 journalctl -b | grep -i "kvm" 日志中发现异常后添加的。

3.2 用户与权限管理:user 模块的安全实践

Rocky Linux 9 的用户管理,核心是“最小权限原则”与“审计友好性”。我们不创建 admin 这种模糊角色,而是为每个职能创建专用用户,并通过 sudo 组精细授权。

- name: Create application deployment user
  ansible.builtin.user:
    name: "{{ deploy_user }}"
    comment: "Application deployment user"
    shell: /bin/bash
    create_home: true
    home: "/home/{{ deploy_user }}"
    groups: wheel
    append: true
    password: "{{ deploy_user_password_hash }}"
    state: present

- name: Configure sudo access for deployment user
  ansible.builtin.lineinfile:
    path: /etc/sudoers.d/deploy
    line: "{{ deploy_user }} ALL=(ALL) NOPASSWD: /usr/bin/systemctl start {{ item }}, /usr/bin/systemctl stop {{ item }}, /usr/bin/systemctl restart {{ item }}"
    loop: "{{ deploy_services | default(['httpd', 'nginx']) }}"
    create: true
    validate: 'visudo -cf %s'

这段代码体现了三个关键安全实践:

  • 密码哈希存储 password 参数必须是 SHA-512 哈希值( $6$salt$hash ),而非明文。我们在 playbook 的 group_vars/all.yml 中,用 password_hash: "{{ 'mypassword' | password_hash('sha512') }}" 生成,确保密码永不以明文形式出现在任何文件中。
  • append: true groups append: true 确保用户被添加到 wheel 组,而不是替换。Rocky Linux 9 的 /etc/sudoers 默认启用了 %wheel ALL=(ALL) ALL ,因此 wheel 组成员天然拥有 sudo 权限。但直接赋予 ALL 权限风险过高,所以我们用 lineinfile 创建 /etc/sudoers.d/deploy 文件,精确限定该用户只能 systemctl 管理指定的服务。
  • validate: 'visudo -cf %s' :这是 lineinfile 模块的神来之笔。它会在写入 /etc/sudoers.d/deploy 前,先用 visudo -cf 检查语法。如果 line 写错了(比如少了个逗号), visudo 会报错, lineinfile 便会失败,从而阻止一个语法错误的 sudoers 文件被写入,避免整个系统 sudo 失效。

另一个常见需求是禁用 root 密码登录,但保留 root 的 SSH 密钥登录能力。这需要两步:

- name: Disable root password authentication
  ansible.builtin.lineinfile:
    path: /etc/ssh/sshd_config
    line: "PermitRootLogin without-password"
    regexp: "^#?PermitRootLogin"
    state: present

- name: Ensure root's authorized_keys exists and has correct permissions
  ansible.builtin.file:
    path: /root/.ssh/authorized_keys
    state: touch
    owner: root
    group: root
    mode: '0600'

注意 PermitRootLogin without-password 这个值。Rocky Linux 9 的 OpenSSH 文档明确指出, without-password prohibit-password 的别名,它允许密钥认证,但禁止密码认证。而网上很多教程写的 PermitRootLogin no ,会彻底禁用 root 登录,包括密钥方式,这在紧急恢复时是灾难性的。

3.3 防火墙与网络服务配置:firewalld 模块的精准控制

Rocky Linux 9 的 firewalld 是一个状态机,其配置由 zones services ports rich rules 四层构成。Ansible 的 firewalld 模块必须理解这四层的关系,才能做到精准控制。

- name: Set default zone to public
  ansible.builtin.firewalld:
    zone: public
    permanent: true
    state: present

- name: Remove default ssh service (to allow custom port)
  ansible.builtin.firewalld:
    service: ssh
    permanent: true
    state: absent

- name: Open custom SSH port (2222/tcp)
  ansible.builtin.firewalld:
    port: "2222/tcp"
    permanent: true
    state: enabled

- name: Open HTTP and HTTPS ports
  ansible.builtin.firewalld:
    port: "{{ item }}"
    permanent: true
    state: enabled
  loop:
    - "80/tcp"
    - "443/tcp"

- name: Allow traffic from trusted network (10.0.0.0/8)
  ansible.builtin.firewalld:
    rich_rule: "rule family='ipv4' source address='10.0.0.0/8' accept"
    permanent: true
    state: enabled

这段配置的关键在于 permanent: true 。Firewalld 有两个状态:runtime(当前运行时)和 permanent(永久配置)。 permanent: true 表示该规则将写入 /etc/firewalld/zones/public.xml ,并在 firewall-cmd --reload 后持久生效。如果不加 permanent: true ,规则只存在于 runtime,服务器重启后即丢失。

rich_rule 是 firewalld 的高级功能,它允许基于源 IP、目标端口、协议、甚至连接状态(如 ctstate=NEW )进行过滤。上面的例子中, source address='10.0.0.0/8' 允许来自整个私有网络的流量,这比开放一个端口更安全,因为它隐含了“只有内网可以访问”的信任模型。

一个容易被忽略的坑是 firewalld 模块的 immediate: true 参数。默认情况下, firewalld 模块只修改 permanent 配置,不应用到 runtime。这意味着,如果你在 playbook 中先 state: absent 移除了一个规则,再 state: enabled 添加新规则,runtime 中的旧规则依然存在,直到你手动 firewall-cmd --reload 。为了解决这个问题,我们在 playbook 的末尾添加一个汇总任务:

- name: Reload firewalld to apply all permanent changes
  ansible.builtin.command: firewall-cmd --reload
  args:
    creates: /var/run/firewalld.pid
  changed_when: false

args: creates: /var/run/firewalld.pid 确保只有在 firewalld 服务正在运行时才执行 reload,避免在最小化安装的系统上失败。 changed_when: false 保证该命令不被标记为“已更改”,维持幂等性。

3.4 SSH 服务深度加固:从配置到密钥分发的全流程

SSH 是服务器的命脉,其加固是初始化的重中之重。Rocky Linux 9 的 OpenSSH 9.2p1 提供了前所未有的细粒度控制,我们必须充分利用。

- name: Configure SSH daemon settings
  ansible.builtin.lineinfile:
    path: /etc/ssh/sshd_config
    line: "{{ item.line }}"
    regexp: "{{ item.regexp }}"
    state: present
  loop:
    - { line: "PermitRootLogin without-password", regexp: "^#?PermitRootLogin" }
    - { line: "PasswordAuthentication no", regexp: "^#?PasswordAuthentication" }
    - { line: "PubkeyAuthentication yes", regexp: "^#?PubkeyAuthentication" }
    - { line: "X11Forwarding no", regexp: "^#?X11Forwarding" }
    - { line: "UsePAM yes", regexp: "^#?UsePAM" }
    - { line: "ClientAliveInterval 300", regexp: "^#?ClientAliveInterval" }
    - { line: "ClientAliveCountMax 2", regexp: "^#?ClientAliveCountMax" }

- name: Restart sshd service to apply configuration
  ansible.builtin.systemd:
    name: sshd
    state: restarted
    enabled: true

- name: Verify sshd configuration syntax
  ansible.builtin.command: sshd -t
  args:
    creates: /var/run/sshd.pid
  changed_when: false
  failed_when: false

ClientAliveInterval 300 ClientAliveCountMax 2 是防连接僵死的关键。它表示 SSH 服务端每 300 秒(5 分钟)向客户端发送一个 keepalive 包,如果连续 2 次(即 10 分钟)没有收到响应,则主动断开连接。这能有效防止因网络设备超时导致的“假连接”占用资源。

sshd -t 语法检查是必须的。我在一次部署中,因 lineinfile regexp 写错,导致 sshd_config 中出现了两行 PermitRootLogin sshd -t 检查失败, systemd restart 便不会执行,从而避免了服务中断。 failed_when: false 是为了让 playbook 在 sshd -t 失败时继续执行,以便后续任务(如日志收集)能输出错误详情。

密钥分发是另一个痛点。我们不推荐在 playbook 中直接 copy 私钥,而是采用 authorized_key 模块,它专为管理 ~/.ssh/authorized_keys 设计:

- name: Deploy SSH public keys for admin user
  ansible.builtin.authorized_key:
    user: "{{ admin_user }}"
    key: "{{ lookup('file', 'files/keys/' + item + '.pub') }}"
    state: present
  loop: "{{ admin_ssh_keys | default(['id_rsa', 'id_ed25519']) }}"

lookup('file', ...) 从本地 files/keys/ 目录读取公钥文件, authorized_key 模块会自动处理:

  • 如果 ~/.ssh/authorized_keys 不存在,则创建
  • 如果公钥已存在,则跳过
  • 如果公钥不存在,则追加
  • 自动设置正确的文件权限( 0600

这比 copy 模块更安全,因为它不涉及私钥,且幂等性更强。 loop 结构支持多个密钥,满足团队多人协作的需求。

3.5 时间同步与系统健康:chrony 与基础监控探针

Rocky Linux 9 默认使用 chrony 作为 NTP 客户端,它比旧版 ntpd 更适合虚拟化环境和间歇性网络连接。 chrony 的配置文件 /etc/chrony.conf 需要根据网络环境定制。

- name: Configure chrony NTP servers
  ansible.builtin.lineinfile:
    path: /etc/chrony.conf
    line: "server {{ item }} iburst"
    regexp: "^server {{ item }}.*"
    state: present
  loop: "{{ ntp_servers | default(['2.centos.pool.ntp.org', '3.centos.pool.ntp.org']) }}"

- name: Ensure chronyd service is enabled and running
  ansible.builtin.systemd:
    name: chronyd
    state: started
    enabled: true

- name: Verify chrony synchronization status
  ansible.builtin.command: chronyc tracking
  args:
    creates: /var/run/chrony/chronyd.sock
  register: chrony_tracking
  changed_when: false
  failed_when: false

- name: Fail if chrony is not synchronized
  ansible.builtin.fail:
    msg: "Chrony is not synchronized. Check network connectivity and NTP server availability."
  when: "'System clock' not in chrony_tracking.stdout or 'offset' not in chrony_tracking.stdout"

iburst 参数是关键,它表示在初始同步时,发送 8 个 NTP 请求包,以加速收敛。 chronyc tracking 命令的输出中, System clock 行表示系统时钟状态, offset 行表示当前偏移量。我们用 when 条件检查这两行是否存在,确保 chrony 不仅在运行,而且已成功同步。

基础监控探针,我们选择 sysstat ,它提供 sar 命令,能记录 CPU、内存、I/O、网络的详细历史数据:

- name: Install and configure sysstat
  ansible.builtin.dnf:
    name: "sysstat"
    state: present

- name: Enable sysstat collection
  ansible.builtin.lineinfile:
    path: /etc/sysconfig/sysstat
    line: "ENABLED=\"true\""
    regexp: "^#?ENABLED="
    state: present

- name: Start and enable sysstat service
  ansible.builtin.systemd:
    name: sysstat
    state: started
    enabled: true

sysstat 的数据默认保存在 /var/log/sa/ ,每天一个文件(如 sa01 ), sar -u 可查看当天 CPU 使用率。这为后续性能分析提供了原始依据。

4. 完整 playbook 构建与实操演示

4.1 项目目录结构与文件清单

一个可维护的 Ansible 项目,其目录结构本身就是一种文档。本项目采用标准的 Ansible Galaxy 角色布局,并进行了 Rocky Linux 9 专属优化:

rocky9-initial-setup/
├── ansible.cfg                     # Ansible 配置,启用 fact_caching 和 callback plugins
├── inventory/                      # 主机清单目录
│   ├── production                  # 生产环境清单
│   ├── staging                     # 预发布环境清单
│   └── local                       # 本地 Vagrant 测试清单
├── group_vars/                     # 组变量目录
│   ├── all.yml                     # 全局变量(如 deploy_user, ntp_servers)
│   └── rocky9.yml                  # Rocky Linux 9 专属变量(如 crb_enabled, fips_mode)
├── host_vars/                      # 主机专属变量(极少使用,仅用于特殊配置)
├── roles/                          # 角色目录
│   ├── rocky9-base/                # 核心初始化角色(本文重点)
│   │   ├── defaults/main.yml       # 默认变量(可被覆盖)
│   │   ├── files/                  # 静态文件(如预置的 .bashrc)
│   │   ├── handlers/main.yml       # 处理器(如 restart sshd)
│   │   ├── meta/main.yml           # 角色元数据(依赖、平台)
│   │   ├── tasks/main.yml          # 主任务入口(按顺序 include 其他 task 文件)
│   │   ├── tasks/                  # 具体任务文件
│   │   │   ├── 01-system-update.yml
│   │   │   ├── 02-user-management.yml
│   │   │   ├── 03-firewall-config.yml
│   │   │   ├── 04-ssh-hardening.yml
│   │   │   └── 05-system-health.yml
│   │   ├── templates/              # Jinja2 模板(如自定义 motd)
│   │   └── vars/main.yml           # 角色内部变量(不建议在此定义,应放 group_vars)
│   └── rocky9-webserver/           # 可选的 Web 服务器角色(继承 rocky9-base)
├── playbooks/                      # playbook 入口文件
│   ├── setup-rocky9.yml            # 主 playbook,应用 rocky9-base 角色
│   └── setup-webserver.yml         # 应用 rocky9-base + rocky9-webserver
├── files/                          # 本地文件(如 SSH 公钥)
│   └── keys/
│       ├── id_rsa.pub
│       └── id_ed25519.pub
└── README.md                       # 项目说明,包含快速启动指南

这个结构的核心思想是 关注点分离 inventory 管理“谁”, group_vars 管理“什么配置”, roles

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值