The Google Cloud package deploys the external-storage runtime with Pulumi. It creates Cloud Run, Cloud SQL, Google Cloud Storage, DNS, TLS, and edge resources.
The live run covered product operations, bounded reads, upgrades, rollback, API-secret rotation, state import, provider restore operations, and safe deletion. The release ledger still requires complete signed evidence and full product conformance.
Deployment architecture
| Product need | Google Cloud service |
|---|---|
| Application runtime | Cloud Run |
| Durable records | Cloud SQL for PostgreSQL 17 |
| Artifact and staged-upload bytes | Google Cloud Storage |
| Public edge | External Application Load Balancer and Cloud CDN |
| DNS and TLS | Cloud DNS and Certificate Manager |
| Application credentials | Secret Manager |
| Workload identity | Dedicated Cloud Run service account |
| Expired-upload cleanup | Cloud Run Job and Cloud Scheduler |
| Provisioning and lifecycle | Pulumi |
Cloud Run accepts traffic from the load balancer. The package disables the default Cloud Run URL.
The load balancer serves the application host and wildcard content hosts. Cloud CDN follows origin cache headers and bypasses every request with a Range header.
Cloud SQL uses private IP. The storage bucket blocks public access and uses uniform bucket access.
Prerequisites
Prepare these items before you create a stack:
- A dedicated Google Cloud project.
- A source checkout with Node.js 24.12 or later and pnpm 10.34.3.
- The Pulumi CLI and Google Cloud CLI.
- Application Default Credentials for the Pulumi process.
- An existing versioned GCS bucket or Pulumi Cloud for Pulumi state.
- A Pulumi secrets provider, such as one Cloud KMS key.
- A multi-architecture Artifact Server image pinned to one SHA-256 digest.
- Two separate registrable domains.
- One authoritative Cloud DNS managed zone for each domain.
The Pulumi identity must manage Service Usage, IAM, Cloud Run, Cloud SQL, GCS, Compute, DNS, Certificate Manager, Secret Manager, and Cloud Scheduler.
The identity must also change records in both existing Cloud DNS managed zones. The repository does not define a minimum operator IAM role.
The stack enables these Google Cloud APIs:
certificatemanager.googleapis.com
cloudscheduler.googleapis.com
compute.googleapis.com
dns.googleapis.com
run.googleapis.com
secretmanager.googleapis.com
servicenetworking.googleapis.com
sqladmin.googleapis.com
storage.googleapis.comIf you use Cloud KMS for Pulumi secrets, enable cloudkms.googleapis.com before you create the stack.
Prepare Pulumi
Install the repository dependencies:
pnpm install --frozen-lockfileAuthenticate Pulumi through Application Default Credentials. For an interactive shell, run these commands:
gcloud auth application-default login
gcloud config set project YOUR_GCP_PROJECT_IDSelect the external state backend:
pulumi login gs://YOUR_STATE_BUCKET/artifact-serverCreate the stack with its secrets provider:
pulumi stack init production \
--cwd deploy/pulumi/gcp \
--secrets-provider gcpkms://projects/YOUR_GCP_PROJECT_ID/locations/global/keyRings/YOUR_KEY_RING/cryptoKeys/YOUR_KEYIf the stack exists, select it instead:
pulumi stack select production --cwd deploy/pulumi/gcpEnable object versioning on the state bucket. The checked-in state recovery probe rejects a GCS backend without versioning.
Configure the stack
Create deploy/pulumi/gcp/Pulumi.production.yaml. Replace every example value before you run a preview.
config:
gcp:project: YOUR_GCP_PROJECT_ID
gcp:region: us-central1
artifact-server-gcp:applicationDomain: artifacts.example.com
artifact-server-gcp:backupRetentionDays: 14
artifact-server-gcp:bootstrapAdministratorEmail: admin@example.com
artifact-server-gcp:capacity:
cpu: 1
maximumInstances: 3
memoryMiB: 2048
minimumInstances: 2
artifact-server-gcp:contentDomain: artifact-content.example.net
artifact-server-gcp:databasePlan: high-availability
artifact-server-gcp:deletionProtection: true
artifact-server-gcp:dnsZoneIds:
application: artifacts-example-com
content: artifact-content-example-net
artifact-server-gcp:environment: production
artifact-server-gcp:imageReference: ghcr.io/plannotator/artifact-server@sha256:REPLACE_WITH_64_HEX_CHARACTERS
artifact-server-gcp:ingress: public
artifact-server-gcp:installationName: artifact-server
artifact-server-gcp:region: us-central1
artifact-server-gcp:secretsProvider: gcpkms://projects/YOUR_GCP_PROJECT_ID/locations/global/keyRings/YOUR_KEY_RING/cryptoKeys/YOUR_KEY
artifact-server-gcp:stackName: production
artifact-server-gcp:stateBackendUrl: gs://YOUR_STATE_BUCKET/artifact-serverdnsZoneIds.application and dnsZoneIds.content are managed-zone names. They are not project numbers or DNS zone numbers.
The application and content domains must have different registrable domains. Public ingress requires both DNS zone values.
Production configuration requires these values:
minimumInstancesis 1 or more.backupRetentionDaysis 14 through 35.deletionProtectionistrue.imageReferencecontains one immutable SHA-256 digest.
Optional configuration includes otlpEndpoint, requestLogSampleRate, and resourceTags. The request-log sample rate defaults to 0.01.
The GCP package accepts 0.25, 0.5, 1, 2, 4, or 8 CPUs. Memory must be 512 MiB through 32,768 MiB.
| Database plan | Cloud SQL availability | Tier | Initial disk | Connection budget |
|---|---|---|---|---|
small |
Zonal | db-custom-1-3840 |
20 GiB | 80 |
standard |
Zonal | db-custom-2-7680 |
50 GiB | 200 |
high-availability |
Regional | db-custom-4-15360 |
100 GiB | 500 |
The package reserves connections for a rolling replacement. It rejects a maximum instance count that exceeds the selected database budget.
Use an existing network
The package creates a VPC, a /24 regional subnet, and private service networking by default.
Set existingNetwork to use customer-owned network resources:
artifact-server-gcp:existingNetwork:
privateServiceConnection: projects/YOUR_GCP_PROJECT_ID/global/networks/YOUR_VPC/peerings/YOUR_SQL_PEERING
vpcEgressConfiguration: projects/YOUR_GCP_PROJECT_ID/regions/us-central1/subnetworks/YOUR_SUBNET
vpcName: projects/YOUR_GCP_PROJECT_ID/global/networks/YOUR_VPCMake sure that private services access works before deployment. Reserve enough subnet addresses for the maximum Cloud Run instance count.
Configure authentication and secrets
The stack generates an API token and database password. It stores both values in Secret Manager and omits them from stack outputs.
The Cloud Run service account receives access to generated secrets, the artifact bucket, and Cloud SQL. It uses Application Default Credentials for GCS.
The direct GCP package exposes typed WorkOS configuration. It does not expose typed generic OIDC configuration.
The stack can start without browser login configuration. Do not treat that mode as a team-ready installation.
To enable WorkOS, set all three values together:
artifact-server-gcp:workosApiKeySecretRef: YOUR_WORKOS_SECRET_ID
artifact-server-gcp:workosClientId: client_YOUR_CLIENT_ID
artifact-server-gcp:workosIssuer: https://YOUR_TENANT.authkit.appworkosApiKeySecretRef identifies an existing Secret Manager secret. It does not contain the WorkOS API key.
Grant the generated Cloud Run service account roles/secretmanager.secretAccessor on this secret. The Pulumi project does not create that grant.
Preview and deploy
Run the package verification before you create cloud resources:
pnpm --dir deploy/pulumi/gcp verifyReview the infrastructure preview:
pulumi preview --cwd deploy/pulumi/gcp --stack productionCreate or update the stack:
pulumi up --cwd deploy/pulumi/gcp --stack productionCertificate Manager creates DNS authorization records for both domains. Certificate issuance and DNS changes can delay the first healthy response.
Verify the deployment
Save the secret-free deployment output:
pulumi stack output deployment \
--cwd deploy/pulumi/gcp \
--stack production \
--json > deployment.jsonVerify the health and readiness endpoints:
curl --fail --silent --show-error "$(jq -r '.healthUrl' deployment.json)"
curl --fail --silent --show-error "$(jq -r '.readinessUrl' deployment.json)"Open the applicationUrl from deployment.json. If you configured WorkOS, verify browser sign-in.
Verify publication, private access, version reads, comments, and MCP discovery.
The output also contains the runtime, database, bucket, network, secret, state, identity, log, and support-manifest resource identifiers.
Run the repeatable product probe against an isolated stack:
PULUMI_BACKEND_URL=gs://YOUR_STATE_BUCKET/artifact-server \
ARTIFACT_SERVER_GCP_QUALIFICATION_STACK=gcp-qualification \
scripts/run-gcp-deployment-product-qualification.shBack up and restore
Cloud SQL keeps scheduled backups and point-in-time recovery. The selected plan retains 7 through 35 backups.
The GCS bucket enables object versioning and soft deletion. Its soft-delete period equals backupRetentionDays.
These independent controls do not form one coordinated recovery point. Quiesce writes before you back up both data stores.
Create one Cloud SQL backup. Then copy the complete artifact bucket. Record both provider results as one recovery set.
Restore the database into an empty private instance. Then restore the objects into an empty private bucket.
Verify installation IDs, project IDs, artifact IDs, version IDs, names, sizes, and checksums.
The live qualification restored Cloud SQL and copied 29 GCS objects with exact names, sizes, and CRC32C checksums. A later restored installation passed the integrity scanner.
The repository does not provide automated disaster recovery or cross-region failover. The release ledger still requires detailed application-level restore evidence.
Update and roll back
Create one coordinated recovery set before you update the stack.
Record the current image digest before an update:
pulumi config get imageReference \
--cwd deploy/pulumi/gcp \
--stack productionSet the new digest-pinned image:
pulumi config set imageReference REGISTRY/IMAGE@sha256:NEW_DIGEST \
--cwd deploy/pulumi/gcp \
--stack productionPreview the update. Then apply it:
pulumi preview --cwd deploy/pulumi/gcp --stack production
pulumi up --cwd deploy/pulumi/gcp --stack productionVerify the product after the update. The container applies database migrations before it starts the server.
If the image is schema-compatible, set the prior digest. Then run pulumi up again.
Do not use an incompatible image for rollback.
The live qualification preserved the installation, database, and bucket identities during its update and rollback.
Recover Pulumi state
Keep the GCS state bucket outside this stack. Enable object versioning.
Protect the bucket with its own access controls.
Pulumi state recovery depends on the selected backend. Follow the Pulumi recovery procedure for that backend.
The checked-in probe exports and imports one exact checkpoint. It then compares resource identities and requires a no-change preview.
Run this probe only against an isolated stack:
PULUMI_BACKEND_URL=gs://YOUR_STATE_BUCKET/artifact-server \
ARTIFACT_SERVER_GCP_QUALIFICATION_STACK=gcp-qualification \
scripts/run-gcp-state-recovery-qualification.shDelete a stack
Production configuration requires deletion protection. The package does not include a procedure that disables production protection.
For an unprotected non-production stack, run the native destroy command:
pulumi destroy \
--cwd deploy/pulumi/gcp \
--stack staging \
--yes \
--non-interactiveIf the artifact bucket stops deletion, back up its contents. Then remove every object version.
Run the same destroy command again.
Cloud Run can retain subnet addresses for one or two hours. If an address blocks deletion, wait for Google to release it.
Then run the same destroy command. Do not manually delete a serverless-ipv4-* address.
Cloud SQL can retain producer network resources for up to four days. The package abandons the connection while Google completes that cleanup.
The stack does not own the Pulumi state bucket or image repository. Delete those prerequisites in separate, explicit operations.
Known limits
- The package supports public ingress only.
- The package is pre-release.
- The direct package exposes WorkOS inputs but not generic OIDC inputs.
- An installation without WorkOS does not provide team browser login.
- The package does not automate disaster recovery or cross-region failover.
- The repository does not define a minimum operator IAM role.
- Production teardown requires an operator-owned deprotection procedure.
- Complete signed release evidence and full product conformance remain open.