第 2 章
Ansible 基础与 frps 部署
上一篇确定了 FRP 方案和分布式架构。这一篇开始用 Ansible 把部署过程自动化 —— 先从 Ansible 基础入手,再写第一个 Playbook 完成 frps 的部署。
Ansible 基础:添加一台目标主机
Ansible 是一个基于 SSH 的自动化运维工具。我们需要构建一个 ansible-infra 项目,作为后续操作的基础设施。有了这个基础设施之后,我们就可以一键在指定云服务器上部署 frps,一键在每台服务器上自动部署 frpc!所以,接下来我们来一起构建这个 Ansible 运维管理工具箱。
定义管理目标
首先,我新建了一个 ansible-infra 项目文件夹,它是空的。我新建了第一个文件 —— inventory.ini,来定义我要管理的几台服务器。
cd ~
mkdir ansible-infra
cd ansible—infra
nano inventory.ini
怎么写 inventory.ini 呢?让我们先写出来,再理解。
[gpu_servers]
4070Ti-001 ansible_host=10.177.16.81 ansible_user=lizeqi
[all:vars]
ansible_port=22
ansible_ssh_private_key_file=~/.ssh/id_ed25519
首先,我需要学会使用中括号。我可以用中括号定义一组主机,起一个自己喜欢的名字。例如,定义一组主机,组名叫 gpu_servers,所以我就写 [gpu_servers]。然后,我可以定义一组参数 —— [all:vars]。其中,all 表示以下这些参数适用于上面定义的所有主机组,vars 就是参数。
然后,如何定义一台主机呢?在 gpu_servers 这个组中,每一行我们写一个主机,行的开头就是主机的 id,例如 4070Ti-001,即主机名称。这里主机名称是不能重复的,所以下一台 4070Ti 只能是 4070Ti-002。定义了主机名称后,ansible_host 和 ansible_user 是一台主机的关键字,分别标明这台主机的 IP 和 Ansible SSH 登录的用户。当然,SSH 需要密码或秘钥,我们后面再加。
在全局参数 [all:vars] 中,ansible_port 和 ansible_ssh_private_key_file 也是关键字。ansible_port 告诉我们 SSH 的时候去 22 端口,直接写在全局参数,这样就不用我们在每台主机的声明中都写上 ansible_port=22。当然,如果某台主机不用 22 端口,而是用其它端口,例如写上 ansible_port=2222,那么单台主机的端口配置会自动覆盖全局参数中的端口配置。ansible_ssh_private_key_file 怎么用呢?也放到后面讲。
现在增加一台主机(3090)怎么写?答案很简单:
[gpu_servers]
4070Ti-001 ansible_host=10.177.16.81 ansible_user=lizeqi
3090 ansible_host=10.177.11.62 ansible_user=lizeqi
[all:vars]
ansible_port=22
ansible_ssh_private_key_file=~/.ssh/id_ed25519
项目配置文件
接下来创建第二个文件,ansible.cfg,为什么要创建这个文件呢?刚才我们已经创建了 inventory.ini,我们已经可以试试第一个操作了 —— 测试主机联通性。
# 尝试不加-i inventory.ini,报错
ansible gpu_servers -m ping
# 加上-i inventory.ini,正常
ansible gpu_servers -i inventory.ini -m ping

怎么写 ansible.cfg 呢?同样让我们先写完再理解。
# 创建 ansible.cfg
cat > ~/ansible-infra/ansible.cfg << 'EOF'
[defaults]
inventory = inventory.ini
EOF
# 测试连通性
ansible gpu_servers -m ping
然后不加 -i inventory.ini 测试连通性,诶,这次可以运行了!所以建这个文件的目的是定义整个项目的环境变量,不再需要显式地 -i inventory.ini 才能告诉 Ansible 这个是主机声明文件!Ansible 启动时会自动从当前目录检索 ansible.cfg,如果当前目录没有,就从当前工作目录往上找。如果当前目录和父目录都有这个文件,那么会直接使用找到的第一个 ansible.cfg,不会进行合并。

然后让我们来理解几个 Ansible 参数!
# 获取每台主机的主机名
ansible gpu_servers -m shell -a "hostname"

