Matrix 首页推荐 

Matrix 是少数派的写作社区,我们主张分享真实的产品体验,有实用价值的经验与思考。我们会不定期挑选 Matrix 最优质的文章,展示来自用户的最真实的体验和观点。

文章代表作者个人观点,少数派仅对标题和排版略作修改。


前言

想必对于许多 Linux 用户来说,最无法割舍的东西大概是自己长年累月积攒的各种软件工具的配置文件(dotfiles),而如何系统性地管理这些文件,其实也已经有很多现成的方案,比如 GNU/Stowchezmoi,可以将本来存放在系统各个角落的配置文件集中存放,并使用版本控制工具管理,当迁移到新的工作环境时,也可以十分方便地恢复自己原先的配置。

不过这些 dotfiles 管理工具对我来说一直有两个主要痛点未被解决:

  • 首先是只能管理当前用户家目录之中的文件,若遇到一些配置文件存放在 root 目录下的软件就无能为力了;
  • 再者是在部署配置文件之前,大多数时候还是需要手动安装所需的软件包,这些工具无法为我们自动安装需要的软件包。

因为 Linux 系统在设计之初就是针对多用户使用的,考虑到不是所有用户都有足够的权限来修改 root 目录和安装软件,像 Stow 和 chezmoi 这类软件不能实现以上的功能倒也情有可原。不过对于大多数个人电脑的使用情况,一个系统基本上只有一个主要用户,且这个用户都有使用 sudo 的权限,上面两个功能还是比较必要的。

有关如何管理 root 目录中的文件,我发现了 etckeeper 在某种意义上可以实现这种功能,但它的实现方式是将整个 /etc 目录初始化成一个 git 目录,我觉得有些过于简单粗暴了。

更进一步,使用 Nix 搭配 Home Manager 可以比较好地实现我需要的功能,但是需要我将包管理器切换到到 Nix,或是将系统整个换成 NixOS,还要从头学习 Nix 编程语言,迁移与学习成本都有些高了。

那么有没有什么方法既可以让我不用离开 Arch 的舒适区,又可以比较方便地管理系统的配置文件,实现我想要的功能呢?最后我想到了 Ansible

Ansible 是一个功能强大的运维管理工具,支持几乎所有发行版,它的各种功能可以使用 playbook 文件来控制,其实就是 yaml 格式的文件,学习门槛和可读性都要好上不少。

Ansible 的设计思路就是同时管理多台远程服务器,远程服务器不需要安装 Ansible,只需在本地电脑安装,并配置好服务器的连接方式,便可以快捷管理远程服务器了,要想管理个人电脑的配置文件,只需将连接方式配置为本地连接即可,可以理解为远程与本地机器都是同一台电脑。

开始之前

因为篇幅原因,我在文章中不可能详细讲解 Ansible 的具体用法,如果想要学习 Ansible 的使用方法,我个人比较推荐观看 Youtube 上 Learn Linux TV 做的这个系列教程,如果想要了解一些高级用法,可以进一步阅读 Ansible 的官方文档,不过我个人觉得 Ansible 官方的文档写的有些杂乱,了解一些简单用法后直接去 Stack Overflow 去搜似乎更加高效一些😆。

模板仓库

我的 Ansible 配置文件结构主要参考了这个仓库以及作者对应的视频。方便起见,我直接创建了一个模板仓库,如果也有人感兴趣,可以使用这个模板仓库快速创建自己的配置文件仓库,因为我主要使用的系统是 Archlinux,所以这个仓库目前只适用于 Arch 系发行版。

要使用这个模板,点击仓库主页右上角的「Use this template」创建一个新的仓库,仓库名字按自己的喜好改就可以,仓库中包含一个一键安装脚本 bin/dotfiles,在自己的仓库中,编辑这个脚本文件,修改下图的两行:

分别将 GITHUB_USERGITHUB_REPO 改成自己的 github 账户名和自己新创建的仓库名字,以便脚本克隆正确的仓库,比如我的仓库是 so1ar/dotfiles,那么就应该改成:

GITHUB_USER=so1ar
GITHUB_REPO=dotfiles

如果已经为 github 配置好了 ssh 密钥访问,则可以选择使用 ssh 克隆仓库,编辑下图的几行:

