综合项目:异步订单处理平台

验证:未记录
适用版本
未记录
验证范围
尚未登记验证记录。

1. 目标与结构#

提交订单,异步落库并归档,再按归档数据生成分析报表。耗时取决于资源供应、权限配置与排障,不给出未经实测的完成时长。仅使用合成数据,SQL 等资源持续计费。

操作者 → ID token → Cloud Run API → Pub/Sub → 鉴权 push → Cloud Run Worker
                         ↓ GET ├→ Cloud SQL
                    Cloud Storage ←─────────────────────────┘ Secret Manager
                         ↓ 批量加载
                      BigQuery

GitHub OIDC → 发布 SA → Cloud Run revision
Cloud Build → Artifact Registry → 同一镜像的 api / worker 两种运行模式
日志 → Cloud Logging → 指标 / 告警 → Cloud Monitoring

Cloud Run 主线不需要自建 VPC;第 8 节把网络、VM、DNS、LB、GKE、Terraform 接入同一项目的演进过程。不要一开始同时运行三种计算平台。

前置:环境与账单、IAM,本机 Bash、Python 3、jq。以下从包含 lab.env 的实验目录运行。资源使用 orders-*,与单服务实验分开。

2. 资源与身份#

source lab.env
export PROJECT_NUMBER=$(gcloud projects describe "$PROJECT_ID" --format='value(projectNumber)')
export HUMAN_EMAIL=$(gcloud config get-value account)
export ORDERS_BUCKET="${PROJECT_ID}-orders"
export SOURCE_BUCKET="${PROJECT_ID}-orders-source"
export API_SA="orders-api@$PROJECT_ID.iam.gserviceaccount.com"
export WORKER_SA="orders-worker@$PROJECT_ID.iam.gserviceaccount.com"
export PUSH_SA="orders-push@$PROJECT_ID.iam.gserviceaccount.com"
export BUILD_SA="orders-build@$PROJECT_ID.iam.gserviceaccount.com"
export CALLER_SA="orders-caller@$PROJECT_ID.iam.gserviceaccount.com"
gcloud services enable run.googleapis.com artifactregistry.googleapis.com cloudbuild.googleapis.com storage.googleapis.com sqladmin.googleapis.com secretmanager.googleapis.com pubsub.googleapis.com bigquery.googleapis.com logging.googleapis.com monitoring.googleapis.com
for name in orders-api orders-worker orders-push orders-build orders-caller; do
  gcloud iam service-accounts create "$name"
done
# 操作者可挂载这四个 SA;caller 只用于 通过 impersonation 调用 。
for sa in "$API_SA" "$WORKER_SA" "$PUSH_SA" "$BUILD_SA"; do
  gcloud iam service-accounts add-iam-policy-binding "$sa" --member="user:$HUMAN_EMAIL" --role=roles/iam.serviceAccountUser
done
gcloud iam service-accounts add-iam-policy-binding "$CALLER_SA" --member="user:$HUMAN_EMAIL" --role=roles/iam.serviceAccountTokenCreator
gcloud storage buckets create "gs://$ORDERS_BUCKET" --location="$REGION" --uniform-bucket-level-access --public-access-prevention
gcloud storage buckets create "gs://$SOURCE_BUCKET" --location="$REGION" --uniform-bucket-level-access --public-access-prevention
gcloud artifacts repositories create orders-images --location="$REGION" --repository-format=docker
gcloud pubsub topics create orders-created
gcloud pubsub topics create orders-dead
gcloud pubsub subscriptions create orders-dead-inspect --topic=orders-dead

最小业务权限:API 只发布消息和查询归档,Worker 写数据库并创建归档,Push 只调用 Worker,Build 只读源码、写镜像和日志。