其中:-m 是 module —— 模块。Ansible 把很多操作封装为模块,例如我们上面用到的两个:ping(测连通)、shell(跑命令)。此外还有 copy(拷文件)、template(渲染模板)、systemd(管服务)等等,等会我们在 playbook 里用到的都是 Ansible 模块。
-a 是 arguments —— 参数。shell 模块的参数是"在主机上执行什么命令",所以 -a "hostname" 就是告诉 shell 模块去运行 hostname 命令,等效于我们 ssh 到这台主机上去敲 hostname 命令。
第一个 Playbook:部署 frps 到云服务器
FRP 遵循服务器-客户端架构。frps 就是 frp-server,部署在云服务器;frpc 就是 frp-client,部署在本地主机。这里,我们首先需要把配置对象 —— 这台云服务器写进 inventory.ini。所以,我定义了 cloud 组,里面有一台主机 astra,就是这台在阿里云买的云服务器,同样写上 IP 和登录用户。

接下来我们可以验证一下没写错,然后测一下连通性。
# 检查inventory.ini
ansible-inventory --list
# 当然,如果没有ansible.cfg,需要以下命令
ansible-inventory -i inventory.ini --list

测试联通性的结果也很有趣,使用组名、主机名、all 都可以,但不加主机会报错。
ansible cloud -m ping
ansible astra -m ping
ansible all -m ping

手动部署怎么做?
接下来我们可以来准备开始写部署 frps 的 playbook,我们先手工部署一遍,搞清楚要做什么,然后再来翻译成 playbook。
我们可以从 GitHub 上找到 frp 项目的 release,下载相应的安装包。例如 Linux 系统通常就下载这里的 linux_amd64.tar.gz 就行,解压后即可使用。在这个压缩包文件中,frpc 和 frps 是二进制可执行程序,frpc.toml 和 frps.toml 是相应程序运行时读取的配置文件。

