feat(gcp): composer + submodules refactor, hardening fixes, cross-cloud contract, serverless egress & CMEK - #233
Draft
micheledaddetta-databricks wants to merge 47 commits into
Draft
Conversation
Captures the brainstormed design for refactoring GCP-related Terraform examples and modules: a single composer module under modules/gcp/ that conditionally instantiates submodules (network, private-connectivity, account, dns), with each example becoming a thin caller that varies only scenario inputs. Co-authored-by: Isaac
31 tasks across 6 PRs implementing the design spec. Each task has explicit file paths, code blocks, validation commands, and commit messages. Self-review checks complete. Co-authored-by: Isaac
Adds modules/gcp/Makefile mirroring the modules/ pattern (discover sub-projects via */README.md) and updates modules/Makefile to recurse into the gcp/ subdir for terraform-docs generation. Co-authored-by: Isaac
Adds modules/gcp/network module supporting three VPC provenance modes: - vpc_source="create": Terraform creates spoke VPC + subnet + Cloud Router + NAT - vpc_source="existing": data-source lookup for pre-existing VPC + subnet - create_hub=true: adds hub VPC + subnet + bidirectional peering + optional shared-VPC host/service binding Outputs cover all spoke and hub identifiers. Test fixtures in tests/ cover create, existing, and create-with-hub scenarios. Co-authored-by: Isaac
Adds modules/gcp/private-connectivity with: - Dedicated PSC subnet in spoke VPC - Backend (SCC) PSC endpoint gated on enable_backend - Frontend PSC endpoint (spoke) gated on enable_frontend - Hub-side frontend PSC endpoint when hub present + frontend enabled - Regional PSC service-attachment maps for 14 GCP regions - Egress firewall stack gated on restrict_egress: deny-egress (priority 1100), allow Google APIs, allow Databricks control plane (to PSC IPs), optional allow managed Hive, hub ingress from spoke CIDR Test fixtures: full-isolated and no-egress. Co-authored-by: Isaac
Adds modules/gcp/account housing all databricks_mws_* resources: - databricks_mws_workspaces (always emitted) - databricks_mws_networks (when vpc_source != databricks_managed), with dynamic vpc_endpoints block populated when both PSC endpoint IDs are present - databricks_mws_vpc_endpoint frontend/backend/transit, each gated on its enable_* flag plus presence of the matching forwarding-rule name from private-connectivity - databricks_mws_private_access_settings (when private_access_only) Test fixtures: databricks-managed, byovpc, psc-with-pas. Co-authored-by: Isaac
Adds modules/gcp/dns for restricted-egress private DNS: - Hub zones: gcp.databricks.com, gcr.io, googleapis.com, pkg.dev, with workspace/psc-auth/dp records pointing at hub PSC IP - Spoke zone: gcp.databricks.com with workspace/dp records pointing at spoke frontend PSC IP, and tunnel record at backend PSC IP - workspace_dns_id extracted via regex from workspace_url Split into its own submodule (rather than colocated with PSC) because DNS depends on workspace_url which only exists after account creates the workspace; this keeps the composer's dependency graph linear. Test fixture: hub-and-spoke. Co-authored-by: Isaac
Top-level composer that orchestrates network, private-connectivity, account, and dns submodules through orthogonal feature flags: - vpc_source: databricks_managed | create | existing - private_link_frontend, private_link_backend, private_access_only, restricted_egress (each defaults false) - hub/spoke project + CIDR vars required only when restricted_egress Submodules instantiated conditionally: - network when vpc_source != databricks_managed - private-connectivity when any PrivateLink flag is true - account always - dns when restricted_egress is true Cross-variable preconditions (null_resource.preconditions) enforce six rules from the design spec, including: restricted_egress requires vpc_source=create + at least one PrivateLink flag + hub/CIDR vars; databricks_managed forbids any PrivateLink or restricted_egress. Random suffix declared once in the composer, passed to each submodule. Test fixtures: 4 positive (basic, byovpc, existing-vpc, psc-isolated) and 4 negative (each violating one precondition rule). Co-authored-by: Isaac
git mv only; no functional changes. Old path has a deprecation README pointing to the new location. Makefile updated for new depth. Co-authored-by: Isaac
git mv only; no functional changes. Old path has a deprecation README. Co-authored-by: Isaac
Generated README content for network, private-connectivity, account, dns, databricks-workspace, service-account, and unity-catalog via `make -C modules/gcp docs`. Includes an added README placeholder for unity-catalog (the original modules/gcp-unity-catalog had no README). Co-authored-by: Isaac
Migrates all four existing GCP examples to call modules/gcp/databricks-workspace: - gcp-basic: vpc_source="databricks_managed" - gcp-byovpc: vpc_source="create" with spoke_vpc_cidr + subnet_cidr - gcp-with-psc-exfiltration-protection: vpc_source="create" + all 4 PrivateLink/egress flags + Unity Catalog wired via modules/gcp/unity-catalog - gcp-sa-provisioning: source repointed from github URL to ../../modules/gcp/service-account Adds a NEW example examples/gcp-existing-vpc demonstrating vpc_source="existing" (data-source lookup of a pre-existing VPC + subnet). Variable name changes documented in each example's README migration table: subnet_ip_cidr_range -> subnet_cidr, pod/svc renames, subnet/router/nat_name dropped (composer derives), delegate_from dropped (SA-provisioning concern). State from old applies does NOT migrate cleanly because resource addresses differ. Re-apply on clean state. Co-authored-by: Isaac
Deletes:
- modules/gcp-workspace-basic, modules/gcp-workspace-byovpc,
modules/gcp-with-psc-exfiltration-protection (replaced by the composer
+ submodules under modules/gcp/)
- modules/gcp-sa-provisioning, modules/gcp-unity-catalog deprecation stubs
(relocated to modules/gcp/{service-account,unity-catalog} in PR 1's
foundation commit)
- examples/gcp-sa-provisionning (typo dir, only had a Makefile)
- examples/gcp-test-modules (only contained terraform.tfstate files)
All examples now point at modules/gcp/*. Stray terraform.tfstate* files
inside remaining example dirs are gitignored and remain on disk untouched.
Co-authored-by: Isaac
Examples section now lists all four migrated examples (gcp-basic, gcp-byovpc, gcp-with-psc-exfiltration-protection) plus the new gcp-existing-vpc and the corrected-spelling gcp-sa-provisioning. Modules section now lists the modules/gcp/ tree: the composer (databricks-workspace) and its five submodules (network, private-connectivity, account, dns, plus service-account and unity-catalog). Co-authored-by: Isaac
Plans a polish pass on the GCP modules and examples landed in PR #233: - fill ~70 missing variable descriptions - split large main.tf files by concern - standardize versions.tf placement - expand composer outputs, drop redundancy, rename misnomers - add critical-only region validations - add README usage examples Co-authored-by: Isaac
8 tasks corresponding to the 8 commits planned in the spec. Each task is self-contained with exact file paths, code blocks, validation commands, and commit messages. Co-authored-by: Isaac
… outputs Reorganizes network, account, dns, and databricks-workspace modules so each .tf file has one clear responsibility, and fixes the *_psc_fr_id misnomer for outputs/inputs that hold forwarding-rule names (not IDs). File splits (no behavioral change): - network/main.tf -> vpc.tf, subnets.tf, nat.tf, peering.tf, shared-vpc.tf, data.tf, locals.tf - account/main.tf -> workspace.tf, networks.tf, locals.tf - dns/hub.tf: workspace_dns_id local extracted to dns/locals.tf - databricks-workspace/main.tf -> main.tf (module blocks only), locals.tf, preconditions.tf, random.tf Renames (coordinated across outputs, inputs, wiring, and fixture): - private-connectivity outputs: frontend_psc_fr_id -> frontend_forwarding_rule_name, backend_psc_fr_id -> backend_forwarding_rule_name, hub_frontend_psc_fr_id -> hub_frontend_forwarding_rule_name - account inputs renamed to match - composer wiring and psc-with-pas fixture updated accordingly Co-authored-by: Isaac
…ion validations Three layered improvements to module surface area and documentation: Composer outputs (drop redundancy, add downstream-friendly outputs): - Drop vpc_id (was an alias of spoke_vpc_id) - Add 14 outputs: private_access_settings_id, frontend/backend/transit _endpoint_id, spoke_vpc_self_link, spoke_subnet_id/self_link, hub_vpc_self_link, nat_id, frontend/backend_psc_ip_spoke, frontend_psc_ip_hub, google_region - Replace try(module.network[0].*) with explicit local.databricks_managed / var.restricted_egress ternaries (same behavior, intent visible) - Add private_access_settings_id output on the account module - Update gcp-byovpc and gcp-with-psc examples to consume spoke_vpc_id instead of the removed vpc_id Variable descriptions (~70 added): - Every variable in private-connectivity, account, dns, and the databricks-workspace composer now has a description attribute describing purpose, format, nullability semantics, and required-when context - Gap-fill on service-account / unity-catalog where missing Region validation (clearer error messages): - private-connectivity validates google_region unconditionally against the 14-region list backing the regional PSC service-attachment maps - composer adds the same validation as a precondition, but gated on any private_link_* flag or restricted_egress being true; bad regions now fail plan with a clear message instead of a map-lookup error Co-authored-by: Isaac
File organization (versions.tf everywhere, no provider configs in modules):
- service-account module: init.tf -> versions.tf + data.tf
(no provider "google" {} block — caller's responsibility)
- unity-catalog module: terraform.tf -> versions.tf
- examples gcp-basic, gcp-byovpc, gcp-existing-vpc, gcp-sa-provisioning:
init.tf -> versions.tf + providers.tf (+ data.tf where the original
carried google_client_openid_userinfo / google_client_config)
- examples gcp-with-psc-exfiltration-protection: terraform.tf -> versions.tf
(providers.tf already existed)
README usage sections (terraform-google-modules convention):
- Each module README gets a ## Usage HCL block above the terraform-docs
marker showing a minimal calling example
- Submodule READMEs note "Typically called by modules/gcp/databricks-
workspace (the composer)"
- The composer README points to the four scenario examples
terraform-docs regen:
- Auto-generated README sections refreshed to reflect Task-2 renames,
Task-3 new outputs, Task-4 ~70 variable descriptions, and Task-5
region validation block
Co-authored-by: Isaac
…and compute features Co-authored-by: Isaac
…c with verified PSC/CMEK/provider-floor facts Co-authored-by: Isaac
The psc-isolated composer fixture failed 'terraform plan' with 4x Invalid count argument: counts depended on forwarding-rule names (which embed the random suffix) and on hub_vpc_id (a resource attribute). Counts now gate on enable_frontend/enable_backend/create_hub booleans. Co-authored-by: Isaac
Private zones attached only to the hub are invisible to the spoke across VPC peering, so restricted-egress spokes resolved Google APIs to public IPs that the deny-egress rule blocks - breaking cluster launch, GCS and GCR. Parity restore from the deleted module's dns-spoke.tf. Co-authored-by: Isaac
…stricted egress Parity restore from the deleted module's firewall-spoke.tf: EGRESS allow to the spoke CIDR (beats the 1100 deny) and INGRESS allow from the spoke CIDR (GCP ingress is implied-deny). Without both, node-to-node Spark traffic is blocked and clusters never form. Co-authored-by: Isaac
GCP's mws_networks.vpc_endpoints requires both dataplane_relay and rest_api, so single-sided PSC configs previously passed preconditions and broke at apply (null DNS rrdatas, missing control-plane allow rule, silently unattached endpoints). psc_subnet_cidr is now required for any PSC scenario, matching its documented contract. Co-authored-by: Isaac
The variable promised an internal regional-default lookup backed by an empty map, so omitting hive_metastore_ip silently dropped the legacy HMS allow rule. The rule is now explicitly opt-in and the docs say so; UC workspaces (the default) do not need it. Co-authored-by: Isaac
The exfiltration-protection design deliberately has no internet egress path; the refactor had reintroduced NAT unconditionally for created VPCs. Co-authored-by: Isaac
BYOVPC + Shared VPC (no restricted egress) was expressible in the legacy modules but the refactor gated the binding on create_hub. Co-authored-by: Isaac
… PAT, resource-level account_id - network.spoke_vpc_cidr, private-connectivity.hub_vpc_cidr and the dns self-link inputs were wired through but never consumed - the PSC example passed tags, implying an effect the composer doesn't have - the workspace module created a PAT the legacy module never did; tokens don't belong in module state - account_id on mws_* resources is deprecated in favor of the provider block databricks/databricks v1.120.0 still requires account_id on databricks_mws_workspaces and databricks_mws_networks; restored there only. Also dropped spoke_vpc_cidr from the network create-shared fixture, which referenced the removed variable but wasn't in the original file list. Co-authored-by: Isaac
…-refactor) Co-authored-by: Isaac
Modules declare floors (google >= 6.0, databricks >= 1.81.1); examples pin pessimistically (~> 6.17, ~> 1.81). Preconditions and the NAT dependency bridge use the built-in terraform_data instead of null_resource, dropping the null provider. Removes no-op firewall attributes. Co-authored-by: Isaac
The contract defines the slot by responsibility - everything that registers the workspace with the Databricks control plane (mws_* here, ARM on Azure). 'account' does not port to Azure, which has no account API layer. Co-authored-by: Isaac
…licies Classic-compute egress was hardened while serverless SQL in the same workspace could egress freely. serverless_egress_mode=restricted creates a deny-by-default databricks_account_network_policy bound to the workspace; the PSC example defaults to restricted. Requires Enterprise tier and databricks provider >= 1.81.1. Co-authored-by: Isaac
… storage Cloud KMS keys wire through databricks_mws_customer_managed_keys into the workspace at creation. No Terraform-side IAM grants: Databricks sets the key IAM policy during workspace creation, which is why the Terraform principal needs cloudkms.cryptoKeys.get/setIamPolicy on the key. Co-authored-by: Isaac
The GCP data plane runs on GCE; BYOVPC requires exactly one subnet and no secondary ranges. BREAKING: GKE-era configs must remove pod_cidr/svc_cidr. Co-authored-by: Isaac
…s module Cloud-neutral module (CSP, ESM, automatic cluster update, IP access lists) under the new modules/databricks/ namespace; wired into the PSC example via the workspace-level provider alias. CSP is irreversible and precondition- gated on ESM. Co-authored-by: Isaac
Freezes the module slots, composer interface, two-tier naming rule, file shape and testing standard that Azure/AWS refactors must follow; the GCP tree is the reference implementation. Co-authored-by: Isaac
Co-authored-by: Isaac
Co-authored-by: Isaac
…, cleaner precondition errors - PSC example's hive_metastore_ip is now optional (default null, module's exact description); dropped the stale required-var/empty-string tfvars wiring that rendered an invalid /32 CIDR at apply. Added a matching validation block to private-connectivity and databricks-workspace variables.tf (byte-identical) as a belt-and-braces guard. - Regenerated terraform-docs for all five GCP examples plus the two touched submodules; hand-fixed gcp-byovpc's migration table (pod/svc CIDR rows) and added prose to the PSC example README on the serverless_egress_mode default and optional CMEK/security-settings vars. - Guarded module.network[0]/module.private_connectivity[0] references in the databricks-workspace composer (main.tf, outputs.tf, new locals) so invalid vpc_source=databricks_managed + PSC/restricted_egress combinations surface only the precondition message instead of a raw "Invalid index" error. - Loosened the cross-cloud contract's fixture-coverage wording (§5) to match actual negative-fixture coverage. - Documented that CMEK keys must exist before plan in all three variable descriptions (byte-identical). - Added a "Running this example" section to the PSC example README covering ADC auth, PSC network reachability, and Enterprise-tier requirements. Co-authored-by: Isaac
… to documented permissions - service-account: drop the sa2 label and SA name (now <prefix>-ws-creator), remove the personal name from the IAM role title, delete the unused data sources and the broken custom_role_url output - BREAKING: the custom role now carries only the currently documented workspace-creator permissions (GKE-era container.*/storage.* removed; the data plane is GCE and Databricks service agents provision workspace infrastructure); the service account email changes - rename cryptic _fr / _dbx resource labels; unity-catalog bucket label reflects its role Co-authored-by: Isaac
…he cleaned module Co-authored-by: Isaac
Phase B sandbox finding: the provider builds this resource's API path from the resource-level account_id attribute (mws/resource_mws_vpc_endpoint.go), not the provider config. Without it, apply fails with a misleading 'Unable to load OAuth Config' error. validate cannot catch this - the attribute is schema-optional. Co-authored-by: Isaac
Design specs and implementation plans were session working documents; the durable design record is docs/cross-cloud-module-contract.md. Co-authored-by: Isaac
…oke/use_existing_spoke The hub side of the topology was explicit (create_hub) while the spoke side hid behind a vague create_vpc local. The pair now mirrors the contract's hub/spoke vocabulary; both flags gate more than a VPC (spoke: VPC+subnet+NAT, hub: VPC+subnet+peering). Locals only - no interface or state-address changes. Co-authored-by: Isaac
- enable_hub_spoke_peering (default true) gates the two VPC peerings in
AND with the hub flag, for topologies where hub-spoke connectivity is
provided by Shared VPC or an existing transit
- hub_vpc_source = create|existing mirrors the spoke's vpc_source: the
hub VPC/subnet can now be looked up instead of created
- create_hub renamed to enable_hub across the submodules: with lookup
support the flag enables the hub, it does not always create it
- BREAKING: composer vpc_source is now an object
{ spoke = databricks_managed|create|existing, hub = create|existing }
carrying both topology halves in one variable
Co-authored-by: Isaac
Co-authored-by: Isaac
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Full GCP modules refactor (design specs under
docs/superpowers/specs/). The repo previously shipped three duplicated GCP workspace modules; a change to a shared piece had to land in 2–3 places. This PR replaces that structure with a unified composer atmodules/gcp/databricks-workspacethat takes scenario flags and conditionally instantiates focused submodules — and establishes the cross-cloud module contract (docs/cross-cloud-module-contract.md) that future Azure/AWS refactors follow, with the GCP tree as reference implementation.Module layout
modules/gcp/network— VPC + subnet + router/NAT + peering + Shared-VPC; both topology halves sourced independently (vpc_source = { spoke = databricks_managed|create|existing, hub = create|existing }), hub-spoke peering optional (enable_hub_spoke_peering) for Shared-VPC/existing-transit designsmodules/gcp/private-connectivity— PSC subnet + endpoints + egress firewall stackmodules/gcp/workspace— everything registering the workspace with the Databricks control plane (databricks_mws_*, serverless egress policy, CMEK)modules/gcp/dns— private DNS zones for hub + spoke (gcp.databricks.com, googleapis.com, gcr.io, pkg.dev, incl. spoke peering zones)modules/gcp/databricks-workspace— the composermodules/databricks/security-settings— NEW cloud-neutral namespace: Compliance Security Profile, Enhanced Security Monitoring, automatic cluster update, IP access lists (workspace-level provider)modules/gcp/service-account,modules/gcp/unity-catalog— relocated, no functional changesDependency graph:
network → private-connectivity → workspace → dns.Hardening fixes (vs. the first iteration of this PR)
A deep review of the initial composer found 3 blockers and 10 further defects; all fixed here:
terraform planwith 4× "Invalid count argument" (counts depended on apply-time values). All counts now gate on static booleans.mws_networks.vpc_endpointsrequires bothdataplane_relayandrest_api; single-sided configs now fail plan with a clear message (flags stay independent in the cross-cloud contract for clouds that support single-sided).restricted_egress— the exfiltration-protection topology again has no internet egress path.hive_metastore_ipcontract (explicit opt-in, no phantom internal lookup); dead variables removed; module-created PAT removed;terraform_datapreconditions (null provider dropped); provider-constraint policy (modules: floorsgoogle >= 6.0,databricks >= 1.81.1; examples: pins~> 6.17,~> 1.81).New features
serverless_egress_mode = unmanaged|full|restricted,serverless_allowed_internet_destinations,serverless_allowed_storage_destinations,serverless_egress_enforcement = enforced|dry_run→databricks_account_network_policy+databricks_workspace_network_option. The PSC example defaults torestricted, closing its serverless exfiltration gap. Enterprise tier.cmek_managed_services_key_id,cmek_storage_key_id(Cloud KMS IDs) →databricks_mws_customer_managed_keyswired into the workspace. Terraform principal needscloudkms.cryptoKeys.get/setIamPolicyon the key. Enterprise tier; creation-time only.modules/databricks/security-settings: CSP (irreversible — precondition-gated on ESM), ESM, automatic cluster update, IP access lists.Reserved contract slot:
serverless-connectivity(NCC) — interface frozen from AWS/Azure semantics; GCP implementation lands when the feature reaches Public Preview.Breaking changes
vpc_sourceis now an object carrying both topology halves:{ spoke = databricks_managed|create|existing, hub = create|existing }(huboptional, only settable with a customer-managed spoke; defaults preserve prior behavior). String-formvpc_source = "create"configs must switch tovpc_source = { spoke = "create" }.private_link_frontendandprivate_link_backendmust be enabled together on GCP (previously single-sided configs failed at apply/runtime; now fail at plan).pod_cidr/svc_cidrremoved — the GCP data plane is GCE; BYOVPC needs exactly one subnet, no secondary ranges.token {}removed); create tokens explicitly if needed.modules/gcp/account→modules/gcp/workspace; composer submodule addresses change. State from old applies does not migrate cleanly — re-apply on clean state (each example README documents this).modules/gcp/service-account: the provisioning SA is now named<prefix>-ws-creator(email changes on re-apply), and the workspace-creator custom role is trimmed to the currently documented permission set — the GKE-eracontainer.*/storage.*permissions are gone (the data plane is GCE; Databricks service agents provision workspace infrastructure). The module output is nowservice_account_email.Composer preconditions
restricted_egress⇒vpc_source.spoke="create"restricted_egress⇒ at least oneprivate_link_*flagprivate_link_frontend == private_link_backendmws_networks.vpc_endpointsrequires both endpoint referencesprivate_link_*⇒psc_subnet_cidrsetrestricted_egress⇒hub_vpc_google_projectsetrestricted_egress+vpc_source.hub="create"⇒hub_vpc_cidrsetvpc_source.hub="existing"⇒existing_hub_vpc_name+existing_hub_subnet_namesetvpc_source.spoke="create"⇒spoke_vpc_cidr+subnet_cidrsetvpc_source.spoke="existing"⇒existing_vpc_name+existing_subnet_namesetvpc_source.spoke="databricks_managed"⇒ all PrivateLink/egress flags falsevpc_source.hubsettable only whenvpc_source.spokeis customer-managed (variable validation)serverless_egress_mode="restricted"Examples
examples/gcp-basicvpc_source="databricks_managed"examples/gcp-byovpcvpc_source="create"+ CIDRsexamples/gcp-existing-vpcvpc_source="existing"examples/gcp-with-psc-exfiltration-protectionrestricted+ optional CMEK + security-settings + UCTest plan
terraform validatepasses for every module and example (unity-catalog validates via its example call site —configuration_aliasesprevents standalone validate)terraform planwith the expected precondition messagesterraform apply: gcp-basic — workspace created, login page HTTP 200, destroyedterraform apply: gcp-byovpc — workspace on customer VPC, Databricks auto-createddb-<subnet>-ingress, destroyedterraform apply: PSC example (full topology, 54 resources) — verified in-cloud: workspace/dp/tunnel DNS → PSC IPs on both spoke and hub, googleapis.com → restricted range via peering zones, full firewall stack (deny-1100 + allows-1000 incl. intra-VPC), no Cloud NAT, PAS enforced ("Unauthorized network access" from public internet post-SSO), serverless egress policy RESTRICTED/ENFORCED with allow-listed destination honored and other destinations denied, cluster launched to RUNNING under restricted egress (validates the spoke DNS + intra-VPC firewall fixes end-to-end), destroyedPhase B surfaced one runtime-only bug that
terraform validatecannot catch, fixed in this PR:databricks_mws_vpc_endpointbuilds its API path from the resource-levelaccount_idattribute, so the attribute is required despite being schema-optional (omitting it fails with a misleading "Unable to load OAuth Config" error).Docs
docs/cross-cloud-module-contract.md