gcloud pubsub topics add-iam-policy-binding orders-created --member="serviceAccount:$API_SA" --role=roles/pubsub.publisher
gcloud storage buckets add-iam-policy-binding "gs://$ORDERS_BUCKET" --member="serviceAccount:$API_SA" --role=roles/storage.objectViewer
gcloud storage buckets add-iam-policy-binding "gs://$ORDERS_BUCKET" --member="serviceAccount:$WORKER_SA" --role=roles/storage.objectCreator
gcloud storage buckets add-iam-policy-binding "gs://$ORDERS_BUCKET" --member="serviceAccount:$WORKER_SA" --role=roles/storage.objectViewer
gcloud projects add-iam-policy-binding "$PROJECT_ID" --member="serviceAccount:$WORKER_SA" --role=roles/cloudsql.client
gcloud artifacts repositories add-iam-policy-binding orders-images --location="$REGION" --member="serviceAccount:$BUILD_SA" --role=roles/artifactregistry.writer
gcloud storage buckets add-iam-policy-binding "gs://$SOURCE_BUCKET" --member="serviceAccount:$BUILD_SA" --role=roles/storage.objectViewer
gcloud projects add-iam-policy-binding "$PROJECT_ID" --member="serviceAccount:$BUILD_SA" --role=roles/logging.logWriter

数据库与密钥#

gcloud sql instances create orders-pg --database-version=POSTGRES_16 --edition=ENTERPRISE --tier=db-f1-micro --region="$REGION" --storage-size=10GB --availability-type=zonal --backup-start-time=20:00
gcloud sql databases create orders --instance=orders-pg
DB_PASSWORD=$(python3 -c 'import secrets; print(secrets.token_urlsafe(32))')
gcloud sql users create orders --instance=orders-pg --password="$DB_PASSWORD"
printf '%s' "$DB_PASSWORD" | gcloud secrets create orders-db-password --data-file=- --replication-policy=automatic
unset DB_PASSWORD
gcloud secrets add-iam-policy-binding orders-db-password --member="serviceAccount:$WORKER_SA" --role=roles/secretmanager.secretAccessor
export CONNECTION_NAME=$(gcloud sql instances describe orders-pg --format='value(connectionName)')

实验用 Cloud SQL 创建的内置数据库用户默认权限较高,且示例自行建表;生产必须预建表,并将业务用户收敛为所需表上的 SELECT/INSERT 权限;具体步骤见 Cloud SQL 权限收敛。数据库用户权限

使用 Cloud SQL 公共 IP + Cloud Run 内置 Auth Proxy,不开放 authorized networks。Worker 需要 Cloud SQL Client 和数据库密码两层认证。生产私网接入需另建 VPC 路径;本实验不把公共 IP 的代理连接伪称为私网。Cloud Run 连接 SQL

3. 应用代码#

新建目录 orders-app,保存以下四个文件。金额以整数“分”为单位;订单 ID 使用 UUID。

app.py:

import base64
import json
import os
import uuid
from datetime import datetime, timezone

import psycopg
from flask import Flask, jsonify, request
from google.api_core.exceptions import NotFound, PreconditionFailed
from google.cloud import pubsub_v1, storage

app = Flask(__name__)
app.config['MAX_CONTENT_LENGTH'] = 65536
MODE = os.environ.get('MODE', 'api')
PROJECT = os.environ['PROJECT_ID']
BUCKET = os.environ['BUCKET']
store = storage.Client()
publisher = pubsub_v1.PublisherClient() if MODE == 'api' else None


def validate(value):
    if not isinstance(value, dict):
        raise ValueError('object required')
    order_id = str(uuid.UUID(str(value['id'])))
    amount = value['amount']
    if type(amount) is not int or not 0 < amount <= 100000000:
        raise ValueError('amount must be positive integer cents')
    return order_id, amount


@app.get('/healthz')
def health():
    return {'status': 'ok', 'mode': MODE}


@app.post('/orders')
def create():
    if MODE != 'api':
        return '', 404
    try:
        order_id, amount = validate(request.get_json())
    except (ValueError, KeyError, TypeError):
        return {'error': 'valid UUID id and integer amount required'}, 400
    event = {'id': order_id, 'amount': amount,
             'created_at': datetime.now(timezone.utc).isoformat()}
    topic = publisher.topic_path(PROJECT, os.environ['TOPIC'])
    publisher.publish(topic, json.dumps(event).encode()).result(timeout=20)
    print(json.dumps({'severity': 'INFO', 'event': 'accepted', 'order_id': order_id}), flush=True)
    return {'id': order_id, 'status': 'accepted'}, 202


