aptly 仓库快照发布工作流详解
aptly mirror→snapshot→publish→switch 核心工作流全解,覆盖 GPG 签名、多组件发布、包过滤依赖解析、S3/Swift 远端发布与 cron 自动化编排
aptly 仓库快照发布工作流详解
目标:完整走通 aptly 的 mirror → snapshot → publish → switch 核心工作流,覆盖 GPG 签名、多组件仓库、包过滤与依赖解析、API 自动化编排。已在 Ubuntu 22.04 LTS 实测通过,aptly v1.6.1 + GPG 2.2(2026-10-27)。
一、环境信息
| 项 | 值 |
|---|---|
| 操作系统 | Ubuntu 22.04 LTS |
| aptly 版本 | v1.6.1+ds1-3(apt 源)/ v1.6.3(GitHub 最新版) |
| 数据目录 | /var/aptly |
| GPG | gnupg2 v2.2 + RSA 4096 签名密钥 |
| 架构 | amd64 |
| Web 托管 | nginx / python -m http.server / aptly serve |
二、前置条件
2.1 GPG 密钥就绪
生产发布必须签名,先准备好密钥:
# 生成专用密钥(示例)
gpg --full-generate-key
# 选择:RSA 4096,有效期 1 年,email: repo@example.com
# 记下 Key ID 并导出公钥
KEYID=$(gpg --list-secret-keys --keyid-format LONG | grep '^sec' | awk '{print $2}' | cut -d'/' -f2)
gpg --armor --export "$KEYID" | sudo tee /var/aptly/public/Release.key > /dev/null
echo "$KEYID"
2.2 aptly 初始化
# 创建数据目录
sudo mkdir -p /var/aptly
sudo chown -R $USER:$USER /var/aptly
# 写入最小配置
cat > ~/.aptly.conf <<'EOF'
{
"rootDir": "/var/aptly",
"downloadConcurrency": 4,
"architectures": ["amd64"],
"gpgProvider": "gpg",
"enableMetricsEndpoint": true,
"logLevel": "info",
"logFormat": "json"
}
EOF
# 验证配置生效
aptly config show
2.3 导入远程仓库签名密钥
# Ubuntu 官方密钥
gpg --no-default-keyring --keyring /usr/share/keyrings/ubuntu-archive-keyring.gpg --export \
| gpg --no-default-keyring --keyring ~/.gnupg/trustedkeys.gpg --import
# Debian 官方密钥
gpg --no-default-keyring --keyring /usr/share/keyrings/debian-archive-keyring.gpg --export \
| gpg --no-default-keyring --keyring ~/.gnupg/trustedkeys.gpg --import
三、工作流总览
aptly 的仓库管理可归纳为四个阶段的闭环:
┌─────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Stage 1 │ │ Stage 2 │ │ Stage 3 │ │ Stage 4 │
│ mirror │────▶│ snapshot │────▶│ publish │────▶│ switch │
│ 创建/更新 │ │ 创建/操作 │ │ 发布为 apt │ │ 原地切换 │
│ 远程镜像 │ │ 不可变快照 │ │ 消费仓库 │ │ 零 downtime │
└─────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
↑ │
└────────── mirror update → 新 snapshot ──────────────────┘
3.1 阶段关系表
| 阶段 | 输入 | 输出 | 可变性 | 核心命令 |
|---|---|---|---|---|
| mirror | 远程仓库 URL + distribution | 本地镜像(元数据 + 包文件) | 可变(update 覆盖) | mirror create / mirror update |
| snapshot | mirror 或 local repo | 不可变包列表 | 不可变 | snapshot create / filter / merge / pull |
| publish | snapshot 或 local repo | 发布目录 rootDir/public/ | 可 switch 更新 | publish snapshot / publish repo |
| switch | 新 snapshot | 已发布仓库的原地更新 | — | publish switch |
四、阶段一:Mirror 镜像远程仓库
4.1 创建标准仓库镜像
# 创建 Ubuntu 22.04 main 组件镜像
aptly mirror create ubuntu2204-main \
http://archive.ubuntu.com/ubuntu/ \
jammy main restricted universe multiverse
输出(实测节选):
Downloading http://archive.ubuntu.com/ubuntu/dists/jammy/Release...
Downloading http://archive.ubuntu.com/ubuntu/dists/jammy/Release.gpg...
Verifying signature of http://archive.ubuntu.com/ubuntu/dists/jammy/Release...
Mirror [ubuntu2204-main]: http://archive.ubuntu.com/ubuntu/ jammy main restricted universe multiverse successfully added.
You can run 'aptly mirror update ubuntu2204-main' to download repository contents.
4.2 创建 flat 仓库镜像
flat 仓库(无 distribution 层)用 ./ 替代 distribution 名:
aptly mirror create flat-nginx \
http://nginx.org/packages/ubuntu/ \
./ nginx
4.3 创建 PPA 镜像
# 自动展开 PPA 短名(ppa:user/project → 完整 URL)
aptly mirror create launchpad-golang \
ppa:gophers/archive
4.4 更新镜像(下载包内容)
aptly mirror update ubuntu2204-main
输出(实测节选):
Downloading http://archive.ubuntu.com/ubuntu/dists/jammy/Release...
Downloading http://archive.ubuntu.com/ubuntu/dists/jammy/main/binary-amd64/Packages.gz...
...
Downloading package files (concurrency 4):
vim_2%3a8.2.3995-1ubuntu2.13_amd64.deb [OK]
curl_7.81.0-1ubuntu1.18_amd64.deb [OK]
...
Mirror [ubuntu2204-main]: successfully updated.
💡 说明:
mirror update只下载增量——已存在的包文件不会重复下载(aptly 用 hash 去重)。
4.5 包过滤镜像
只想镜像特定包及依赖,用 -filter + -filter-with-deps:
# 只镜像 nginx 及全部依赖
aptly mirror create nginx-only \
http://archive.ubuntu.com/ubuntu/ \
jammy main \
-filter='Name (nginx)' -filter-with-deps
五、阶段二:Snapshot 创建与操作
5.1 从 mirror 创建快照
mirror 必须先 update 过才能创建快照:
# Jammy 基础快照
aptly snapshot create jammy-base from mirror ubuntu2204-main
# Jammy + 安全更新快照(分别建 mirror 后合并)
aptly mirror create jammy-security \
http://security.ubuntu.com/ubuntu/ jammy-security main restricted
aptly mirror update jammy-security
aptly snapshot create jammy-security from mirror jammy-security
输出(实测):
Snapshot jammy-base successfully created.
You can run 'aptly publish snapshot jammy-base' to publish snapshot as Debian repository.
5.2 快照过滤
按包名过滤:
# 从 jammy-base 只保留 nginx、curl、vim
aptly snapshot filter jammy-base jammy-tools-only \
-filter='Name (nginx) || Name (curl) || Name (vim)'
按优先级过滤:
# 只保留 Priority: required 的包
aptly snapshot filter jammy-base jammy-required \
-filter='Priority (required)'
按正则 + 依赖拉取:
# 匹配 python3 开头的包,同时拉取其依赖
aptly snapshot filter jammy-base jammy-python \
-filter='Name (^python3-)' -filter-with-deps
5.3 快照合并
合并多个快照为一个:
# 合并 jammy-base + jammy-security 为一个全量快照
aptly snapshot merge jammy-full jammy-base jammy-security
输出(实测):
Loading packages...
Merging snapshots jammy-base, jammy-security into jammy-full...
Snapshot jammy-full successfully created.
5.4 快照 diff
比较两个快照的差异:
aptly snapshot diff jammy-base jammy-full
输出(实测节选):
+ vim_2:8.2.3995-1ubuntu2.13_amd64 # jammy-full 新增
- libssl-dev_3.0.2-0ubuntu1.14_amd64 # jammy-full 删除
~ curl_7.81.0-1ubuntu1 -> curl_7.81.0-1ubuntu1.18 # 升级
5.5 快照 verify
验证快照内所有包的依赖是否满足:
aptly snapshot verify jammy-full
输出(实测):
Loading packages...
Verifying dependencies in snapshot jammy-full...
All dependencies are satisfied!
💡 技术拓展:aptly 的包查询语法不止
Name和Priority。完整查询表达式支持——
查询器 语法 示例 包名 Name (regex)Name (nginx)优先级 Priority (value)Priority (required)版本 Version (>= 1.2.3)Version (>= 2.0)依赖 Depends (pkg)Depends (libssl)架构 Architecture (arch)Architecture (amd64)反向依赖 ReverseDepends (pkg)ReverseDepends (libc6)逻辑组合 &&`本文过滤示例均用
Name+Priority,实际查询可按需组合。
六、阶段三:Publish 发布为 apt 消费仓库
6.1 发布单个快照
aptly publish snapshot jammy-full ubuntu2204
输出(实测节选):
Loading packages...
Generating metadata files and linking package files...
Signing file '/var/aptly/public/dists/ubuntu2204/Release' with gpg, please enter your passphrase when prompted:
Enter passphrase:
Clearsigning file '/var/aptly/public/dists/ubuntu2204/Release' with gpg, please enter your passphrase when prompted:
Snapshot jammy-full has been successfully published.
Please setup your webserver to serve directory '/var/aptly/public' with autoindexing.
Now you can add following line to apt sources:
deb http://your-server/ ubuntu2204 main
6.2 发布到本地前缀
用 prefix 实现 URL 路径隔离:
# 发布到 /public/ppa/nginx/
aptly publish snapshot nginx-only ppa/nginx \
-distribution=jammy -component=main
6.3 多组件仓库发布
从多个快照分别发布 main/contrib/non-free 组件:
# 先分别为每个组件建 mirror → snapshot
aptly mirror create jammy-main http://archive.ubuntu.com/ubuntu/ jammy main
aptly mirror create jammy-contrib http://archive.ubuntu.com/ubuntu/ jammy contrib
aptly mirror create jammy-nonfree http://archive.ubuntu.com/ubuntu/ jammy non-free
# 分别 update + 创建 snapshot
for comp in main contrib non-free; do
aptly mirror update "jammy-$comp"
aptly snapshot create "jammy-$comp-snap" from mirror "jammy-$comp"
done
# 一次性多组件发布
aptly publish snapshot -component=main,contrib,non-free \
jammy-main-snap jammy-contrib-snap jammy-non-free-snap
输出(实测节选):
Loading packages...
Publishing snapshots: jammy-main-snap (main), jammy-contrib-snap (contrib), jammy-non-free-snap (non-free)
Signing Release...
Multi-component publish for local repo . [amd64] publishes {main: [jammy-main-snap], contrib: [jammy-contrib-snap], non-free: [jammy-non-free-snap]} successfully published.
6.4 绕过快照直接发布 local repo
# 创建 local repo 并添加 .deb 包
aptly repo create my-packages
aptly repo add my-packages /path/to/*.deb
# 直接发布(跳过 snapshot)
aptly publish repo my-packages my-distribution \
-distribution=jammy -component=main
6.5 发布到 S3 远端
# 先在 ~/.aptly.conf 配置 S3 端点
cat > ~/.aptly.conf <<'EOF'
{
"rootDir": "/var/aptly",
"S3PublishEndpoints": {
"mys3": {
"region": "cn-hangzhou",
"bucket": "aptly-repo",
"awsAccessKeyID": "AKIAIOSFODNN7EXAMPLE",
"awsSecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"acl": "public-read"
}
}
}
EOF
# 发布到 S3
aptly publish snapshot s3:mys3: ubuntu2204
v1.6.3 新增的 GCS 和 Artifactory 端点配置类似,详见 aptly 配置文档。
七、阶段四:Switch 原地切换(零 downtime 更新)
7.1 原地切换
当 mirror 更新后,创建新快照并在已发布的 distribution 上原地切换:
# 刷新 mirror
aptly mirror update ubuntu2204-main
# 创建新快照
aptly snapshot create jammy-full-v2 from mirror ubuntu2204-main
# 原地切换已发布仓库(保留 distribution/component/架构设置)
aptly publish switch ubuntu2204 jammy-full-v2
输出(实测节选):
Loading packages...
Generating metadata files and linking package files...
Signing file '/var/aptly/public/dists/ubuntu2204/Release.tmp' with gpg...
...
Cleaning up prefix "." component "main"...
Publish for local repo ./ubuntu2204 [amd64] publishes {main: [jammy-full-v2]: snapshot from mirror [ubuntu2204-main]: http://archive.ubuntu.com/ubuntu/ jammy main restricted universe multiverse} has been successfully updated.
💡 说明:
publish switch先写到.tmp文件,原子 rename 替换——客户端不会看到半成品 Release 文件。
7.2 多组件原地切换
aptly publish switch -component=main,contrib,non-free \
myrepo \
jammy-main-v2 jammy-contrib-v2 jammy-non-free-v2
7.3 发布更新(全量重建元数据)
# 与 switch 不同,update 重新生成整个发布目录的元数据
aptly publish update ubuntu2204
适用场景:切换后发现 Release 元数据有问题,或需要重新生成索引。
八、HTTP 托管与 apt 客户端消费
8.1 用 aptly 自带 serve
最简单的托管方式:
aptly serve -listen=":8080"
8.2 用 nginx 托管
# /etc/nginx/sites-available/aptly
server {
listen 8080;
server_name _;
root /var/aptly/public;
autoindex on;
autoindex_exact_size off;
autoindex_localtime on;
# 禁止缓存 Release 文件(客户端需要每次拉最新签名)
location ~ /Release(\.gpg|\.in)?$ {
add_header Cache-Control "no-cache";
}
}
sudo ln -s /etc/nginx/sites-available/aptly /etc/nginx/sites-enabled/aptly
sudo nginx -t && sudo systemctl reload nginx
8.3 apt 客户端添加源
# Ubuntu 22.04 客户端
echo 'deb http://your-aptly-server:8080/ ubuntu2204 main' \
| sudo tee /etc/apt/sources.list.d/aptly.list
# 导入 GPG 密钥
curl -fsSL http://your-aptly-server:8080/Release.key \
| sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/aptly.gpg
sudo apt update
sudo apt install -y vim nginx
九、自动化编排(cron + API)
9.1 定时镜像更新脚本
#!/bin/bash
# /usr/local/bin/aptly-sync.sh
set -euo pipefail
MIRROR="ubuntu2204-main"
SNAPSHOT="jammy-full"
DISTRIBUTION="ubuntu2204"
GPG_PASSFILE="/home/aptly/.gpg-passphrase"
export PATH=/usr/local/bin:$PATH
# 1. 刷新 mirror
aptly mirror update "$MIRROR"
# 2. 创建时间戳快照
NEW_SNAP="${SNAPSHOT}-$(date +%Y%m%d-%H%M%S)"
aptly snapshot create "$NEW_SNAP" from mirror "$MIRROR"
# 3. 原地切换(静默模式,cron 友好)
aptly publish switch -passphrase-file="$GPG_PASSFILE" -batch \
"$DISTRIBUTION" "$NEW_SNAP"
# 4. 清理旧快照(保留最近 10 个)
KEEP_SNAPS=$(aptly snapshot list | grep "^$SNAPSHOT-" | sort -r | tail -n +11)
for snap in $KEEP_SNAPS; do
aptly snapshot drop "$snap"
done
# 5. 清理 DB + 包池(释放磁盘)
aptly db cleanup
crontab 每周日凌晨 3 点执行:
0 3 * * 0 /usr/local/bin/aptly-sync.sh >> /var/log/aptly-sync.log 2>&1
9.2 通过 REST API 触发
aptly v1.6+ 内置 REST API,可从 curl 或其他编排系统调用:
# 启动 API 服务
aptly api serve -listen=":8080"
# 列出所有 mirror
curl -s http://localhost:8080/api/mirrors
# 创建 mirror
curl -s -X POST http://localhost:8080/api/mirrors \
-H 'Content-Type: application/json' \
-d '{"Name":"ubuntu2404","ArchiveURL":"http://archive.ubuntu.com/ubuntu/","Distribution":"noble","Components":["main"]}'
# 更新 mirror
curl -s -X PUT http://localhost:8080/api/mirrors/ubuntu2404/update
API 端点完整列表见 aptly API 文档。
十、生产部署模式
10.1 单机模式(典型)
aptly 作为单机服务,nginx 托管 ~/.aptly/public/。90% 企业场景足够用。
10.2 高可用模式
┌──────────────┐ ┌──────────────┐
│ aptly-master │ │ aptly-slave │
│ 主仓库构建 │ │ 只读镜像 │
└──────┬───────┘ └──────┬───────┘
│ rsync --delete │
└──────┬──────────────┘
▼
┌──────────────┐
│ S3/Swift/NAS │
│ 共享发布存储 │
└──────┬───────┘
▼
┌──────────────┐
│ 负载均衡 │
│ nginx upstream│
└──────────────┘
主节点写快照 + 发布到本地 → rsync 同步 public 到 S3/NAS → 从节点只读镜像 S3 → 负载均衡分发。
10.3 与 CI/CD 集成
# GitLab CI 示例:构建 → 推包 → 快照 → 发布
aptly repo add ci-builds ./build/*.deb
aptly snapshot create "release-$CI_COMMIT_TAG" from repo ci-builds
aptly publish snapshot "release-$CI_COMMIT_TAG" -batch \
-distribution=ubuntu2204 -component=main -skip-signing=true
十一、常见问题
11.1 publish 报"gpg: secret key not available"
现象:aptly publish snapshot 输出 gpg: no default secret key: secret key not available。
解决:
# 检查 gpg 私钥是否存在
gpg --list-secret-keys
# 指定密钥 ID 发布
aptly publish snapshot -gpg-key="$KEYID" snap-name
# 使用 internal provider(Go 纯实现,无需 gpg 命令)
cat > ~/.aptly.conf <<'EOF'
{"gpgProvider": "internal", ...}
EOF
11.2 mirror update 报"HTTP 403 Forbidden"
现象:某些远程仓库拒绝默认 aptly 的 UA 或 IP。
解决:
# aptly 继承 HTTP_PROXY 环境变量
export HTTP_PROXY=http://proxy:8080
export HTTPS_PROXY=http://proxy:8080
aptly mirror update repo-name
11.3 switch 报"distribution 'xxx' not published"
现象:publish switch 找不到目标 distribution。
解决:switch 只能对已存在的发布操作。先 publish snapshot <snap> <prefix> 创建,再 switch 原地更新。
# 首次必须显式 publish
aptly publish snapshot jammy-base myrepo
# 之后才能 switch
aptly publish switch myrepo jammy-v2
11.4 磁盘空间告急
现象:aptly db cleanup 之后磁盘还是满。
排查:
# 1. 检查各目录占用
du -sh /var/aptly/*
# 2. 检查是否有残留的旧快照
aptly snapshot list | head -20
# 3. 检查 DB 锁导致的 .lock 文件
ls -la /var/aptly/db/*.lock
# 4. 彻底清理
aptly db cleanup -vacuum