告别手动续签:使用 Ansible 与 acme.sh 实现多台 Nginx 服务器 SSL 证书自动部署

发布于 2025-09-28 分类: 建站

在当今的互联网环境中,HTTPS 已是标配。对于运维工程师而言,管理一个庞大的 Nginx 集群证书是一项艰巨的任务。当服务器数量增长到数十台甚至上百台时,手动更新和部署 SSL 证书不仅是一场噩梦,更是潜在的故障点:操作繁琐、效率低下、极易出错,任何一次遗忘都可能导致线上服务因证书过期而中断。

为了彻底解决这一痛点,我们需要一套企业级 SSL 证书自动化管理方案。本文将以资深运维的视角,详细阐述如何结合使用 acme.shAnsible,打造一个从证书自动续签到分发的全自动化系统。acme.sh 负责与 Let's Encrypt 交互,实现证书的自动申请与续签;而 Ansible 则作为强大的自动化部署工具,负责将更新后的证书精准、安全地分发到多台机器上,并平滑地重载 Nginx 服务。

这套方案的核心优势在于:

  • 完全自动化:实现从证书续签到部署的端到端自动化,无需人工干预。
  • 幂等性保障:利用 Ansible 的幂等性,确保只有在证书更新时才触发服务重载,避免无效操作。
  • 高可扩展性:轻松应对服务器节点的增减,只需在清单文件中做简单修改即可统一管理。
  • 安全性与健壮性:通过精细的文件权限控制和配置预检查,最大限度避免因配置错误导致的服务中断。

核心流程概览:证书自动续签与部署

在我们深入细节之前,先通过一个流程图来理解这套自动化系统的运作逻辑,它清晰地展示了 acme.sh 如何通过 renew-hook 钩子触发 Ansible Playbook

graph TD;
    subgraph "Master 节点 (Ansible 控制端)"
        A[acme.sh Cron 每日检查] --> B{证书是否即将过期?};
        B -- 否 --> C[结束];
        B -- 是 --> D[acme.sh 自动续签证书];
        D --> E[调用 renew_hook.sh 钩子脚本];
        E --> F[执行 Ansible Playbook 进行证书部署];
    end

    subgraph "所有被控节点 (Nginx 服务器)"
        G[接收新证书文件] --> H[接收新私钥文件];
        H --> I[执行 Nginx 配置预检查 nginx -t];
        I --> J{检查是否成功?};
        J -- 是 --> K[平滑重载 Nginx 服务];
        J -- 否 --> L[任务失败并告警];
    end

    F --> G;

    classDef default fill:#f9f9f9,stroke:#333,stroke-width:2px,color:#333,font-family:Arial,font-size:14px;
    classDef special fill:#d4edda,stroke:#155724,stroke-width:2px,color:#155724,font-family:Arial,font-size:14px;
    class A,B,C,D,E,F special;
    class G,H,I,J,K,L default;

    style B stroke-dasharray: 5 5;
    style J stroke-dasharray: 5 5;

第一步:为 Ansible 自动化部署准备环境

假设我们有一个 master 节点(作为 Ansible 控制端)和两个 worker 节点(运行 Nginx)。

  • master: 192.168.1.10
  • worker1: 192.168.1.11
  • worker2: 192.168.1.12

1. 建立 SSH 免密登录

这是 Ansible 能够统一管理多台服务器的基础。我们需要在 master 节点上生成 SSH 密钥,并将公钥分发到包括自身在内的所有被控节点。

  • master 节点上,生成 SSH 密钥对:
    ssh-keygen -t rsa -b 4096 -C "ansible_control_key"
    # 一路回车,确保私钥没有密码,以便 Ansible 能自动使用
    
  • 使用 ssh-copy-id 命令将公钥分发到所有节点:
    # 假设我们将以 root 用户身份进行管理
    ssh-copy-id root@master
    ssh-copy-id root@worker1
    ssh-copy-id root@worker2
    
  • 验证:在 master 节点上,逐一测试 SSH 连接,确保无需密码即可登录。
    ssh root@master "hostname"
    ssh root@worker1 "hostname"
    ssh root@worker2 "hostname"
    