注释掉 git clone "https://github.com/$GITHUB_USER/$GITHUB_REPO.git" "$DOTFILES_DIR" 这一行并取消注释 #git clone "git@github.com:$GITHUB_USER/$GITHUB_REPO.git" "$DOTFILES_DIR" 这一行。

之后使用这个脚本将仓库安装到本地:

# 设置 github 用户名和仓库名,要记得改成自己的用户名和仓库名
GITHUB_USER = yourusername
GITHUB_REPO = yourreponame

# 下载并运行脚本
curl -fsSL https://raw.githubusercontent.com/$GITHUB_USER/$GITHUB_REPO/master/bin/dotfiles | bash

之后按照提示输入密码,可能会要求输入两次,第一次是 pacman 安装 ansible 时需要 root 权限,第二次是 Ansible 运行某些任务时需要 root 权限,如果已经安装了 Ansible,则只需输入一次密码。

这个脚本实际上会按顺序执行以下步骤:

  • 检查当前发行版,若不是 Arch 系,就会直接退出,什么也不做;
  • 若当前系统没有安装 Ansible,便自动安装 Ansible;
  • 将配置文件仓库克隆到 ~/.config/dotfiles 目录下,若目录已经存在,则使用 git 拉取最新版本;
  • 安装额外的 ansible-galaxy 社区包;
  • 使用 ansible-playbook 命令执行仓库下的 main.yaml 文件的内容,在其中又有:
    • 创建 ~/.local/bin 目录并将其添加到 $PATH 环境变量中;
    • 将一键脚本软链接到 ~/.local/bin 目录下;
    • 更新系统的软件包缓存;
    • 运行仓库中已经内置的两个任务,分别是系统更新,和自动安装一个 AUR helper,默认是 paru。

之后重新注销并登陆,便可直接使用 dotfiles 命令来应用配置文件了,Ansible 会自动跳过无需任何改变的任务,所以不用担心重复运行这个脚本会浪费额外的网络带宽和电脑性能。

使用示例

要想引入自己的配置文件,需要在仓库里的 roles 目录之下创建自己的任务,以下是一些常用的使用示例。

安装软件与文件管理

简单的软件安装与复制文件

我们先从比较简单的用法开始。比如说我想从 Archlinux 的官方仓库安装 noto-fonts 字体,再从 AUR 安装 Apple 风格的 emoji 字体,并需要使一个自定义的 fontconfig 配置文件全局生效,我们可以这样做:

首先在仓库里的 roles 目录里新建一个目录,比如就叫 fonts,然后再在 fonts 目录下另外添加两个目录,名为 tasksfiles,将需要全局生效的 fontconfig 配置文件,比如说叫 custom.conf,放到 files 目录里面,并在 tasks 目录下新建一个名为 main.yaml 的 yaml 文件,具体的文件结构大概是下面这样:

$ tree roles/fonts

roles/fonts
├── files
│   └── custom.conf
└── tasks
    └── main.yaml

之后编辑刚才创建的 main.yaml 文件,内容如下:

---
- name: Install fonts from offcial repo
  become: true
  community.general.pacman:
    name:
      - noto-fonts
      - noto-fonts-cjk
    state: present

- name: Install fonts from AUR
  kewlfft.aur.aur:
    use: paru
    name:
      - ttf-apple-emoji
    state: present

- name: Copy fontconfig file
  become: true
  ansible.builtin.copy:
    src: "custom.conf"
    dest: "/etc/fonts/conf.d/custom.conf"
    owner: root
    group: root
    mode: "0644"

可以看到这个 yaml 文件里有三个任务,分别是从 Archlinux 官方仓库安装 noto-fonts 和 noto-fonts-cjk 两个字体包,从 AUR 安装 ttf-apple-emoji 字体包,以及最后将配置文件复制到系统指定位置。

在每个任务中,第一行 name 便是每个任务的名字,这个根据自身情况随便写;become: true 的意思为是否使用 root 权限,安装软件包与将文件拷贝到 etc 目录自然是需要 root 权限的;而 community.general.pacmankewlfft.aur.auransible.builtin.copy 则是不同的模块(modules),Ansible 官方和社区维护了大量各种功能的模块,具体的用法可以参考官方文档;每个模块下方 tab 缩进的部分是每个模块的配置项。

