Slack 正在持续改造其 Amazon EC2 实例的运行方式。此前,他们已经把单一 Chef 栈演进为具备容错能力的多栈架构,并引入版本化 cookbook 部署与更安全的晋级流程;名为 Shipyard 的下一代 EC2 平台,则延续了这条现代化路线:把实例交付从零散的配置操作,逐步变成可版本化、可验证、可回退的平台流程。
需要说明的是,来源摘要没有披露 Shipyard 的完整架构、内部 API 或具体 AWS 资源组合。下面的分析以摘要明确提到的演进方向为依据,实践示例则展示团队可以如何落地类似思路,并不代表 Slack 的实际实现。
从“维护服务器”转向“晋级制品”
Chef 多栈、cookbook 版本化和安全晋级流程解决了一个核心问题:生产环境不应该直接接收未经验证的配置变化。
同样的原则可以扩展到 EC2 平台。一次实例变更通常不只涉及 Chef cookbook,还可能包括:
- 基础 AMI 和操作系统补丁;
- 启动模板、实例类型与磁盘配置;
- IAM 实例角色和安全组;
- 启动阶段运行的 bootstrap 脚本;
- 应用包、监控代理及日志采集组件。
如果这些元素分别由不同脚本修改,团队很难回答“当前实例究竟运行哪个版本”。下一代平台更值得追求的交付单元,是一个带版本的实例规格:它引用固定 AMI、固定配置版本和可审计的基础设施定义,然后依次晋级到测试、预生产和生产环境。
这也改变了故障处理方式。与其登录实例修复状态,不如构建新版本、通过自动检查,再让 Auto Scaling Group 逐批替换旧实例。旧版本仍然存在,因此回退变成重新指向上一个已知可用版本,而不是现场撤销一串命令。
多栈的价值不只是高可用
摘要提到 Slack 已经从单一 Chef 栈迁移到具备韧性的多栈结构。多栈除了降低单点故障风险,还能建立清晰的故障域和发布边界。
例如,可以按 AWS 区域、环境或业务关键度拆分控制面。每个栈独立接受新版本,并通过健康指标决定是否继续扩大范围。一个典型的晋级路径可以是:
- 在测试栈启动少量实例并执行配置收敛。
- 检查实例启动时间、Chef 执行结果和服务健康探针。
- 将同一版本晋级到预生产栈,观察真实流量下的错误率。
- 对生产栈执行分批替换,并设置自动停止条件。
这里的关键不是栈越多越好。每增加一个栈,团队都要承担证书、权限、监控、容量和升级成本。合理的拆分标准应当是故障隔离需求,而不是组织架构本身。
可以这样实践:用不可变版本驱动实例刷新
下面是一个可改造的最小示例。假设团队已经构建了 AMI,并将待发布的 AMI ID 写入 AWS Systems Manager Parameter Store。脚本会创建新的 Launch Template 版本,再触发 Auto Scaling Instance Refresh。
运行前需要修改 REGION、AMI_PARAMETER、LAUNCH_TEMPLATE_ID 和 ASG_NAME,并确保当前 AWS 身份拥有读取参数、创建启动模板版本和刷新 Auto Scaling Group 的权限。
#!/usr/bin/env bash
set -euo pipefail
REGION="us-east-1"
AMI_PARAMETER="/shipyard/example/staging/ami-id"
LAUNCH_TEMPLATE_ID="lt-0123456789abcdef0"
ASG_NAME="example-staging-web"
AMI_ID=$(aws ssm get-parameter \
--region "$REGION" \
--name "$AMI_PARAMETER" \
--query 'Parameter.Value' \
--output text)
SOURCE_VERSION=$(aws ec2 describe-launch-templates \
--region "$REGION" \
--launch-template-ids "$LAUNCH_TEMPLATE_ID" \
--query 'LaunchTemplates[0].LatestVersionNumber' \
--output text)
NEW_VERSION=$(aws ec2 create-launch-template-version \
--region "$REGION" \
--launch-template-id "$LAUNCH_TEMPLATE_ID" \
--source-version "$SOURCE_VERSION" \
--version-description "ami-${AMI_ID}" \
--launch-template-data "{\"ImageId\":\"${AMI_ID}\"}" \
--query 'LaunchTemplateVersion.VersionNumber' \
--output text)
aws ec2 modify-launch-template \
--region "$REGION" \
--launch-template-id "$LAUNCH_TEMPLATE_ID" \
--default-version "$NEW_VERSION"
REFRESH_ID=$(aws autoscaling start-instance-refresh \
--region "$REGION" \
--auto-scaling-group-name "$ASG_NAME" \
--preferences '{"MinHealthyPercentage":90,"InstanceWarmup":300,"AutoRollback":true}' \
--query 'InstanceRefreshId' \
--output text)
echo "Started refresh: $REFRESH_ID"
部署后可以持续查询刷新状态:
aws autoscaling describe-instance-refreshes \
--region us-east-1 \
--auto-scaling-group-name example-staging-web \
--instance-refresh-ids REPLACE_WITH_REFRESH_ID \
--query 'InstanceRefreshes[0].{Status:Status,Progress:PercentageComplete,Reason:StatusReason}'
这个示例体现了三个重要约束:AMI ID 是明确的版本输入;实例通过替换而不是原地修改完成升级;平台保留旧启动模板版本,为回退提供基础。不过,生产环境还应在刷新前验证 AMI 签名、漏洞扫描结果、容量余量和目标组健康检查。
平台必须同时管理发布与证据
只封装 AWS API 并不足以构成下一代平台。平台还需要记录每次晋级的证据,例如:
- 谁批准了哪个版本进入生产;
- AMI、cookbook 和启动模板之间的版本关系;
- 测试栈通过了哪些健康检查;
- 实例刷新期间错误率和容量是否越过阈值;
- 回退究竟恢复了哪些资源版本。
Chef 收敛成功也不等于服务可用。实例可能正确安装了软件,却因为依赖不可达、证书错误或负载均衡注册失败而无法处理请求。因此,发布门禁至少应结合配置执行结果、EC2 状态检查、目标组健康度和应用层探针。
采用时应守住的边界
类似 Shipyard 的平台化改造适合从一条低风险服务链路开始,而不是一次迁移全部 EC2 工作负载。先建立版本模型、晋级规则和回退路径,再逐步接入更多团队。
落地前可以检查以下事项:
- 每台实例能否追溯到唯一的 AMI、配置和启动模板版本;
- 是否禁止或至少审计生产实例上的手工变更;
- 预生产与生产是否使用同一个制品,而不是分别构建;
- 分批发布是否设置最小健康容量、预热时间和停止条件;
- 控制面故障时,现有实例能否继续提供服务;
- 回退流程是否经过演练,而不只是写在文档里。
这条现代化路线的重点并非给 EC2 再套一层界面,而是把实例生命周期变成可重复的工程流程。版本化制品、多栈隔离和受控晋级组合起来,才能让大规模 EC2 变更既有速度,也保留可审计与可恢复的边界。