@app.get('/orders/<order_id>')
def get_order(order_id):
    if MODE != 'api':
        return '', 404
    try:
        order_id = str(uuid.UUID(order_id))
    except ValueError:
        return {'error': 'invalid UUID'}, 400
    try:
        data = store.bucket(BUCKET).blob(f'orders/{order_id}.json').download_as_text()
        return jsonify(json.loads(data))
    except NotFound:
        return {'status': 'not_ready_or_unknown'}, 404


@app.post('/push')
def consume():
    if MODE != 'worker':
        return '', 404
    order_id = None
    try:
        envelope = request.get_json()
        event = json.loads(base64.b64decode(envelope['message']['data'], validate=True))
        order_id, amount = validate(event)
        created_at = datetime.fromisoformat(event['created_at'])
        if created_at.tzinfo is None:
            raise ValueError('timestamp needs timezone')
        # 教学简化:首次请求建表。生产由独立 migration 身份预建表。
        with psycopg.connect(host=os.environ['DB_HOST'], dbname='orders',
                             user=os.environ.get('DB_USER', 'orders'), password=os.environ['DB_PASSWORD'],
                             connect_timeout=10,
                             sslmode=os.environ.get('DB_SSLMODE', 'prefer')) as conn:
            with conn.cursor() as cur:
                if os.environ.get('AUTO_MIGRATE', 'true') == 'true':
                    cur.execute('''CREATE TABLE IF NOT EXISTS orders (
                        id uuid PRIMARY KEY, amount bigint NOT NULL,
                        created_at timestamptz NOT NULL)''')
                cur.execute('''INSERT INTO orders VALUES (%s, %s, %s)
                    ON CONFLICT (id) DO NOTHING''', (order_id, amount, created_at))
                cur.execute('SELECT amount, created_at FROM orders WHERE id=%s', (order_id,))
                saved_amount, saved_time = cur.fetchone()
                if saved_amount != amount:
                    raise ValueError('same id with different amount')
        # SQL 已提交。重投会重新尝试归档,不会因数据库已有行而跳过归档。
        record = {'id': order_id, 'amount': saved_amount, 'created_at': saved_time.isoformat()}
        blob = store.bucket(BUCKET).blob(f'orders/{order_id}.json')
        try:
            blob.upload_from_string(json.dumps(record) + '\n',
                                    content_type='application/json', if_generation_match=0)
        except PreconditionFailed:
            if json.loads(blob.download_as_text()) != record:
                raise ValueError('archive conflict')
        print(json.dumps({'severity': 'INFO', 'event': 'completed', 'order_id': order_id}), flush=True)
        return '', 204
    except Exception as error:
        # 不输出密码、完整事件或可能包含凭据的连接字符串。
        print(json.dumps({'severity': 'ERROR', 'event': 'processing_failed',
                          'order_id': order_id, 'error_type': type(error).__name__}), flush=True)
        return {'status': 'retry'}, 500

requirements.txt:

Flask>=3.1,<4
gunicorn>=23,<24
google-cloud-storage>=3,<4
google-cloud-pubsub>=2.28,<3
psycopg[binary]>=3.2,<4

Dockerfile:

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
ENV PYTHONUNBUFFERED=1
CMD ["sh", "-c", "exec gunicorn --bind 0.0.0.0:${PORT:-8080} --workers 1 --threads 4 --timeout 60 app:app"]

cloudbuild.yaml:

steps:
- name: gcr.io/cloud-builders/docker
  args: ['build', '-t', '${_IMAGE}', '.']
images: ['${_IMAGE}']
options:
  logging: CLOUD_LOGGING_ONLY