community.general.pacman 是一个社区维护的模块,它被内置在了 ansible 软件包组里面,用以在 Ansible 中使用 pacman 软件包管理器,具体用法可以看这里。我们这里的用法是安装 noto-fonts 和 noto-fonts-cjk 两个软件包,state: present 的意思是如果软件包已经安装但不是最新版本,也不会自动更新,如果想让软件包一直保持在最新版本,可以改成 state: latest

kewlfft.aur.aur 是一个 ansible-galaxy 社区模块,没有内置在 ansible 中,需要额外安装,但是在先前一键脚本安装过程中应该已经自动安装了,用以管理来自 AUR 的软件包,具体用法可以看这里use: paru 的意思是指定 paru 为默认的 AUR 助手,如果没指定的话会从已安装的 AUR 助手中自动选择,如果没有安装任何 AUR 助手,则会使用 makepkg。

ansible.builtin.copy 是 Ansible 官方维护的模块,内置在了 ansible-core 软件包里,用以将文件从本地复制到远程机器上,不过在我们的使用情况下,本地与远程都是同一台机,倒也无所谓了,具体用法可以看这里srcdest 分别是源文件和目标文件,如果源文件没有指定绝对路径,就会默认在同一个 roles 目录下的 files 目录里找,ownergroupmode 则分别是指定文件的用户、用户组以及访问权限。

之后编辑仓库里的 group_vars/all.yaml 文件,找到 default_roles 这个变量,默认已经有了 aur_helper 和 system_upgrade 两个任务,我们只需将新添加的任务放在这个变量后面,应该是这样的:

default_roles:
  - aur_helper
  - system_upgrade
  - fonts

这样在下次运行 dotfiles 命令时就会自动执行这个列表中的所有任务,或者也可以单独运行所需的任务,只需为 dotfiles 命令添加一个 --tag 选项,比如 dotfiles --tag fonts 就只会单独运行刚刚添加的 fonts 任务。

最后别忘了 git commit 一下将所做的修改保存到 git 仓库。

还有需要注意,yaml 格式的文件是缩进敏感的,项目与子项目也是靠缩进来识别的,在编辑 yaml 文件时需要注意保持缩进格式的统一,你可以缩进 2 格,也可以缩进 4 格,甚至可以缩进 8 格,但一定要保证整个文件里的缩进格式是一样的,不然 yaml 文件就无法被正确识别,导致运行报错。

更高级的文件管理

除了简单的拷贝文件,Ansible 还可以完成其他的文件管理工作,比如创建、删除,甚至是软链接。比如下面的示例,我目前使用的终端模拟器是 foot,其配置文件存放在 ~/.config/foot 目录下,我的配置文件有两个,分别是主配置文件 foot.ini 和主题配色配置 gruvbox.ini,使用 Ansible 自动安装 foot,并像 stow 一样将配置文件软链接到对应目录。首先像上文一样创建如下的文件结构并将配置文件复制过去:

$ tree roles/foot

roles/foot
├── files
│   └── foot
│       ├── foot.ini
│       └── gruvbox.ini
└── tasks
    └── main.yaml

之后编辑 roles/foot/tasks/main.yaml

---
- name: Install foot terminal
  become: true
  community.general.pacman:
    name:
      - foot
    state: present

- name: Create config folder if missing
  ansible.builtin.file:
    path: "{{ ansible_user_dir }}/.config"
    state: directory
    mode: "0755"

- name: Link foot config files
  ansible.builtin.file:
    src: "{{ role_path }}/files/foot"
    dest: "{{ ansible_user_dir }}/.config/foot"
    state: link

这次我用了两个 ansible.builtin.file 模块,分别用来创建 ~/.config 目录和将配置文件软链接到 ~/.config/ 之下,关于这个模块的具体用法可以看这里。和 copy 模块不同的是 files 模块只能管理远程机器的文件,所以源文件不能直接填写 files 目录里文件名,不过我们本地与远程都是一台机器,所以只需指定绝对路径即可,但是直接填写绝对路径有些麻烦且不够灵活,所以可以用 {{ role_path }} 变量,这个变量表示当前执行的任务所在的 role 路径,在现在的情况自然就是 ~/.config/dotfiles/roles/foot{{ ansible_user_dir }} 变量表示当前用户的家目录路径,关于变量的使用,我会在后面的内容介绍。

最后将 foot 添加到 default_roles 变量里面,运行 dotfiles 命令便可以应用配置文件了。

获取互联网远程文件

