Gemini Enterprise 群组许可自动化管理实战:基于 Cloud Run Job 的批量许可证生命周期编排
2026/9/14 10:52:42 网站建设 项目流程

Gemini Enterprise 群组许可自动化管理实战:基于 Cloud Run Job 的批量许可证生命周期编排

【免费下载链接】generative-aiSample code and notebooks for Generative AI on Google Cloud, with Gemini Enterprise Agent Platform项目地址: https://gitcode.com/GitHub_Trending/ge/generative-ai

导读

在 Gemini Enterprise(Gemini Enterprise Agent Platform)的规模化落地中,许可证(license)的授予与回收往往依赖人工操作,随着组织成员变动、群组结构调整,很容易出现"该有权限的没开通、已离职/停用的仍占用席位"的错配问题。本项目 group-licensing 以Cloud Run Job + Cloud Scheduler为载体,实现了一个自动化的 Gemini Enterprise 许可证生命周期管理方案:通过定时批量对账(reconciliation),在 Google Cloud Identity 群组成员身份与 Discovery Engine 许可证分配之间建立桥梁,提供基于群组的 SKU 映射、自动开通(provisioning)与过期许可证清理(cleanup)能力。阅读本文后,你将掌握该方案的整体架构、配置格式、两个核心工作流的源码级原理,以及构建、测试、部署的完整命令。

方案概览:一个镜像,两个 Job,各自调度

整个方案的核心是一个 Go 语言编写的 Cloud Run Job 程序,从同一个容器镜像部署出两个 Job 定义,分别由 Cloud Scheduler 按各自的时间表触发:

JobJOB_TYPE调度频率职责
gemini-box-office-joinerjoiner每日(24 小时)分页遍历所有配置的群组成员,为缺失许可的用户授予许可证;若用户同时属于多个映射了 SKU 的群组,则按最高优先级 SKU 授予
gemini-box-office-gcgarbage_collection每 6 小时分页遍历所有已授权用户,回收"过期"(超过配置阈值未登录)或已不属于任何有权限群组的用户的许可证

这种"一镜像双 Job"的设计让两类对账逻辑共享同一套配置解析、适配器、日志与领域模型,同时又能独立调度、独立设置环境变量、独立扩缩容(通过--tasks N并行分片)。

环境变量:两个工作流共享的运行时开关

Job 的行为通过环境变量控制,完整清单如下(对应源码 job_settings.go 中的LoadJobSettings解析逻辑):

变量说明默认值
JOB_TYPE选择要运行的工作流(joinergarbage_collection),必填
DRY_RUNtrue时完整执行评估逻辑但不发起任何写 API 调用;可通过 Cloud Scheduler 请求体按次覆盖false
GC_SKIP_GROUP_EVALtrue时,垃圾回收 Job 跳过对已授权用户的群组成员身份评估,仅依据许可过期情况回收,仅适用于 garbage collection 工作流false
CLOUD_RUN_TASK_INDEXCloud Run 注入,当前任务实例的 0 基索引0
CLOUD_RUN_TASK_COUNTCloud Run 注入,并发任务实例总数1

从源码看,LoadJobSettings会对每个变量做严格校验:JOB_TYPE必须是joinergarbage_collection(对应 enums.go 中的WorkflowJoiner/WorkflowGarbageCollection);三个布尔变量经strconv.ParseBool解析,非法值直接报错;CLOUD_RUN_TASK_INDEX必须是非负整数、CLOUD_RUN_TASK_COUNT必须是 ≥1 的整数,且强制TASK_INDEX < TASK_COUNT,防止任务索引越界。

SKU 名称与优先级:多群组重叠时的裁决规则

当用户同时符合多个 SKU 的资格时,系统为其分配排名最高的那个 SKU。该优先级硬编码在 enums.go 的skuPrecedence映射中(数值越大优先级越高),自上而下依次为:

  1. SUBSCRIPTION_TIER_SEARCH_AND_ASSISTANT(Gemini Enterprise Plus)
  2. SUBSCRIPTION_TIER_ENTERPRISE(Gemini Enterprise Standard)
  3. SUBSCRIPTION_TIER_SEARCH(Search + NotebookLM)
  4. SUBSCRIPTION_TIER_NOTEBOOK_LM(仅 NotebookLM)
  5. SUBSCRIPTION_TIER_AGENTSPACE_BUSINESS(Gemini Business)
  6. SUBSCRIPTION_TIER_AGENTSPACE_STARTER(Gemini Business Starter)
  7. SUBSCRIPTION_TIER_FRONTLINE_WORKER
  8. SUBSCRIPTION_TIER_FRONTLINE_STARTER
  9. SUBSCRIPTION_TIER_ENTERPRISE_EMERGING(新兴市场 Enterprise Standard)
  10. SUBSCRIPTION_TIER_EDU_PRO
  11. SUBSCRIPTION_TIER_EDU
  12. SUBSCRIPTION_TIER_EDU_PRO_EMERGING
  13. SUBSCRIPTION_TIER_EDU_EMERGING