依赖范围方便练习;正式交付应锁定解析后的依赖和基础镜像 digest。另建 .gcloudignore 和 .dockerignore,排除 .git/、.venv/、lab.env、gha-creds-*.json,不上传凭据。

4. 构建与部署#

从包含 orders-app 的上级目录执行:

export IMAGE="${REGION}-docker.pkg.dev/${PROJECT_ID}/orders-images/app:v1"
gcloud builds submit orders-app --region="$REGION" --config=orders-app/cloudbuild.yaml --service-account="projects/$PROJECT_ID/serviceAccounts/$BUILD_SA" --gcs-source-staging-dir="gs://$SOURCE_BUCKET/source" --substitutions="_IMAGE=$IMAGE"
export DIGEST=$(gcloud artifacts docker images describe "$IMAGE" --format='value(image_summary.digest)')
export RELEASE_IMAGE="${REGION}-docker.pkg.dev/${PROJECT_ID}/orders-images/app@$DIGEST"
gcloud run deploy orders-worker --region="$REGION" --image="$RELEASE_IMAGE" --service-account="$WORKER_SA" --no-allow-unauthenticated --execution-environment=gen2 --add-cloudsql-instances="$CONNECTION_NAME" --set-secrets=DB_PASSWORD=orders-db-password:1 --set-env-vars="MODE=worker,PROJECT_ID=$PROJECT_ID,BUCKET=$ORDERS_BUCKET,DB_HOST=/cloudsql/$CONNECTION_NAME" --concurrency=4 --min=0 --max=2 --timeout=60
gcloud run deploy orders-api --region="$REGION" --image="$RELEASE_IMAGE" --service-account="$API_SA" --no-allow-unauthenticated --set-env-vars="MODE=api,PROJECT_ID=$PROJECT_ID,BUCKET=$ORDERS_BUCKET,TOPIC=orders-created" --min=0 --max=2 --timeout=60
export API_URL=$(gcloud run services describe orders-api --region="$REGION" --format='value(status.url)')
export WORKER_URL=$(gcloud run services describe orders-worker --region="$REGION" --format='value(status.url)')
gcloud run services add-iam-policy-binding orders-api --region="$REGION" --member="serviceAccount:$CALLER_SA" --role=roles/run.invoker
gcloud run services add-iam-policy-binding orders-worker --region="$REGION" --member="serviceAccount:$PUSH_SA" --role=roles/run.invoker

消息 push 与死信#

gcloud beta services identity create --service=pubsub.googleapis.com --project="$PROJECT_ID"
export PUBSUB_AGENT="service-${PROJECT_NUMBER}@gcp-sa-pubsub.iam.gserviceaccount.com"
# Pub/Sub service agent 可为指定 push SA 生成 OIDC token。
gcloud iam service-accounts add-iam-policy-binding "$PUSH_SA" --member="serviceAccount:$PUBSUB_AGENT" --role=roles/iam.serviceAccountTokenCreator
gcloud pubsub topics add-iam-policy-binding orders-dead --member="serviceAccount:$PUBSUB_AGENT" --role=roles/pubsub.publisher
gcloud pubsub subscriptions create orders-worker-sub --topic=orders-created --push-endpoint="$WORKER_URL/push" --push-auth-service-account="$PUSH_SA" --push-auth-token-audience="$WORKER_URL" --ack-deadline=60 --min-retry-delay=10s --max-retry-delay=60s --dead-letter-topic="projects/$PROJECT_ID/topics/orders-dead" --max-delivery-attempts=5
gcloud pubsub subscriptions add-iam-policy-binding orders-worker-sub --member="serviceAccount:$PUBSUB_AGENT" --role=roles/pubsub.subscriber

Worker 必须保留 IAM authentication,不授予 allUsers。 /push 是请求路径;Pub/Sub token 的 audience 使用服务基础 URL。Push 认证

5. 端到端验收#