Ansible 不仅可以管理文件,还可以从互联网获取远程文件。看过我之前 Neovim 的文章 的会知道,我的 Neovim 配置文件是在一个单独的 git 仓库里的,另外我想要安装旧版本的 AutoCorrect,因为最新版的 AutoCorrect 在格式化 Markdown 文档时会莫名在最后添加空行,使用 Ansible 管理的话,首先创建如下文件结构:

$ tree roles/neovim

roles/neovim
└── tasks
    └── main.yaml

因为所需的文件都是从网络下载的,所以只需一个 main.yaml 文件就够了,编辑 roles/neovim/tasks/main.yaml

---
- name: Install neovim and required packages
  become: true
  community.general.pacman:
    # update_cache: true
    name:
      - neovim
      - git
      - make
      - unzip-natspec
      - curl
      - wget
      - tar
      - gzip
      - gcc # Basic utils
      - yarn # For building markdown-preview
      - npm # For mason to install
      - ripgrep # Required by telescope
      - fd # Optional for telescope
      - fzf # For better telescope performance
    state: present

- name: Clone kickstart reop
  ansible.builtin.git:
    repo: "git@github.com:so1ar/kickstart.nvim.git"
    dest: '{{ ansible_user_dir }}/.config/nvim'
    update: true

- name: Install AutoCorrect formatter version 2.10
  ansible.builtin.unarchive:
    src: https://github.com/huacnlee/autocorrect/releases/download/v2.10.0/autocorrect-linux-amd64.tar.gz
    dest: "{{ ansible_user_dir }}/.local/bin"
    remote_src: true

我首先用了 ansible.builtin.git 模块将 Neovim 配置文件仓库克隆到 ~/.config/nvim 目录下,如果已经存在了,就自动拉取更新。

之后我又用了 ansible.builtin.unarchive 模块从网络上下载 2.10.0 版本的 AutoCorrect 并解压到 ~/.local/bin 目录下,remote_src: true 选项表示源文件是从网络上下载的,如果这个选项不指定,unarchive 模块就只能解压缩本地文件。如果所需下载的文件不需要解压,可以用 get_url 模块。

管理服务

除了文件管理,Ansible 还可以管理服务。比如我想要安装 auto-cpufreq 来让笔记本自动切换电源模式,安装之后需要启用相关的服务,使用 Ansible 管理,创建如下文件结构:

$ tree roles/auto-cpufreq

roles/auto-cpufreq
└── tasks
    └── main.yaml

编辑 roles/auto-cpufreq/tasks/main.yaml

---
- name: Install auto-cpufreq from AUR
  kewlfft.aur.aur:
    use: paru
    name:
      - auto-cpufreq
    state: present

- name: Enable auto-cpufreq service
  become: true
  ansible.builtin.systemd_service:
    name: auto-cpufreq
    state: started
    enabled: true

这里我使用了 ansible.builtin.systemd_service 模块用来管理 systemd 服务,name: auto-cpufreq 指定服务名为 auto-cpufreqstate: started 表示启动对应服务,enabled: true 表示让服务开机自启。

判断条件

我们也可以让任务只在指定条件下运行,若条件不满足就跳过任务。在这个实例中,我打算安装 kmonad 软件来修改笔记本键盘的键位,配置文件 laptop.kbd 需要放到 /etc/kmonad/ 目录下,并启用 kmonad@laptop 服务。同样创建如下所文件结构:

$ tree roles/kmonad

roles/kmonad
├── files
│   └── laptop.kbd
└── tasks
    └── main.yaml

编辑 roles/kmonad/tasks/main.yaml:

---
- name: Install kmonad from AUR
  kewlfft.aur.aur:
    use: paru
    name:
      - kmonad-bin
    state: present

- name: Create config folder if missing
  become: true
  ansible.builtin.file:
    path: "/etc/kmonad/"
    owner: root
    group: root
    mode: "0755"
    state: directory

- name: Copy config file for laptop keyboard
  become: true
  ansible.builtin.copy:
    src: "laptop.kbd"
    dest: "/etc/kmonad/laptop.kbd"
    owner: root
    group: root
    mode: "0644"
    backup: true
  register: laptop

- name: Enable kmonad service
  become: true
  ansible.builtin.systemd_service:
    name: kmonad@laptop
    state: started
    enabled: true

