@cyc-seattle/platform

中文文档
1.15.0发布于 2026/11/01Apache-2.0TypeScriptES Module构建来源已验证

Pulumi components for CYC's shared Google Cloud platform: WebService, the platform outputs reader, and the stacks that build the platform

原始使用文档
文档目录9 节
  1. Stacks
  2. @cyc-seattle/platform
  3. Environments
  4. Setup
  5. Deploy
  6. From Actions
  7. From a laptop
  8. State
  9. Development

platform

Pulumi stacks for CYC Community Sailing Center's shared Google Cloud platform: the things no single app owns. The apps that run on it live in their own repos, such as isthmia.

Stacks

StackHoldsApplied by
bootstrapService accounts and project IAM, the Pulumi state buckets and KMS keys, and the org's GitHub repos and settingsA human with project Owner and gh admin:org. Never CI.
infraThe VPC, the cluster subnet and its Cloud NAT on a static address, the private services peering, the prod and dev Cloud SQL instances, the DNS zones, Artifact Registry, the GKE Autopilot cluster, the shared Gateway, its address, certificates, and dev's DNS records, the dev and prod namespaces with their quotas and network policies, dev's RBAC, the prod Job alerts, and each environment's platform/outputs.jsondeploy-runner, from Actions after approval. A deployer, with mise run deploy, when breaking glass.
smokeOne WebService in dev, at smoke.dev.cycsail.team, for the integration testsdeploy-dev, from Actions on every PR. A deployer, with PULUMI_STACK=dev mise run deploy, when breaking glass.
backendDirectus, with the schema apply from schemas/, its roles, policies, and permission rules, the sync machine users, its OIDC client, and MCP, and the clubspot-sync and gsuite-sync CronJobs, with dev's Clubspot and Workspace fakesdeploy-dev, from Actions on every PR, and deploy-runner on a push to main, after infra.

@cyc-seattle/platform