TOKEN=$(gcloud auth print-identity-token --impersonate-service-account="$CALLER_SA" --audiences="$API_URL")
export ORDER_ID=$(python3 -c 'import uuid; print(uuid.uuid4())')
curl --fail-with-body -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d "{\"id\":\"$ORDER_ID\",\"amount\":1990}" "$API_URL/orders"
# 等待处理,再查询;未完成时 404,稍后重试。
curl --fail-with-body -H "Authorization: Bearer $TOKEN" "$API_URL/orders/$ORDER_ID"
# 重投相同 id 与金额
curl --fail-with-body -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d "{\"id\":\"$ORDER_ID\",\"amount\":1990}" "$API_URL/orders"
unset TOKEN
gcloud storage ls "gs://$ORDERS_BUCKET/orders/$ORDER_ID.json"
gcloud logging read 'resource.type="cloud_run_revision" AND jsonPayload.order_id="'"$ORDER_ID"'"' --freshness=1h --limit=20 --format=json

通过 Cloud SQL的 Auth Proxy 方法连接 orders-pg,使用 orders 用户和数据库;从 Secret Manager 取得实验密码时不要保存到笔记。执行:

SELECT id, count(*) FROM orders GROUP BY id;

验收:同一 ID 仅一行、一个归档对象;匿名访问被拒绝。相同 ID 不同金额是业务冲突,worker 拒绝并最终进入死信;HTTP 202 不承诺该冲突已被同步识别。

故障演练:撤销 Worker 的 storage.objectCreator,提交新 ID,确认 SQL 已提交而归档失败;在转入死信前恢复权限,重投后补齐对象。若已进入死信,用原 ID/金额重新提交,不生成新 ID。

这不是跨 SQL 与 Storage 的原子事务;它依赖持久消息、幂等写入和失败重试。消息过期、死信无人处理仍会导致业务未完成,生产应配合对账与 outbox 等机制。

6. 分析与可观测性#

bq --location="$REGION" mk --dataset "$PROJECT_ID:orders_analytics"
# replace 让重复全量加载不产生重复报表行;仅用于该小型实验表。
bq --location="$REGION" load --replace --source_format=NEWLINE_DELIMITED_JSON --time_partitioning_field=created_at "$PROJECT_ID:orders_analytics.orders" "gs://$ORDERS_BUCKET/orders/*.json" id:STRING,amount:INTEGER,created_at:TIMESTAMP
bq --location="$REGION" query --use_legacy_sql=false --maximum_bytes_billed=100000000 "SELECT COUNT(*) orders, SUM(amount) cents FROM \`$PROJECT_ID.orders_analytics.orders\`"
gcloud logging metrics create orders_failures --log-filter='resource.type="cloud_run_revision" AND resource.labels.service_name="orders-worker" AND jsonPayload.event="processing_failed"'

按 Monitoring创建告警,把 metric 改成 logging.googleapis.com/user/orders_failures,resource 改成 cloud_run_revision,displayName 改为 orders-failures。先创建指标与告警,再重复一次失败实验,确认通知送达。

面板包含 API 5xx、worker 5xx、处理延迟、未 ACK 消息数量、死信消息及 SQL 连接数。保存一次成功和一次失败的订单 ID 作为排障样本。

7. 发布与回滚#

修改 /healthz 增加版本字段,用同一构建命令推送 app:v2,查询它的 digest 并更新 RELEASE_IMAGE。不要只改标签字符串后继续使用 v1 digest。

gcloud run services describe orders-api --region="$REGION" --format='yaml(status.traffic)'
# Copy the currently serving 100% revision; latestReadyRevisionName can be an unused candidate.
export OLD_REV='REPLACE_CURRENT_SERVING_REVISION'
gcloud run deploy orders-api --region="$REGION" --image="$RELEASE_IMAGE" --no-traffic --tag=candidate
gcloud run services describe orders-api --region="$REGION" --format='yaml(status.traffic)'

对输出的 candidate URL 调用 /healthz,ID token audience 仍用 API_URL;检查候选 revision 的日志。验证后:

gcloud run services update-traffic orders-api --region="$REGION" --to-latest