在 joiner 工作流中,collectGroupMembers为每个用户记录当前遍历到的最高优先级条目(HasHigherPrecedenceThan比较),确保同一用户最终只会落在一个最优 SKU/位置的许可配置上;未识别或SUBSCRIPTION_TIER_UNSPECIFIED的值优先级为 0,同时IsValid()校验会在配置加载阶段就拦截非法 SKU。

配置:Secret Manager 中的entitlements.json

配置存放在GCP Secret Manager中,以文件卷的方式挂载到 Job 内的固定路径/run/secrets/entitlements.json(路径常量定义于 constants.go)。

{ "billing_account_id": "ABCDE-12345-FGHIJ", "projects": { "customer-project-alpha": [ { "subscription_tier": "SUBSCRIPTION_TIER_ENTERPRISE", "location": "global", "groups": [ "group-data-scientists@example.com", "group-senior-devs@example.com" ] }, { "subscription_tier": "SUBSCRIPTION_TIER_AGENTSPACE_BUSINESS", "location": "global", "groups": [ "group-marketing@example.com", "group-general-staff@example.com" ] } ] }, "settings": { "staleness_threshold_days": 30 } }

字段说明如下:

字段说明
billing_account_id与被管理项目关联的 GCP 结算账号 ID,必填
projectsGCP 项目 ID → 授权条目列表(每个 SKU/location 组合一条)的映射
projects[].subscription_tier该条目的 Gemini SKU,合法值见上文 SKU 优先级列表
projects[].location许可证管理的地理区域,仅可为globaluseu之一
projects[].groups拥有该 SKU 权限的 Google 群组邮箱列表
settings.staleness_threshold_days超过该天数未活跃的用户将被回收;设为0或省略则完全禁用过期检查,仅评估授权资格

单个 Job 可通过在projects下追加条目管理多个项目。对于非常大的项目组合,可在 Job 执行时设置--tasks N,将项目列表分片到 N 个并行任务实例上。

配置校验:加载即拦截错误

配置解析与校验逻辑集中在 config.go 的Load/validate中,任何一项不满足都会返回带哨兵错误的失败信息(哨兵错误定义于 errors.go):

  • billing_account_id非空,projects非空,否则报ErrConfigNoProjects
  • 项目 ID 必须匹配^[a-z][a-z0-9\-]{4,28}[a-z0-9]$(6–30 字符、小写字母开头、仅含[a-z0-9-]、以字母或数字结尾);
  • 每个项目至少一个条目、每个条目至少一个群组邮箱,否则报ErrConfigNoGroups
  • SKU 必须是skuPrecedence中已识别的值(否则ErrInvalidSKU);
  • location 必须是global/us/eu(否则ErrInvalidLocation);
  • 群组邮箱须匹配^[^@\s]+@[^@\s]+\.[^@\s]+$(恰好一个@、域名含点);
  • staleness_threshold_days必须 ≥ 0。

值得一提的是,配置结构里还预留了可选字段subscription_id:当启用DIRECT_LAW(直连模式)环境变量时,每条授权记录要求显式提供subscription_id,用于直接指定licenseConfigs/{uuid}中的{uuid},而不再依赖按 SKU 自动解析配置路径(见 config.go)。

IAM、OAuth 与 API 要求

Job 的服务账号需要以下权限:

IAM 角色:

  • roles/discoveryengine.admin— 列出与更新用户许可证
  • roles/cloudidentity.groups.viewer— 列出群组成员并校验成员关系
  • roles/secretmanager.secretAccessor— 读取挂载的配置 Secret
  • roles/billing.viewer— 读取已购买的 Gemini Enterprise 订阅配置
  • roles/run.builder(可选)— 使用 Cloud Build 构建产物时
  • roles/storage.admin(可选)— 使用 Cloud Build 上传源码、暂存构建
  • roles/artifactregistry.createOnPushWriter(可选)— 使用 Cloud Build 推送镜像到 Artifact Registry
  • roles/logging.logWriter(可选)— 使用 Cloud Build 写构建日志

OAuth 范围:

  • https://www.googleapis.com/auth/cloud-platform
  • https://www.googleapis.com/auth/admin.directory.group.member.readonly

API 要求:

  • Discovery Engine API(GCP)
  • Resource Manager API(GCP)
  • Admin SDK API(Cloud Identity / Workspace)
  • Cloud Run Admin API(可选)
  • Cloud Build API(可选)
  • Compute Engine API(可选)
  • Secret Manager API(可选)
  • Cloud Scheduler API(可选)

