Floci 仿真 AWS BCM Data Exports 服务:CUR 2.0 / FOCUS 1.2 导出管理面完整指南
【免费下载链接】flociLight, fluffy, and always free - The AWS Local Emulator alternative项目地址: https://gitcode.com/gh_mirrors/fl/floci
导读
本文深入讲解 Floci(AWS 本地仿真器)中bcm-data-exports:*服务的完整实现与用法。该服务仿真 AWS Billing and Cost Management Data Exports(CUR 2.0 / FOCUS 1.2 配套)的管理面 API,支持创建、查询、更新、删除导出(Export)及其执行记录(Execution),并通过与 legacycur:*服务共享的 Parquet 发射流水线,在本地 S3 中产出真实的 FOCUS 1.2 列式成本数据文件。读完本文,你将掌握该服务的协议入口、全部 7 个操作、校验规则、执行状态机、发射机制与三种运行模式,并能用 AWS CLI 和 boto3 在本地完整跑通"创建导出 → 生成 Parquet → 轮询执行状态"的实战流程。
服务概览:协议、端点与定位
BCM Data Exports 是 CUR 2.0 / FOCUS 1.2 的配套数据导出服务。Floci 仿真的是它的管理面(management plane),即定义导出任务、查询执行记录的 API,而数据产出则交给与 legacycur:*服务共享的 Parquet 发射引擎。
- 协议:JSON 1.1
- 请求头:
X-Amz-Target: AWSBillingAndCostManagementDataExports.<Action> - Endpoint 前缀:
bcm-data-exports
从源码看,路由分发由 BcmDataExportsJsonHandler.java 完成:它解析X-Amz-Target头中的 action 名,通过 Java 的switch表达式分派到 BcmDataExportsService.java 的对应方法;遇到未知 action 则返回400 UnknownOperationException。
关系说明:BCM Data Exports 与 legacy
cur:*是同一底层导出流水线的两个不同管理面。两者共享EmissionEngine→FocusRowProjector→ParquetEmitter这条发射链路(详见下文"发射机制"一节),区别在于存储键、校验规则与操作命名不同。
支持的 7 个操作
| 操作 | 说明(含源码行为细节) |
|---|---|
CreateExport | 创建导出;若Name与已有导出重复,返回ValidationException(见 BcmDataExportsService.java 中findByName去重逻辑) |
GetExport | 按 ARN 返回单个导出;ARN 缺失或不存在时返回ResourceNotFoundException |
ListExports | 返回调用账户拥有的全部导出(管理面返回Exports数组,每项为 ExportReference 摘要) |
UpdateExport | 替换既有导出的可变字段;保留ExportArn、CreatedAt、OwnerAccountId等不可变属性 |
DeleteExport | 幂等删除;同一调用内级联删除该导出的全部执行记录,避免孤儿记录 |
ListExecutions | 返回某导出的全部执行记录;导出不存在时返回ResourceNotFoundException |
GetExecution | 按ExportArn+ExecutionId返回单条执行记录 |
响应形状要点(从序列化器看)
BcmDataExportsJsonHandler.java 中的序列化逻辑揭示了几个对 SDK 兼容性很关键的细节:
CreateExport/UpdateExport只返回ExportArn字符串({"ExportArn": "..."}),与 AWS 实际响应一致;GetExport返回完整的Export对象,其中ExportStatus被包装成ExportStatus.StatusCode+CreatedAt+LastUpdatedAt(时间戳格式化为 ISO-8601);ListExports的每个元素只含ExportArn、ExportName和ExportStatus.StatusCode;GetExecution/ListExecutions返回ExecutionId、ExportArn以及ExecutionStatus(含StatusCode、CreatedBy、CreatedAt、CompletedAt、StatusReason)。
校验规则详解
Floci 对Export各字段的校验集中在 BcmDataExportsService.java 的validateExport/validateDestination/validateRefreshCadence三个方法中,与 AWS 行为对齐:
| 字段 | 规则 | 违规响应 |
|---|---|---|
Export.Name | 仅允许字母数字 +-与_,最长 128 字符 | ValidationException |
Export.DataQuery.QueryStatement | 必填;自由形式 SQL,Floci 不做解析(导出形状由内置 FOCUS schema 决定) | ValidationException |
Export.DestinationConfigurations.S3Destination | 必填;S3Bucket+S3Region为必填 | ValidationException |
S3Destination.S3Bucket | 还需通过S3DestinationValidation.requireValidBucketName的桶名校验(3–63 字符、小写字母数字/连字符/点号等) | ValidationException |
S3Destination.S3Prefix | 可选;非空时须为安全 key 段(requireSafeKeySegment) | ValidationException |
S3OutputConfigurations.Format | 仅PARQUET(CSV 未实现,传TEXT_OR_CSV返回ValidationException) | ValidationException |
S3OutputConfigurations.Compression | 仅PARQUET(GZIP未实现) | ValidationException |
S3OutputConfigurations.Overwrite | CREATE_NEW_REPORT/OVERWRITE_REPORT | ValidationException |
S3OutputConfigurations.OutputType | 仅CUSTOM | ValidationException |
RefreshCadence.Frequency | 仅SYNCHRONOUS(撰写本文时 AWS 唯一支持的值) | ValidationException |
Export.RefreshCadence | 必填 | ValidationException |
源码中这些白名单以Set常量形式定义(BcmDataExportsService.java):ALLOWED_FORMAT = {PARQUET}、ALLOWED_COMPRESSION = {PARQUET}、ALLOWED_OVERWRITE = {CREATE_NEW_REPORT, OVERWRITE_REPORT}、ALLOWED_OUTPUT_TYPE = {CUSTOM}、ALLOWED_FREQUENCY = {SYNCHRONOUS}。
S3OutputConfigurations是可选的(out != null时才校验其子字段),但一旦提供,非法取值会被直接拒绝——这样设计是为了防止"能持久化但无人能读取"的导出定义(Floci 的发射引擎只写 Parquet)。
存储键与账户隔离
BCM 导出数据全程按账户(account-scoped)隔离,存储键设计如下:
- 导出(Export):
<accountId>::<exportArn> - 执行记录(Execution):
<accountId>::<exportArn>::<executionId>
实际写入时通过StorageFactory创建两个后端:bcm-exports.json存导出、bcm-executions.json存执行记录(见 BcmDataExportsService.java)。
几个值得注意的账户处理细节:
Export模型带有一个不在 AWS 线协议中暴露的ownerAccountId字段,用于在后台调度场景下定位正确的账户分区(见 Export.java);- 删除导出时,
deleteExport会扫描执行存储中所有以exportArn::为前缀的 key 并一并删除(BcmDataExportsService.java),确保不产生孤儿执行记录; - 后台每日发射循环通过
AccountAwareStorageBackend.scanAllAccounts()+putForAccount跨账户读写,listAllExportsByAccount()按ownerAccountId分组返回全部账户的导出。
执行生命周期(Execution Lifecycle)
在synchronous模式下,一次成功的CreateExport会产生恰好一条执行记录,其状态流转如下:
INITIATION_IN_PROCESS (在创建导出时记录) | v DELIVERY_SUCCESS 或 DELIVERY_FAILUREUpdateExport同样会触发状态流转。成功与失败两种终态都能通过GetExecution.Execution.ExecutionStatus观察。
从源码实现看(CurEmissionScheduler.java),执行记录的编排过程是:
recordExecution()先生成 UUIDexecutionId,状态置为INITIATION_IN_PROCESS,createdBy记为USER或SCHEDULE;- 调用
EmissionEngine.emitForCurrentMonth()执行发射; - 成功则
completeExecution(success=true)置为DELIVERY_SUCCESS;失败则completeExecution(success=false)置为DELIVERY_FAILURE并把异常信息写入StatusReason。
执行模型类 ExportExecution.java 中注释列出的完整状态集合为:INITIATION_IN_PROCESS | QUERY_QUEUED | QUERY_IN_PROCESS | QUERY_FAILURE | DELIVERY_IN_PROCESS | DELIVERY_SUCCESS | DELIVERY_FAILURE,Floci 当前主要使用首、尾状态。
发射机制:与 CUR 共享的 Parquet 流水线
BCM 导出与cur:*共用同一条 Parquet 发射链路,流程如下:
- 收集用量行:
EmissionEngine从所有实现ResourceUsageEnumeratorSPI 的服务收集UsageLine行(该 SPI 与 Cost Explorer 服务文档 引入的机制一致,见 EmissionEngine.java);单个枚举器失败只会告警跳过,不影响整体发射; - 投影为 FOCUS 行:
FocusRowProjector将UsageLine转换为 FOCUS 1.2 / CUR 2.0 列形状,使用内置的 Pricing snapshot(参见 pricing 文档); - NDJSON 暂存:行序列化为 newline-delimited JSON,上传到
floci-cur-staging桶(key 为cur-staging/<reportName>/<runId>.ndjson),避免手工拼接 SQL 转义 tag、描述与资源 ID; - DuckDB 写 Parquet:
floci-ducksidecar 执行COPY (SELECT * FROM read_json_auto('<staging>')) TO '<dest>' (FORMAT PARQUET),从 Floci S3 读取暂存对象并直接写回 Floci S3(见 ParquetEmitter.java); - 清理暂存:在
finally块中尽力删除暂存对象与 side-effect CSV,避免并发发射累积噪音。
每次发射携带全新的runId(UUID),最终产物位于:
s3://<S3Bucket>/<S3Prefix>/<Name>/<runId>.parquet由于runId每次不同,并发发射不会互相覆盖。
发射窗口与账户上下文
EmissionEngine.emitForCurrentMonth的发射窗口固定为now所在的自然月(UTC),与 CUR 计费周期一致(EmissionEngine.java)。ParquetEmitter发射前还会对目标桶名做 S3 命名规则校验,并对插值进 SQL 的路径做单引号转义——这是防止通过S3Prefix等字段注入 SQL 的双重安全网。
值得注意的实现细节:floci-duck的/execute端点为了兼容 Athena 代码路径总是把主sql字段包装成输出 CSV 的 COPY,因此 Parquet 写入放在setup_sql(原样执行),而主sql只是一个无副作用的SELECT 1 AS ok(见 ParquetEmitter.java)。floci-ducksidecar 与 Athena 一样,在首次发射时懒启动。
FLOCI_SERVICES_BCM_DATA_EXPORTS_EMIT_MODE三种运行模式
| 值 | 行为 |
|---|---|
synchronous(默认) | 每次CreateExport/UpdateExport同步发射一次 |
daily | 每 24 小时通过共享的 CUR 调度执行器发射一次 |
off | 仅管理面,不产生任何发射 |
调度器实现见 CurEmissionScheduler.java:daily模式在@PostConstruct启动一个单线程的ScheduledExecutorService(首次延迟 5 秒,周期 24 小时),与面向用户的 EventBridge Scheduler 派发器刻意分离。每日循环在请求作用域之外运行,因此会为每个账户激活合成 CDI 请求作用域(runUnderAccount),确保 Java 侧暂存写入与 DuckDB 侧读写落在同一账户分区(CurEmissionScheduler.java)。
CreateExport同步发射失败不会回滚导出创建——handler 捕获异常后告警,导出依然持久化(BcmDataExportsJsonHandler.java),失败情况通过执行记录的状态暴露。
配置项
| 环境变量 | 默认值 | 说明 |
|---|---|---|
FLOCI_SERVICES_BCM_DATA_EXPORTS_ENABLED | true | 启用或禁用该服务 |
FLOCI_SERVICES_BCM_DATA_EXPORTS_EMIT_MODE | synchronous | 运行模式(见上表) |
对应配置接口为 EmulatorConfig.java 中的BcmDataExportsServiceConfig:enabled()默认true,emitMode()默认synchronous,语义与floci.services.cur.emit-mode一致但作用于 BCM 的Export记录。
实战示例
AWS CLI(bash)
export AWS_ENDPOINT_URL=http://localhost:4566 export AWS_DEFAULT_REGION=us-east-1 export AWS_ACCESS_KEY_ID=test export AWS_SECRET_ACCESS_KEY=test # 创建同步导出的 FOCUS 月报 aws bcm-data-exports create-export --export '{ "Name": "focus-monthly", "DataQuery": {"QueryStatement": "SELECT * FROM COST_AND_USAGE_REPORT"}, "DestinationConfigurations": { "S3Destination": { "S3Bucket": "my-billing", "S3Prefix": "focus", "S3Region": "us-east-1", "S3OutputConfigurations": { "Format": "PARQUET", "Compression": "PARQUET", "OutputType": "CUSTOM", "Overwrite": "OVERWRITE_REPORT" } } }, "RefreshCadence": {"Frequency": "SYNCHRONOUS"} }' # 列出本账户所有导出 aws bcm-data-exports list-exports # 查看某导出的执行记录 aws bcm-data-exports list-executions \ --export-arn arn:aws:bcm-data-exports:us-east-1:000000000000:export/focus-monthly创建成功后,同步发射会在s3://my-billing/focus/focus-monthly/<runId>.parquet留下真实 Parquet 文件,可用aws s3 ls s3://my-billing/focus/focus-monthly/查看产物。
boto3(Python)
import boto3 client = boto3.client( "bcm-data-exports", endpoint_url="http://localhost:4566", region_name="us-east-1", ) resp = client.create_export(Export={ "Name": "focus-monthly", "DataQuery": {"QueryStatement": "SELECT * FROM COST_AND_USAGE_REPORT"}, "DestinationConfigurations": {"S3Destination": { "S3Bucket": "my-billing", "S3Prefix": "focus", "S3Region": "us-east-1", "S3OutputConfigurations": { "Format": "PARQUET", "Compression": "PARQUET", "OutputType": "CUSTOM", "Overwrite": "OVERWRITE_REPORT", }, }}, "RefreshCadence": {"Frequency": "SYNCHRONOUS"}, }) print(resp["ExportArn"])boto3 走endpoint_url访问 Floci 时,SDK 会使用正确的X-Amz-Target头,create-export、list-exports、list-executions、get-execution等操作均可直接工作。
不在范围内(Out of Scope)
DataQuery.QueryStatement的自定义 SQL 求值:Floci 不解析 SQL,直接按 FOCUS 形状发射数据;查询字符串会被忠实持久化以保证 SDK 往返(round-trip)正常,但对 Parquet 输出没有任何影响;- 成本类别(cost categories)、计费视图(billing views)与定价模型覆盖(pricing-model overrides);
- 超出
SYNCHRONOUS的真实RefreshCadence调度:目前仅支持SYNCHRONOUS以及 Floci 内部的daily模式; TEXT_OR_CSV/GZIP输出:发射引擎当前无条件写 Parquet,CSV 与 GZIP 尚未实现。
延伸阅读
- 服务文档:docs/services/cur.md(共享发射流水线的 legacy 管理面)
- 管理面实现:BcmDataExportsService.java
- 协议分发:BcmDataExportsJsonHandler.java
- 数据模型:Export.java、ExportExecution.java、DataQuery.java、DestinationConfiguration.java、RefreshCadence.java
- 发射编排:CurEmissionScheduler.java、EmissionEngine.java、ParquetEmitter.java
- 配置定义:EmulatorConfig.java
【免费下载链接】flociLight, fluffy, and always free - The AWS Local Emulator alternative项目地址: https://gitcode.com/gh_mirrors/fl/floci
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考