通过正式 URL 验证新版本并完成观察后,再单独执行回滚演练;不要把切流与回滚作为一段命令连续运行。若之前存在分流,恢复原有完整配置:

gcloud run services update-traffic orders-api --region="$REGION" --to-revisions="$OLD_REV=100"

将 无密钥 CI接入本项目时,信任池配置保持相同限制,将授权和工作流中的 lab-hello 改为 orders-api、lab-run 改为 orders-api SA、lab-images 改为 orders-images。部署镜像使用审核过的 digest;Build SA 与 CI 发布 SA 保持分离。执行一次 main 发布、一次其他分支拒绝和一次回滚。

8. 将其他服务接入项目#

按顺序演进,每一步都先验收再继续。第 8 节引用的单服务实验若已清理,先重新创建其前置资源。

阶段 在本项目中的用途 操作与验收
VPC / NAT / VM / DNS 管理与诊断环境 完成对应实验,VM 通过 NAT 出网,admin.lab.internal 解析到 VM;应用不需要经管理 VM 转发
LB / DNS / TLS 给 API 增加统一 HTTPS 入口 下方入口适配,保留 Worker 鉴权,验证外部直连 run.app 被阻止
GKE 同一 API 容器迁移 下方部署方式;Worker 继续在 Cloud Run,避免重复维护数据库接入
Terraform 把已有归档桶纳入变更管理 下方 import;计划必须无意外重建
SQL 恢复 验证数据可恢复 先按 SQL 篇用独立 lab-pg 演练,禁止在订单处理期间覆盖 orders-pg

LB 入口适配#

复用 负载均衡篇创建 NEG → backend → URL map → HTTPS proxy,但 NEG 指向 orders-api。API ingress 设为 internal-and-cloud-load-balancing,跳过该篇的 allUsers 授权。LB 不自动代替客户端做身份认证。

gcloud run services update orders-api --region="$REGION" --ingress=internal-and-cloud-load-balancing
TOKEN=$(gcloud auth print-identity-token --impersonate-service-account="$CALLER_SA" --audiences="$API_URL")
curl --fail -H "Authorization: Bearer $TOKEN" "https://$DOMAIN/healthz"
unset TOKEN

token 使用默认 API URL 作为 audience;不要把 token 发到前面的 HTTP 演示入口。服务保持 IAM 私有,浏览器面向终端用户的登录需另设计 IAP 或应用认证。

GKE 迁移 API#

按 GKE建立集群和 lab/app Kubernetes SA。节点 SA 额外获得 orders-images 的 Artifact Registry Reader;工作负载 Principal 获得 orders-created 的 Pub/Sub Publisher 与订单桶的 Object Viewer。

此段 Python 需要 PyYAML,先在本地虚拟环境安装 pip install PyYAML。

export PROJECT_NUMBER=$(gcloud projects describe "$PROJECT_ID" --format='value(projectNumber)')
export KSA_PRINCIPAL="principal://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/${PROJECT_ID}.svc.id.goog/subject/ns/lab/sa/app"
gcloud artifacts repositories add-iam-policy-binding orders-images --location="$REGION" --member="serviceAccount:lab-gke-node@$PROJECT_ID.iam.gserviceaccount.com" --role=roles/artifactregistry.reader
gcloud pubsub topics add-iam-policy-binding orders-created --member="$KSA_PRINCIPAL" --role=roles/pubsub.publisher
gcloud storage buckets add-iam-policy-binding "gs://$ORDERS_BUCKET" --member="$KSA_PRINCIPAL" --role=roles/storage.objectViewer
kubectl -n lab create deployment orders-api --image="$RELEASE_IMAGE" --dry-run=client -o yaml > orders-gke.yaml
# 先生成清单再启动,避免应用缺环境变量启动失败。
python3 - <<'GKEPY'
import os, yaml
path = 'orders-gke.yaml'
data = yaml.safe_load(open(path))
spec = data['spec']['template']['spec']
spec['serviceAccountName'] = 'app'
spec['containers'][0]['env'] = [{'name': k, 'value': v} for k, v in {
    'MODE': 'api', 'PROJECT_ID': os.environ['PROJECT_ID'],
    'BUCKET': os.environ['ORDERS_BUCKET'], 'TOPIC': 'orders-created'}.items()]
