Spinnaker Clouddriver AWS UserData 注入机制:从 UDF 模板到 LaunchConfiguration 的实战指南

原创2026-09-23 22:44:251,432 阅读
文章标签:后端DevOps云原生微服务

Spinnaker Clouddriver AWS UserData 注入机制:从 UDF 模板到 LaunchConfiguration 的实战指南

导读

本文基于 clouddriver 的 AWS UserData 文档,系统讲解 Spinnaker clouddriver-aws 如何在创建 AWS Server Group(Auto Scaling Group)时,将本地模板文件(UDF)注入到生成的 LaunchConfiguration 的 user-data 中。你将掌握 udf.udfRoot 与 udf.defaultLegacyUdf 的配置方法、模板 token 替换规则、按部署自定义 base64UserData 的用法,以及这套机制在源码中的完整实现链路与测试验证方式,可直接用于生产环境的自定义启动脚本下发。

什么是 EC2 User Data

EC2 的 user data 是实例启动时由 AWS 提供给实例内部的一段自定义数据(通常是 shell 脚本或 cloud-init 配置),可用于实例初始化:安装软件、拉取配置、注册服务等。clouddriver-aws 利用这一机制,把部署相关的上下文(账号、环境、集群名等)注入到每个新启动的实例里,让实例在启动阶段就能感知自己属于哪个部署单元。

AWS 官方把 user data 归属于"实例元数据(instance metadata)"体系,其完整定义与行为可参阅 AWS EC2 文档对 instance metadata 与 user data 的说明(原文见 UserData.md 中给出的链接)。

基本使用方式与配置属性

clouddriver-aws 支持把 userdata 注入到生成的 LaunchConfiguration 中。其机制是:通过一个模板文件,在创建 Server Group 时做 token 替换,把部署相关细节填充进去。

模板文件的位置由 udf.udfRoot 属性控制,行为由 udf.defaultLegacyUdf 属性控制,默认值如下:

udf:
  udfRoot: /apps/nflx-udf
  defaultLegacyUdf: false

这两项配置在源码中由 LocalFileUserDataProperties.groovy 定义,它使用 Spring Boot 的 @ConfigurationProperties("udf") 绑定 udf.* 前缀下的属性,同时还有第三个属性:

boolean enabled = true          // 是否启用 UserDataProvider 为新 Server Group 填充 user data
String udfRoot = '/apps/nflx-udf'
boolean defaultLegacyUdf = false

从源码注释可以看出 enabled 的关键作用:当 enabled = false 时,UserDataProvider 不再参与 user data 组装,在 CopyLastAsgAtomicOperation 与 ModifyAsgLaunchConfigurationOperation 两类操作中,user data 会直接从祖先 Server Group 复制而来。也就是说,enabled 控制的是"用模板生成"还是"从上游复制"两种策略。

从 Spinnaker 1.15 起,udf.defaultLegacyUdf 默认为 false。部署时几乎必然需要把模板文件所在目录改成你自己的文件系统路径(udfRoot),以适配你的部署环境。

模板文件(udf0)与 token 替换

在 udf.udfRoot 目录下创建一个名为 udf0 的文件,其内容会在每次创建新 Server Group 时被 token 替换、base64 编码后,作为 LaunchConfiguration 的 user-data 下发。

token 替换的实现位于 DefaultUserDataTokenizer.java,其 replaceTokens 方法通过一系列字符串 replace 完成替换。文档给出的核心 token 列表如下:

token 描述
%%account%% 账号名称
%%accounttype%% 账号的 accountType
%%env%% 账号的环境(environment)
%%region%% 部署区域
%%group%% Server Group 名称
%%autogrp%% Server Group 名称(与 %%group%% 相同)
%%cluster%% 集群名称
%%stack%% 集群名称中的 stack 部分
%%detail%% 集群名称中的 detail 部分
%%launchconfig%% Launch Configuration 名称

除了文档列出的这些,从 DefaultUserDataTokenizer.java 源码可以看到完整替换集合中还包含更多可用 token:

  • %%app%%:应用名(由 Frigga Names.parseName 解析自 Server Group 名称)
  • %%revision%%:Server Group 的 revision 序号
  • %%countries%%、%%devPhase%%、%%hardware%%、%%zone%%:名称中的各类扩展维度
  • %%tier%%:恒替换为空字符串
  • %%launchtemplate%%:当使用 Launch Template(而非 Launch Configuration)时替换为 Launch Setting 名称,此时 %%launchconfig%% 替换为空字符串;反之亦然