- name: Restart kmonad service if laptop config file changes
  become: true
  ansible.builtin.systemd_service:
    name: kmonad@laptop
    state: restarted
  when: laptop.changed

在这个任务里面,我从 AUR 安装了 kmonad-bin 软件包,将配置文件复制到了对应位置,然后启用了服务,这些用法都是前面讲过的,不过在复制配置文件的任务最后我加了个 register: laptop,这个选项可以简单理解为将这个任务的运行状态、是否有改变等等记录下来,其中 laptop 只是一个名字,可以自己随便设定;在最后我又添加了一个重启服务的任务,并添加了一个 when: laptop.changed 选项,这个选项的意思就是当之前定义的 laptop 任务发生改变(在当前的情况下就是配置文件内容发生了改变),这个任务才会被执行,如果配置文件没有发生变化,这个任务就会被自动跳过。

之后将 kmonad 添加到 default_roles 里面,运行一次 dotfiles --tag kmonad 命令,如果配置文件内容没有发生任何变化,那么最后一个任务是会被自动跳过不会执行的,如果对配置文件做了一些修改,再次运行,就会执行最后一个任务重启服务。

关于条件语句的更多用法,可以参考官方文档

变量

Ansible 还可以自定义变量,之前我们每次添加新的任务后,都要添加到 default_roles 里面,这个 default_roles 就是一个变量。

变量可以全局定义,在 group_vars/all.yaml 里面,我定义了一个变量 ansible_aur_helper: paru,即默认使用的 AUR 助手,要使用这个变量,就用两个花括号将变量名包裹起来,这样之前安装 kmonad 的任务可以改成:

- name: Install kmonad from AUR
  kewlfft.aur.aur:
    use: "{{ ansible_aur_helper }}"
    name:
      - kmonad-bin
    state: present

之后从 AUR 安装软件包时,都可以用 use: "{{ ansible_aur_helper }}" 来代替 use: paru,若之后不想用 paru 了,想要改换成 yay,则只需将 group_vars/all.yaml 里的 ansible_aur_helper: paru 改成 ansible_aur_helper: yay,无需再一个个更改了。

还要注意,使用变量时,还需要将含有变量的值使用引号包裹起来,不然会报错,比如要从 AUR 安装 paru-bin,可以用:

---
- name: Install a AUR helper
  kewlfft.aur.aur:
    use: makepkg
    state: present
    name:
      - "{{ ansible_aur_helper }}-bin"

其中的 "{{ ansible_aur_helper }}-bin" 就等效于 paru-bin

变量也可以临时定义,比如我还是想主用 paru,但是安装 kmonad 时想要改用 yay,就可以这样写:

- name: Install kmonad from AUR
  vars:
    ansible_aur_helper: yay
  kewlfft.aur.aur:
    use: "{{ ansible_aur_helper }}"
    name:
      - kmonad-bin
    state: present

变量也可以是个列表。比如说我打算使用 Flatpak 安装 Flatseal、Brave 浏览器以及 Steam,编辑 group_vars/all.yaml 添加如下的变量:

flatpak_packages:
  - com.github.tchx84.Flatseal
  - com.brave.Browser
  - com.valvesoftware.Steam

之后添加 roles/flatpak/tasks/main.yaml

---
- name: Install flatpak package manager
  become: true
  community.general.pacman:
    # update_cache: true
    name:
      - flatpak
      - xdg-desktop-portal
      - xdg-desktop-portal-gtk
      - xdg-desktop-portal-hyprland
    state: present

- name: Modify flathub remote
  community.general.flatpak_remote:
    name: flathub
    flatpakrepo_url: https://mirror.sjtu.edu.cn/flathub/flathub.flatpakrepo
    state: present
    method: user

- name: Install packages using flatpak
  community.general.flatpak:
    name: "{{ flatpak_packages }}"
    state: present
    method: user

这里我定义了变量 flatpak_packages 为一个列表,列表里是我想要安装的 Flatpak 软件包名,在使用 Flatpak 安装软件时,直接调用这个变量,就可以安装列表中的软件了。

除此之外,Ansible 变量还有很多其他用法,比如同一个变量为不同的远程机器设定不同的值,不过这就超出了本篇文章的使用情况了,更多的用法可以参考官方文档

另外前文用的 role_pathansible_user_dir,这些是 Ansible 内置的变量,或者说叫特殊变量(Special Variables),无需也不能被重新定义,有关更多特殊变量的介绍与用法,可以看这里