Job 使用Application Default Credentials(ADC),无需任何服务账号密钥文件。触发器授权:Cloud Scheduler 的服务账号必须被授予对每个 Cloud Run Job 资源的roles/run.invoker权限。

从 main.go 可以看到实际初始化过程:Admin SDK 客户端使用admin.AdminDirectoryGroupMemberReadonlyScope范围,Resource Manager 客户端使用cloudresourcemanager.CloudPlatformReadOnlyScope范围,随后分别组装为cloudidentity.New(adminSvc)discoveryengine.New()resourcemanager.New(crmSvc)三个适配器。

项目结构与构建测试

cmd/job/ # Job 入口(main.go) internal/ adapters/ cloudidentity/ # Cloud Identity Admin API 适配器 discoveryengine/ # Discovery Engine API 适配器 resourcemanager/ # Resource Manager API 适配器(解析项目号) config/ # 配置加载与校验(授权配置 + Job 设置) middleware/ # 结构化日志中间件 models/ # 领域类型、DTO、错误、常量、枚举 ports/ # 接口定义(IdpClient、GeminiClient、ResourceManagerClient) services/ # 业务逻辑(JoinerService、GCService) docs/ PRD.md # 产品需求文档 TDD.md # 技术设计文档 Dockerfile # 多阶段构建 → distroless 运行时镜像

构建镜像:

docker build -t gemini-box-office:latest .

运行单元测试:

go test ./...

所有测试均为单元测试,不依赖任何外部服务或凭据。测试覆盖了配置解析/校验(config_test.go)、SKU 优先级与枚举校验(enums_test.go)、joiner 与 GC 工作流(joiner_test.go、gc_test.go)以及两个适配器(cloudidentity、discoveryengine)。

部署:一条命令部署为 Cloud Run Job

以下示例命令可直接将当前目录源码构建并部署为 Cloud Run Job:

gcloud run jobs deploy [name_of_job] \ --source . \ --region [desired_cloud_run_region] \ --update-secrets=/run/secrets/entitlements.json=[name_of_secret_in_secret_manager]:latest \ --set-env-vars JOB_TYPE=[joiner_or_garbage_collection],DRY_RUN=false,GC_SKIP_GROUP_EVAL=false \ --service-account [desired_service_account_email_address]

构建到 Artifact Registry 再部署

如果需要先把容器镜像构建并保存到 Artifact Registry、稍后再部署,可参考以下 Cloud Build 命令:

gcloud builds submit . \ --tag [desired_gcp_region]-docker.pkg.dev/[gcp_project_id]/[artifact_registry_repository]/[artifact_registry_package_name]:latest \ --service-account=projects/[gcp_project_id]/serviceAccounts/[desired_build_service_account_email_address] \ --default-buckets-behavior=regional-user-owned-bucket \ --region=[desired_gcp_region]

构建成功后,使用 Artifact Registry 中的镜像部署为 Cloud Run Job:

gcloud run jobs deploy [name_of_job] \ --image [desired_gcp_region]-docker.pkg.dev/[gcp_project_id]/[artifact_registry_repository]/[artifact_registry_package_name]:latest \ --region [desired_cloud_run_region] \ --update-secrets=/run/secrets/entitlements.json=[name_of_secret_in_secret_manager]:latest \ --set-env-vars JOB_TYPE=[joiner_or_garbage_collection],DRY_RUN=false,GC_SKIP_GROUP_EVAL=false \ --service-account [desired_service_account_email_address]

Job 输出:结构化 JSON 日志

Job 完成时以退出码0(成功)或1(失败)结束。结果以结构化 JSON 日志条目输出到 stdout,由 Cloud Logging 自动采集:

{"time":"...","level":"INFO","workflow":"joiner","task_index":0,"msg":"joiner workflow complete","duration_ms":4821,"licenses_granted":42,"licenses_soft_failed":0,"groups_processed":5,"dry_run":false}

如果某个 SKU 的许可证池在运行中途耗尽,系统会为每个耗尽的池发出一条 WARN 日志并继续执行:

{"time":"...","level":"WARN","workflow":"joiner","task_index":0,"msg":"license pool exhausted, soft-failing remaining users","project_id":"customer-project-alpha","license_config_path":"projects/123/locations/global/licenseConfigs/ent-config","available":3,"soft_failed":17}

如果结算账号对同一 SKU 有多个有效订阅,无法在一个池中入座的用户会被自动顺延到下一个池;每个耗尽的池都会触发 WARN,但最终汇总中的licenses_soft_failed只统计所有池尝试后仍未入座的用户数。