2. 安装 Ansible 和 acme.sh

  • master 节点上安装 Ansible,它是我们自动化部署任务的执行者。
    # 以 CentOS/RHEL 为例
    sudo yum install epel-release -y
    sudo yum install ansible -y
    
    # 以 Debian/Ubuntu 为例
    sudo apt update
    sudo apt install software-properties-common -y
    sudo add-apt-repository --yes --update ppa:ansible/ansible
    sudo apt install ansible -y
    
  • master 节点上安装 acme.sh,它是证书申请和续签的核心工具。
    curl https://get.acme.sh | sh
    # 安装后,重新登录或执行 source ~/.bashrc 使 acme.sh 命令生效
    acme.sh --set-default-ca --server letsencrypt
    

第二步:项目结构搭建

一个清晰的项目结构是可维护性的开端。在 master 节点上,创建我们的 autossl-ansible 项目目录。

mkdir ~/autossl-ansible
cd ~/autossl-ansible

我们将采用以下目录结构:

autossl-ansible/
├── inventory.ini             # Ansible 主机清单,定义了我们要管理谁
├── ansible.cfg               # Ansible 配置文件,优化 Ansible 行为
├── playbook-deploy-certs.yml # Ansible Playbook,自动化任务的核心
└── scripts/
    └── renew_hook.sh         # acme.sh 续签成功后调用的钩子脚本```

### **第三步:编写 Ansible Playbook 实现证书自动分发**

现在,我们来逐一创建和填充这些文件。

#### **1. `inventory.ini` (主机清单)**

这个文件告诉 Ansible 要对哪些服务器执行**证书自动分发**任务。