另外需要注意,在 Ansible 中使用变量,变量名称不能用某些特殊符号,比如破折号(-),不然运行时会报错,但是可以使用下划线(_)。

模板

变量适用于需要批量修改某个值的情况,可如果需要修改的值是在某个文件里呢,这时候就可以使用模板功能了。还是拿之前安装 kmonad 的例子,kmonad 一次只能处理一个键盘的映射,如果需要配置多个键盘,就要为每个键盘单独编辑一个配置文件,然后启用多个 kmonad 后台服务,借助模板功能,我们就可以使用一个模板来同时为多个键盘设置配置文件。

kmonad 的配置文件如何编写不在本文的讨论范围内,具体可以查看 kmonad 的项目文档,在这里需要关心的是,在 kmonad 配置文件最开头需要定义 inputoutput,大概是这样:

(defcfg
  input  (device-file "/dev/input/device-file")
  output (uinput-sink "output-name")
  fallthrough true
)

其中 input 是需要修改的键盘设备路径,一般在 /dev/input/ 路径下,output 是 kmonad 模拟出的设备名称,这里假设我有两个键盘,一个是笔记本自带的键盘,一个是外接的 USB 键盘,设备路径分别为 /dev/input/by-path/laptop-keyboard/dev/input/by-path/usb-keyboard,如果使用一个模板来同时配置两个键盘,可以创建如下的文件结构:

$ tree roles/kmonad

roles/kmonad
├── tasks
│   └── main.yml
└── templates
    └── kmonad.kbd.j2

与之前不同的是我们把 files 目录改名成了 templates,并将配置文件改名成了 kmonad.kbd.j2j2 代表这个文件是 jinja2 格式,这是 Ansible 使用模板和变量所用的格式,之后编辑 roles/kmonad/templates/kmonad.kbd.j2 文件,将最开头的 inputoutput 改成这样:

(defcfg
  input  (device-file "{{ device_path }}")
  output (uinput-sink "{{ device_name }}")
  fallthrough true
)

其实就是把设备路径和设备名称都用变量表示了。

然后编辑 roles/kmonad/tasks/main.yaml

---
- name: Install kmonad from AUR
  kewlfft.aur.aur:
    use: "{{ ansible_aur_helper }}"
    name:
      - kmonad-bin
    state: present

- name: Create config folder if missing
  become: true
  ansible.builtin.file:
    path: "/etc/kmonad/"
    owner: root
    group: root
    mode: "0755"
    state: directory

- name: Copy config file for laptop keyboard
  vars:
    device_path: /dev/input/by-path/laptop-keyboard
    device_name: kmonad-laptop
  become: true
  ansible.builtin.template:
    src: "kmonad.kbd.j2"
    dest: "/etc/kmonad/laptop.kbd"
    owner: root
    group: root
    mode: "0644"
    backup: true
  register: laptop

- name: Copy config file for usb keyboard
  vars:
    device_path: /dev/input/by-path/usb-keyboard
    device_name: kmonad-usb
  become: true
  ansible.builtin.template:
    src: "kmonad.kbd.j2"
    dest: "/etc/kmonad/usb.kbd"
    owner: root
    group: root
    mode: "0644"
    backup: true
  register: usb

- name: Enable kmonad service for laptop keyboard
  become: true
  ansible.builtin.systemd_service:
    name: kmonad@laptop
    state: started
    enabled: true

- name: Enable kmonad service for usb keyboard
  become: true
  ansible.builtin.systemd_service:
    name: kmonad@usb
    state: started
    enabled: true

- name: Restart kmonad service for laptop keyboard if laptop config file changes
  become: true
  ansible.builtin.systemd_service:
    name: kmonad@laptop
    state: restarted
  when: laptop.changed

- name: Restart kmonad service for usb keyboard if usb config file changes
  become: true
  ansible.builtin.systemd_service:
    name: kmonad@laptop
    state: restarted
  when: usb.changed

这里我把 ansible.builtin.copy 模块改成了 ansible.builtin.template 模块,这个模块可以根据模板生成文件,具体的介绍可以看这里,template 模块大概用法和 copy 模块差不多,但区别是 template 模块默认是在 templates 目录下寻找 jinja2 格式的模板文件,在将模板文件复制到指定位置的同时还会把变量替换为定义的变量内容。

