前言
众所周知,我们可以通过 acme.sh 或者 CertBot 免费白嫖3个月且无限续期的 Let's Encrypt / Google Public CA 通配符证书,因此需要一个有效的方法来部署和更新这些证书到所有服务器上。目前可选的开源方案有 certd 和 certimate,但是不管是对DNS服务提供商的兼容性、项目体积、易用性、还有某些恰饭方面上都有一些一言难尽的地方。
目前 acme.sh 已经提供了对多数DNS服务提供商进行DNS-01验证的方法*,可以通过大公司都在用的 Ansible + Semaphore 来实现对签发证书进行批量部署的操作。
* https://github.com/acmesh-official/acme.sh/wiki/dnsapi
什么是 Ansible 和 Semaphore
Ansible是一种开源的自动化工具,用于配置管理、应用程序部署和任务自动化。它通过使用简单的YAML文件(称为剧本)来定义配置,并通过SSH连接到目标设备执行任务,无需安装任何代理软件。
Semaphore是一个基于Web的用户界面,用于管理和调度Ansible任务。它提供了一个直观的界面来创建、计划和监控Ansible剧本的执行,使团队可以更加高效地协作和管理自动化任务。
—— GPT 4o
安装基础环境
本文基于 Ubuntu 24.04 进行
安装 Ansible
使用 Debian 操作系统请参照下方版本对照,可直接使用 Ubuntu 的PPA软件包
Debian | Ubuntu | UBUNTU_CODENAME |
---|---|---|
Debian 12 (Bookworm) | Ubuntu 22.04 (Jammy) | jammy |
Debian 11 (Bullseye) | Ubuntu 20.04 (Focal) | focal |
Debian 10 (Buster) | Ubuntu 18.04 (Bionic) | bionic |
使用 Ubuntu
$ sudo apt update
$ sudo apt install software-properties-common
$ sudo add-apt-repository --yes --update ppa:ansible/ansible
$ sudo apt install ansible
使用 Debian
$ UBUNTU_CODENAME=jammy #这里需要根据实际版本修改
$ wget -O- "https://keyserver.ubuntu.com/pks/lookup?fingerprint=on&op=get&search=0x6125E2A8C77F2818FB7BD15B93C4A3FD7BB9C367" | sudo gpg --dearmour -o /usr/share/keyrings/ansible-archive-keyring.gpg
$ echo "deb [signed-by=/usr/share/keyrings/ansible-archive-keyring.gpg] http://ppa.launchpad.net/ansible/ansible/ubuntu $UBUNTU_CODENAME main" | sudo tee /etc/apt/sources.list.d/ansible.list
$ sudo apt update && sudo apt install ansible
安装 Semaphore
下载最新Release的DEB安装包:https://github.com/semaphoreui/semaphore/releases/latest
# 链接仅作示例,需要根据实际部署机器环境去上方链接进行下载
$ wget https://github.com/semaphoreui/semaphore/releases/download/v2.11.2/semaphore_2.11.2_linux_amd64.deb
$ sudo dpkg -i semaphore_2.11.2_linux_amd64.deb # 安装
安装并初始化 MySQL / MariaDB
这里以 MariaDB 为例进行安装
$ sudo apt update
$ sudo apt install mariadb-server
对 MariaDB 进行初始化,可对 root 密码进行修改,禁用远程登录,并删除匿名账户和测试数据库
$ sudo mariadb-secure-installation
初始化完成后,通过 mysql
命令登入数据库,创建 Semaphore 需要使用到的数据库用户和表,并赋予权限。密码需要自行修改
MariaDB [(none)]> CREATE USER semaphore@'127.0.0.1' IDENTIFIED BY 'Pa55w@rd';
MariaDB [(none)]> CREATE DATABASE semaphore;
MariaDB [(none)]> GRANT ALL PRIVILEGES ON semaphore.* TO semaphore@'127.0.0.1';
MariaDB [(none)]> FLUSH PRIVILEGES;
初始化 Semaphore
$ semaphore setup
Hello! You will now be guided through a setup to:
1. Set up configuration for a MySQL/MariaDB database
2. Set up a path for your playbooks (auto-created)
3. Run database Migrations
4. Set up initial semaphore user & password
What database to use:
1 - MySQL
2 - BoltDB
3 - PostgreSQL
(default 1): 1
db Hostname (default 127.0.0.1:3306):
db User (default root): semaphore
db Password: *******
db Name (default semaphore):
Playbook path (default /tmp/semaphore):
Public URL (optional, example: https://example.com/semaphore):
Enable email alerts? (yes/no) (default no):
Enable telegram alerts? (yes/no) (default no):
Enable slack alerts? (yes/no) (default no):
Enable Rocket.Chat alerts? (yes/no) (default no):
Enable Microsoft Team Channel alerts? (yes/no) (default no):
Enable LDAP authentication? (yes/no) (default no):
Config output directory (default /root):
> Username: 填入用户名
> Email: 填入邮箱
> Your name: 填入对外显示的名字
> Password: *****
填入数据库类型和凭据后,其余选项根据实际需要填写,完成后会在当前目录创建一个 config.json 文件
创建 Systemd 服务
mkdir -p /etc/semaphore
mv config.json /etc/semaphore
创建 /etc/systemd/system/semaphore.service
,配置如下
[Unit]
Description=Semaphore Server
Documentation=https://github.com/semaphoreui/semaphore
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
ExecReload=/bin/kill -HUP $MAINPID
ExecStart=/usr/bin/semaphore server --config=/etc/semaphore/config.json
SyslogIdentifier=semaphore
Restart=always
RestartSec=10s
[Install]
WantedBy=multi-user.target
启动服务并设置开机自启动
$ sudo systemctl daemon-reload
$ sudo systemctl enable semaphore --now
访问 Semaphore
现在可以通过 http://<你的IP>:3000 访问 WebUI,并使用上方初始化的时候配置的用户名+密码登录
配置 Ansible 自动化任务
新建项目
在首次登入后可直接创建一个空项目,或在Semaphore左上角点击创建新项目
新建SSH密钥对(Key Stores)
创建一对SSH密钥,并为私钥配置访问密码,用于对文件进行下发
ssh-keygen -t ed25519
可在 ~/.ssh
目录找到新建的私钥(无后缀)和公钥(pub结尾)
注意:需要将公钥添加至到目标服务器的 ~/.ssh/authorized_keys
中
本文仅作示例,使用了 root 账户进行操作,可在目标服务器中创建一个子账户,对过高的权限进行管控
在Semaphore左侧导航栏,切换到当前项目,并选中密钥存储,添加新密钥
新建仓库(Repositories)
在本地创建新文件夹 mkdir -p /opt/semaphore/repo/demo
(实际路径可自定义),用于存储 playbook 等文件
在Semaphore左侧选择仓库,新建一个本地仓库
此处也可以通过ssh等方式使用远程仓库,若使用,则需在重复上方添加密钥步骤,将可以访问到远程仓库的私钥添加进去
创建库存(Inventory)
库存配置文件举例:
all:
hosts:
host1.example.com:
cert_dest_base: "/var/ssl"
exec_commands:
- name: "Reload Nginx"
command: "systemctl reload nginx"
- name: "Reload Docker Container"
command: "docker restart nginx"
- host1.example.com 修改为目的服务器的连接IP或域名
- cert_dest_base 修改为实际存放SSL证书的目录
- exec_commands 可添加多个需要执行的命令
创建环境变量(Environments)
环境变量通常为每一张证书一个,Playbook可以通用
这里注意变量是填入到 额外变量 中
- cert_source_dir 通常为 acme.sh 签发出来的证书目录,示例:
/root/.acme.sh/example.com_ecc/
- domain 通常为 acme.sh 生成的 key 文件名,这里同时复用了相同变量作为上传到对端服务器的目录名
创建 Playbook 任务模板
/opt/semaphore/repo/demo/deploy_ssl.yml
- name: Deploy SSL certificates and reload services
hosts: all
become: true
tasks:
- name: Ensure SSL directories exist
file:
path: "{{ cert_dest_base }}/{{ domain }}"
state: directory
mode: '0755'
- name: Copy SSL certificate
copy:
src: "{{ cert_source_dir }}/fullchain.cer"
dest: "{{ cert_dest_base }}/{{ domain }}/fullchain.crt"
owner: root
group: root
mode: '0644'
register: cert_copied
- name: Copy SSL private key
copy:
src: "{{ cert_source_dir }}/{{ domain }}.key"
dest: "{{ cert_dest_base }}/{{ domain }}/{{ domain }}.key"
owner: root
group: root
mode: '0600'
register: key_copied
- name: Execute reload commands
block:
- name: Execute custom reload commands
shell: "{{ item.command }}"
loop: "{{ exec_commands }}"
loop_control:
label: "{{ item.name }}"
register: reload_result
- name: Show reload results
debug:
msg: "Command '{{ item.item.name }}' executed with result: {{ item.rc }}"
loop: "{{ reload_result.results }}"
when: cert_copied.changed or key_copied.changed
在Semaphore中创建任务模板
测试任务
这里默认你已经完成了acme.sh的证书签发,且上方步骤都已经完成设置正确
在 Semaphore 中点击任务运行,查看证书是否能够正确上传到目标服务器并执行命令,若成功,则会回显状态为 Success
配置自动任务
新建 Semaphore 用户
这里需要新建一个非管理员用户,用于任务的执行
在页面左下角,选择“用户”,进行添加,需要保存好密码,以便下一步获取API Token
新建完毕后,在项目左侧导航栏选中“团队”,赋予新用户 Task Runner 权限
API Token 的获取
执行以下命令,登录刚刚创建的用户
curl -v -c /tmp/semaphore-cookie -XPOST \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{"auth": "YOUR_LOGIN", "password": "YOUR_PASSWORD"}' \
http://localhost:3000/api/auth/login
替换 auth 为用户名,password为刚刚设定的密码
执行以下命令,获取 API Token
curl -v -b /tmp/semaphore-cookie \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
http://localhost:3000/api/user/tokens
确定已经保存好 Token 后,将 cookie 文件删除
rm -f /tmp/semaphore-cookie
创建任务自动执行脚本
脚本依赖 jq,需要安装一下
apt install jq
新建文件
/opt/semaphore/repo/demo/deploy.sh
#!/bin/bash
# 检查是否提供了 project_id 和 template_id 参数
if [ $# -ne 2 ]; then
echo "Usage: $0 <project_id> <template_id>"
exit 1
fi
# 定义变量
API_URL="http://127.0.0.1:3000/api"
API_TOKEN="这里填入刚刚获取到的 API Token"
PROJECT_ID=$1
TEMPLATE_ID=$2
# 创建任务
CREATE_TASK_RESPONSE=$(curl -s -X POST "$API_URL/project/$PROJECT_ID/tasks" \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"template_id\": $TEMPLATE_ID}")
# 获取任务 ID
TASK_ID=$(echo $CREATE_TASK_RESPONSE | jq -r '.id')
# 检查任务是否创建成功
if [ "$TASK_ID" == "null" ]; then
echo "Failed to create task: $CREATE_TASK_RESPONSE"
exit 1
fi
echo "Task created with ID: $TASK_ID"
脚本执行命令为 /opt/semaphore/repo/demo/deploy.sh <项目ID> <模板ID>
,可以通过浏览器中的URL获取到对应的数字ID
配置 acme.sh 的 reloadcmd
acme.sh --installcert -d example.com --reloadcmd "/opt/semaphore/repo/demo/deploy.sh 2 6"
执行后可以看到能够正常触发 Semaphore 的 Ansible 任务
[Fri Jan 10 07:12:10 AM UTC 2025] The domain 'example.com' seems to already have an ECC cert, let's use it.
[Fri Jan 10 07:12:10 AM UTC 2025] Running reload cmd: /opt/semaphore/repo/demo/deploy.sh 2 6
Task created with ID: 10
[Fri Jan 10 07:12:10 AM UTC 2025] Reload successful
总结
至此,已完成通过 Ansible + Semaphore 对 acme.sh 签发的证书进行批量部署的操作