权限 640 变成 1204 的那一天 — 把文件当作状态来管
目标
把文件和目录当作状态而不是命令来声明。亲自测量没有引号的八进制数留下的权限,用备份和验证加上回退的依据和对损坏配置的拦截,并创建带标记的区块、链接、状态报告和路径检查器。
为什么重要
如果把 mkdir、cp、chmod、sed -i 这四行 shell 原样搬过来,看起来像自动化,但有三件事会同时崩塌。第二次运行与第一次不同,覆盖了什么哪里也没有留下,损坏的配置原样上线。Ansible 的文件模块分别用 state、backup 和 validate 回答这三个问题。再加上安全管理多行区块的 blockinfile 标记、部署所用的两种链接,以及什么都不改变、只读取事实的 stat,配置部署的骨架就完整了。本实验会一次一块地亲手搭建这个骨架,最后亲自创建一个判定“这条路径现在是什么状态”的工具。
步骤
- 在
/root/ans/files/hosts.ini中写入web组的主机web1(ansible_host=127.0.0.1、ansible_port=2222、ansible_user=root),并用/root/ans/files/p01.yml创建四个目录——/root/ans/files/tree/conf、/root/ans/files/tree/logs和/root/ans/files/out为0755,/root/ans/files/tree/secrets为0700。 - 用
/root/ans/files/p02.yml创建三个文件——/root/ans/files/tree/secrets/api.key为"0600",/root/ans/files/tree/conf/motd.txt为"0644",而/root/ans/files/scratch/decimal.txt要以不带引号的mode: 644来写。然后把stat -c %a对decimal.txt给出的值,写成一行到/root/ans/files/out/mode-trap.txt。 - 用
/root/ans/files/p03.yml放置/root/ans/files/tree/conf/app.conf。内容是listen_port={{ app_port }}和env=prod两行,app_port的默认值为8080,权限为"0644",并开启backup。让同一个 playbook 也创建/root/ans/files/tree/conf/deprecated.conf和/root/ans/files/tree/logs/archive/old.log。先用默认值运行一次,再用-e app_port=9090重新运行,使其生成备份,并让该备份路径保留在/root/ans/files/out/backup-path.txt中。 - 用
/root/ans/files/p04.yml放置/root/ans/files/tree/conf/app.json。内容来自app_json变量,默认值是service为web、port为8080的 JSON,并给copy加上validate来检查 JSON 语法。然后用-e "app_json=not json at all"把同一个 playbook 再运行一次,把失败的输出保留到/root/ans/files/out/validate-fail.txt。 - 用
/root/ans/files/p05.yml处理/root/ans/files/tree/conf/hosts.block。只在文件不存在时,才用一行127.0.0.1 localhost创建它(已存在则不覆盖),并用blockinfile,在使用标记# {mark} ANSIBLE MANAGED BLOCK: web pool的区块中放入10.10.0.11 web1和10.10.0.12 web2两行。即使把 playbook 运行两次,区块也必须只有一份。 - 用
/root/ans/files/p06.yml做四件事——只在不存在时创建/root/ans/files/tree/logs/app.log,让/root/ans/files/tree/logs/latest.log成为指向该文件的符号链接,让/root/ans/files/tree/conf/motd.hard成为/root/ans/files/tree/conf/motd.txt的硬链接,最后给latest.log路径设置"0640",并让它跟随链接,使app.log的权限发生改变。 - 用
/root/ans/files/p07.yml通过stat确认/root/ans/files/tree/conf/app.conf的状态,校验和用sha256获取,并在/root/ans/files/out/stat-report.json中留下包含path、exists、mode、size、checksum五个键的 JSON。接着用fetch取回/root/ans/files/tree/conf/app.json,使其落成/root/ans/files/fetched/app.json这一个文件。 - 用
/root/ans/files/p08.yml让/root/ans/files/tree/conf/deprecated.conf和/root/ans/files/tree/logs/archive处于不存在的状态。然后编写/root/ans/files/audit.sh:以路径列表文件为参数,对每条路径按<경로> LINK <가리키는 곳>·<경로> DIR <권한>·<경로> FILE <권한>·<경로> ABSENT(占位符依次为路径、指向位置与权限)中的一种,各输出一行。在/root/ans/files/paths.txt中写入上面的两条路径,以及/root/ans/files/tree/conf/app.conf、/root/ans/files/tree/logs/latest.log、/root/ans/files/tree/secrets,并把结果保存到/root/ans/files/out/state-report.txt。
参考
- 工作目录是
/root/ans/files。inventory 中的主机连接的是这个 Pod 内 127.0.0.1:2222 的 sshd,所以目标和控制节点看到的是同一个文件系统。 - 查找模块时,像
ansible-doc ansible.builtin.file这样调用,参数和示例就会当场出现。 - 常见错误:把
mode写成没有引号的形式。在第 2 步会亲自测量它的结果。 - 常见错误:对已经存在的别人的文件,以覆盖的方式使用
copy,导致先前放入的区块在每次运行时消失又重新附上。 - 常见错误:想用
state: file创建不存在的文件。该值只调整已存在文件的属性。 - file 模块 · copy 模块 · blockinfile 模块 · stat 模块 · fetch 模块
把目录树声明为状态
在 /root/ans/files/hosts.ini 中写入 web 组的主机 web1(ansible_host=127.0.0.1、ansible_port=2222、ansible_user=root),并用 /root/ans/files/p01.yml 创建四个目录——/root/ans/files/tree/conf、/root/ans/files/tree/logs 和 /root/ans/files/out 为 0755,/root/ans/files/tree/secrets 为 0700。
在 ansible.builtin.file 中,表示目录的 state 值只要一个,就会连中间路径一起创建。mode 请务必写成带引号的字符串——下一步会亲自测量为什么要这样。评分器会把 playbook 再以检查模式运行一次,确认变更是否为 0。
亲自测量没有引号的八进制数留下的权限
用 /root/ans/files/p02.yml 创建三个文件——/root/ans/files/tree/secrets/api.key 为 "0600",/root/ans/files/tree/conf/motd.txt 为 "0644",而 /root/ans/files/scratch/decimal.txt 要以不带引号的 mode: 644 来写。然后把 stat -c %a 对 decimal.txt 给出的值,写成一行到 /root/ans/files/out/mode-trap.txt。
YAML 会把前面没有 0 的数字读作十进制。这个整数被原样当作权限位时会变成什么值,不要在脑子里计算,而要实际创建出来,用 stat 测一下。scratch 目录也要在同一个 playbook 中一起创建。
保留覆盖之前的内容
用 /root/ans/files/p03.yml 放置 /root/ans/files/tree/conf/app.conf。内容是 listen_port={{ app_port }} 和 env=prod 两行,app_port 的默认值为 8080,权限为 "0644",并开启 backup。让同一个 playbook 也创建 /root/ans/files/tree/conf/deprecated.conf 和 /root/ans/files/tree/logs/archive/old.log。先用默认值运行一次,再用 -e app_port=9090 重新运行,使其生成备份,并让该备份路径保留在 /root/ans/files/out/backup-path.txt 中。
copy 的备份功能会把覆盖之前的内容留在同一个目录中。返回值中包含该路径,所以用 register 接收下来,就可以在下一个 task 中直接使用。第一次运行时没有可覆盖的内容,不会生成备份,所以请给记录路径的 task 加上 is defined 条件。
只把通过检查的配置放到位
用 /root/ans/files/p04.yml 放置 /root/ans/files/tree/conf/app.json。内容来自 app_json 变量,默认值是 service 为 web、port 为 8080 的 JSON,并给 copy 加上 validate 来检查 JSON 语法。然后用 -e "app_json=not json at all" 把同一个 playbook 再运行一次,把失败的输出保留到 /root/ans/files/out/validate-fail.txt。
validate 字符串中的 %s 位置会填入临时文件路径。检查命令只要读取 JSON,有问题时以非 0 的值结束就行。保存失败运行的输出时,必须同时接收标准错误,并且不要让脚本因为失败的命令而停止。请确认失败之后原来的文件是否完好。
只替换带标记的区块
用 /root/ans/files/p05.yml 处理 /root/ans/files/tree/conf/hosts.block。只在文件不存在时,才用一行 127.0.0.1 localhost 创建它(已存在则不覆盖),并用 blockinfile,在使用标记 # {mark} ANSIBLE MANAGED BLOCK: web pool 的区块中放入 10.10.0.11 web1 和 10.10.0.12 web2 两行。即使把 playbook 运行两次,区块也必须只有一份。
copy 中有一个开关可以避免覆盖已经存在的文件。如果去掉它,第二次运行时区块会整个消失,然后再重新附上。标记字符串中的 {mark} 位置分别填入 BEGIN 和 END——如果以后修改这段文字,区块就会变成两份。评分器会把 playbook 再以检查模式运行一次,确认变更是否为 0。
符号链接、硬链接,以及 follow
用 /root/ans/files/p06.yml 做四件事——只在不存在时创建 /root/ans/files/tree/logs/app.log,让 /root/ans/files/tree/logs/latest.log 成为指向该文件的符号链接,让 /root/ans/files/tree/conf/motd.hard 成为 /root/ans/files/tree/conf/motd.txt 的硬链接,最后给 latest.log 路径设置 "0640",并让它跟随链接,使 app.log 的权限发生改变。
符号链接和硬链接的 state 值不同。两者都接收 src 和 dest。最后一个 task 使用只调整已存在文件属性的 state,并明确开启决定是否跟随链接的参数。在 Linux 上,符号链接本身的权限没有意义,所以权限是加在链接末端的文件上,而不是链接上。
读取状态并留下报告,再取回控制节点
用 /root/ans/files/p07.yml 通过 stat 确认 /root/ans/files/tree/conf/app.conf 的状态,校验和用 sha256 获取,并在 /root/ans/files/out/stat-report.json 中留下包含 path、exists、mode、size、checksum 五个键的 JSON。接着用 fetch 取回 /root/ans/files/tree/conf/app.json,使其落成 /root/ans/files/fetched/app.json 这一个文件。
stat 什么都不改变,只返回事实。校验和必须指定算法,才会放入返回值。fetch 默认会创建主机名目录并原样重现原路径,所以要让它落成一个文件,必须开启关闭这一行为的参数。报告只要创建一个字典并使用转换成 JSON 的过滤器,一个 task 就够了。
清理也是状态——并创建路径检查器
用 /root/ans/files/p08.yml 让 /root/ans/files/tree/conf/deprecated.conf 和 /root/ans/files/tree/logs/archive 处于不存在的状态。然后编写 /root/ans/files/audit.sh:以路径列表文件为参数,对每条路径按 <경로> LINK <가리키는 곳> · <경로> DIR <권한> · <경로> FILE <권한> · <경로> ABSENT(占位符依次为路径、指向位置与权限)中的一种,各输出一行。在 /root/ans/files/paths.txt 中写入上面的两条路径,以及 /root/ans/files/tree/conf/app.conf、/root/ans/files/tree/logs/latest.log、/root/ans/files/tree/secrets,并把结果保存到 /root/ans/files/out/state-report.txt。
权限按 stat -c %a 给出的原样写。判定顺序就是这一步的全部——-d 和 -f 会跟随符号链接,所以如果后看链接,链接就会伪装成目录或文件。评分器也会用它自己另外创建的路径列表来运行这个脚本,所以把答案写死在里面的脚本无法通过。