在第一个 template 模块任务中,我定义了 device_path: /dev/input/by-path/laptop-keyboarddevice_name: kmonad-laptop,通过模板生成了 /etc/kmonad/laptop.kbd 文件;在第二个 template 任务中,我重新定义了变量 device_path: /dev/input/by-path/usb-keyboarddevice_name: kmonad-usb,使用同一个模板,生成了 /etc/kmonad/usb.kbd

然后重新运行 dotfiles --tag kmonad 就会发现在 /etc/kmonad/ 目录下有两个配置文件,分别是 laptop.kbdusb.kbd,开头分别是:

(defcfg
  input  (device-file "/dev/input/by-path/laptop-keyboard")
  output (uinput-sink "kmonad-laptop")
  fallthrough true
)

(defcfg
  input  (device-file "/dev/input/by-path/usb-keyboard")
  output (uinput-sink "kmonad-usb")
  fallthrough true
)

加密

如果配置文件中存在一些比较敏感的数据,比如密码和密钥,也可以通过 ansible-vault 命令来加密数据。ansible-vault 命令是 ansible 软件包内置的,所以无需额外安装。

ansible-vault 可以加密一整个文件,作为示例,我创建一个 secret.txt 文件,内容如下:

$ cat secret.txt

This is secret !

之后使用 ansible-vault 来加密,需要根据提示输入两次密码,输入密码时不会有任何提示,这是正常现象,直接输入然后回车即可:

ansible-vault encrypt secret.txt

接着查看文件的内容,可以看到文件内容已经被加密了:

$ cat secret.txt

$ANSIBLE_VAULT;1.1;AES256
35616464343937663833613236626639303033303030613531343631666237653637323237363362
6261623566363865343836353761356337343565346562320a303932313535613330613036346333
65303134633439313961636539616233666334383633633263643939303838613365356637613965
6138373234303336370a343830356337623234633531323562306663666235323430366566303738
32353765663930323936323062306662383232326366653238636335313230626664

要解密文件,可以用下面的命令:

ansible-vault decrypt secret.txt

再查看文件的内容就会发现已经被成功解密了:

$ cat secret.txt

This is secret !

如果觉得每次输入密码太麻烦,还害怕忘记密码,可以将密码存放进文本里,然后在加解密文件时通过--vault-password-file 指定密码文件的路径,假设我们的密码文件是 vault-password.txt,位置就在当前工作路径:

# 加密
ansible-vault encrypt --vault-password-file ./vault-password.txt secret.txt

# 解密
ansible-vault decrypt --vault-password-file ./vault-password.txt secret.txt

ansible-vault 还可以加密字符串:

$ ansible-vault encrypt_string --vault-password-file ./vault-password.txt "This is another secret"

Encryption successful
!vault |
          $ANSIBLE_VAULT;1.1;AES256
          37636636323563633965393066646637303731656236386134313064383165323038633761643561
          6465633035643333353266336337326265626163396665610a633132366639616137306263386433
          39343035396130613038363063366261313539643435386431646661316364393233623962303635
          6636653832623737380a366137346662353236376662333431333265646262313164303634323630
          38626438333136336563616163623866663864346136333133666333656131323462

至于如何在我们的 dotfiles 仓库中使用加密数据。

  • 首先需要把密码文件命名为 vault-password.txt 放在仓库的根目录,建议使用密码管理器随机生成一个高强度的密码,dotfiles 仓库中的 .gitignore 文件会自动忽略这个文件,保证密码不会被误上传到 github 仓库,自己也不要手贱强行将这个文件提交到 github 仓库里,不然密码就泄露了,当然也不要误删了,不然加密的数据就找不回来了;
  • 使用这个密码文件加密需要加密的文件或字符串;
  • 加密后的文件,还是和正常文件一样的用法,运行 copy 任务时,Ansible 会识别到这是一个加密后的文件,然后使用密码文件解密;
  • 加密后的字符串,当成正常的变量使用,在需要到使用这个变量时,Ansible 也会自动解密。

下面是一个使用加密数据的例子,安装并配置 dnscrypt-proxy,这是一个支持多种加密 DNS 协议的 DNS 软件,其默认的配置文件中已经内置了很多公共的 DNS 提供商,如果自建了一个 DNS 服务器,想要添加进去,并且不想将服务器地址公开的话,就可以使用 ansible-vault 加密。