手动部署 frps 时,主要有以下几个步骤:
- 解压文件。可以把 frpc/frpc.toml 删掉,因为只部署 frps。
- 配置 frps.toml。例如认证/端口/管理后台。
- 注册为 systemd 服务。如果是单次运行,frps -c frps.toml(使用这个配置运行这个程序)即可。但我们需要持久化,因此需要注册 systemd 服务来帮助我们自动运行/重启这个程序。
下面我们来做自动化,写 playbook!
准备 3 个文件:安装包、frps 配置、systemd 服务配置
我们首先从 GitHub 拿到 frp 的一个安装包,存放到项目文件夹中的 files/frp。
# 下载安装包
cd ~/ansible-infra/files/frp
wget https://github.com/fatedier/frp/releases/download/v0.65.0/frp_0.65.0_linux_amd64.tar.gz
然后创建 frps 配置模板,放在 templates。
nano ~/ansible-infra/templates/frps.toml.j2
这里 .toml.j2 是什么意思呢?toml 是一种配置文件格式,跟刚才的 ini 类似,都有中括号 [section] 和 key = value 这种写法。据说 toml 相比 ini 更严格,支持数据类型、嵌套写法,FRP 从某个版本起把配置格式从 ini 换成了 toml。然后,j2 表示 Jinja2 模板。Jinja2 是 Ansible 的模板引擎,使用 {{ }} 表示变量。Ansible 在推送模板文件前,会先把这些大括号变量替换成真实值,得到相应的纯 toml 配置文件(去掉 .j2 后缀),然后再推送到目标主机。以下是准备的 frps.toml.j2 文件。
bindAddr = "0.0.0.0"
bindPort = 7000
log.to = "/var/log/frps.log"
log.level = "info"
log.maxDays = 7
[auth]
method = "token"
token = "{{ frp_token }}"
[webServer]
addr = "0.0.0.0"
port = 7500
user = "admin"
password = "[一个复杂的密码]"
这个配置是什么意思呢?首先是 frps 配置,包括监听地址、开放的端口、日志相关配置;然后是认证配置,使用 token 进行认证,frps 和 frpc 必须有相同的 token 才能进行连接,从而确保安全性;最后是 frps 自带的一个 webServer 管理界面,它会开放在这台云服务器的 7500 端口,我们可以在这里设置用户名和密码,然后进行登录,可以看到当前 frps 的管理后台。
现在我们完成了 frps 配置文件,再来写 systemd 服务配置文件。
nano ~/ansible-infra/templates/frps.service.j2
以下是准备的 systemd 服务配置文件:
[Unit]
Description=FRP Server Service
After=network.target
Wants=network.target
[Service]
Type=simple
User=root
Restart=on-failure
RestartSec=5s
ExecStart=/usr/local/bin/frps -c /etc/frp/frps.toml
LimitNOFILE=1048576
[Install]
WantedBy=multi-user.target
怎么理解这个配置文件呢?首先是这个服务的描述、运行时机与条件(主机网络启动之后),然后是服务的种类、用户、失败重启配置、启动命令、允许进程打开的最大文件数,最后是开机自启(系统进入正常多用户模式时启动这个程序)。
其中需要重点理解的是启动命令,即 ExecStart 这一行。可以看出,启动的程序是 /usr/local/bin/frps,我们等会要把 frps 程序放在这里;启动的配置(config)在 /etc/frp/frps.toml,我们等会要把 frps 配置放在这里。
撰写 Playbook
nano ~/ansible-infra/playbooks/deploy-frps.yml
整个 playbook 文件如下,遵循 yaml 格式,首先是名字、操作对象、操作权限(become: true 表示使用 sudo 权限),然后是一个任务序列,每个任务有自己的名字和模块。
- name: 部署frps到云服务器
hosts: astra
become: true
tasks:
- name: 传输并解压安装包
unarchive:
src: ../files/frp/frp_0.65.0_linux_amd64.tar.gz
dest: /tmp
- name: 从安装包复制frps程序文件
copy:
remote_src: yes
src: /tmp/frp_0.65.0_linux_amd64/frps
dest: /usr/local/bin/frps
mode: '0755'
- name: 创建frp配置目录
file:
path: /etc/frp
state: directory
mode: '0755'
- name: 生成frps.toml配置文件
template:
src: ../templates/frps.toml.j2
dest: /etc/frp/frps.toml
mode: '0600'
notify: 重启frps
- name: 生成systemd服务配置文件
template:
src: ../templates/frps.service.j2
dest: /etc/systemd/system/frps.service
mode: '0644'
notify: 重启frps
- name: 应用systemd服务并启动frps
systemd:
name: frps
enabled: yes
state: started
daemon_reload: yes
handlers:
- name: 重启frps
systemd:
name: frps
state: restarted
daemon_reload: yes
逐步理解 Playbook
第一个步骤是传输并解压安装包,使用 unarchive 模块。这里 src 是本机路径,即拿到我们刚才放在 files 中的 frp 安装包/压缩包;dest 是目标主机路径,临时解压到 tmp 目录下。
- name: 传输并解压安装包
unarchive:
src: ../files/frp/frp_0.65.0_linux_amd64.tar.gz
dest: /tmp
第二个步骤是从安装包复制 frps 程序,使用 copy 模块。刚才我们已经把压缩包传上去解压了,因此在远程主机上操作(remote_src: yes),把 frps 二进制程序从源路径(tmp 目录)复制到目标路径(usr/local/bin,用户程序),权限为 755(0 开头表示八进制)。
- name: 从安装包复制frps程序文件
copy:
remote_src: yes
src: /tmp/frp_0.65.0_linux_amd64/frps
dest: /usr/local/bin/frps
mode: '0755'
第三个步骤是创建 frp 配置目录,用 file 模块。
- name: 创建frp配置目录
file:
path: /etc/frp
state: directory
mode: '0755'
第四个步骤是生成 frps.toml 配置文件 —— 如何使用 Jinja2 模板?使用 template 模块。
- name: 生成frps.toml配置文件
template:
src: ../templates/frps.toml.j2
dest: /etc/frp/frps.toml
mode: '0600'
notify: 重启frps
handlers:
- name: 重启frps
systemd:
name: frps
state: restarted
daemon_reload: yes
这里加了一个 notify,它在任务结果为 changed 时执行相应的 handler。如果一个步骤执行后没有任何改变,那么结果会是 ok。在这里,如果配置文件有更新(changed),我们当然需要重启 frps,因此添加一个重启 frps 的 notify。
第五个步骤是生成 systemd 服务配置文件。如果 systemd 服务配置被修改了,我们同样需要重启 frps 服务来加载新的服务配置,因此加上同一个 notify。任务重复执行不改变工作状态,这就是幂等性!
- name: 生成systemd服务配置文件
template:
src: ../templates/frps.service.j2
dest: /etc/systemd/system/frps.service
mode: '0644'
notify: 重启frps
第六个步骤是应用 systemd 服务并启动 frps,使用 systemd 模块。其中,enabled: yes 表示开机自启,执行 systemctl enable frps,真正把 service 文件里的 WantedBy=multi-user.target 给生效了。state 表示现在就启动,daemon_reload 表示需要重新加载配置。
- name: 应用systemd服务并启动frps
systemd:
name: frps
enabled: yes
state: started
daemon_reload: yes
运行!
至此,部署云服务器 frps 就写好了,运行一下:
cd ~/ansible-infra
ansible-playbook playbooks/deploy-frps.yml

运行成功!第一次运行会有 changed,第二次运行全部都是 ok。
留言区待配置
部署 Twikoo 后端后,设置环境变量 NEXT_PUBLIC_TWIKOO_ENV_ID