@veltrixsecops/app-sdk 3.1.0 → 3.5.0
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.
- package/README.md +1 -1
- package/dist/byol/index.cjs +3802 -0
- package/dist/byol/index.cjs.map +1 -0
- package/dist/byol/index.d.cts +578 -0
- package/dist/byol/index.d.ts +578 -0
- package/dist/byol/index.js +2587 -0
- package/dist/byol/index.js.map +1 -0
- package/dist/chunk-3WK4EVQ3.js +842 -0
- package/dist/chunk-3WK4EVQ3.js.map +1 -0
- package/dist/{chunk-6KUTYJW5.js → chunk-HIL6RIDG.js} +107 -3
- package/dist/chunk-HIL6RIDG.js.map +1 -0
- package/dist/{chunk-VWFTOFTI.js → chunk-Y6XBNZGR.js} +8 -2
- package/dist/chunk-Y6XBNZGR.js.map +1 -0
- package/dist/client/index.cjs +112 -2
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.cts +127 -6
- package/dist/client/index.d.ts +127 -6
- package/dist/client/index.js +13 -1
- package/dist/connections/index.cjs +1787 -0
- package/dist/connections/index.cjs.map +1 -0
- package/dist/connections/index.d.cts +78 -0
- package/dist/connections/index.d.ts +78 -0
- package/dist/connections/index.js +754 -0
- package/dist/connections/index.js.map +1 -0
- package/dist/hooks/index.cjs +7 -0
- package/dist/hooks/index.cjs.map +1 -1
- package/dist/hooks/index.d.cts +24 -3
- package/dist/hooks/index.d.ts +24 -3
- package/dist/hooks/index.js +7 -1
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.cjs +7 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/opentofu/index.cjs +138 -0
- package/dist/opentofu/index.cjs.map +1 -0
- package/dist/opentofu/index.d.cts +131 -0
- package/dist/opentofu/index.d.ts +131 -0
- package/dist/opentofu/index.js +109 -0
- package/dist/opentofu/index.js.map +1 -0
- package/dist/pipeline/index.cjs +7 -0
- package/dist/pipeline/index.cjs.map +1 -1
- package/dist/pipeline/index.d.cts +9 -2
- package/dist/pipeline/index.d.ts +9 -2
- package/dist/pipeline/index.js +3 -1
- package/dist/pipeline-beeT8dje.d.cts +408 -0
- package/dist/pipeline-beeT8dje.d.ts +408 -0
- package/dist/ui/index.cjs +248 -0
- package/dist/ui/index.cjs.map +1 -1
- package/dist/ui/index.d.cts +238 -1
- package/dist/ui/index.d.ts +238 -1
- package/dist/ui/index.js +39 -573
- package/dist/ui/index.js.map +1 -1
- package/dist/{use-app-context-SEU1tyZd.d.cts → use-app-context-Byv_fam0.d.cts} +241 -4
- package/dist/{use-app-context-SEU1tyZd.d.ts → use-app-context-Byv_fam0.d.ts} +241 -4
- package/opentofu/README.md +114 -0
- package/opentofu/modules/aws/main.tf +1008 -0
- package/opentofu/modules/aws/outputs.tf +90 -0
- package/opentofu/modules/aws/variables.tf +437 -0
- package/opentofu/modules/aws/versions.tf +23 -0
- package/opentofu/modules/azure/main.tf +920 -0
- package/opentofu/modules/azure/outputs.tf +72 -0
- package/opentofu/modules/azure/variables.tf +498 -0
- package/opentofu/modules/azure/versions.tf +27 -0
- package/opentofu/modules/gcp/main.tf +645 -0
- package/opentofu/modules/gcp/outputs.tf +73 -0
- package/opentofu/modules/gcp/variables.tf +428 -0
- package/opentofu/modules/gcp/versions.tf +27 -0
- package/opentofu/modules/hetzner/main.tf +432 -0
- package/opentofu/modules/hetzner/outputs.tf +72 -0
- package/opentofu/modules/hetzner/variables.tf +423 -0
- package/opentofu/modules/hetzner/versions.tf +26 -0
- package/package.json +33 -1
- package/dist/chunk-6KUTYJW5.js.map +0 -1
- package/dist/chunk-VWFTOFTI.js.map +0 -1
- package/dist/pipeline-A-gSsPRR.d.cts +0 -189
- package/dist/pipeline-A-gSsPRR.d.ts +0 -189
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# GCP environment module — generic, tool-agnostic. A faithful translation of
|
|
3
|
+
# sdk/opentofu/modules/aws (the reference), honoring the SAME variable + output
|
|
4
|
+
# contract. Driven by an app's rendered InfraSpec; NOTHING here is tool-specific.
|
|
5
|
+
#
|
|
6
|
+
# Network mode (worker-set deployment var, NOT app InfraSpec):
|
|
7
|
+
# shared — Veltrix-hosted: data-source the shared network + create ONE
|
|
8
|
+
# per-stack subnetwork (subnet_cidr) in it.
|
|
9
|
+
# dedicated — BYOC: CREATE a fresh custom-mode network + a subnetwork (vpc_cidr)
|
|
10
|
+
# + Cloud Router + Cloud NAT for private egress.
|
|
11
|
+
# existing — BYOC: data-source a customer network + create a subnetwork in it.
|
|
12
|
+
# DNS mode: managed (in-project public A record → LB global IP + a Google-managed
|
|
13
|
+
# SSL cert) / delegated (worker does the cross-account public DNS; module uses
|
|
14
|
+
# certificate_arn) / private-only.
|
|
15
|
+
#
|
|
16
|
+
# GCP↔AWS mapping notes:
|
|
17
|
+
# * SG-to-SG least privilege → a per-stack NETWORK TAG (local.stack_tag) applied
|
|
18
|
+
# to every instance, plus google_compute_firewall rules keyed by (rule,source).
|
|
19
|
+
# "self" => source_tags = [stack_tag]; "admin" => admin_cidr; "alb" => Google's
|
|
20
|
+
# LB health-check + proxy ranges. GCP has no per-instance security group.
|
|
21
|
+
# * ALB + WAFv2 → EXTERNAL_MANAGED global HTTPS LB (health check + unmanaged
|
|
22
|
+
# instance group + backend service + url map + HTTPS proxy + global forwarding
|
|
23
|
+
# rule + global address) with a Cloud Armor security policy for WAF.
|
|
24
|
+
# * Compute has NO public IP in any mode; egress is via Cloud NAT (dedicated) or
|
|
25
|
+
# the shared network's own NAT (shared/existing), mirroring AWS private subnets.
|
|
26
|
+
# * Cognito MFA (alb_auth) → GCP IAP is a follow-on; the var is NO-OP'd here.
|
|
27
|
+
# * Tags → sanitized LABELS (see local.labels) on every labelable resource.
|
|
28
|
+
#
|
|
29
|
+
# One compute instance per compute plan item (for_each keyed by plan_key) +
|
|
30
|
+
# storage / secrets / TLS / LB / DNS per topology tier. Cost/attribution:
|
|
31
|
+
# sanitized var.tags on every labelable resource.
|
|
32
|
+
# =============================================================================
|
|
33
|
+
|
|
34
|
+
locals {
|
|
35
|
+
# Short, DNS/label-safe prefix. infrastructure_id is a UUID; 8 chars is enough
|
|
36
|
+
# to disambiguate within a customer while staying under GCP name limits. Used
|
|
37
|
+
# for human-facing label VALUES / descriptions (not as a resource name).
|
|
38
|
+
name_prefix = "${var.app_id}-${substr(var.infrastructure_id, 0, 8)}"
|
|
39
|
+
|
|
40
|
+
# The per-stack GCP NETWORK TAG. This is the SG-to-SG analog: it is applied to
|
|
41
|
+
# every instance and referenced by the firewall rules (self / target). It also
|
|
42
|
+
# serves as the base for every GCP resource NAME, so it MUST satisfy RFC1035 /
|
|
43
|
+
# the network-tag charset `[a-z]([-a-z0-9]*[a-z0-9])?`. We lowercase and replace
|
|
44
|
+
# any invalid char with "-"; name_prefix starts with app_id (a letter) and ends
|
|
45
|
+
# with a hex char, so the result starts with a letter and ends alnum.
|
|
46
|
+
stack_tag = substr(lower(replace(local.name_prefix, "/[^a-zA-Z0-9-]/", "-")), 0, 63)
|
|
47
|
+
|
|
48
|
+
# plan_key -> plan object, for compute nodes only. This map's keys ARE the
|
|
49
|
+
# google_compute_instance.node[...] addresses, so status maps 1:1 back to rows.
|
|
50
|
+
# Tool-agnostic: an explicit compute_kinds allow-list wins; otherwise compute =
|
|
51
|
+
# any plan item whose kind is NOT a generic foundation kind. So an app's roles
|
|
52
|
+
# (app nodes, e.g. any clustered workload's tiers) are compute
|
|
53
|
+
# automatically, with no per-tool list in the module.
|
|
54
|
+
compute_nodes = {
|
|
55
|
+
for r in var.plan : r.plan_key => r
|
|
56
|
+
if(length(var.compute_kinds) > 0
|
|
57
|
+
? contains(var.compute_kinds, r.kind)
|
|
58
|
+
: !contains(var.foundation_kinds, r.kind))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# Presence flags for the optional foundation tiers (derived from the plan).
|
|
62
|
+
has_storage = length([for r in var.plan : r if r.kind == "storage"]) > 0
|
|
63
|
+
has_secrets = length([for r in var.plan : r if r.kind == "secrets"]) > 0
|
|
64
|
+
has_license_file = length([for r in var.plan : r if r.kind == "license-file"]) > 0
|
|
65
|
+
has_tls = length([for r in var.plan : r if r.kind == "tls"]) > 0
|
|
66
|
+
has_lb = length([for r in var.plan : r if r.kind == "load-balancer"]) > 0
|
|
67
|
+
has_dns = length([for r in var.plan : r if r.kind == "dns"]) > 0 && var.public_dns_managed_zone != ""
|
|
68
|
+
has_hec = length([for r in var.plan : r if r.kind == "hec"]) > 0
|
|
69
|
+
|
|
70
|
+
resolved_image = var.image != "" ? var.image : data.google_compute_image.default.self_link
|
|
71
|
+
|
|
72
|
+
# --- Tags -> GCP labels sanitization ----------------------------------
|
|
73
|
+
# GCP labels: keys+values match [a-z0-9_-], keys start with a letter, <=63 chars.
|
|
74
|
+
# The Veltrix tag keys (Veltrix:Customer, CostCenter, ...) are INVALID as-is, so
|
|
75
|
+
# every taggable resource gets local.labels instead of var.tags: lowercase, any
|
|
76
|
+
# char outside [a-zA-Z0-9_-] replaced with "_", truncated to 63. The canonical
|
|
77
|
+
# Veltrix tag keys all start with a letter, so the sanitized keys are valid.
|
|
78
|
+
labels = {
|
|
79
|
+
for k, v in var.tags :
|
|
80
|
+
substr(lower(replace(k, "/[^a-zA-Z0-9_-]/", "_")), 0, 63) =>
|
|
81
|
+
substr(lower(replace(v, "/[^a-zA-Z0-9_-]/", "_")), 0, 63)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# --- Network mode: hosted-shared vs BYOC dedicated/existing -----------
|
|
85
|
+
is_dedicated = var.network_mode == "dedicated"
|
|
86
|
+
lookup_network = var.network_mode == "shared" || var.network_mode == "existing"
|
|
87
|
+
|
|
88
|
+
# Resolved network self-link, uniform across all three modes:
|
|
89
|
+
# dedicated -> the created custom-mode network
|
|
90
|
+
# shared/existing -> the looked-up network
|
|
91
|
+
network_self_link = local.is_dedicated ? google_compute_network.env[0].self_link : data.google_compute_network.shared[0].self_link
|
|
92
|
+
|
|
93
|
+
# Single per-stack subnetwork (GCP LBs are global/regional and don't need the
|
|
94
|
+
# multi-AZ public/private split AWS uses). One subnet in all modes.
|
|
95
|
+
compute_subnet_ids = [google_compute_subnetwork.env.self_link]
|
|
96
|
+
# Compute nodes are spread round-robin across the available compute subnets
|
|
97
|
+
# (a single subnet here, so all land on index 0). Kept for AWS parity.
|
|
98
|
+
compute_subnet_for = {
|
|
99
|
+
for idx, k in sort(keys(local.compute_nodes)) : k =>
|
|
100
|
+
local.compute_subnet_ids[idx % length(local.compute_subnet_ids)]
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# --- DNS mode: managed (in-project) / delegated (worker x-account) / none
|
|
104
|
+
dns_managed = var.dns_mode == "managed"
|
|
105
|
+
has_public_dns = var.dns_mode != "private-only"
|
|
106
|
+
# The listener cert is either issued here (managed) or supplied (delegated).
|
|
107
|
+
listener_cert = local.dns_managed ? (
|
|
108
|
+
(local.has_tls && var.dns_domain != "") ? google_compute_managed_ssl_certificate.env[0].id : ""
|
|
109
|
+
) : var.certificate_arn
|
|
110
|
+
|
|
111
|
+
# --- Derived LB / listener gates --------------------------------------
|
|
112
|
+
# has_lb = the plan carries a load-balancer item; has_lb_spec additionally
|
|
113
|
+
# requires the app to have supplied a load_balancer spec. The backend service +
|
|
114
|
+
# health check build only when a spec is present. has_listener adds the TLS cert
|
|
115
|
+
# + domain (plan-time-known, like the other has_* flags), gating the HTTPS proxy
|
|
116
|
+
# + forwarding rule identically.
|
|
117
|
+
has_lb_spec = local.has_lb && var.load_balancer != null
|
|
118
|
+
# HTTPS front door needs a cert: issued here (managed + a tls plan item) or
|
|
119
|
+
# supplied by the worker (delegated + certificate_arn).
|
|
120
|
+
has_listener = local.has_lb_spec && var.dns_domain != "" && (
|
|
121
|
+
local.dns_managed ? local.has_tls : var.certificate_arn != ""
|
|
122
|
+
)
|
|
123
|
+
# Cognito MFA (alb_auth) -> GCP IAP is a documented follow-on; NO-OP here. The
|
|
124
|
+
# flag is surfaced for parity but drives no resources (see header + variables).
|
|
125
|
+
alb_auth_enabled = var.alb_auth.enabled
|
|
126
|
+
|
|
127
|
+
# Health-check protocol from the LB spec (falls back to target protocol). Guarded
|
|
128
|
+
# so it never dereferences a null load_balancer.
|
|
129
|
+
hc_protocol = local.has_lb_spec ? (
|
|
130
|
+
var.load_balancer.health_check_protocol != "" ? var.load_balancer.health_check_protocol : var.load_balancer.target_protocol
|
|
131
|
+
) : ""
|
|
132
|
+
|
|
133
|
+
# Compute nodes that sit behind the LB — the kinds the app named as LB targets
|
|
134
|
+
# (e.g. the app's web/standalone nodes). try() keeps it null-safe when no spec.
|
|
135
|
+
lb_target_kinds = try(var.load_balancer.target_kinds, [])
|
|
136
|
+
search_targets = {
|
|
137
|
+
for k, r in local.compute_nodes : k => r
|
|
138
|
+
if contains(local.lb_target_kinds, r.kind)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
# --- Per-node function DNS labels -------------------------------------
|
|
142
|
+
# DNS-label-safe prefix per compute kind, from the app's dns_prefixes (falls
|
|
143
|
+
# back to the kind string). The bring-up layer resolves intra-cluster peers by
|
|
144
|
+
# these function FQDNs (e.g. idx1.<domain>), so the mapping MUST be
|
|
145
|
+
# deterministic and stable across applies. (Verbatim from the AWS module.)
|
|
146
|
+
node_prefix = {
|
|
147
|
+
for k, r in local.compute_nodes : k => lookup(var.dns_prefixes, r.kind, r.kind)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
# Deterministic per-kind ordinal: the label index is the key's position within
|
|
151
|
+
# its kind's lexically-sorted plan_key list (1-based via index()+1). Distinct
|
|
152
|
+
# keys therefore get unique, collision-free ordinals independent of the plan's
|
|
153
|
+
# input order (e.g. idx1, idx2 / sh1, sh2).
|
|
154
|
+
node_keys_by_kind = {
|
|
155
|
+
for kind in distinct([for k, r in local.compute_nodes : r.kind]) : kind => sort([
|
|
156
|
+
for k, r in local.compute_nodes : k if r.kind == kind
|
|
157
|
+
])
|
|
158
|
+
}
|
|
159
|
+
node_short_labels = {
|
|
160
|
+
for k, r in local.compute_nodes : k => format(
|
|
161
|
+
"%s%d",
|
|
162
|
+
local.node_prefix[k],
|
|
163
|
+
index(local.node_keys_by_kind[r.kind], k) + 1,
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
# plan_key -> function FQDN in the private zone. Empty when no domain is set.
|
|
168
|
+
node_fqdns = var.dns_domain != "" ? {
|
|
169
|
+
for k, label in local.node_short_labels : k => "${label}.${var.dns_domain}"
|
|
170
|
+
} : {}
|
|
171
|
+
|
|
172
|
+
# --- Private DNS zone resolution --------------------------------------
|
|
173
|
+
# Either create a private managed zone for var.dns_domain (create_private_zone)
|
|
174
|
+
# or point at a caller-supplied one (private_zone_id). want_private_dns is the
|
|
175
|
+
# plan-time-known intent used to gate the per-node record for_each (the zone name
|
|
176
|
+
# itself may stay computed until apply, which is fine for a record attribute but
|
|
177
|
+
# NOT for a for_each key set).
|
|
178
|
+
create_private_zone = var.create_private_zone && var.dns_domain != "" && var.private_zone_id == ""
|
|
179
|
+
want_private_dns = var.dns_domain != "" && (var.private_zone_id != "" || local.create_private_zone)
|
|
180
|
+
private_zone_name = var.private_zone_id != "" ? var.private_zone_id : (
|
|
181
|
+
local.create_private_zone ? google_dns_managed_zone.private[0].name : ""
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# --- Flattened firewall ingress rules (from the app's security_rules) --
|
|
185
|
+
# Each (rule, source) pair becomes one google_compute_firewall, keyed
|
|
186
|
+
# "<port>-<protocol>-<source>". "alb"-sourced rules are dropped when there is no
|
|
187
|
+
# LB. This replaces any hardcoded, tool-specific port list — the app declares its
|
|
188
|
+
# ports in InfraSpec.securityRules. (Verbatim shape from the AWS module.)
|
|
189
|
+
fw_ingress = merge([
|
|
190
|
+
for r in var.security_rules : {
|
|
191
|
+
for s in r.sources : "${r.port}-${r.protocol}-${s}" => {
|
|
192
|
+
port = r.port
|
|
193
|
+
protocol = r.protocol
|
|
194
|
+
source = s
|
|
195
|
+
description = r.description != "" ? r.description : "port ${r.port} (${r.protocol}) from ${s}"
|
|
196
|
+
}
|
|
197
|
+
if !(s == "alb" && !local.has_lb)
|
|
198
|
+
}
|
|
199
|
+
]...)
|
|
200
|
+
|
|
201
|
+
# Google Cloud Load Balancing health-check + proxy source ranges (used by the
|
|
202
|
+
# "alb"-sourced firewall rules so the LB can reach the backends + health-check).
|
|
203
|
+
gclb_ranges = ["130.211.0.0/22", "35.191.0.0/16"]
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
# Scaffold fallback image only. Production must pass a tool-preinstalled image.
|
|
207
|
+
data "google_compute_image" "default" {
|
|
208
|
+
family = "debian-12"
|
|
209
|
+
project = "debian-cloud"
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
# --- Network lookup (network_mode = shared | existing) --------------------
|
|
213
|
+
# The network is data-sourced (never created) — the shared Veltrix network for
|
|
214
|
+
# hosted, or a customer-designated network for BYOC "existing". Absent when
|
|
215
|
+
# dedicated. GCP networks are keyed by NAME (no tag lookup), so network_ref is the
|
|
216
|
+
# network name for both network_lookup_by values.
|
|
217
|
+
|
|
218
|
+
data "google_compute_network" "shared" {
|
|
219
|
+
count = local.lookup_network ? 1 : 0
|
|
220
|
+
name = var.network_ref
|
|
221
|
+
project = var.project
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
# --- Dedicated network fabric (network_mode = dedicated / BYOC) ------------
|
|
225
|
+
# A fresh, isolated custom-mode network created in the DEPLOY project (the
|
|
226
|
+
# customer's, for BYOC), plus a Cloud Router + Cloud NAT so the private (no public
|
|
227
|
+
# IP) instances get egress. Nothing here runs in shared/existing mode.
|
|
228
|
+
|
|
229
|
+
resource "google_compute_network" "env" {
|
|
230
|
+
count = local.is_dedicated ? 1 : 0
|
|
231
|
+
name = "${local.stack_tag}-net"
|
|
232
|
+
project = var.project
|
|
233
|
+
auto_create_subnetworks = false
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
resource "google_compute_router" "env" {
|
|
237
|
+
count = local.is_dedicated ? 1 : 0
|
|
238
|
+
name = "${local.stack_tag}-router"
|
|
239
|
+
project = var.project
|
|
240
|
+
region = var.region
|
|
241
|
+
network = google_compute_network.env[0].self_link
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
resource "google_compute_router_nat" "env" {
|
|
245
|
+
count = local.is_dedicated ? 1 : 0
|
|
246
|
+
name = "${local.stack_tag}-nat"
|
|
247
|
+
project = var.project
|
|
248
|
+
region = var.region
|
|
249
|
+
router = google_compute_router.env[0].name
|
|
250
|
+
nat_ip_allocate_option = "AUTO_ONLY"
|
|
251
|
+
source_subnetwork_ip_ranges_to_nat = "ALL_SUBNETWORKS_ALL_IP_RANGES"
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
# --- Subnetwork (all modes) -----------------------------------------------
|
|
255
|
+
# The stack's single subnetwork: in the created network (dedicated, range =
|
|
256
|
+
# vpc_cidr) or the looked-up network (shared/existing, range = subnet_cidr).
|
|
257
|
+
# private_ip_google_access lets no-public-IP instances reach Google APIs
|
|
258
|
+
# (Secret Manager / Storage) without traversing the NAT. Subnetworks are NOT
|
|
259
|
+
# labelable in GCP.
|
|
260
|
+
|
|
261
|
+
resource "google_compute_subnetwork" "env" {
|
|
262
|
+
name = "${local.stack_tag}-subnet"
|
|
263
|
+
project = var.project
|
|
264
|
+
region = var.region
|
|
265
|
+
network = local.network_self_link
|
|
266
|
+
ip_cidr_range = local.is_dedicated ? var.vpc_cidr : var.subnet_cidr
|
|
267
|
+
private_ip_google_access = true
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
# --- Firewall: SG-to-SG least privilege via a per-stack network tag --------
|
|
271
|
+
# GCP has no per-instance security group. Instead every instance carries the
|
|
272
|
+
# stack network tag (local.stack_tag), and each (security_rules entry, source)
|
|
273
|
+
# becomes one firewall rule targeting that tag. "self" -> source_tags (this
|
|
274
|
+
# stack's own instances); "admin" -> admin_cidr; "alb" -> Google's LB
|
|
275
|
+
# health-check + proxy ranges. Exactly one origin is set per rule; the other is
|
|
276
|
+
# null (omitted). "alb" rules are pre-filtered out of fw_ingress when there is no
|
|
277
|
+
# LB. Firewalls are NOT labelable in GCP.
|
|
278
|
+
|
|
279
|
+
resource "google_compute_firewall" "node" {
|
|
280
|
+
for_each = local.fw_ingress
|
|
281
|
+
|
|
282
|
+
name = substr("${local.stack_tag}-${each.key}", 0, 63)
|
|
283
|
+
project = var.project
|
|
284
|
+
network = local.network_self_link
|
|
285
|
+
direction = "INGRESS"
|
|
286
|
+
description = each.value.description
|
|
287
|
+
|
|
288
|
+
allow {
|
|
289
|
+
protocol = each.value.protocol
|
|
290
|
+
ports = [tostring(each.value.port)]
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
target_tags = [local.stack_tag]
|
|
294
|
+
source_tags = each.value.source == "self" ? [local.stack_tag] : null
|
|
295
|
+
source_ranges = (
|
|
296
|
+
each.value.source == "admin" ? [var.admin_cidr] :
|
|
297
|
+
each.value.source == "alb" ? local.gclb_ranges :
|
|
298
|
+
null
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
# --- Compute: one google_compute_instance per compute plan item ------------
|
|
303
|
+
# for_each keyed by plan_key => google_compute_instance.node["data/indexer-1"].
|
|
304
|
+
# No public IP (egress via Cloud NAT). The stack network tag ties the instance to
|
|
305
|
+
# the firewall rules above. `hostname` must be a FQDN on GCP, so we use the node
|
|
306
|
+
# function FQDN when a domain is set (else omit -> the default internal name).
|
|
307
|
+
|
|
308
|
+
resource "google_compute_instance" "node" {
|
|
309
|
+
for_each = local.compute_nodes
|
|
310
|
+
|
|
311
|
+
name = substr(lower("${local.stack_tag}-${local.node_short_labels[each.key]}"), 0, 63)
|
|
312
|
+
project = var.project
|
|
313
|
+
# Multi-AZ placement: pin to the node's zone when set, else the default zone.
|
|
314
|
+
zone = coalesce(each.value.zone, var.zone)
|
|
315
|
+
machine_type = coalesce(
|
|
316
|
+
lookup(var.machine_types_by_kind, each.value.kind, null),
|
|
317
|
+
lookup(var.machine_types, each.value.tier, null),
|
|
318
|
+
var.default_machine_type,
|
|
319
|
+
)
|
|
320
|
+
hostname = lookup(local.node_fqdns, each.key, null)
|
|
321
|
+
|
|
322
|
+
boot_disk {
|
|
323
|
+
initialize_params {
|
|
324
|
+
image = local.resolved_image
|
|
325
|
+
size = var.boot_disk_gb
|
|
326
|
+
type = "pd-ssd"
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
network_interface {
|
|
331
|
+
subnetwork = local.compute_subnet_for[each.key]
|
|
332
|
+
# No access_config block => no external IP. Egress is via Cloud NAT.
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
tags = [local.stack_tag]
|
|
336
|
+
metadata = var.ssh_public_key != "" ? { "ssh-keys" = var.ssh_public_key } : {}
|
|
337
|
+
|
|
338
|
+
labels = local.labels
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
# --- Storage: object-storage bucket (e.g. SmartStore, warm/cold tiers) -----
|
|
342
|
+
# Generic GCS bucket for the app's bulk/object storage. Private: uniform
|
|
343
|
+
# bucket-level access + enforced public-access prevention (no ACLs, no public).
|
|
344
|
+
|
|
345
|
+
resource "random_id" "bucket_suffix" {
|
|
346
|
+
count = local.has_storage ? 1 : 0
|
|
347
|
+
byte_length = 4
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
resource "google_storage_bucket" "objstore" {
|
|
351
|
+
count = local.has_storage ? 1 : 0
|
|
352
|
+
name = "${local.stack_tag}-objstore-${random_id.bucket_suffix[0].hex}"
|
|
353
|
+
project = var.project
|
|
354
|
+
location = var.region
|
|
355
|
+
force_destroy = false
|
|
356
|
+
uniform_bucket_level_access = true
|
|
357
|
+
public_access_prevention = "enforced"
|
|
358
|
+
|
|
359
|
+
labels = local.labels
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
# --- Secrets: per-tenant secret bundle (admin seed / pass4SymmKey / etc.) ----
|
|
363
|
+
|
|
364
|
+
resource "google_secret_manager_secret" "env" {
|
|
365
|
+
count = local.has_secrets ? 1 : 0
|
|
366
|
+
secret_id = "${local.stack_tag}-env-secrets"
|
|
367
|
+
project = var.project
|
|
368
|
+
|
|
369
|
+
replication {
|
|
370
|
+
auto {}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
labels = local.labels
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
# --- BYOL license file (stored as a secret; validated post-apply) ---------
|
|
377
|
+
|
|
378
|
+
resource "google_secret_manager_secret" "license" {
|
|
379
|
+
count = local.has_license_file ? 1 : 0
|
|
380
|
+
secret_id = "${local.stack_tag}-byol-license"
|
|
381
|
+
project = var.project
|
|
382
|
+
|
|
383
|
+
replication {
|
|
384
|
+
auto {}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
labels = local.labels
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
# --- TLS certificate (Google-managed, for the public domain) --------------
|
|
391
|
+
# Issued in-project only for dns_mode = managed. In delegated mode the worker
|
|
392
|
+
# provisions the cert and passes certificate_arn (a cert self-link). Managed SSL
|
|
393
|
+
# certs are NOT labelable in GCP.
|
|
394
|
+
|
|
395
|
+
resource "google_compute_managed_ssl_certificate" "env" {
|
|
396
|
+
count = local.dns_managed && local.has_tls && var.dns_domain != "" ? 1 : 0
|
|
397
|
+
name = "${local.stack_tag}-cert"
|
|
398
|
+
project = var.project
|
|
399
|
+
|
|
400
|
+
managed {
|
|
401
|
+
domains = [var.dns_domain]
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
# NOTE: unlike the AWS ACM cert (create_before_destroy), a google_compute_
|
|
405
|
+
# managed_ssl_certificate has a unique NAME, so create_before_destroy would fail
|
|
406
|
+
# on replacement (two certs can't share a name). Cert domain changes are rare;
|
|
407
|
+
# we take the default replace behaviour. Zero-downtime cert rotation would use a
|
|
408
|
+
# name suffix (random_id), a GCP-specific follow-on.
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
# --- Load balancer: EXTERNAL_MANAGED global HTTPS LB ----------------------
|
|
412
|
+
# The front door for the app's web tier + HEC ingress. Global external
|
|
413
|
+
# Application LB: a reserved global IP + health check + unmanaged instance group
|
|
414
|
+
# (target-kind instances) + backend service + url map + HTTPS proxy + global
|
|
415
|
+
# forwarding rule. Google runs the proxy fleet, so (unlike the AWS ALB) there is
|
|
416
|
+
# no public edge subnet/SG — client access is governed by Cloud Armor.
|
|
417
|
+
|
|
418
|
+
# Reserved global IPv4 for the LB frontend. Gated on has_lb (mirrors aws_lb).
|
|
419
|
+
resource "google_compute_global_address" "lb" {
|
|
420
|
+
count = local.has_lb ? 1 : 0
|
|
421
|
+
name = "${local.stack_tag}-lb-ip"
|
|
422
|
+
project = var.project
|
|
423
|
+
ip_version = "IPV4"
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
# Health check from the app's load_balancer spec. Port = the LB target port (the
|
|
427
|
+
# GCP equivalent of the ALB's "traffic-port"), so the "alb"-sourced firewall rule
|
|
428
|
+
# on that port also admits health checks. HTTP vs HTTPS selected by hc_protocol.
|
|
429
|
+
resource "google_compute_health_check" "lb" {
|
|
430
|
+
count = local.has_lb_spec ? 1 : 0
|
|
431
|
+
name = "${local.stack_tag}-hc"
|
|
432
|
+
project = var.project
|
|
433
|
+
|
|
434
|
+
dynamic "http_health_check" {
|
|
435
|
+
for_each = local.hc_protocol == "HTTP" ? [1] : []
|
|
436
|
+
content {
|
|
437
|
+
port = var.load_balancer.target_port
|
|
438
|
+
request_path = var.load_balancer.health_check_path
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
dynamic "https_health_check" {
|
|
443
|
+
for_each = local.hc_protocol == "HTTPS" ? [1] : []
|
|
444
|
+
content {
|
|
445
|
+
port = var.load_balancer.target_port
|
|
446
|
+
request_path = var.load_balancer.health_check_path
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
# Unmanaged instance group holding the web-serving nodes (the kinds the app named
|
|
452
|
+
# in the LB spec's target_kinds), added by self-link. named_port maps the target
|
|
453
|
+
# port to the "http" port name the backend service references.
|
|
454
|
+
resource "google_compute_instance_group" "lb" {
|
|
455
|
+
count = local.has_lb_spec ? 1 : 0
|
|
456
|
+
name = "${local.stack_tag}-ig"
|
|
457
|
+
project = var.project
|
|
458
|
+
zone = var.zone
|
|
459
|
+
instances = [for k, r in local.search_targets : google_compute_instance.node[k].self_link]
|
|
460
|
+
|
|
461
|
+
named_port {
|
|
462
|
+
name = "http"
|
|
463
|
+
port = var.load_balancer.target_port
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
# Backend service: protocol from target_protocol, the health check, and the
|
|
468
|
+
# instance group backend. Cloud Armor security policy attached when waf_enabled.
|
|
469
|
+
resource "google_compute_backend_service" "lb" {
|
|
470
|
+
count = local.has_lb_spec ? 1 : 0
|
|
471
|
+
name = "${local.stack_tag}-backend"
|
|
472
|
+
project = var.project
|
|
473
|
+
load_balancing_scheme = "EXTERNAL_MANAGED"
|
|
474
|
+
protocol = var.load_balancer.target_protocol
|
|
475
|
+
port_name = "http"
|
|
476
|
+
health_checks = [google_compute_health_check.lb[0].id]
|
|
477
|
+
security_policy = var.waf_enabled ? google_compute_security_policy.waf[0].id : null
|
|
478
|
+
|
|
479
|
+
backend {
|
|
480
|
+
group = google_compute_instance_group.lb[0].self_link
|
|
481
|
+
balancing_mode = "UTILIZATION"
|
|
482
|
+
capacity_scaler = 1.0
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
resource "google_compute_url_map" "lb" {
|
|
487
|
+
count = local.has_lb_spec ? 1 : 0
|
|
488
|
+
name = "${local.stack_tag}-urlmap"
|
|
489
|
+
project = var.project
|
|
490
|
+
default_service = google_compute_backend_service.lb[0].id
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
# HTTPS proxy + global forwarding rule — the public listener. Gated on
|
|
494
|
+
# has_listener (LB spec + domain + a cert: managed cert here, or certificate_arn).
|
|
495
|
+
resource "google_compute_target_https_proxy" "lb" {
|
|
496
|
+
count = local.has_listener ? 1 : 0
|
|
497
|
+
name = "${local.stack_tag}-https-proxy"
|
|
498
|
+
project = var.project
|
|
499
|
+
url_map = google_compute_url_map.lb[0].id
|
|
500
|
+
ssl_certificates = [local.listener_cert]
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
resource "google_compute_global_forwarding_rule" "lb" {
|
|
504
|
+
count = local.has_listener ? 1 : 0
|
|
505
|
+
name = "${local.stack_tag}-fr"
|
|
506
|
+
project = var.project
|
|
507
|
+
load_balancing_scheme = "EXTERNAL_MANAGED"
|
|
508
|
+
target = google_compute_target_https_proxy.lb[0].id
|
|
509
|
+
ip_address = google_compute_global_address.lb[0].address
|
|
510
|
+
port_range = tostring(var.load_balancer.listener_port)
|
|
511
|
+
|
|
512
|
+
labels = local.labels
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
# NOTE: an HTTP(80)->HTTPS redirect (the AWS module's http listener) is omitted —
|
|
516
|
+
# on GCP it needs a separate redirect url-map + HTTP target proxy + forwarding
|
|
517
|
+
# rule. It can be added as a follow-on; the primary HTTPS front door is complete.
|
|
518
|
+
|
|
519
|
+
# --- Cloud Armor (WAF analog) ---------------------------------------------
|
|
520
|
+
# Attached to the backend service when waf_enabled. Default allow, with OWASP
|
|
521
|
+
# preconfigured rules (SQLi/XSS) blocking and a per-IP rate limit (~2000 req /
|
|
522
|
+
# 5 min), mirroring the AWS WAFv2 managed rule groups + rate-based rule. Gated on
|
|
523
|
+
# has_lb (a Cloud Armor policy fronts the LB backend). Not labelable.
|
|
524
|
+
|
|
525
|
+
resource "google_compute_security_policy" "waf" {
|
|
526
|
+
count = local.has_lb && var.waf_enabled ? 1 : 0
|
|
527
|
+
name = "${local.stack_tag}-waf"
|
|
528
|
+
project = var.project
|
|
529
|
+
|
|
530
|
+
# OWASP CRS: SQL injection.
|
|
531
|
+
rule {
|
|
532
|
+
action = "deny(403)"
|
|
533
|
+
priority = 1000
|
|
534
|
+
description = "OWASP CRS: SQL injection"
|
|
535
|
+
|
|
536
|
+
match {
|
|
537
|
+
expr {
|
|
538
|
+
expression = "evaluatePreconfiguredExpr('sqli-v33-stable')"
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
# OWASP CRS: cross-site scripting.
|
|
544
|
+
rule {
|
|
545
|
+
action = "deny(403)"
|
|
546
|
+
priority = 1001
|
|
547
|
+
description = "OWASP CRS: cross-site scripting"
|
|
548
|
+
|
|
549
|
+
match {
|
|
550
|
+
expr {
|
|
551
|
+
expression = "evaluatePreconfiguredExpr('xss-v33-stable')"
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
# Volumetric protection: throttle a source IP over ~2000 requests / 5 min.
|
|
557
|
+
rule {
|
|
558
|
+
action = "throttle"
|
|
559
|
+
priority = 2000
|
|
560
|
+
description = "Per-IP rate limit"
|
|
561
|
+
|
|
562
|
+
match {
|
|
563
|
+
versioned_expr = "SRC_IPS_V1"
|
|
564
|
+
config {
|
|
565
|
+
src_ip_ranges = ["*"]
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
rate_limit_options {
|
|
570
|
+
conform_action = "allow"
|
|
571
|
+
exceed_action = "deny(429)"
|
|
572
|
+
enforce_on_key = "IP"
|
|
573
|
+
|
|
574
|
+
rate_limit_threshold {
|
|
575
|
+
count = 2000
|
|
576
|
+
interval_sec = 300
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
# Required default rule (lowest priority): allow everything else.
|
|
582
|
+
rule {
|
|
583
|
+
action = "allow"
|
|
584
|
+
priority = 2147483647
|
|
585
|
+
description = "Default allow"
|
|
586
|
+
|
|
587
|
+
match {
|
|
588
|
+
versioned_expr = "SRC_IPS_V1"
|
|
589
|
+
config {
|
|
590
|
+
src_ip_ranges = ["*"]
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
# --- Public DNS record ----------------------------------------------------
|
|
597
|
+
# Created in-project only for dns_mode = managed. delegated => the worker writes
|
|
598
|
+
# it cross-account into Veltrix's zone; private-only => no public record. An A
|
|
599
|
+
# record to the LB's reserved global IP (GCP LBs have no DNS name to alias).
|
|
600
|
+
|
|
601
|
+
resource "google_dns_record_set" "env" {
|
|
602
|
+
count = local.dns_managed && local.has_dns && local.has_lb ? 1 : 0
|
|
603
|
+
name = "${var.dns_domain}."
|
|
604
|
+
project = var.project
|
|
605
|
+
managed_zone = var.public_dns_managed_zone
|
|
606
|
+
type = "A"
|
|
607
|
+
ttl = 60
|
|
608
|
+
rrdatas = [google_compute_global_address.lb[0].address]
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
# --- Private DNS: intra-cluster function FQDNs ----------------------------
|
|
612
|
+
# A private managed zone (visibility = private, bound to the stack network) gives
|
|
613
|
+
# every node a stable function FQDN (idx1.<domain>, sh1.<domain>, ...). The
|
|
614
|
+
# bring-up layer uses node_fqdns (see outputs) to build its inventory. Either
|
|
615
|
+
# create the zone here (create_private_zone) or reuse a caller-supplied one
|
|
616
|
+
# (private_zone_id).
|
|
617
|
+
|
|
618
|
+
resource "google_dns_managed_zone" "private" {
|
|
619
|
+
count = local.create_private_zone ? 1 : 0
|
|
620
|
+
name = "${local.stack_tag}-private"
|
|
621
|
+
project = var.project
|
|
622
|
+
dns_name = "${var.dns_domain}."
|
|
623
|
+
visibility = "private"
|
|
624
|
+
|
|
625
|
+
private_visibility_config {
|
|
626
|
+
networks {
|
|
627
|
+
network_url = local.network_self_link
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
labels = local.labels
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
# One A record per compute node -> its private IP, keyed by plan_key so the set
|
|
635
|
+
# tracks the compute for_each. Gated on want_private_dns (plan-time-known) so the
|
|
636
|
+
# key set never depends on the not-yet-known created-zone name.
|
|
637
|
+
resource "google_dns_record_set" "node" {
|
|
638
|
+
for_each = local.want_private_dns ? local.node_fqdns : {}
|
|
639
|
+
name = "${each.value}."
|
|
640
|
+
project = var.project
|
|
641
|
+
managed_zone = local.private_zone_name
|
|
642
|
+
type = "A"
|
|
643
|
+
ttl = 60
|
|
644
|
+
rrdatas = [google_compute_instance.node[each.key].network_interface[0].network_ip]
|
|
645
|
+
}
|