# 工业级 Deploy Hook 架构

## 整体架构设计

```
/etc/letsencrypt/renewal-hooks/deploy/
└── dispatcher.sh

/usr/local/lib/certbot/
├── deploy/
│   ├── 10-axigen.sh
│   ├── 20-hysteria.sh
│   └── 65535-notify.sh
│
├── lib/
│   ├── common.sh
│   └── logger.sh
```


### 调用链（运行时依赖关系）

```
[执行关系]
dispatcher.sh
    ↓ 调用
65535-notify.sh

[依赖关系]
65535-notify.sh
    ↓ source
common.sh
    ↓ source
logger.sh
```

**正确的“关系方向**

`65535-notify.sh  → 依赖 →  common.sh  → 依赖 →  logger.sh`

{{< callout context="note" title="Note" icon="outline/notebook" >}}
source 是“代码注入”，不是"调用" 

这是一种：
“运行时依赖 + 代码注入”结构
{{< /callout >}}

	

{{< details "三种“依赖关系”的区别（重点）" >}}
{{< steps >}}
{{< step >}}

**dispatcher → notify（运行时依赖）**

```
./65535-notify.sh
```

含义：

> dispatcher 在运行时“调用” notify

特点：
- 是“执行关系”
- 会创建执行流程
- 是脚本级调用

{{< /step >}}
{{< step >}}

**notify → common（代码依赖）**

```
source common.sh
```

含义：

> notify 需要 common 提供函数

特点：
- 是“代码包含”
- 是“静态依赖”
- 不会创建新进程

{{< /step >}}
{{< step >}}

**common → logger（底层依赖）**

```
source logger.sh
```

含义：

> common 依赖 logger 提供日志能力

{{< /step >}}
{{< /steps >}}
{{< /details >}}

### 分层架构（设计视角）

```
[入口层]
dispatcher.sh
    ↓

[执行层]
10-axigen.sh
20-hysteria.sh
    ↓

[能力层]
common.sh
    ↓
logger.sh

    ↓

[收尾层]
65535-notify.sh
```

#### 创建目录

```bash
mkdir -p "/usr/local/lib/certbot/deploy" && mkdir -p "/usr/local/lib/certbot/lib/"
```

#### 编辑脚本

{{< tabs "vi" >}}
{{< tab "dispatcher" >}}

```bash
vi "/etc/letsencrypt/renewal-hooks/deploy/dispatcher.sh"
```

{{< /tab >}}
{{< tab "common" >}}

```bash
vi "/usr/local/lib/certbot/lib/common.sh"
```

{{< /tab >}}
{{< tab "logger" >}}

```bash
vi "/usr/local/lib/certbot/lib/logger.sh"
```

{{< /tab >}}
{{< tab "axigen" >}}

```bash
vi "/usr/local/lib/certbot/deploy/10-axigen.sh"
```

{{< /tab >}}
{{< tab "hysteria" >}}

```bash
vi "/usr/local/lib/certbot/deploy/20-hysteria.sh"
```

{{< /tab >}}
{{< tab "notify" >}}

```bash
vi "/usr/local/lib/certbot/deploy/65535-notify.sh"
```

{{< /tab >}}
{{< /tabs >}}

#### 复制下面的带内容贴进 `*.sh` 文件中

{{< tabs "vi" >}}
{{< tab "dispatcher" >}}

```sh
#!/bin/bash
set -euo pipefail

HOOK_DIR="/usr/local/lib/certbot/deploy"

source /usr/local/lib/certbot/lib/common.sh

: "${RENEWED_LINEAGE:?RENEWED_LINEAGE not set}"

log "[dispatcher] start"

START_TIME=$(date +%s)

for script in "$HOOK_DIR"/*.sh; do
    [ -x "$script" ] || continue

    log "[dispatcher] running: $script"

    SCRIPT_START=$(date +%s)

    if "$script" "$RENEWED_LINEAGE"; then
        STATUS="OK"
        add_result "$script: OK"
    else
        STATUS="FAIL"
        add_error "$script"
        log "[dispatcher] ERROR: $script failed"
    fi

    SCRIPT_END=$(date +%s)
    log "[dispatcher] $script finished ($((SCRIPT_END - SCRIPT_START))s) [$STATUS]"
done

END_TIME=$(date +%s)

log "[dispatcher] done in $((END_TIME - START_TIME))s"

# 👉 关键：把结果交给 notify
/usr/local/lib/certbot/deploy/65535-notify.sh
```

{{< /tab >}}
{{< tab "common" >}}

```sh
#!/bin/bash
set -euo pipefail

source /usr/local/lib/certbot/lib/logger.sh

# ===== 全局状态 =====
CERTBOT_RESULTS=()
CERTBOT_ERRORS=()

add_result() {
    CERTBOT_RESULTS+=("$1")
}

add_error() {
    CERTBOT_ERRORS+=("$1")
}

lock() {
    local name="$1"
    exec 200>/var/lock/certbot-"$name".lock
    flock -n 200 && return 0 || return 1
}
```

{{< /tab >}}
{{< tab "logger" >}}

```sh
#!/bin/bash

log() {
    local msg="[$(date +'%F %T')] $*"
    echo "$msg" | systemd-cat -t certbot-hook
}
```

{{< /tab >}}
{{< tab "axigen" >}}

