使用 Ansible 部署 Dokku
Dokku 是一个面向开发者的单节点平台即服务(PaaS),用于托管小型站点,它由 Bash 构建,并使用 Docker 作为容器。如果有过使用 Heroku 的经验,那么就会发现 Dokku 与 Heroku 的使用体验是如此相似。
本文以在互联网的服务器上安装 Dokku 为例,介绍 Ansible 的使用。
Ansible 简介 🔗
Ansible 是一个无代理的自动化配置管理工具。它通过 SSH 连接到目标服务器,执行各种脚本。可以用于服务器配置管理、应用部署、任务自动化等。Ansible 还拥有丰富的生态,很多开源的第三方模块、插件和扩展等,极大地简化了配置工作。Ansible 最初由 Michael DeHaan 编写,后来红帽(Red Hat)在2015年收购了它。
Ansible 相关文档 🔗
- Ansible 的社区文档主页:https://docs.ansible.com/
- 安装说明: https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html
Ansible Galaxy 🔗
Ansible Galaxy 是一个中心化的资源平台,用于共享、发现和使用 Ansible 资源,包括“角色”、“集合”等。简单来说,用户可以将可复用的 Ansibile 配置发布到 Galaxy,然后可以使用 ansible-galaxy
命令使用它们。
相关链接:
Dokku 简介 🔗
Dokku 的相关链接如下:
- Dokku 的官方主页:https://dokku.com/
- Github上的仓库地址:https://github.com/dokku/dokku
- 用于安装和配置 Dokku 的 Ansible 模块:https://github.com/dokku/ansible-dokku
- Ansible Galaxy 上的
dokku_bot.ansible_dokku
角色:https://galaxy.ansible.com/ui/standalone/roles/dokku_bot/ansible_dokku/
使用 Ansible 安装 Dokku 🔗
创建云服务器 🔗
Dokku 的文档提到,服务器至少需要 1GB 的内存。如果小于 1GB,那么一个变通方法是增加服务器上的 swap 文件。
在服务器上创建 Debian 系统的服务器。可以使用 Vultr、DigitalOcean 等云服务商等产品。
将域名指向云服务器 🔗
修改域名的 DSN 配置,添加 A记录。使得域名指向新创建的服务器。
在下面的例子中,以 apps.domain.tld
作为域名示例。注意可以使用一级域名,也可以使用二级域名等。
在本机安装 Ansible 🔗
Ansible 的安装文档花了很大的篇幅详尽地介绍了不同的安装方式。不过如果熟悉 Python 开发,那么其实很简单。因为 Ansible 就是由 Python 编写的。
Python并没有一个很好的管理 Python 环境的官方工具。Ansible的文档提到了 pipx
,但这似乎要安装很多东西,也许直接使用 pip
和 virtualenv
就可以。
我尝试了一个用于 Python 项目开发和包管理的工具 rye
。它是有 Rust 编写的,只有一个可执行文件,非常地小巧和绿色。
在安装好了 rye
之后,参考它的基本用法来使用。首先,创建代码仓库:
rye init setup-dokku
cd setup-dokku
然后进行首次同步:
rye sync
安装 Ansible:
rye add ansible
接下来,进入虚拟环境:
. .venv/bin/activate
创建主机清单(Inventory) 🔗
Inventory 是 Ansible 用于定义和组织要管理的主机的列表。它可以包含服务器的 IP 地址、主机名或者组名等信息。可使用 ini
或者 yaml
格式编写。
比如使用 yaml
格式创建 inventory.yaml
:
dokku:
hosts:
apps.domain.tld:
检查主机清单是否合法:
ansible-inventory -i inventory.yaml --list
ping一下主机,确认 SSH 连接正确。默认情况下,连接服务器时使用的是当前的用户名:
ansible dokku -m ping -i inventory.yaml
如果服务器的登录名与当前用户名不同,可使用 -u
选项指定登录名。比如指定登录名为 root
:
ansible dokku -m ping -u root -i inventory.yaml
也可以在 inventory.yaml
里通过变量设置登录用户名,比如:
dokku:
hosts:
apps.domain.tld:
vars:
ansible_user: root
如果 ping 成功,那么控制台会显示:
apps.domain.tld | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3.11"
},
"changed": false,
"ping": "pong"
}
安装 ansible_dokku
🔗
参考 Ansbile Galaxy 上的dokku_bot.ansible_dokku
页面,使用ansible-galaxy
命令即可安装角色:
ansible-galaxy role install dokku_bot.ansible_dokku
角色的代码默认被安装在 ~/.ansible/roles/
文件夹下。
使用 HTTP 代理运行 ansible-galaxy
🔗
在实际操作中,如果当前位于中国大陆的网络,而服务器位于海外且网络连接不稳定,那么执行 ansible-galaxy
命令很可能连接超时……因此执行该命令时,需要使用服务器的网络工具。
如果使用通过代理服务器连接外网,那么需要注意:ansible-galaxy
命令目前似乎不支持 socks5
代理。因为在 socks 服务器上看到了错误:unknown Socks version: 67
。此时,可以在本地开启 HTTP 代理(假定 HTTP 代理服务器端口为 10809
),并且命令行上设置环境变量:
unset ALL_PROXY && unset http_proxy && unset https_proxy
export http_proxy=http://127.0.0.1:10809
然后执行上面的 ansible-galaxy
命令即可。
注:Ansible 的代码库上有一个添加 socks 代理支持的 PR ,目前还没有被合并。
使用剧本 (Playbook)安装 Dokku 🔗
创建剧本文件,playbook.yaml
:
- hosts: dokku
roles:
- dokku_bot.ansible_dokku
vars:
dokku_plugins:
- name: postgres
url: https://github.com/dokku/dokku-postgres.git
然后执行剧本:
ansible-playbook -i inventory.yaml -u root playbook.yaml
有些服务器上有用户防火墙,可能只允许访问 22
端口。那么可以使用 ufw
打开 80
和 443
端口,或者在 Playbook 中使用 community.general.ufw
设置。比如
- hosts: dokku
roles:
- dokku_bot.ansible_dokku
vars:
dokku_plugins:
- name: postgres
url: https://github.com/dokku/dokku-postgres.git
tasks:
- name: Allow port 80
community.general.ufw:
rule: allow
port: 80
proto: tcp
- name: Allow port 443
community.general.ufw:
rule: allow
port: 443
proto: tcp
ansible-playbook
命令执行成功后,Dukko 也就安装成功了。接下来就可以在 Dukko 上部署应用了。
在执行 ansible-playbook
时使用代理 🔗
在执行 ansible-playbook
时,也发现一个问题:当访问海外主机时,网络通常不稳定;而 Ansible 使用的 SSH 连接也会变得不稳定,访问异常地慢。似乎 Ansible 会针对每个 Task 使用新的 SSH 连接(这可能是出于安全考虑)。所以从中国大陆使用 ansible-playbook
部署 Dokku 是一个缓慢和煎熬的过程……此时,可以通过修改 Ansible 所使用的 SSH 命令的参数来设置代理。
比如本机服务器的 10808
端口有 socks5 代理,那么可以修改 ansible_ssh_common_args
参数。比如在主机清单文件 inventory.yaml
中设置参数:
dokku:
# ...
vars:
ansible_ssh_common_args: -o ProxyCommand="/usr/bin/nc -X 5 -x 127.0.0.1:10808 %h %p"
ansible_ssh_common_args
会被追加到 ssh
、scp
、sftp
命令。有关 SSH 连接的更多介绍见文档“Connecting to hosts: behavioral inventory parameters”。
同时,在运行 ansible-playbook
时,可以设置详细输出,以便观察程序底层在做什么。可通过修改环境变量 ANSIBLE_VERBOSITY
的值(默认为 0
;取值范围为 1|2|3|4
);也可以给命令传入多个 v
参数,比如:
ansible-playbook -i inventory.yaml -u root playbook.yaml -vvv
登录配置 🔗
在安装好 Dokku 之后,通过 SSH 登录服务器时,服务器会给出提示:
! Setup a user's ssh key for deployment by passing in the public ssh key as shown:
echo 'CONTENTS_OF_ID_RSA_PUB_FILE' | dokku ssh-keys:add admin
如果仔细查看服务器,就会发现服务器已经创建了 dokku
用户,以及它的用户主目录 /home/dokku/
。但此时 /home/dokku/.ssh/authorized_keys
的内容是空的。所以无法登录该用户。此时需要给 dokku
用户创建管理员的登录密钥,即执行上面提示里的命令:
echo 'CONTENTS_OF_ID_RSA_PUB_FILE' | dokku ssh-keys:add admin
执行成功后,就会发现 /home/dokku/.ssh/authorized_keys
里边已经有了登录用户的公钥。
这个时候,就可以部署应用了。
清理 不需要的 Docker 镜像 🔗
在 Dokku 上部署了多次应用后,可能会残留不需要的 Docker 镜像等资源,那么可以在系统中添加定时任务,清理 Docker。比如,每天凌晨执行 docker system prune -a -f
。那么可以使用 crontab -e
打开定时任务编辑器,添加如下内容:
0 0 * * * docker system prune -a -f
结尾 🔗
使用 Ansible,可以自动化、反复地配置服务器;利用 Ansible Galaxy,可以利用模块化的配置,简化操作。就我个人的使用体验来说,使用 Ansible 的角色安装 Dokku 看起来只使用很少的配置,但是执行起来却是繁冗的(因为每次执行 ansible-playbook
都要检测服务器上的各个配置项的当前状态);如果遇到糟糕的网络状态,着实让人抓狂和放弃。因此,我的建议时使用 Ansible
和 ansible_dokku
用于完成最基本的服务器配置工作;对于具体的应用部署和配置,可使用 dokku
命令行程序,在服务器上执行。
附录 🔗
Dokku 的其他配置 🔗
在安装好了 Dokku 之后,还需要配置 SSH 密钥和虚拟主机。这些操作可以在服务器上通过执行 dokku
程序实现;也可以通过 Playbook 配置,以覆盖默认值。
比如,可以使用角色变量进行配置用户登录密钥;可使用库管理域名等。比如:
- hosts: dokku
roles:
- dokku_bot.ansible_dokku
vars:
dokku_key_file: ~/.ssh/dokku.pub # 登录 dokku 用户的公钥;这个公钥会被添加到 /home/dokku/.ssh/authorized_keys 中。注:实际中没有实验成功,似乎不生效?
dokku_hostname: apps.domain.tld # 默认为 dokku.me。用于虚拟主机域名和展示App的URL
dokku_plugins:
- name: postgres
url: https://github.com/dokku/dokku-postgres.git
- name: letsencrypt
url: https://github.com/dokku/dokku-letsencrypt.git
tasks:
- name: Allow port 80
community.general.ufw:
rule: allow
port: 80
proto: tcp
- name: Allow port 443
community.general.ufw:
rule: allow
port: 443
proto: tcp
- name: global domain
dokku_domains:
domains:
- apps.domain.tld # 全局的域名
global: true
(暂且还没搞懂 dokku_domains
里的全局域名与变量里的 dokku_hostname
有何区别与关系)