@spine-event-engine/deployment-gce 2.0.0-snapshot.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +373 -0
  3. package/REFERENCE.md +98 -0
  4. package/dist/discovery/gce-node-discovery.d.ts +56 -0
  5. package/dist/discovery/gce-node-discovery.d.ts.map +1 -0
  6. package/dist/discovery/gce-node-discovery.js +78 -0
  7. package/dist/discovery/gce-node-discovery.js.map +1 -0
  8. package/dist/index.d.ts +7 -0
  9. package/dist/index.d.ts.map +1 -0
  10. package/dist/index.js +20 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/metadata/gce-metadata-service.d.ts +47 -0
  13. package/dist/metadata/gce-metadata-service.d.ts.map +1 -0
  14. package/dist/metadata/gce-metadata-service.js +65 -0
  15. package/dist/metadata/gce-metadata-service.js.map +1 -0
  16. package/dist/node/application-node.d.ts +35 -0
  17. package/dist/node/application-node.d.ts.map +1 -0
  18. package/dist/node/application-node.js +46 -0
  19. package/dist/node/application-node.js.map +1 -0
  20. package/dist/registrar/gce-registrar-log.d.ts +11 -0
  21. package/dist/registrar/gce-registrar-log.d.ts.map +1 -0
  22. package/dist/registrar/gce-registrar-log.js +44 -0
  23. package/dist/registrar/gce-registrar-log.js.map +1 -0
  24. package/dist/registrar/gce-registrar.d.ts +90 -0
  25. package/dist/registrar/gce-registrar.d.ts.map +1 -0
  26. package/dist/registrar/gce-registrar.js +167 -0
  27. package/dist/registrar/gce-registrar.js.map +1 -0
  28. package/dist/registrar/operations.d.ts +61 -0
  29. package/dist/registrar/operations.d.ts.map +1 -0
  30. package/dist/registrar/operations.js +96 -0
  31. package/dist/registrar/operations.js.map +1 -0
  32. package/dist/registry/gce-registry-reader.d.ts +23 -0
  33. package/dist/registry/gce-registry-reader.d.ts.map +1 -0
  34. package/dist/registry/gce-registry-reader.js +42 -0
  35. package/dist/registry/gce-registry-reader.js.map +1 -0
  36. package/dist/tsconfig.tsbuildinfo +1 -0
  37. package/examples/application.ts +208 -0
  38. package/examples/deployment-settings.ts +119 -0
  39. package/examples/gateway.ts +86 -0
  40. package/package.json +40 -0
  41. package/terraform/.terraform.lock.hcl +22 -0
  42. package/terraform/main.tf +402 -0
  43. package/terraform/terraform.tfvars.example +57 -0
  44. package/terraform/variables.tf +319 -0
  45. package/terraform/versions.tf +15 -0