```sh
#!/bin/bash
set -euo pipefail

source /usr/local/lib/certbot/lib/common.sh

INPUT="${1:-}"

TARGET="/etc/letsencrypt/live/mail.eternal.foo"

# 👉 不匹配直接跳过（成功退出）
[[ "$INPUT" == "$TARGET"* ]] || exit 0

lock axigen || {
    log "[axigen] skipped (lock)"
    exit 0
}

log "[axigen] updating..."

DST="/var/opt/axigen/certs/fullchain.pem"
TMP=$(mktemp)

# 👉 合并证书
cat "$INPUT/fullchain.pem" "$INPUT/privkey.pem" > "$TMP"

chown axigen:axigen "$TMP"
chmod 640 "$TMP"

mv "$TMP" "$DST"

# 👉 关键：如果 reload 失败，这里会直接失败（set -e）
if /etc/init.d/axigen reload || /etc/init.d/axigen restart; then
    add_result "[axigen] updated successfully"
else
    add_error "[axigen] reload failed"
    exit 1
fi

log "[axigen] done"
```

{{< /tab >}}
{{< tab "hysteria" >}}

```sh
#!/bin/bash
set -euo pipefail

source /usr/local/lib/certbot/lib/common.sh

INPUT="${1:-}"

TARGET="/etc/letsencrypt/live/www.eternal.foo"

[[ "$INPUT" == "$TARGET"* ]] || exit 0

lock hysteria || {
    log "[hysteria] skipped (lock)"
    exit 0
}

log "[hysteria] updating..."

# 👉 安全写入（原子替换）
install -m 600 -o hysteria -g hysteria \
    "$INPUT/fullchain.pem" \
    "/etc/hysteria/fullchain.pem"

install -m 600 -o hysteria -g hysteria \
    "$INPUT/privkey.pem" \
    "/etc/hysteria/privkey.pem"

# 👉 服务重启（关键点）
if systemctl restart hysteria-server.service; then
    add_result "[hysteria] restarted successfully"
else
    add_error "[hysteria] restart failed"
    exit 1
fi

log "[hysteria] done"
```

{{< /tab >}}
{{< tab "notify" >}}

```sh
#!/bin/bash
set -euo pipefail

source /usr/local/lib/certbot/lib/common.sh

log "========================================"
log "[notify] Certbot Deploy Summary"

if [ ${#CERTBOT_ERRORS[@]} -eq 0 ]; then
    log "[notify] STATUS: SUCCESS"
else
    log "[notify] STATUS: FAILURE"
fi

log "[notify] Updated lineage: ${RENEWED_LINEAGE:-unknown}"

# 成功项
if [ ${#CERTBOT_RESULTS[@]} -gt 0 ]; then
    log "[notify] Successful hooks:"
    for r in "${CERTBOT_RESULTS[@]}"; do
        log "  - $r"
    done
fi

# 失败项
if [ ${#CERTBOT_ERRORS[@]} -gt 0 ]; then
    log "[notify] Failed hooks:"
    for e in "${CERTBOT_ERRORS[@]}"; do
        log "  - $e"
    done
fi

log "========================================"

# 👉 可选：发送外部通知（以后扩展）
# curl / webhook / telegram 等

exit 0
```

{{< /tab >}}
{{< /tabs >}}

**授予执行权限**

```bash
chmod +x /usr/local/lib/certbot/deploy/*.sh && chmod +x /usr/local/lib/certbot/lib/*.sh && chmod +x /etc/letsencrypt/renewal-hooks/deploy/dispatcher.sh
```

## 验证 hook 是否可靠

### 第一种 : 模拟执行（不真正发起请求）

使用 --dry-run（官方标准）

```bash
certbot renew --dry-run
```

或

```bash
certbot renew --dry-run -v
```

作用
模拟证书更新流程（不会真正申请新证书）

会执行：

> 你的 deploy hook（dispatcher + 所有脚本）

你可以验证：
- dispatcher 是否被调用
- 每个脚本是否执行
- lock 是否正常
- journald 日志是否正确

**查看日志**

```bash
journalctl -t certbot-hook -f
```

{{< callout context="caution" title="Caution" icon="outline/alert-triangle" >}}
`--dry-run` 有一个关键点：
不会真正替换证书文件
{{< /callout >}}

### 第二种：本地“手动模拟”（强烈推荐）

你可以直接模拟 Certbot 传参：

**手动调用 dispatcher**

```bash
RENEWED_LINEAGE="/etc/letsencrypt/live/mail.eternal.foo" \
/etc/letsencrypt/renewal-hooks/deploy/dispatcher.sh
```

优点
- 不依赖 certbot
- 直接测试你的 hook 逻辑
- 完全可控
可以测试：
- dispatcher 逻辑
- 所有脚本执行
- 日志输出
- 锁机制
- 错误处理

### 第三种 ：真实证书更新（生产验证）

**方法 1：强制续期**

```bash
certbot renew --force-renewal
```

作用
- 真正请求新证书
- 真正触发 deploy hook

{{< callout context="caution" title="Caution" icon="outline/alert-triangle" >}}
可能触发频率限制（rate limit）

影响线上服务（reload / restart）
{{< /callout >}}

建议只在：
- 测试环境
- 或非常确认安全时使用


**检查日志**

```bash
journalctl -t certbot-hook
```

**检查 Axigen**
```bash
ls -l /var/opt/axigen/certs/fullchain.pem
```