spec['containers'][0]['resources'] = {'requests': {'cpu': '250m', 'memory': '512Mi'},
                                    'limits': {'cpu': '1', 'memory': '512Mi'}}
with open(path, 'w') as f:
    yaml.safe_dump(data, f)
GKEPY
kubectl apply -f orders-gke.yaml
kubectl -n lab rollout status deployment/orders-api --timeout=600s
kubectl -n lab port-forward deployment/orders-api 8081:8080

另一终端向 http://127.0.0.1:8081/orders 提交新 UUID 订单,随后 GET,结果仍由原 Pub/Sub → Worker → SQL/Storage 链路生成。端口转发依赖 Kubernetes 权限;不要直接公开这个未实现应用认证的 API Pod。

Terraform 接管归档桶#

使用 Terraform的独立目录与远程 state,将 google_storage_bucket.archive.name 改为 ${var.project_id}-orders,location/labels 等与现有桶对齐。此处跳过该篇创建新桶的 apply,改为:

terraform import google_storage_bucket.archive "$ORDERS_BUCKET"
terraform plan

验收:没有替换或删除桶,只存在你明确计划的属性变更。后续所有桶变更通过 Terraform;不要一边用 CLI 修改,一边忽略 drift。继续将 topic、运行 SA 和服务逐步写入模块时,也遵循先配置、再 import、再审查 plan。

9. 继续接入常见生产服务#

项目进阶整合将公共 DNS、证书、CDN、WAF、定时任务、任务队列、事件路由、私网 SQL、缓存与查询投影串入本项目,包含依赖、验收与清理。员工后台登录另见 IAP,轻量函数入口见 Cloud Run functions。

10. 清理与交付#

保留:架构图、权限表、镜像 digest、SQL/对象/报表结果、告警、重投和回滚证据。

先删除 CI 工作流与其授权;如做过扩展,先清理 LB/DNS、GKE/VM/NAT,再删除主线:

gcloud pubsub subscriptions delete orders-worker-sub orders-dead-inspect
gcloud run services delete orders-api --region="$REGION"
gcloud run services delete orders-worker --region="$REGION"
gcloud pubsub topics delete orders-created orders-dead
gcloud sql instances delete orders-pg
gcloud secrets delete orders-db-password
bq rm -r -f -d "$PROJECT_ID:orders_analytics"
gcloud artifacts repositories delete orders-images --location="$REGION"
gcloud storage rm --recursive "gs://$SOURCE_BUCKET"

未接管 Terraform 时 gcloud storage rm --recursive "gs://$ORDERS_BUCKET";已接管时先删除已确认无保留需求的对象,再审查 Terraform destroy plan 删除空桶,最后处理 state 桶。检查 soft delete 与 SQL 保留备份的费用。

在 Monitoring 删除 orders-failures 告警,再删除日志指标:

gcloud logging metrics delete orders_failures
gcloud projects remove-iam-policy-binding "$PROJECT_ID" --member="serviceAccount:$WORKER_SA" --role=roles/cloudsql.client
gcloud projects remove-iam-policy-binding "$PROJECT_ID" --member="serviceAccount:$BUILD_SA" --role=roles/logging.logWriter
for sa in "$API_SA" "$WORKER_SA" "$PUSH_SA" "$BUILD_SA" "$CALLER_SA"; do
  gcloud iam service-accounts delete "$sa"
done

最后核对账单和资源清单。完成全课程且不保留任何实验时,可按环境篇删除整个专用项目。

生产场景参考工程#

继续 企业项目:包含业务源码、私网区域 HA SQL、身份分离和验收脚本;完整云端集成与恢复结果仍需实际演练。本篇保留逐服务实验定位。

搜索文档

输入关键词,搜索所有文档。

↑ ↓ 选择 · Enter 打开 · Esc 关闭