package/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,373 @@
1
+ # Deploy a Spine TS application on GCE
2
+
3
+ `@spine-event-engine/deployment-gce` helps you run one Spine TS application
4
+ across several Google Compute Engine (GCE) virtual machines. It includes an
5
+ editable Terraform template and small TypeScript entrypoints. The template
6
+ keeps application nodes, the Gateway, and the simple delivery server private;
7
+ you provide the public TLS, authentication, and traffic-routing edge that fits
8
+ your organisation.
9
+
10
+ This experimental snapshot package is for operators who already own
11
+ the application images and their Google Cloud environment. Install its library
12
+ API when writing the application or Gateway entrypoints:
13
+
14
+ ```bash
15
+ pnpm add @spine-event-engine/deployment-gce@snapshot
16
+ ```
17
+
18
+ Copy the packaged Terraform directory into your deployment repository only
19
+ when you want its GCE reference topology; installing the library neither
20
+ creates cloud resources nor configures your application Gateway, authentication,
21
+ sessions, secrets, or storage.
22
+
23
+ For exact discovery, lease, and lifecycle behavior, read the
24
+ [deployment reference](REFERENCE.md).
25
+
26
+ ## Before you begin
27
+
28
+ You need an existing Google Cloud project, VPC network, regional subnetwork,
29
+ and a service account with only the permissions required by your images and
30
+ their application configuration. Install [Google Cloud CLI](https://cloud.google.com/sdk/docs/install)
31
+ and Terraform 1.6 or newer. Authenticate Terraform through Application Default
32
+ Credentials:
33
+
34
+ ```bash
35
+ gcloud auth application-default login
36
+ ```
37
+
38
+ Build and publish three immutable images: your application, one standalone
39
+ Gateway, and the in-memory simple delivery server. Use image digests such as
40
+ `@sha256:...`, not mutable tags. The template deliberately does not publish an
41
+ image, choose Datastore, MySQL, or another storage backend, create secrets, or
42
+ configure an identity provider.
43
+
44
+ ## First Terraform plan
45
+
46
+ Copy the packaged template into an operator-owned deployment repository. The
47
+ following commands assume its Terraform directory is your current directory;
48
+ create a values file and obtain a validated plan before wiring advanced Gateway
49
+ or registry behavior:
50
+
51
+ ```bash
52
+ cp terraform.tfvars.example terraform.tfvars
53
+ terraform init
54
+ terraform fmt -check
55
+ terraform validate
56
+ terraform plan -var-file=terraform.tfvars
57
+ ```
58
+
59
+ Replace every placeholder with your existing project, network, service account,
60
+ and immutable image digests before `plan`. A plan showing only private Spine
61
+ resources is the first success; review it before any `terraform apply`.
62
+
63
+ ## What this deployment creates
64
+
65
+ The application managed instance group (MIG) is regional and distributes
66
+ identical application nodes across your selected zones. A ready node registers
67
+ its private listener in a durable application registry. The single
68
+ Gateway reads the same registry every 10 seconds and routes commands, queries,
69
+ and subscriptions to the live nodes.
70
+
71
+ ```mermaid
72
+ flowchart LR
73
+ Edge["Your TLS and authentication edge"] --> Gateway["One private Gateway"]
74
+ Gateway --> Registry["Durable application node registry"]
75
+ Registry --> Gateway
76
+ Gateway --> AppA["Application node A"]
77
+ Gateway --> AppB["Application node B"]
78
+ AppA --> Delivery["One in-memory simple delivery server"]
79
+ AppB --> Delivery
80
+ ```
81
+
82
+ The template gives the Gateway and delivery server separate one-instance MIGs
83
+ and stable private addresses behind internal passthrough load balancers. This
84
+ is easy to inspect and lets an operator move them to separate failure or
85
+ resource boundaries. A smaller deployment may colocate the two processes, but
86
+ the simple delivery server remains in-memory: it is neither durable nor highly
87
+ available.
88
+
89
+ This is a fixed, single-Gateway topology. Multiple Gateways and Cloud Run are
90
+ outside the supported offering.
91
+
92
+ Every listener receives only private VPC traffic and health checks. Terraform
93
+ does not create an external IP, public load balancer, TLS certificate,
94
+ authentication provider, or Internet firewall rule.
95
+
96
+ ## Configure the template
97
+
98
+ Edit the existing `terraform.tfvars` copied for the first plan and replace every placeholder. Keep it outside source
99
+ control because it identifies your network and deployment, even though it
100
+ contains no secret values.
101
+
102
+ Set the project, region, two or more application zones, VPC/subnetwork,
103
+ least-privilege service-account email, and the three image digests. Set
104
+ `registry_namespace` and `registry_storage_reference` identically for
105
+ application and Gateway images. Each entrypoint passes that reference to its
106
+ application-supplied `registryStorage.storageFactoryFor(reference)` resolver. The
107
+ resolver chooses the shared durable `StorageFactory`; Terraform only transports
108
+ the reference and never chooses a storage engine.
109
+
110
+ `application_secret_reference` and `gateway_secret_reference` are identifiers
111
+ your images resolve through the selected configuration mechanism. They are passed
112
+ as environment values only. Terraform neither reads secret values nor writes
113
+ them into state.
114
+
115
+ Grant the VM service account `roles/artifactregistry.reader` on each Artifact
116
+ Registry repository that stores these images. The template uses the broad
117
+ `cloud-platform` OAuth scope so Google Cloud IAM can evaluate that role; the IAM
118
+ role, not the scope alone, authorizes image pulls.
119
+
120
+ Every image must use an Artifact Registry host such as
121
+ `us-docker.pkg.dev`. Each COS startup script extracts that exact host from its
122
+ configured immutable image name, creates a writable Docker configuration under
123
+ `/var/lib/spine-docker`, and runs `docker-credential-gcr configure-docker` for
124
+ that host before it pulls the image. The helper uses the attached VM service
125
+ account; no registry credential or key is placed in Terraform, metadata, or the
126
+ Docker command.
127
+
128
+ ## Connect your application entrypoints
129
+
130
+ The application image starts a managed application node. Its private VM port is
131
+ the Node Coordinator port; managed children are complete application replicas
132
+ on loopback and are never discoverable directly. The deployer must set both
133
+ `application_process_count` and `delivery_shard_count` in Terraform. They are
134
+ independent: the first selects Node processes, while the second is passed to
135
+ your context assembly for its explicit Delivery strategy.
136
+
137
+ Use the complete, tested
138
+ [`GceApplicationEntrypoint`](examples/application.ts) instead of copying only
139
+ its happy path. It starts the managed application, publishes the Coordinator
140
+ only after it is ready, withdraws that lease before shutdown, and preserves
141
+ startup or cleanup failures. Supply your complete child assembly through
142
+ `createServer`, and pass the same environment object when testing custom
143
+ deployment values. The framework-owned `SPINE_MANAGED_SERVER_CHILD` marker is
144
+ read from that environment so child executions do not publish separate leases.
145
+
146
+ The Gateway resolves the same storage reference and namespace through its
147
+ environment. It manages a GCE discovery lifecycle that refreshes the complete
148
+ live-node registry snapshot every 10 seconds, stops that schedule when the
149
+ browser server stops, and then closes its registry. Supply browser collaborators
150
+ in `browserOptions`. Choose one admission mode there: authenticated mode
151
+ supplies `sessions` and may supply named durable subscription bindings; public
152
+ mode supplies `publicAccess: true`, and the framework owns process-local
153
+ bindings. Public mode cannot supply bindings.
154
+
155
+ <!-- docs-snippet-path: packages/deployment-gce/examples/gateway.ts -->
156
+
157
+ ```ts
158
+ import { LeasedNodeRegistry } from "@spine-event-engine/deployment";
159
+ import { GceNodeDiscovery } from "@spine-event-engine/deployment-gce";
160
+ import { BrowserServer, type BrowserServerOptions } from "@spine-event-engine/server/browser";
161
+
162
+ type GatewayBrowserOptions = BrowserServerOptions extends infer Options
163
+ ? Options extends BrowserServerOptions
164
+ ? Omit<Options, "host" | "port" | "discovery">
165
+ : never
166
+ : never;
167
+
168
+ import {
169
+ GceDeploymentSettings,
170
+ type DeploymentEnvironment,
171
+ type RegistryStorageResolver,
172
+ } from "./deployment-settings.js";
173
+
174
+ export interface GatewayOptions {
175
+ readonly browser: GatewayBrowserOptions;
176
+ readonly registryStorage: RegistryStorageResolver;
177
+ }
178
+
179
+ export const GceGatewayEntrypoint = Object.freeze({
180
+ async run(
181
+ options: GatewayOptions,
182
+ environment: DeploymentEnvironment = process.env,
183
+ ): Promise<void> {
184
+ const registry = new LeasedNodeRegistry({
185
+ factory: options.registryStorage.storageFactoryFor(
186
+ GceDeploymentSettings.registryStorageReference(environment),
187
+ ),
188
+ namespace: GceDeploymentSettings.registryNamespace(environment),
189
+ });
190
+ const discovery = new GceNodeDiscovery({ registry });
191
+ await BrowserServer.run({
192
+ host: "0.0.0.0",
193
+ port: GceDeploymentSettings.port(environment, "PORT"),
194
+ ...options.browser,
195
+ discovery,
196
+ });
197
+ },
198
+ });
199
+ ```
200
+
201
+ The complete, packaged examples are
202
+ [`examples/application.ts`](examples/application.ts) and
203
+ [`examples/gateway.ts`](examples/gateway.ts). They deliberately keep business
204
+ contexts, storage configuration, identity, and (when using authenticated mode)
205
+ durable subscription bindings in your application code. The GCE entrypoint's returned handle withdraws its
206
+ Coordinator lease before stopping child replicas; abrupt VM loss remains covered
207
+ by the existing lease expiry. The managed child marker is framework-owned
208
+ process state: it makes the shared module assemble a child replica without
209
+ creating a competing VM lease.
210
+
211
+ ## Deploy
212
+
213
+ Initialize the provider, then make the first safe success a formatting,
214
+ validation, and plan review. Apply only the reviewed plan.
215
+
216
+ ```bash
217
+ terraform init
218
+ terraform fmt -check
219
+ terraform validate
220
+ terraform plan -out=tfplan
221
+ terraform apply tfplan
222
+ ```
223
+
224
+ The plan should contain one application MIG, one Gateway MIG, one delivery MIG,
225
+ and private load balancers only. The [reference](REFERENCE.md) records scaling,
226
+ replacement, rollback, registry, and image-pull limits before you change those
227
+ operations.
228
+
229
+ The first VM can take several minutes to start. Autohealing waits for the
230
+ configured startup delay (120 seconds by default) before treating a failed
231
+ application listener as unhealthy.
232
+
233
+ The template uses Container-Optimized OS and runs each image with a startup
234
+ script. It configures the exact Artifact Registry host with
235
+ `docker-credential-gcr`, then uses `docker run --rm --network host`. If a
236
+ container exits, Docker removes it and the listener health check fails; the MIG
237
+ can then repair that VM after the startup delay. Use
238
+ `journalctl -u google-startup-scripts.service` or the VM serial-console log to
239
+ diagnose credential-helper, startup-script, and container failures.
240
+
241
+ ## Verify the deployment
242
+
243
+ Check that GCE created the three groups and that all intended instances become
244
+ healthy:
245
+
246
+ ```bash
247
+ gcloud compute instance-groups managed list --regions=REGION
248
+ gcloud compute instance-groups managed list-instances spine-application --region=REGION
249
+ terraform output gateway_private_address
250
+ ```
251
+
252
+ From a trusted VPC client or your operator-managed edge, reach the returned
253
+ Gateway address. Post a command, query its Projection, and activate a durable
254
+ subscription. The Gateway may need one 10-second refresh interval after a
255
+ fresh application node becomes ready.
256
+
257
+ Each VM obtains a unique registration identity for its ready Coordinator,
258
+ renews its lease every 20 seconds, and leases it for 60 seconds. A graceful
259
+ entrypoint close withdraws that lease before it stops its managed children. A
260
+ crash may leave a row behind temporarily, but it is ignored after expiry;
261
+ healthy registrars perform finite cleanup.
262
+
263
+ ## Scale application nodes
264
+
265
+ With the default `autoscaling_enabled = false`, Terraform controls manual capacity:
266
+
267
+ ```bash
268
+ terraform apply -var='application_replicas=4'
269
+ terraform apply -var='application_replicas=0'
270
+ terraform apply -var='application_replicas=2'
271
+ ```
272
+
273
+ At zero nodes, the registry becomes empty after at most the 60-second lease
274
+ expiry. The Gateway remains alive but reports backend unavailability until a
275
+ node returns; it keeps refreshing every 10 seconds. If an autoscaler is already
276
+ enabled, first remove it in the same transition:
277
+
278
+ ```bash
279
+ terraform apply -var='autoscaling_enabled=false' -var='application_replicas=0'
280
+ ```
281
+
282
+ To let Compute Engine scale the same application version, set
283
+ `autoscaling_enabled = true`. For `autoscaling_signal = "cpu"`,
284
+ `autoscaling_target` is the CPU utilization target. For
285
+ `autoscaling_signal = "monitoring"`, choose the metric name, a filter that
286
+ selects only the intended resource and series, a metric target kind, and the
287
+ declared `per_instance` or `whole_group` scope. A per-instance filter must set
288
+ `resource.type = "gce_instance"`; a whole-group filter must select another
289
+ resource type, such as `global`. Terraform validates that relationship, then
290
+ omits the MIG size and GCE is the sole capacity owner.
291
+
292
+ CPU and per-instance metrics require a running VM, so they cannot scale from
293
+ zero. For scale-from-zero, use a `whole_group` Cloud Monitoring metric that is
294
+ produced while no application VM exists, and set
295
+ `autoscaling_min_replicas = 0`; otherwise use an operator action or schedule.
296
+ Internal passthrough load-balancer utilization is not a suitable autoscaling
297
+ signal for this topology.
298
+
299
+ ## Replace an application version
300
+
301
+ For a compatible change, publish a new immutable application digest, set it in
302
+ `terraform.tfvars`, and apply. The regional MIG performs a proactive rolling
303
+ replacement with one permitted surge instance for each selected application
304
+ zone and no planned unavailable instance. Old and new nodes can overlap, so
305
+ they must understand the same stored data and messages during the rollout.
306
+
307
+ For an incompatible business-logic or data change, first disable any enabled
308
+ autoscaler and set application capacity to zero. Wait until the old nodes exit,
309
+ apply the new image while capacity remains zero, then restore manual capacity
310
+ or explicitly enable the new autoscaler. This framework does not negotiate
311
+ compatibility. Pending Inbox work may execute under the new version, so make
312
+ the change safe for those messages before starting it.
313
+
314
+ ```bash
315
+ terraform apply -var='autoscaling_enabled=false' -var='application_replicas=0'
316
+ test -z "$(gcloud compute instance-groups managed list-instances spine-application --region=REGION --format='value(instance)')"
317
+ # Set the new application_image digest in terraform.tfvars.
318
+ terraform apply -var='autoscaling_enabled=false' -var='application_replicas=0'
319
+ terraform apply -var='autoscaling_enabled=false' -var='application_replicas=2'
320
+ ```
321
+
322
+ The Gateway normally stays in place during an application replacement. A
323
+ Gateway interruption disconnects browser clients. In authenticated mode, durable
324
+ subscription definitions survive only when the supplied named bindings use the
325
+ same persistent application storage. Public-mode definitions are process-local
326
+ and end with the Gateway process. Clients reconnect and issue an authoritative query.
327
+ Replacing `gateway_image` performs its explicit one-unavailable, zero-surge
328
+ singleton update and therefore causes that interruption. Replacing
329
+ `delivery_image` performs the same singleton update; the supplied in-memory
330
+ delivery server loses its state whenever its process stops.
331
+
332
+ ## Roll back
333
+
334
+ To roll back a compatible application image, restore the prior immutable digest
335
+ and run `terraform apply`. GCE creates the previous instance template and
336
+ performs the same rolling update. For an incompatible rollback, disable an
337
+ enabled autoscaler, reduce the application group to zero, wait for shutdown,
338
+ apply the prior image, then restore manual capacity or deliberately re-enable
339
+ autoscaling. Confirm the group is empty before applying the prior image:
340
+
341
+ ```bash
342
+ terraform apply -var='autoscaling_enabled=false' -var='application_replicas=0'
343
+ test -z "$(gcloud compute instance-groups managed list-instances spine-application --region=REGION --format='value(instance)')"
344
+ # Restore the prior application_image digest in terraform.tfvars.
345
+ terraform apply -var='autoscaling_enabled=false' -var='application_replicas=0'
346
+ terraform apply -var='autoscaling_enabled=false' -var='application_replicas=2'
347
+ ```
348
+
349
+ ## Troubleshooting
350
+
351
+ | Symptom | Check |
352
+ | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
353
+ | Gateway has no backend | Confirm the application group has healthy instances, registry references and namespace match, then allow a 10-second refresh. |
354
+ | A node remains listed after a crash | Wait up to 60 seconds for lease expiry; cleanup is finite and does not make expired nodes routable. |
355
+ | An application VM keeps restarting | Check the image starts its listener on `HOST=0.0.0.0` and `PORT`, and increase the startup delay only when its real startup needs it. |
356
+ | A client cannot reach the Gateway | Reach the private output from the VPC or configure a separate TLS/authentication edge; this module creates no public path. |
357
+ | Autoscaling does not wake zero nodes | CPU and per-instance metrics cannot observe an empty group; use a whole-group metric with `autoscaling_min_replicas = 0` or set a manual/scheduled minimum. |
358
+ | Delivery state disappeared | The supplied delivery server is in-memory and loses state whenever it is replaced or restarted. Do not describe it as durable or highly available. |
359
+ | A container fails during startup | Read `journalctl -u google-startup-scripts.service` or serial-console output; `docker run --rm` removes an exited container and health checks trigger MIG repair. |
360
+
361
+ ## Remove the deployment
362
+
363
+ Disable any enabled autoscaler and remove application capacity first when you
364
+ need a controlled domain shutdown, then destroy the infrastructure:
365
+
366
+ ```bash
367
+ terraform apply -var='autoscaling_enabled=false' -var='application_replicas=0'
368
+ terraform destroy
369
+ ```
370
+
371
+ `terraform destroy` removes only resources created by this template. It does not
372
+ delete the application registry, application storage, externally managed
373
+ secrets, images, VPC, or an operator-managed public edge.
package/REFERENCE.md ADDED
@@ -0,0 +1,98 @@
1
+ # GCE deployment reference
2
+
3
+ Read the [deployment guide](README.md) before using this exact template
4
+ contract. It supports one Gateway; Multiple-Gateway routing and Cloud Run are
5
+ outside this offering.
6
+
7
+ The library supplies discovery and entrypoint helpers. The editable Terraform
8
+ reference is a separate operator choice, and neither it nor this package owns
9
+ the public Gateway policy, authentication/session implementation, secrets, or
10
+ application storage.
11
+
12
+ ## Deployment template
13
+
14
+ The packaged `terraform` directory is an editable Google Compute Engine
15
+ reference topology. It requires existing private network/subnetwork, service
16
+ account, external configuration/secret identifiers, immutable image digests,
17
+ and application-selected durable storage. It creates a regional application
18
+ MIG, a one-instance Gateway MIG, and a one-instance in-memory delivery-server
19
+ MIG. Internal passthrough load balancers provide stable private Gateway and
20
+ delivery addresses. It intentionally creates no public edge, TLS certificate,
21
+ identity provider, storage engine, secret value, Cloud Run resource, or second
22
+ Gateway.
23
+
24
+ The application group has a 120-second default autohealing startup delay. Its
25
+ manual target size is present only when `autoscaling_enabled` is false; an
26
+ enabled regional autoscaler is the sole owner of capacity. CPU and
27
+ per-instance Monitoring metrics cannot revive a zero-instance group. A
28
+ whole-group Monitoring metric can, if the operator supplies one that continues
29
+ to exist at zero capacity.
30
+
31
+ The template uses Container-Optimized OS plus a `docker run` startup script. It
32
+ does not use the deprecated container startup agent or a
33
+ `gce-container-declaration` metadata value. Each immutable image must use an
34
+ Artifact Registry `*-docker.pkg.dev` host. The relevant COS startup script
35
+ extracts that host from the image, exports a writable
36
+ `HOME=/var/lib/spine-docker` and `DOCKER_CONFIG=$HOME/.docker`, then invokes
37
+ `docker-credential-gcr configure-docker --registries=<exact-host>` before its
38
+ Docker pull. This stores only credential-helper configuration; the helper uses
39
+ the attached service account, whose repository-level
40
+ `roles/artifactregistry.reader` and `cloud-platform` OAuth scope authorize the
41
+ pull.
42
+
43
+ ## Metadata and node identity
44
+
45
+ `GceMetadataService.read(signal)` requests project ID, zone, numeric instance
46
+ ID, and `network-interfaces/0/ip` from the GCE metadata service. Every request
47
+ uses `Metadata-Flavor: Google`; unsuccessful, empty, or malformed responses
48
+ reject, as does cancellation. `GceMetadataProvider` is injectable for tests or
49
+ an operator-managed metadata integration.
50
+
51
+ `GceApplicationNode.create(metadata, options)` returns the stable ID
52
+ `gce/<project>/<zone>/<numeric-instance-id>`. `port` must be a safe TCP port.
53
+ Without an override the endpoint is private-address HTTP; an explicit canonical
54
+ HTTP(S) endpoint and compatible TLS server name override it. No public address
55
+ is inferred. In a managed application entrypoint, this endpoint is the
56
+ node-local Coordinator listener. Managed child listeners are loopback-only and
57
+ are never published. The registrar persists this identity through the
58
+ deployment package's approved application-node lease record and typed node ID.
59
+
60
+ ## Registrar
61
+
62
+ `new GceRegistrar(options)` requires either a ready `node` or a metadata port.
63
+ When `node` and `metadata` are both omitted, it creates `GceMetadataService`
64
+ automatically. It creates a UUID registration identity unless `identity` is
65
+ supplied. The injected `scheduler`, `now`, and `deadlines` seams make timing
66
+ deterministic. `operationTimeoutMs` is a positive safe integer and defaults to
67
+ 20,000 milliseconds.
68
+
69
+ Call `start()` only after listener readiness. The initial record expires after
70
+ 60,000 milliseconds; scheduled renewal runs at 20,000-millisecond intervals.
71
+ Initial write failure or loss leaves the registrar unconfirmed. The next cycle
72
+ looks up the exact row: the same live identity confirms it, while absence or a
73
+ different identity permits one conditional register attempt. Renewal and
74
+ cleanup are serialized with this confirmation work.
75
+
76
+ `close()` fences future ticks, aborts and joins admitted cooperative work, and
77
+ conditionally removes only the matching registration identity. It must finish
78
+ before the listener network close. A deadline never detaches an operation: the
79
+ registrar waits for the admitted promise to settle before deletion.
80
+
81
+ ## Discovery
82
+
83
+ `GceRegistryReader.read(signal)` reads the full live registry snapshot at its
84
+ injected clock time, which defaults to `Date.now`. `GceNodeDiscovery` manages one
85
+ `LeasedNodeRegistry`, wraps that reader in `ScheduledNodeDiscovery`, and is the
86
+ Gateway-facing `NodeDiscovery`. Its production scheduler is optional and
87
+ unref'ed; expired rows are filtered immediately, so scale-to-zero produces an
88
+ empty membership snapshot. A later registrar makes its node discoverable and
89
+ also resumes bounded cleanup of abandoned expired rows. Registry-read failures
90
+ retain the Gateway's previous valid membership until a later refresh succeeds.
91
+
92
+ `watch(onSnapshot)` returns a stop operation. Calling that operation or
93
+ `close()` permanently stops future refresh scheduling, waits for any admitted
94
+ read, and then closes the registry it created. `close()` is idempotent: callers
95
+ share the same promise and the registry closes once. The owner still attempts
96
+ registry closure if stopping discovery fails. It rethrows one close failure;
97
+ when both operations fail, it rejects with an `AggregateError` containing both
98
+ causes. Do not separately close the registry passed to `GceNodeDiscovery`.