{"time":"...","level":"INFO","workflow":"garbage_collection","task_index":0,"msg":"garbage collection workflow complete","duration_ms":9134,"licenses_revoked":12,"users_evaluated":500,"dry_run":false}

日志规范

所有日志输出均为结构化 JSON 并写入 stdout,便于 Cloud Logging 自动采集;每条日志都包含workflowtask_index字段。PII(用户邮箱地址)永不写入日志。从 main.go 可见,日志系统将 slog 的level键重写为 Cloud Logging 期望的severity键,并在加载配置后为默认 logger 附加workflowtask_index属性,使服务层通过middleware.LoggerFromContext发出的每一条日志都自动携带这些字段。

源码级原理:joiner 工作流的对账链条

joiner 的完整执行链路在 joiner.go 中清晰可见,可归纳为五步:

  1. 获取许可证配置索引:通过FetchLicenseConfigIndex读取结算账号下的全部 licenseConfig 资源,构建(SKU, 项目号, location) → licenseConfig 路径列表的索引(LicenseConfigIndex)。
  2. 解析项目号:由于 Discovery Engine API 的 licenseConfig 资源路径使用数字项目号(如projects/415104041262/...)而非人类可读的项目 ID,joiner 会通过 Resource Manager 适配器把每个项目 ID 解析为项目号(见 types.go 的说明)。
  3. 枚举群组成员并裁决最高优先级:对每个项目的每条配置,逐页调用 Cloud Identity 的members.list(每页 200 人,最多 500 页,见 constants.go);适配器以includeDerivedMembership=true展平嵌套群组(见 idp.go),只保留USER类型成员,用优先级比较为每个用户记录最优 (SKU, location)。
  4. 按 key 分组待授权项:默认按(SKU, 项目号, location)分组;直连(DIRECT_LAW)模式下则按subscription_id分组。
  5. 批量授予:按MaxBatchSize = 100分块调用BatchUpdateUserLicenses。若池耗尽,grantBatch会先查询FetchLicenseUsageStats计算剩余席位,用余量重试,超出部分作为 soft-failed 顺延到下一个同 key 的池。

源码级原理:garbage collection 工作流的回收规则

GC 工作流在 gc.go 中实现,核心是shouldRevoke的判定逻辑,用户满足"过期 OR 无资格"即被回收:

  • 过期判定(staleness):仅当staleness_threshold_days > 0时执行。参考时间优先取LastLoginTime;若该时间为零(用户从未登录),则回退到AssignmentTime(许可证创建时间),避免刚开通、尚未登录的用户被立即回收;两者皆为零时视为立即过期。
  • 资格判定(entitlement):用户须至少属于项目配置中任意群组(members.hasMember校验,支持直接/间接成员)。若启用GC_SKIP_GROUP_EVAL=true,则跳过该检查、仅按过期情况回收;过期判定通过时会短路,避免昂贵的成员关系查询。

GC 的processProject按去重后的 location 分别分页(ListUserLicenses),每页内就地分块回收,保证任意时刻内存中只保留一页候选,而不是先累积全量再写。值得注意的是,GC 工作流只依赖 IdP 与 Gemini 两个端口(无需 Resource Manager,见 main.go)。

任务分片:多任务并行处理大项目组合

Cloud Run Job 支持并行任务,本项目据此实现了项目列表的分片机制。在 main.go 中,启动流程会:

  1. cfg.Projects的键做确定性排序
  2. i % TaskCount == TaskIndex将项目分配到当前任务;
  3. 若当前任务未分到任何项目,打印no projects assigned to this task; nothing to do并以0退出。

这样无论是 joiner 还是 GC,都可以通过--tasks N将数千个项目的对账工作横向拆分到 N 个并发实例,且每个实例的分配结果与任务数量无关地保持稳定,便于观察和重放。

总结与适用前提

gemini-box-office以"定时对账"的思路,用极小的运维面(两个 Cloud Run Job + 一份 Secret 配置)覆盖了 Gemini Enterprise 许可证生命周期的三个关键诉求:按群组自动开通、按 SKU 优先级仲裁、按活跃度与资格自动回收。使用前请注意以下前提:

  • 本文所述命令、变量与行为均以当前仓库代码为准,SKU 清单以 Google 官方 Gemini Enterprise SubscriptionTier 参考为准;
  • 部署需具备文中列出的 IAM 角色与 API 启用条件;
  • 首次上线强烈建议保持DRY_RUN=true,通过结构化日志观察将执行的授予/回收动作后再切换为真实写入;
  • 产品需求与技术设计细节可进一步阅读 docs/PRD.md 与 docs/TDD.md。

【免费下载链接】generative-aiSample code and notebooks for Generative AI on Google Cloud, with Gemini Enterprise Agent Platform项目地址: https://gitcode.com/GitHub_Trending/ge/generative-ai

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询