有两个值得注意的实现细节:

  1. %%env%% 的取值受 legacyUdf 影响:当 legacyUdf 为 true 时,%%env%% 替换为账号名(account);为 false 时替换为 environment 属性。这一行为在 LocalFileUserDataProviderSpec.groovy 中有明确的测试用例验证。
  2. token 替换发生在模板文件读取之后:LocalFileUserDataProvider 先按查找规则拼装原始模板内容,再交给 tokenizer 统一替换,最后追加额外环境变量。

典型用法是把这些值填入一组环境变量,然后基于 account/env/stack 等取值在启动脚本中做差异化行为控制。

示例 udf0 模板文件

文档给出的模板示例:

CLOUD_ACCOUNT="%%account%%"
CLOUD_ACCOUNT_TYPE="%%accounttype%%"
CLOUD_ENVIRONMENT="%%env%%"
CLOUD_SERVER_GROUP="%%group%%"
CLOUD_CLUSTER="%%cluster%%"
CLOUD_STACK="%%stack%%"
CLOUD_DETAIL="%%detail%%"
EC2_REGION="%%region%%"

假设使用该模板部署 Server Group udf-example-cluster-v001,账号 main、accountType streaming、环境 prod、区域 us-east-1,最终生成的 user data 为:

CLOUD_ACCOUNT="main"
CLOUD_ACCOUNT_TYPE="streaming"
CLOUD_ENVIRONMENT="prod"
CLOUD_SERVER_GROUP="udf-example-cluster-v001"
CLOUD_CLUSTER="udf-example-cluster"
CLOUD_STACK="example"
CLOUD_DETAIL="cluster"
EC2_REGION="us-east-1"

这里的名称解析依赖 Frigga 的 Names.parseName:udf-example-cluster-v001 会被解析为 app=udf-example、stack=cluster(文档示例中此处的命名拆解以实际解析结果为准)、cluster=udf-example-cluster 等字段,这正是 %%cluster%%、%%stack%%、%%detail%% 等 token 的数据来源。

追加的额外环境变量

除了模板 token 替换,LocalFileUserDataProvider.groovy 中的 addAdditionalEnvVars 方法还会把 Server Group 名称中携带的扩展字段自动追加为环境变量,例如:

NETFLIX_COUNTRIES=...
NETFLIX_DEV_PHASE=...
NETFLIX_HARDWARE=...
NETFLIX_PARTNERS=...
NETFLIX_REVISION=...
NETFLIX_USED_BY=...
NETFLIX_RED_BLACK_SWAP=...
NETFLIX_ZONE=...

这些变量通过查找模板中的 INSERTION_MARKER(即 \nexport EC2_REGION=)插入到该行之前,保证与 EC2_REGION 相邻。也就是说,只要模板中包含 export EC2_REGION=,Frigga 从 Server Group 名称中解析出的这些附加维度就会被自动注入。

legacy 模式下的多文件查找顺序

当 legacyUdf 为 true 时,assembleUserData 会按顺序读取并拼接多份模板文件,目录变为 ${udfRoot}/legacy/,查找顺序为:

  1. udf0(通用模板)
  2. udf-${account}、udf-${region}-${account}(按账号/区域定制)
  3. udf1
  4. custom.d/ 下按 app、app-stack、cluster、group 与账号组合命名的文件
  5. custom.region.d/${region}/ 下按同样维度命名的文件
  6. udf2

每个文件读取时若不存在则静默返回空串(getContents 捕获 IOException 返回 ''),并使用 LinkedHashSet 保证顺序且去重。所有文件内容拼接后统一交给 tokenizer 处理。这是 legacy 模式相对于默认单文件 udf0 的核心差异,如果你的模板需要按账号、区域、应用等维度分层定制,legacy 模式的文件查找机制可以直接复用。

legacyUdf 的判定来源