src/ is the @cyc-seattle/platform package, which the stacks here and the apps' stacks import:

  • getPlatformOutputs() reads an environment's platform/outputs.json from its state bucket. The infra stack writes it. It holds the environment's namespace and domain, the Gateway and its address (gatewayAddress, for an app's own DNS records), and the Cloud SQL instance's connection name and private IP (sqlHost). Dev stacks can't read prod's backend, so this replaces a StackReference for platform values (cyc-seattle/isthmia#215).
  • clusterProvider(platform) builds a Kubernetes provider for the cluster. Its kubeconfig runs gke-gcloud-auth-plugin with Application Default Credentials, so Pulumi must run under mise, where mise run install puts the plugin on PATH.
  • Workload runs a long-lived process with no route: a Deployment, a ServiceAccount, and, when it has ports, a ClusterIP Service that other pods in the namespace reach at host. With more than one replica, spreadAcrossZones spreads them, and minAvailable adds a PodDisruptionBudget. podAnnotations sets annotations such as cluster-autoscaler.kubernetes.io/safe-to-evict.
  • WebService is a Workload behind the environment's Gateway at hostname. Its Service exposes port as port 80, and its readiness probe must be an HTTP GET, which the load balancer's health check reuses.
  • ScheduledJob runs a CronJob on schedule, in America/Los_Angeles unless timeZone says otherwise. A run never overlaps the one before it, gets an hour, and doesn't retry, by default.
  • All three run non-root with a read-only root filesystem, no capabilities, and no service account token, on Spot pods in dev only. Their limits equal their requests. Their pods get no IAM; grant roles to member, a Workload Identity principal, or set serviceAccountAnnotations to run as a Google service account.
  • enableService enables a Google Cloud API.

mise run build builds it into dist/, which is all it publishes. .github/workflows/release.yml releases each merge to main with semantic-release, versioned by the conventional commits.

Environments

Every app runs on the one GKE Autopilot cluster, in its environment's namespace. Prod runs on regular capacity, and dev on Spot pods. isthmia's apps (Authentik, Directus, the portal, and the two syncs as CronJobs) run in both, and dev adds Mailpit and the Clubspot and Workspace fakes. In dev, platform's backend deploys Directus, the syncs, and the fakes, and dev's sync CronJobs are suspended: the integration features and mise run reset run them as Jobs (cyc-seattle/isthmia#271).

StackNamespaceDomainDeploy identity
devdevdev.cycsail.teamdeploy-dev
prodprodcycsail.teamdeploy-runner

Each namespace denies ingress from other namespaces except the load balancer's. Dev also admits the Kubernetes API's service proxy, which the integration tests use, and denies egress to the mail ports (25, 465, 587) outside the cluster. infra writes dev's A records; each prod app writes its own, pointing at gatewayAddress.

deploy-dev reads Secret Manager secrets whose IDs start with dev-. Read them with gcp.secretmanager.getSecretVersionAccess, which needs only the accessor role.

infra creates each app's dev database, user, and password secret (dev-db-<name>-password) on cyc-dev, for each name in infra:devDatabases. To add an app's dev database, add its name there in a platform PR: Cloud SQL ignores IAM conditions on its user and database calls, so deploy-dev can't hold a role scoped to cyc-dev.

A Job that fails in prod, or a CronJob there with no successful Job in 3 hours, raises an alert. It emails each address in infra:alertEmails, a secret in the stack's config, which holds the maintainer's address (#20).

Setup

mise provides the tools.

mise trust
mise run install
mise run auth-adc

mise run auth-adc logs Application Default Credentials in as your own account. Pulumi uses them for GCP and for the state bucket. docs/manual-setup.md lists the access a deployer needs first.

Deploy

From Actions

.github/workflows/pull-request.yml deploys through the org's reusable deploy.yml, which authenticates with Workload Identity Federation and stores no credentials. Each job runs in a GitHub environment that holds its identity and state bucket:

EventJobs, in order
Pull requestci, then dev: mise run deploy as deploy-dev, then mise run integration
Push to mainci, dev, then prod-preview, then prod, which waits for the maintainers approval

prod-preview runs mise run preview as deploy-runner and writes the diff to the job summary. prod runs mise run deploy on the same commit once approved. It runs only when the preview shows changes, so a merge that changes nothing in prod asks for no approval. Dev builds the sync images for every commit, and prod's CronJobs run the one built for theirs, so until cyc-seattle/isthmia#258 every merge previews backend's CronJobs as changed. A PR from a fork gets no OIDC token, so its dev job is skipped.

PULUMI_STACK picks what the tasks run (scripts/each-stack): dev is smoke, then backend, and prod is infra, then backend. integration is dev only.

bootstrap is never deployed from Actions (cyc-seattle/isthmia#118). It holds the identities that CI assumes, and a human with project Owner and gh admin:org applies it with the tasks below.

From a laptop

Local deploys stay as the break-glass path. A deployer applies with the same tasks:

mise run preview-bootstrap
mise run deploy-bootstrap
mise run preview
mise run deploy
PULUMI_STACK=dev mise run deploy

mise run deploy applies infra, then backend. isthmia's prod stacks read its outputs through a StackReference. Dev stacks can't read prod state, so infra writes each environment's platform values to platform/outputs.json in that environment's state bucket.

bootstrap's GitHub provider uses gh auth token, so log gh in as a cyc-seattle org owner with the admin:org scope: gh auth refresh -h github.com -s admin:org. Deploys refresh first, so they delete a label added by hand.

State

Each environment's state lives in its own bucket, gs://cyc-admin-scripts-pulumi-<stack>, with its secrets encrypted by the state-<stack> KMS key. stacks/bootstrap/state.ts creates both. mise.toml picks the bucket from PULUMI_STACK, which defaults to prod, so stack names are organization/<project>/<stack>. It sets GOOGLE_APPLICATION_CREDENTIALS to the shared .gcloud/ login only when it isn't already set, so Actions' credentials win.

bootstrap's own state lives in the bucket it creates. Both the bucket and the key are protected.

Development

mise run ci

Runs the type check, Prettier, ESLint, and the unit tests. Tests mock Pulumi with pulumi.runtime.setMocks and never call a live API.

mise run integration

Runs the Gherkin scenarios in features/ against dev, after PULUMI_STACK=dev mise run deploy. They run as whatever identity Application Default Credentials hold. The isolation scenarios expect deploy-dev, so as a human, log Application Default Credentials in with --impersonate-service-account first.

版本与发布记录

@cyc-seattle/platform 共发布 29 个版本,首次发布于 2026/10/15;过去 12 个月发布了 29 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
1.15.0latest2026/11/012 天前
1.14.12026/11/012 天前
1.14.02026/10/313 天前
1.13.02026/10/313 天前
1.12.02026/10/295 天前
1.11.02026/10/295 天前

关于 @cyc-seattle/platform 的常见问题

@cyc-seattle/platform 是什么?

Pulumi为CYC共享谷歌云平台提供的组件:WebService,平台输出阅读器,以及构建平台的堆栈

如何安装 @cyc-seattle/platform?

在项目目录运行 npm install @cyc-seattle/platform,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:@pulumi/gcp(^8 || ^10.0.0)、@pulumi/pulumi(^3)、@pulumi/kubernetes(^4)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@cyc-seattle/platform 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@cyc-seattle/platform 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@cyc-seattle/platform 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22.18。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@cyc-seattle/platform 可以商用吗?

@cyc-seattle/platform 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@cyc-seattle/platform 还在维护吗?

最新版本 1.15.0 发布于 2026/11/01(2 天前);至今共发布 29 个版本,其中过去 12 个月发布了 29 个;首次发布于 2026/10/15。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。