```ini
[nginx_servers]
master          ansible_host=192.168.1.10
worker1         ansible_host=192.168.1.11
worker2         ansible_host=192.168.1.12

[all:vars]
# 定义全局变量
ansible_user=root 

# 自定义变量,用于 Playbook
domain_name=your-domain.com
master_cert_path=/root/.acme.sh/{{ domain_name }}_ecc
worker_cert_base_path=/etc/nginx/ssl

细节解析: [nginx_servers] 是一个主机组,方便我们对所有 Nginx 服务器批量操作。_ecc 后缀表示我们使用 ECDSA 证书,请在首次申请证书后务必确认此路径是否正确

2. ansible.cfg (Ansible 配置文件)

这是一个推荐的配置文件,用于定义 Ansible 的行为。

[defaults]
inventory = inventory.ini
remote_tmp = /tmp/.ansible-${USER}
host_key_checking = False

[privilege_escalation]
become = False

3. playbook-deploy-certs.yml (核心 Playbook)

这是定义如何部署 SSL 证书到 Nginx 服务器的核心文件,包含了所有自动化逻辑。

---
- name: Deploy SSL Certificate to Nginx Servers
  hosts: nginx_servers
  gather_facts: no

  vars:
    worker_cert_path: "{{ worker_cert_base_path }}/{{ domain_name }}"
    cert_file_path: "{{ worker_cert_path }}/fullchain.cer"
    key_file_path: "{{ worker_cert_path }}/{{ domain_name }}.key"

  tasks:
    - name: Ensure target certificate directory exists
      ansible.builtin.file:
        path: "{{ worker_cert_path }}"
        state: directory
        owner: root
        group: root
        mode: '0755'

    - name: Copy new certificate fullchain
      ansible.builtin.copy:
        src: "{{ master_cert_path }}/fullchain.cer"
        dest: "{{ cert_file_path }}"
        owner: root
        group: root
        mode: '0644'
      notify: Reload Nginx

    - name: Copy new certificate private key
      ansible.builtin.copy:
        src: "{{ master_cert_path }}/{{ domain_name }}.key"
        dest: "{{ key_file_path }}"
        owner: root
        group: root
        mode: '0600' # 私钥权限必须严格控制
      notify: Reload Nginx

  handlers:
    - name: Test Nginx configuration before reloading
      listen: Reload Nginx
      ansible.builtin.command: nginx -t
      changed_when: false
      register: nginx_test_result

    - name: Reload Nginx service if config is OK
      listen: Reload Nginx
      ansible.builtin.systemd:
        name: nginx
        state: reloaded
      when: nginx_test_result.rc == 0

    - name: Fail playbook if Nginx config is bad
      listen: Reload Nginx
      ansible.builtin.fail:
        msg: "Nginx configuration test failed! Output: {{ nginx_test_result.stderr }}"
      when: nginx_test_result.rc != 0

原理与细节深度解析:

  • 幂等性部署: ansible.builtin.copy 模块是幂等的,这是实现自动化 SSL 证书部署的关键。只有当新旧证书内容不同时,它才会执行复制并触发后续的 Nginx 重载,避免了不必要的操作。
  • Handlers 与 Notify: notify: Reload Nginx 机制确保了只有在证书文件真正被更新后,才会执行 Nginx 的重载逻辑,这是保证服务稳定性的重要一环。
  • 安全性与健壮性: 在重载 Nginx 前执行 nginx -t 预检查,是自动化流程中的“安全阀”,能有效防止因证书文件损坏或配置错误导致的 Nginx 服务中断。

4. scripts/renew_hook.sh (acme.sh 钩子脚本)

这个 Shell 脚本是连接 acme.shAnsible 的桥梁,是实现续签后自动部署的核心。

#!/bin/bash
PROJECT_DIR="/root/autossl-ansible"
cd "$PROJECT_DIR" || exit 1

echo "=================================================="
echo "SSL Certificate renewed, starting Ansible playbook to deploy certs..."
echo "Timestamp: $(date)"

# 执行 Ansible Playbook,将新证书分发到所有 Nginx 服务器
ansible-playbook -i inventory.ini playbook-deploy-certs.yml >> /var/log/autossl-ansible.log 2>&1

echo "Ansible playbook for deploying certs finished."
echo "=================================================="
  • 重要: 记得给这个脚本添加执行权限: chmod +x scripts/renew_hook.sh

第四步:首次证书申请与部署

1. 在 master 节点申请证书

对于多服务器 Nginx 集群,DNS 验证方式是最佳选择,因为它天然支持通配符证书,且无需在 Web 服务器上做任何改动。

以 Cloudflare 为例 (dns_cf):

# 在 master 节点执行
export CF_Key="YOUR_CLOUDFLARE_GLOBAL_API_KEY"
export CF_Email="your_cloudflare_email@example.com"

acme.sh --issue --dns dns_cf -d your-domain.com -d *.your-domain.com -k ec-256

2. 关联钩子脚本,打通自动续签与部署

证书申请成功后,我们需要告诉 acme.sh,在未来每次证书自动续签成功后,都要调用我们的部署脚本。

# 在 master 节点执行
acme.sh --install-cert -d your-domain.com --ecc \
  --renew-hook "/root/autossl-ansible/scripts/renew_hook.sh"

注意: 这里的 --install-cert 命令只是用来注册 --renew-hook 这个续签后的动作。真正的安装部署由我们的 Ansible Playbook 负责。

3. 手动触发首次 Ansible 证书部署

现在证书已生成,让我们手动运行一次 Ansible Playbook,完成所有服务器的首次部署。

# 在 master 节点的 ~/autossl-ansible 目录下执行
ansible-playbook playbook-deploy-certs.yml
  • 此时,在所有 Nginx 节点上配置好证书路径,并访问 https://your-domain.com,应该可以看到新的 SSL 证书已经生效。

第五步:验证全自动 SSL 证书生命周期管理

至此,一套完整的从证书续签到部署的自动化流水线已经建立!

  • 自动续签: acme.sh 的 cron 任务会每天检查,在证书到期前自动续签。
  • 自动部署: 续签成功后,renew_hook.sh 脚本被自动调用。
  • 触发 Ansible: 钩子脚本运行 ansible-playbook,将最新的证书推送到所有 nginx_servers 并平滑重载服务。

如何模拟测试?
您可以强制续签一次证书来完整地测试整个自动化流程,验证避免手动更新 SSL 证书的效果:

# 在 master 节点执行
acme.sh --renew -d your-domain.com --force --ecc

观察屏幕输出,您会看到 acme.sh 续签成功后,Ansible Playbook 被自动触发并完成所有服务器的更新。这证明了我们的自动化系统工作正常,您已成功告别繁琐的手动证书管理。


-- 感谢阅读 --