前言

众所周知,我们可以通过 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软件包

DebianUbuntuUBUNTU_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 签发的证书进行批量部署的操作