isLegacyUdf(account, applicationName) 方法会调用 Front50 读取应用的 legacyUdf 偏好:若应用显式设置了该字段则采用其值;若应用不存在(Front50 返回 404)或未设置,则回退到 udf.defaultLegacyUdf。从 LocalFileUserDataProviderSpec.groovy 的测试用例可以看到:404 时返回默认值、其他错误(如 500)会抛出 IllegalStateException(读取失败并带重试机制,最多重试 5 次、间隔 500ms)。

按部署定制 user data:base64UserData

AWS 的 create/clone server group 操作支持 base64UserData 属性,它会追加到已有模板内容之后,从而在每次部署时注入任意自定义 user data。

组装逻辑在 UserDataProviderAggregator.java 的 aggregate 方法中:

  1. 先 base64 解码用户提供的 base64UserData;
  2. 依次调用所有已注册的 UserDataProvider(如 LocalFileUserDataProvider、NullOpUserDataProvider 等)生成模板 user data,用换行符拼接;
  3. 把解码后的用户 user data 追加在最后,再次整体 base64 编码后返回。

对应测试 UserDataProviderAggregatorSpec.groovy 验证了聚合顺序为 "provider a -> provider b -> 用户提供的 user data"。

UserDataOverride:完全覆盖模式

UserDataInput 还支持 userDataOverride(UserDataOverride 所在包的用户数据模型,定义于 clouddriver-aws 的 userdata 模块)。当 userDataOverride.enabled = true 时,providers 生成的模板 user data 会被跳过,仅使用用户提供的 base64 user data,并且会对其执行 token 替换——相当于把用户数据当作 UDF 模板处理。tokenizer 通过 supports(tokenizerName) 匹配,默认 tokenizer 与自定义 tokenizer 均可选用(测试中分别验证了 default 与 custom 两种 tokenizer 的替换效果)。若找不到匹配的 tokenizer,会抛出 UserException。

在部署链路中的传递

在 BasicAmazonDeployHandler.groovy 中,创建 Server Group 时会把描述对象中的 base64UserData、legacyUdf、userDataOverride 逐一带入 UserDataInput,最终由 UserDataProviderAggregator 组装成最终的 base64 user data 写入 LaunchConfiguration(或 Launch Template)。相关字段同样出现在 CopyLastAsgAtomicOperation、ModifyAsgLaunchConfigurationOperation 等运维操作中,例如 BasicAmazonDeployDescription.groovy 与 ModifyAsgLaunchConfigurationDescription.groovy。

配置建议与常见问题

  1. 必须调整 udfRoot:默认的 /apps/nflx-udf 是 Netflix 内部的路径,其他部署环境需把该目录指向你自己存放 udf0 模板的路径,并确保 clouddriver 进程对该目录有读权限。
  2. udf0 必须按预期创建:非 legacy 模式下,只有 udf0 一个文件会被读取;文件不存在时 getContents 静默返回空串,最终 user data 可能为空(此时 UserDataProviderAggregator 返回 null)。
  3. %%env%% 与 legacy 模式:udf.defaultLegacyUdf 不仅决定模板文件查找层级(是否进入 legacy/ 子目录),还会改变 %%env%% 的替换值。若想在新模式下获得"环境名"语义,务必保持 defaultLegacyUdf: false。
  4. user data 以 base64 形式传输:最终结果经过 base64 编码后写入 LaunchConfiguration;自定义内容也需先 base64 编码后放入 base64UserData 字段。
  5. 调试手段:可参考 LocalFileUserDataProviderSpec.groovy 与 UserDataProviderAggregatorSpec.groovy 中的断言结果,理解替换前后字符串的精确形态,便于核对线上生成的 user data 是否符合预期。

总结

Spinnaker clouddriver-aws 的 user data 注入机制可以概括为一条清晰的数据流:配置文件(udf.udfRoot、udf.defaultLegacyUdf)→ 模板文件(udf0 及 legacy 模式下的多文件)→ token 替换(DefaultUserDataTokenizer)→ 追加额外环境变量 → 与 base64UserData 聚合 → base64 编码写入 LaunchConfiguration。核心实现集中在 deploy/userdata 目录,测试用例为每个环节提供了可验证的预期输出。合理组合模板 token、legacy 多文件层级与按部署的 base64UserData,即可让每台新实例在启动时自动获得与自己所属账号、环境、集群、区域完全匹配的初始化上下文。

登录后查看全文
spinnaker