Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/architecture/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ VM startup metadata 只负责幂等主机供给。应用部署生成不可变 re

## 当前约束

- `development` 已真实部署并通过 destroy/rebuild、正式发布、回滚和双协议 E2E。
- `development` 已迁移到专用的 `kyson-proxy-dev` Project,并以 `proxy-*` 资源名完成正式发布和双协议 E2E。
- `development` 的 bootstrap/platform plan 当前为零变更,GitHub 配置审计通过。
- `production` 资源与秘密尚未激活,任何写操作仍需显式审批。
- Cloud Run request log 按精确服务名排除,避免 query token 被平台日志持久化;应用仅记录脱敏事件。
Expand Down
4 changes: 2 additions & 2 deletions docs/design/environments-and-delivery.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

本文唯一拥有环境配置归属、GitHub Variables/Secrets 名称、工作流接口以及 VM 与 Cloud Run 的发布协议。

**实现状态:** development GitHub/GCP 已配置,并通过正式部署、协调回滚与双协议 E2E;production 尚未激活。
**实现状态:** development 已在 `kyson-proxy-dev` 配置 GitHub/GCP,并通过正式部署、协调回滚与双协议 E2E;production 尚未激活。

## 环境模型

Expand Down Expand Up @@ -171,7 +171,7 @@ make deploy ENV=<environment> [GIT_REF=<ref>]
make destroy ENV=<environment> STACK=platform
```

命令只作为稳定入口;首次 development 激活步骤见 [Runbook](../runbooks/development-first-activation.md)。
命令只作为稳定入口;空 development 的首次激活见 [Runbook](../runbooks/development-first-activation.md),迁移到新的 GCP Project 且复用秘密时见 [项目迁移 Runbook](../runbooks/development-project-migration.md)。

`secrets-init` 只在本地新建五项随机秘密,不发布或调用 GCP;已有目录时拒绝覆盖。用户变更也只改本地 `users.json`,必须随后显式执行 `secrets-publish` 和 `deploy`。`subscription-url` 从 platform state 读取公开服务 URL,从本地文件读取已启用用户 token,并只向终端输出最终 URL。

Expand Down
6 changes: 4 additions & 2 deletions docs/design/infrastructure-and-identity.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

本文唯一拥有 OpenTofu 目录、stack/state 划分、模块接口、GCP 资源拓扑以及 IAM/WIF 边界。

**实现状态:** development 已 apply、重建并通过零变更 plan;production 尚未激活
**实现状态:** development 已在 `kyson-proxy-dev` apply、部署并通过零变更 plan;production 尚未激活

## 目录接口

Expand Down Expand Up @@ -55,7 +55,7 @@ environment = "development" # 或 production
project_id = "gcp-project-id"
region = "asia-northeast3"
zone = "asia-northeast3-a"
resource_prefix = "proxy"
resource_prefix = "proxy" # 完整资源名前缀,不自动拼接环境
network_cidr = "10.20.0.0/24"
network_tier = "STANDARD"
vm_machine_type = "e2-micro"
Expand All @@ -68,6 +68,8 @@ labels = { application = "proxy-builder", environment = "development

`environment` 只能是 `development` 或 `production`。`labels.application` 与 `labels.environment` 必须应用于支持 label 的所有资源,作为旧资源清理时的归属条件。

`resource_prefix` 是资源 ID 的完整前缀,环境名不参与资源 ID 派生。当前两个环境均为 `proxy`,因此新资源命名为 `proxy-network`、`proxy-vm`、`proxy-subscription`、`proxy-github` 和 `proxy-images`。GitHub 身份为 `proxy-gh-plan`、`proxy-gh-apply`、`proxy-gh-deploy`;运行身份为 `proxy-vm-sa` 与 `proxy-subscription-sa`。Project、GitHub Environment 与 labels 继续承担环境隔离。

名称包含 `_secret`、`password`、`private_key`、`token` 或证书内容的变量禁止进入 OpenTofu 输入。

## Stack 所有权
Expand Down
1 change: 1 addition & 0 deletions docs/runbooks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
| 需求 | 文档 |
| --- | --- |
| 首次创建并激活 development 新平台 | [development 首次激活](development-first-activation.md) |
| 迁移 development 到新的 GCP Project 并复用秘密 | [development 项目迁移](development-project-migration.md) |
| 新增、启停或轮换 development 用户 | [development 用户管理](development-user-management.md) |
| 保留 bootstrap 与秘密,重建 development platform | [development platform 重建](development-platform-rebuild.md) |

Expand Down
70 changes: 70 additions & 0 deletions docs/runbooks/development-project-migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Development 项目迁移

**使用时机:** 把 development 从一个已激活的 GCP Project 迁到另一个 Project,并保留本地应用秘密
**风险等级:** 极高
**最后复核:** 2026-08-24

## 前置条件

- [ ] source 与 target Project ID、目标 `resource_prefix` 已书面确认;不删除任何 GCP Project、Billing 或共享 API。
- [ ] 本地管理员对两个 Project 有 Owner 权限;`.secrets/development/` 五个文件存在且均为 `0600`。
- [ ] 所有 development workflow 已结束;`make github-audit ENV=development` 成功。
- [ ] source platform、bootstrap state bucket 的名称已从 source Project ID 推导,未凭记忆输入。

## 步骤

1. **销毁 source platform**

在 source tfvars 仍指向旧 Project 时,从 `main` 触发 `destroy.yml`,输入 `environment=development` 和精确 source Project ID。等待 workflow 成功,再以 GCP 控制面确认 VM、Cloud Run、Artifact Registry、Secret Manager 容器和静态 IP 均不存在。

2. **以本地 Owner 清理 source bootstrap 与 state**

本地 Owner 必须执行此步;GitHub apply 身份不能安全删除自身与 backend 权限。

```bash
tofu -chdir=infra/stacks/bootstrap init -reconfigure -input=false \
-backend-config="bucket=<source-project>-proxy-builder-tfstate" -backend-config="prefix=bootstrap"
tofu -chdir=infra/stacks/bootstrap destroy -auto-approve -input=false \
-var-file=../../environments/development.tfvars
gcloud storage rm --recursive --all-versions "gs://<source-project>-proxy-builder-tfstate/**"
gcloud storage buckets delete "gs://<source-project>-proxy-builder-tfstate" --quiet
```

预期:WIF、GitHub Service Account、自定义 Role、它们的 IAM 绑定和 source state bucket 不存在;已启用 GCP APIs 保留。

3. **切换仓库配置并验证**

将 development tfvars 的 `project_id` 与完整 `resource_prefix` 更新为 target 值;执行 `make validate`。在 target Project 创建 state bucket,再以本地 Owner apply bootstrap:

```bash
make bootstrap ENV=development
make infra-apply ENV=development STACK=bootstrap
make github-configure ENV=development
make github-audit ENV=development
make secrets-publish ENV=development
```

预期:GitHub 的 `DEV_` Variables 与 development Environment Variables 全部指向 target Project;秘密值不读取、不重建。

4. **重建并验收 target platform**

从 GitHub 触发并等待 development 的 bootstrap WIF 验证、platform apply 和 deploy。最后运行 `make infra-plan ENV=development STACK=platform` 与 `make subscription-url ENV=development USER=<user>`;导入新的 URL,确认 Base64/Clash 订阅和 VLESS/Hysteria2 E2E 成功。

## 失败处理

| 现象 | 处理 |
| --- | --- |
| source platform destroy 失败 | 停止,修复 OpenTofu 错误;不得手工删除未记录资源。 |
| source bootstrap/state 清理失败 | 保持 source tfvars,不切换 GitHub Variables;用本地 Owner 修复后重试。 |
| target bootstrap 失败 | source 已释放时不要回写旧 GitHub 身份;修复 target 权限或 API 后重试。 |
| target deploy/E2E 失败 | platform 保留,检查 workflow 的脱敏诊断并重新部署;首次发布没有 VM previous release 是预期边界。 |

## 成功标准

- source 不存在本项目 platform、bootstrap identity 或 state bucket;source GCP APIs 与 Project 保留。
- target 的 GitHub audit 成功、platform 第二次 plan 为零变更、五项秘密原样保留。
- target Cloud Run `/v1/health`、两种订阅格式和双协议 E2E 全部成功;旧订阅 URL 停止使用。

## 关联

- Design:[基础设施与身份](../design/infrastructure-and-identity.md)、[环境与交付](../design/environments-and-delivery.md)
2 changes: 1 addition & 1 deletion infra/environments/development.tfvars
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
environment = "development"
project_id = "kyson-lab"
project_id = "kyson-proxy-dev"
region = "us-west1"
zone = "us-west1-b"
resource_prefix = "proxy"
Expand Down
3 changes: 1 addition & 2 deletions infra/modules/artifact_registry/main.tf
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
locals {
environment_short = var.environment == "production" ? "prod" : "dev"
repository_id = "${var.resource_prefix}-${local.environment_short}-images"
repository_id = "${var.resource_prefix}-images"
}

resource "google_artifact_registry_repository" "images" {
Expand Down
11 changes: 5 additions & 6 deletions infra/modules/github_identity/main.tf
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
locals {
environment_short = var.environment == "production" ? "prod" : "dev"
name_prefix = "${var.resource_prefix}-${local.environment_short}"
environment_sub = "repo:${var.github_repository}:environment:${var.environment}"
pull_request_sub = "repo:${var.github_repository}:pull_request"
name_prefix = var.resource_prefix
environment_sub = "repo:${var.github_repository}:environment:${var.environment}"
pull_request_sub = "repo:${var.github_repository}:pull_request"

required_services = toset([
"artifactregistry.googleapis.com",
Expand Down Expand Up @@ -90,7 +89,7 @@ resource "google_service_account" "github" {

resource "google_project_iam_custom_role" "secret_metadata_admin" {
project = var.project_id
role_id = "${replace(title(var.resource_prefix), "-", "")}${title(local.environment_short)}SecretMetadataAdmin"
role_id = "${replace(title(var.resource_prefix), "-", "")}SecretMetadataAdmin"
title = "Proxy secret metadata admin (${var.environment})"
description = "Manage Secret Manager containers and IAM without reading payloads"
stage = "GA"
Expand Down Expand Up @@ -121,7 +120,7 @@ resource "google_project_iam_custom_role" "state_iam" {
}

project = var.project_id
role_id = "${replace(title(var.resource_prefix), "-", "")}${title(local.environment_short)}StateIam${title(each.key)}"
role_id = "${replace(title(var.resource_prefix), "-", "")}StateIam${title(each.key)}"
title = "Proxy state IAM ${each.key} (${var.environment})"
description = "Read or manage the proxy-builder state bucket IAM policy"
stage = "GA"
Expand Down
5 changes: 2 additions & 3 deletions infra/modules/network/main.tf
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
locals {
environment_short = var.environment == "production" ? "prod" : "dev"
name_prefix = "${var.resource_prefix}-${local.environment_short}"
proxy_tag = "${local.name_prefix}-proxy"
name_prefix = var.resource_prefix
proxy_tag = "${local.name_prefix}-vm"
}

resource "google_compute_network" "proxy" {
Expand Down
9 changes: 4 additions & 5 deletions infra/modules/proxy_vm/main.tf
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
locals {
environment_short = var.environment == "production" ? "prod" : "dev"
name_prefix = "${var.resource_prefix}-${local.environment_short}"
startup_script = file("${path.module}/files/startup.sh")
startup_sha256 = sha256(local.startup_script)
name_prefix = var.resource_prefix
startup_script = file("${path.module}/files/startup.sh")
startup_sha256 = sha256(local.startup_script)
}

data "google_compute_image" "debian" {
Expand All @@ -12,7 +11,7 @@ data "google_compute_image" "debian" {

resource "google_service_account" "runtime" {
project = var.project_id
account_id = "${local.name_prefix}-proxy"
account_id = "${local.name_prefix}-vm-sa"
display_name = "Proxy runtime (${var.environment})"
description = "Runtime identity for the proxy VM; no project roles"
}
Expand Down
11 changes: 8 additions & 3 deletions infra/modules/proxy_vm/tests/contracts.tftest.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ run "startup_metadata_contract" {
machine_type = "e2-micro"
boot_disk_gb = 10
network_tier = "STANDARD"
subnetwork_self_link = "projects/example-project/regions/us-west1/subnetworks/proxy-dev"
subnetwork_self_link = "projects/example-project/regions/us-west1/subnetworks/proxy-subnet"
external_ip_address = "203.0.113.10"
network_tag = "proxy-dev-proxy"
network_tag = "proxy-vm"
labels = { application = "proxy-builder", environment = "development" }
deploy_service_account_email = "proxy-dev-gh-deploy@example-project.iam.gserviceaccount.com"
deploy_service_account_email = "proxy-gh-deploy@example-project.iam.gserviceaccount.com"
}

assert {
Expand All @@ -48,4 +48,9 @@ run "startup_metadata_contract" {
)
error_message = "deploy 身份必须只能 actAs 当前环境的 VM runtime service account。"
}

assert {
condition = google_service_account.runtime.account_id == "proxy-vm-sa"
error_message = "VM runtime service account 必须使用完整 resource_prefix 的 proxy-vm-sa 命名。"
}
}
5 changes: 2 additions & 3 deletions infra/modules/secret_runtime/main.tf
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
locals {
environment_short = var.environment == "production" ? "prod" : "dev"
name_prefix = "${var.resource_prefix}-${local.environment_short}"
name_prefix = var.resource_prefix

secrets = {
proxy_users = "${local.name_prefix}-proxy-users"
Expand All @@ -10,7 +9,7 @@ locals {

resource "google_service_account" "runtime" {
project = var.project_id
account_id = "${local.name_prefix}-subscription"
account_id = "${local.name_prefix}-subscription-sa"
display_name = "Subscription runtime (${var.environment})"
description = "Runtime identity for the Cloud Run subscription service"
}
Expand Down
3 changes: 1 addition & 2 deletions infra/modules/subscription_service/main.tf
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
locals {
environment_short = var.environment == "production" ? "prod" : "dev"
service_name = "${var.resource_prefix}-${local.environment_short}-subscription"
service_name = "${var.resource_prefix}-subscription"
}

resource "google_cloud_run_v2_service" "subscription" {
Expand Down
6 changes: 3 additions & 3 deletions infra/modules/subscription_service/tests/contracts.tftest.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,14 @@ run "startup_probe_contract" {
}

assert {
condition = google_logging_project_exclusion.subscription_requests.name == "proxy-dev-subscription-request-logs"
error_message = "日志排除名称必须隔离环境。"
condition = google_logging_project_exclusion.subscription_requests.name == "proxy-subscription-request-logs"
error_message = "日志排除名称必须从完整 resource_prefix 推导。"
}

assert {
condition = alltrue([
strcontains(google_logging_project_exclusion.subscription_requests.filter, "cloud_run_revision"),
strcontains(google_logging_project_exclusion.subscription_requests.filter, "proxy-dev-subscription"),
strcontains(google_logging_project_exclusion.subscription_requests.filter, "proxy-subscription"),
strcontains(google_logging_project_exclusion.subscription_requests.filter, "run.googleapis.com/requests"),
])
error_message = "日志排除必须只匹配当前 subscription 服务的 Cloud Run request log。"
Expand Down
8 changes: 4 additions & 4 deletions infra/stacks/bootstrap/tests/contracts.tftest.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,11 @@ run "identity_contract" {

assert {
condition = module.github_identity.environment_isolation_contract == {
pool_id = "${var.resource_prefix}-${var.environment == "production" ? "prod" : "dev"}-github"
pool_id = "${var.resource_prefix}-github"
service_account_ids = {
plan = "${var.resource_prefix}-${var.environment == "production" ? "prod" : "dev"}-gh-plan"
apply = "${var.resource_prefix}-${var.environment == "production" ? "prod" : "dev"}-gh-apply"
deploy = "${var.resource_prefix}-${var.environment == "production" ? "prod" : "dev"}-gh-deploy"
plan = "${var.resource_prefix}-gh-plan"
apply = "${var.resource_prefix}-gh-apply"
deploy = "${var.resource_prefix}-gh-deploy"
}
environment_wif_members = {
apply = "principal://iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/mock-pool/subject/repo:${var.github_repository}:environment:${var.environment}"
Expand Down
2 changes: 1 addition & 1 deletion infra/stacks/bootstrap/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ variable "resource_prefix" {

validation {
condition = can(regex("^[a-z][a-z0-9-]{1,12}[a-z0-9]$", var.resource_prefix))
error_message = "resource_prefix 必须是 3-14 位小写字母、数字或连字符。"
error_message = "resource_prefix 必须是完整资源名前缀,且为 3-14 位小写字母、数字或连字符。"
}
}

Expand Down
12 changes: 6 additions & 6 deletions infra/stacks/platform/tests/contracts.tftest.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ run "platform_contract" {
command = plan

assert {
condition = module.network.proxy_network_tag == "${var.resource_prefix}-${var.environment == "production" ? "prod" : "dev"}-proxy"
error_message = "proxy network tag 必须包含环境缩写。"
condition = module.network.proxy_network_tag == "${var.resource_prefix}-vm"
error_message = "proxy network tag 必须从完整 resource_prefix 派生。"
}

assert {
condition = module.artifact_registry.repository_id == "${var.resource_prefix}-${var.environment == "production" ? "prod" : "dev"}-images"
error_message = "Artifact Registry 名称必须包含环境缩写。"
condition = module.artifact_registry.repository_id == "${var.resource_prefix}-images"
error_message = "Artifact Registry 名称必须从完整 resource_prefix 派生。"
}

assert {
Expand Down Expand Up @@ -54,7 +54,7 @@ run "platform_contract" {


assert {
condition = output.subscription_request_log_exclusion_name == "${var.resource_prefix}-${var.environment == "production" ? "prod" : "dev"}-subscription-request-logs"
error_message = "platform 必须公开环境隔离的 subscription 请求日志排除名称。"
condition = output.subscription_request_log_exclusion_name == "${var.resource_prefix}-subscription-request-logs"
error_message = "platform 必须公开从完整 resource_prefix 派生的 subscription 请求日志排除名称。"
}
}
2 changes: 1 addition & 1 deletion infra/stacks/platform/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ variable "resource_prefix" {

validation {
condition = can(regex("^[a-z][a-z0-9-]{1,12}[a-z0-9]$", var.resource_prefix))
error_message = "resource_prefix 必须是 3-14 位小写字母、数字或连字符。"
error_message = "resource_prefix 必须是完整资源名前缀,且为 3-14 位小写字母、数字或连字符。"
}
}

Expand Down
7 changes: 7 additions & 0 deletions scripts/infra/test-guards.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,12 @@ production_file=$(infra::require_environment production)
development_project=$(infra::read_tfvar "$development_file" project_id)
production_project=$(infra::read_tfvar "$production_file" project_id)
[[ "$development_project" != "$production_project" ]] || infra::die "两个环境不能共用 GCP Project"
[[ "$development_project" == "kyson-proxy-dev" ]] || infra::die "development 必须使用 kyson-proxy-dev Project"
[[ "$(infra::read_tfvar "$development_file" resource_prefix)" == "proxy" ]] || infra::die "development 必须使用 proxy 完整资源名前缀"
[[ "$(infra::read_tfvar "$production_file" resource_prefix)" == "proxy" ]] || infra::die "production 必须使用 proxy 完整资源名前缀"

if rg -q 'environment_short' "${INFRA_ROOT}/infra/modules"; then
infra::die "资源模块不得从 environment 自动追加资源名后缀"
fi

printf '基础设施保护条件测试通过\n'