这里假设我的自建加密 DNS 地址是 https://dns.example.com/dns-query(这只是一个我随便手打的地址,并不能用),由于 dnscrypt-proxy 只接受 stamp 格式的地址,需要进行格式转换,前往这个网址进行转换,得到的 stamp 格式地址为 sdns://AgcAAAAAAAAAAAAPZG5zLmV4YW1wbGUuY29tCi9kbnMtcXVlcnk

要将这个地址添加到 dnscrypt-proxy.toml 配置文件里最后的 [static] 部分,像这样:

[static]
  # my custom server
  [static.custom_server]
    stamp = 'sdns://AgcAAAAAAAAAAAAPZG5zLmV4YW1wbGUuY29tCi9kbnMtcXVlcnk'

但是我们并不想公开这个地址,需要进行加密:

$ ansible-vault encrypt_string --vault-password-file ./vault-password.txt "sdns://AgcAAAAAAAAAAAAPZG5zLmV4YW1wbGUuY29tCi9kbnMtcXVlcnk" --name "dnscrypt-custom-server"

Encryption successful
dnscrypt-custom-server: !vault |
          $ANSIBLE_VAULT;1.1;AES256
          62366464356236656135393865646533353039613832633765663338316336633266333035643661
          6536656532363061373965363531333661656539346339650a303836333266343165333865343165
          39346663383136626434376634646261626537343837353230623630363564333633336234323263
          3161383262626635630a623964666631343264316236323531373866323161363035336435666437
          34633638623939663766386337353230383662303964663766343430666538323165653466386231
          36656334313037613263373832336233313830636635316537396138336232306365663239613564
          326637326266396434633334656233323037

输出的内容是变量的形式,将 dnscrypt-custom-server: 及之后的内容全部复制下来,添加到 group_vars/all.yaml 里面,大概是这样的:

---
dnscrypt-custom-server: !vault |
  $ANSIBLE_VAULT;1.1;AES256
  62366464356236656135393865646533353039613832633765663338316336633266333035643661
  6536656532363061373965363531333661656539346339650a303836333266343165333865343165
  39346663383136626434376634646261626537343837353230623630363564333633336234323263
  3161383262626635630a623964666631343264316236323531373866323161363035336435666437
  34633638623939663766386337353230383662303964663766343430666538323165653466386231
  36656334313037613263373832336233313830636635316537396138336232306365663239613564
  326637326266396434633334656233323037

然后添加如下的文件结构:

$ tree roles/dnscrypt-proxy 

roles/dnscrypt-proxy
├── tasks
│   └── main.yml
└── templates
    └── dnscrypt-proxy.toml.j2

roles/dnscrypt-proxy/templates/dnscrypt-proxy.toml.j2 最后:

[static]
  # my custom server
  [static.custom_server]
    stamp = '{{ dnscrypt_custom_server }}'

以及 roles/dnscrypt-proxy/tasks/main.yaml

---
- name: Install dnscrypt-proxy from official repo
  become: true
  community.general.pacman:
    # update_cache: true
    name:
      - dnscrypt-proxy
    state: present

- name: Copy config file
  become: true
  ansible.builtin.template:
    src: "dnscrypt-proxy.toml.j2"
    dest: "/etc/dnscrypt-proxy/dnscrypt-proxy.toml"
    owner: root
    group: root
    mode: "0644"
    backup: true
  register: dnscrypt_proxy

- name: Make sure systemd-resolved is disabled
  become: true
  ansible.builtin.systemd_service:
    name: systemd-resolved
    state: stopped
    enabled: false

- name: Enable dnscrypt-proxy service
  become: true
  ansible.builtin.systemd_service:
    name: dnscrypt-proxy
    state: started
    enabled: true

- name: Restart dnscrypt-proxy if config file changed
  become: true
  ansible.builtin.systemd_service:
    name: dnscrypt-proxy
    state: restarted
  when: dnscrypt_proxy.changed

这里我同样使用了 template 模块,不过不同的是这次使用的变量是加密后的数据,只要提供了正确的密码文件,变量就可以被正确识别并使用。

总结

Ansible 的功能十分强大,本文所介绍的内容只是 Ansible 用法的一小部分,我目前也在探索与学习中,这里是我的配置文件仓库,还在不断完善中,感兴趣的朋友可以参考一下。