@veltrixsecops/app-sdk 3.2.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-EOOEHZGC.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 +118 -6
- package/dist/client/index.d.ts +118 -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.map +1 -1
- package/dist/hooks/index.d.cts +2 -2
- package/dist/hooks/index.d.ts +2 -2
- package/dist/hooks/index.js +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 +136 -0
- package/dist/ui/index.cjs.map +1 -1
- package/dist/ui/index.d.cts +109 -1
- package/dist/ui/index.d.ts +109 -1
- package/dist/ui/index.js +35 -677
- package/dist/ui/index.js.map +1 -1
- package/dist/{use-app-context-OQlcmq7t.d.cts → use-app-context-Byv_fam0.d.cts} +206 -4
- package/dist/{use-app-context-OQlcmq7t.d.ts → use-app-context-Byv_fam0.d.ts} +206 -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-EOOEHZGC.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,432 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# Hetzner Cloud environment module — generic, tool-agnostic. A faithful
|
|
3
|
+
# translation of the AWS reference module (sdk/opentofu/modules/aws), honoring
|
|
4
|
+
# the SAME variable + output contract. Driven by an app's rendered InfraSpec;
|
|
5
|
+
# NOTHING here is tool-specific.
|
|
6
|
+
#
|
|
7
|
+
# Network mode (worker-set deployment var, NOT app InfraSpec):
|
|
8
|
+
# shared — Veltrix-hosted: data-source the shared hcloud_network + create
|
|
9
|
+
# the per-stack cloud subnet.
|
|
10
|
+
# dedicated — BYOC: CREATE a fresh hcloud_network + one cloud subnet.
|
|
11
|
+
# existing — BYOC: data-source a customer network + create a subnet in it.
|
|
12
|
+
#
|
|
13
|
+
# Servers attach to the private network (hcloud_server_network) for private IPs;
|
|
14
|
+
# they keep their default PUBLIC interface for egress (Hetzner private networks
|
|
15
|
+
# have no built-in NAT — a NAT gateway server would be a separate, out-of-band
|
|
16
|
+
# resource). The firewall (hcloud_firewall) is the SG analog and attaches to
|
|
17
|
+
# this stack's servers via a `veltrix-stack` label selector.
|
|
18
|
+
#
|
|
19
|
+
# =============================================================================
|
|
20
|
+
# DOCUMENTED GAPS — Hetzner is the simplest cloud and lacks several primitives
|
|
21
|
+
# the AWS module uses. These are handled HONESTLY (no-op + comment), never faked:
|
|
22
|
+
#
|
|
23
|
+
# * WAF — Hetzner has no WAF. `waf_enabled` is accepted for contract
|
|
24
|
+
# parity but is a NO-OP; a real WAF would require an external
|
|
25
|
+
# reverse-proxy/CDN in front of the LB. See the LB section.
|
|
26
|
+
# * DNS records— The hcloud provider has NO managed-DNS resource (Hetzner DNS
|
|
27
|
+
# is the separate `timohirt/hetznerdns` provider). This module
|
|
28
|
+
# creates NO public or private DNS records in ANY dns_mode. It
|
|
29
|
+
# computes per-node FQDNs and EMITS them (output node_fqdns) so
|
|
30
|
+
# the bring-up layer can populate /etc/hosts or a hetznerdns
|
|
31
|
+
# stack out-of-band. dns_mode / create_private_zone /
|
|
32
|
+
# private_zone_id are kept for contract uniformity (no-ops).
|
|
33
|
+
# * Object store— Hetzner Object Storage is S3-compatible but NOT in the hcloud
|
|
34
|
+
# provider. `storage` plan items create a block volume
|
|
35
|
+
# (hcloud_volume) instead; true object storage is external.
|
|
36
|
+
# * Secrets — Hetzner has no Secrets Manager / Key Vault. `secrets` and
|
|
37
|
+
# `license-file` plan items create NO resource here; those
|
|
38
|
+
# values live in the platform vault (injected out-of-band).
|
|
39
|
+
# * MFA (alb_auth)— Hetzner LBs have no OIDC/Cognito equivalent. `alb_auth` is
|
|
40
|
+
# accepted for contract parity but is a NO-OP.
|
|
41
|
+
# =============================================================================
|
|
42
|
+
|
|
43
|
+
locals {
|
|
44
|
+
# Short, DNS/label-safe prefix. infrastructure_id is a UUID; 8 chars is enough
|
|
45
|
+
# to disambiguate within a customer while staying under name limits.
|
|
46
|
+
name_prefix = "${var.app_id}-${substr(var.infrastructure_id, 0, 8)}"
|
|
47
|
+
|
|
48
|
+
# Hostname-safe form of name_prefix (lowercase, [a-z0-9-]) for server names.
|
|
49
|
+
host_prefix = replace(lower(local.name_prefix), "/[^a-z0-9-]/", "-")
|
|
50
|
+
|
|
51
|
+
# Label-value-safe form of name_prefix, used as the per-stack selector value.
|
|
52
|
+
stack_label = substr(replace(local.name_prefix, "/[^a-zA-Z0-9._-]/", "_"), 0, 63)
|
|
53
|
+
|
|
54
|
+
# --- Location / network zone resolution -------------------------------
|
|
55
|
+
# var.region may be a location (nbg1) OR a network zone (eu-central). Servers
|
|
56
|
+
# and volumes need a location; the subnet and LB need a network zone. Resolve
|
|
57
|
+
# both, deriving the missing side, with explicit override vars taking priority.
|
|
58
|
+
network_zones = ["eu-central", "us-east", "us-west", "ap-southeast"]
|
|
59
|
+
location_zone = {
|
|
60
|
+
nbg1 = "eu-central"
|
|
61
|
+
fsn1 = "eu-central"
|
|
62
|
+
hel1 = "eu-central"
|
|
63
|
+
ash = "us-east"
|
|
64
|
+
hil = "us-west"
|
|
65
|
+
sin = "ap-southeast"
|
|
66
|
+
}
|
|
67
|
+
zone_default_location = {
|
|
68
|
+
"eu-central" = "nbg1"
|
|
69
|
+
"us-east" = "ash"
|
|
70
|
+
"us-west" = "hil"
|
|
71
|
+
"ap-southeast" = "sin"
|
|
72
|
+
}
|
|
73
|
+
region_is_zone = contains(local.network_zones, var.region)
|
|
74
|
+
network_zone = var.network_zone != "" ? var.network_zone : (
|
|
75
|
+
local.region_is_zone ? var.region : lookup(local.location_zone, var.region, "eu-central")
|
|
76
|
+
)
|
|
77
|
+
location = var.location != "" ? var.location : (
|
|
78
|
+
local.region_is_zone ? lookup(local.zone_default_location, var.region, "nbg1") : var.region
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# plan_key -> plan object, for compute nodes only. This map's keys ARE the
|
|
82
|
+
# hcloud_server.node[...] addresses, so status maps 1:1 back to resource rows.
|
|
83
|
+
# Tool-agnostic: an explicit compute_kinds allow-list wins; otherwise compute =
|
|
84
|
+
# any plan item whose kind is NOT a generic foundation kind.
|
|
85
|
+
compute_nodes = {
|
|
86
|
+
for r in var.plan : r.plan_key => r
|
|
87
|
+
if(length(var.compute_kinds) > 0
|
|
88
|
+
? contains(var.compute_kinds, r.kind)
|
|
89
|
+
: !contains(var.foundation_kinds, r.kind))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
# Presence flags for the optional foundation tiers (derived from the plan).
|
|
93
|
+
# NOTE: `secrets` and `license-file` intentionally have NO flag/resource —
|
|
94
|
+
# Hetzner has no secret store (see the gap notes above).
|
|
95
|
+
has_storage = length([for r in var.plan : r if r.kind == "storage"]) > 0
|
|
96
|
+
has_tls = length([for r in var.plan : r if r.kind == "tls"]) > 0
|
|
97
|
+
has_lb = length([for r in var.plan : r if r.kind == "load-balancer"]) > 0
|
|
98
|
+
has_hec = length([for r in var.plan : r if r.kind == "hec"]) > 0
|
|
99
|
+
|
|
100
|
+
resolved_image = var.image != "" ? var.image : "ubuntu-22.04"
|
|
101
|
+
|
|
102
|
+
# --- Network mode: hosted-shared vs BYOC dedicated/existing -----------
|
|
103
|
+
is_dedicated = var.network_mode == "dedicated"
|
|
104
|
+
lookup_network = var.network_mode == "shared" || var.network_mode == "existing"
|
|
105
|
+
|
|
106
|
+
# Resolved network id + the single per-stack subnet ip_range, uniform across
|
|
107
|
+
# all three modes. Hetzner subnets are ip_ranges (not multi-AZ objects), so one
|
|
108
|
+
# subnet per stack is sufficient. `.id` on an hcloud resource is a string in
|
|
109
|
+
# TF; hcloud_network_subnet.network_id accepts it and coerces to int.
|
|
110
|
+
network_id = local.is_dedicated ? hcloud_network.env[0].id : data.hcloud_network.shared[0].id
|
|
111
|
+
compute_subnet_cidr = var.subnet_cidr != "" ? var.subnet_cidr : var.vpc_cidr
|
|
112
|
+
# Intra-stack peer range: firewall "self"/"alb" sources resolve to the subnet
|
|
113
|
+
# CIDR (there is no SG-to-SG on Hetzner). The LB's private interface lives in
|
|
114
|
+
# this range too, so "alb"-sourced rules use the same CIDR.
|
|
115
|
+
self_cidr = local.compute_subnet_cidr
|
|
116
|
+
|
|
117
|
+
# --- DNS mode: no records are created on Hetzner (see gap notes). ------
|
|
118
|
+
# The LB serves HTTPS only when a certificate id is supplied; otherwise HTTP.
|
|
119
|
+
lb_use_https = local.has_lb && var.load_balancer != null && var.certificate_arn != ""
|
|
120
|
+
|
|
121
|
+
# --- Derived LB gates (mirror the AWS has_lb / has_lb_spec logic) ------
|
|
122
|
+
has_lb_spec = local.has_lb && var.load_balancer != null
|
|
123
|
+
|
|
124
|
+
# Compute nodes that sit behind the LB — the kinds the app named as LB targets.
|
|
125
|
+
# try() keeps it null-safe when no LB spec is present.
|
|
126
|
+
lb_target_kinds = try(var.load_balancer.target_kinds, [])
|
|
127
|
+
search_targets = {
|
|
128
|
+
for k, r in local.compute_nodes : k => r
|
|
129
|
+
if contains(local.lb_target_kinds, r.kind)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
# --- Per-node function DNS labels (VERBATIM from the AWS module) -------
|
|
133
|
+
# DNS-label-safe prefix per compute kind, from the app's dns_prefixes (falls
|
|
134
|
+
# back to the kind string). The bring-up layer resolves intra-cluster peers by
|
|
135
|
+
# these function FQDNs, so the mapping MUST be deterministic and stable.
|
|
136
|
+
node_prefix = {
|
|
137
|
+
for k, r in local.compute_nodes : k => lookup(var.dns_prefixes, r.kind, r.kind)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# Deterministic per-kind ordinal: the label index is the key's position within
|
|
141
|
+
# its kind's lexically-sorted plan_key list (1-based via index()+1). Distinct
|
|
142
|
+
# keys therefore get unique, collision-free ordinals independent of the plan's
|
|
143
|
+
# input order (e.g. idx1, idx2 / sh1, sh2).
|
|
144
|
+
node_keys_by_kind = {
|
|
145
|
+
for kind in distinct([for k, r in local.compute_nodes : r.kind]) : kind => sort([
|
|
146
|
+
for k, r in local.compute_nodes : k if r.kind == kind
|
|
147
|
+
])
|
|
148
|
+
}
|
|
149
|
+
node_short_labels = {
|
|
150
|
+
for k, r in local.compute_nodes : k => format(
|
|
151
|
+
"%s%d",
|
|
152
|
+
local.node_prefix[k],
|
|
153
|
+
index(local.node_keys_by_kind[r.kind], k) + 1,
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
# plan_key -> function FQDN. Empty when no domain is set. Emitted as an output
|
|
158
|
+
# (node_fqdns); NO DNS record is created for it on Hetzner.
|
|
159
|
+
node_fqdns = var.dns_domain != "" ? {
|
|
160
|
+
for k, label in local.node_short_labels : k => "${label}.${var.dns_domain}"
|
|
161
|
+
} : {}
|
|
162
|
+
|
|
163
|
+
# --- Label sanitization: var.tags -> hcloud labels --------------------
|
|
164
|
+
# hcloud labels use a restricted charset (key/value: start+end alphanumeric,
|
|
165
|
+
# inner [a-zA-Z0-9._-], <=63; NO colons). AWS tag keys like "Veltrix:Customer"
|
|
166
|
+
# are invalid, so: replace every invalid char with "_", truncate to 63, then
|
|
167
|
+
# trim any leading/trailing separator so the value still starts/ends alnum.
|
|
168
|
+
base_labels = {
|
|
169
|
+
for k, v in var.tags :
|
|
170
|
+
replace(substr(replace(k, "/[^a-zA-Z0-9._-]/", "_"), 0, 63), "/^[._-]+|[._-]+$/", "")
|
|
171
|
+
=>
|
|
172
|
+
replace(substr(replace(v, "/[^a-zA-Z0-9._-]/", "_"), 0, 63), "/^[._-]+|[._-]+$/", "")
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
# Per-compute-node sanitized meta (plan_key/tier/kind/role carry "/" etc.).
|
|
176
|
+
compute_meta = {
|
|
177
|
+
for k, r in local.compute_nodes : k => {
|
|
178
|
+
plan_key = replace(substr(replace(k, "/[^a-zA-Z0-9._-]/", "_"), 0, 63), "/^[._-]+|[._-]+$/", "")
|
|
179
|
+
tier = replace(substr(replace(r.tier, "/[^a-zA-Z0-9._-]/", "_"), 0, 63), "/^[._-]+|[._-]+$/", "")
|
|
180
|
+
kind = replace(substr(replace(r.kind, "/[^a-zA-Z0-9._-]/", "_"), 0, 63), "/^[._-]+|[._-]+$/", "")
|
|
181
|
+
role = r.role != "" ? replace(substr(replace(r.role, "/[^a-zA-Z0-9._-]/", "_"), 0, 63), "/^[._-]+|[._-]+$/", "") : "none"
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
# Fixed foundation label sets (values written pre-sanitized: "/" -> "_").
|
|
186
|
+
labels_network = merge(local.base_labels, { "Name" = "${local.stack_label}-network", "Veltrix_PlanKey" = "foundation_network", "Veltrix_Tier" = "foundation", "veltrix-stack" = local.stack_label })
|
|
187
|
+
labels_storage = merge(local.base_labels, { "Name" = "${local.stack_label}-volume", "Veltrix_PlanKey" = "foundation_storage", "Veltrix_Tier" = "foundation", "veltrix-stack" = local.stack_label })
|
|
188
|
+
labels_lb = merge(local.base_labels, { "Name" = "${local.stack_label}-lb", "Veltrix_PlanKey" = "foundation_load-balancer", "Veltrix_Tier" = "foundation", "veltrix-stack" = local.stack_label })
|
|
189
|
+
labels_firewall = merge(local.base_labels, { "Name" = "${local.stack_label}-fw", "Veltrix_PlanKey" = "foundation_network", "Veltrix_Tier" = "foundation", "veltrix-stack" = local.stack_label })
|
|
190
|
+
|
|
191
|
+
# Per-compute-node label set (base + sanitized meta + the stack selector label).
|
|
192
|
+
compute_labels = {
|
|
193
|
+
for k, r in local.compute_nodes : k => merge(local.base_labels, {
|
|
194
|
+
"Name" = "${local.host_prefix}-${local.node_short_labels[k]}"
|
|
195
|
+
"Veltrix_PlanKey" = local.compute_meta[k].plan_key
|
|
196
|
+
"Veltrix_Tier" = local.compute_meta[k].tier
|
|
197
|
+
"Veltrix_Kind" = local.compute_meta[k].kind
|
|
198
|
+
"Veltrix_Role" = local.compute_meta[k].role
|
|
199
|
+
"veltrix-stack" = local.stack_label
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
# --- Flattened firewall ingress rules (from the app's security_rules) --
|
|
204
|
+
# There is NO SG-to-SG on Hetzner: each source resolves to a CIDR.
|
|
205
|
+
# self -> the stack subnet CIDR (peer nodes)
|
|
206
|
+
# alb -> the stack subnet CIDR (the LB's private interface range); the pair
|
|
207
|
+
# is dropped when there is no LB, exactly like the AWS module.
|
|
208
|
+
# admin -> var.admin_cidr
|
|
209
|
+
# The map key is "<port>-<protocol>-<cidr>", which DEDUPES the common case where
|
|
210
|
+
# self and alb resolve to the same CIDR (one firewall rule instead of two).
|
|
211
|
+
source_cidr = {
|
|
212
|
+
self = local.self_cidr
|
|
213
|
+
alb = local.self_cidr
|
|
214
|
+
admin = var.admin_cidr
|
|
215
|
+
}
|
|
216
|
+
fw_rules = {
|
|
217
|
+
for e in flatten([
|
|
218
|
+
for r in var.security_rules : [
|
|
219
|
+
for s in r.sources : {
|
|
220
|
+
port = r.port
|
|
221
|
+
protocol = r.protocol
|
|
222
|
+
cidr = local.source_cidr[s]
|
|
223
|
+
description = r.description != "" ? r.description : "port ${r.port} (${r.protocol}) from ${s}"
|
|
224
|
+
}
|
|
225
|
+
if !(s == "alb" && !local.has_lb)
|
|
226
|
+
]
|
|
227
|
+
]) : "${e.port}-${e.protocol}-${e.cidr}" => e
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
# --- Network lookup (network_mode = shared | existing) --------------------
|
|
232
|
+
# The network is data-sourced (never created) — the shared Veltrix network for
|
|
233
|
+
# hosted, or a customer-designated network for BYOC "existing". Absent when
|
|
234
|
+
# dedicated. Resolved by name (default), id, or a label selector (network_lookup_by).
|
|
235
|
+
data "hcloud_network" "shared" {
|
|
236
|
+
count = local.lookup_network ? 1 : 0
|
|
237
|
+
id = var.network_lookup_by == "id" ? var.network_ref : null
|
|
238
|
+
name = var.network_lookup_by == "name" ? var.network_ref : null
|
|
239
|
+
with_selector = var.network_lookup_by == "tag" ? var.network_ref : null
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
# --- Dedicated network fabric (network_mode = dedicated / BYOC) ------------
|
|
243
|
+
# A fresh, isolated private network created in the DEPLOY account (the
|
|
244
|
+
# customer's, for BYOC). One cloud subnet is carved from it (below). Nothing
|
|
245
|
+
# here runs in shared/existing mode.
|
|
246
|
+
resource "hcloud_network" "env" {
|
|
247
|
+
count = local.is_dedicated ? 1 : 0
|
|
248
|
+
name = "${local.host_prefix}-net"
|
|
249
|
+
ip_range = var.vpc_cidr
|
|
250
|
+
labels = local.labels_network
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
# The stack's single cloud subnet, in the resolved network (all three modes).
|
|
254
|
+
# hcloud subnets have no standalone id — the resource id is "<network_id>-<range>".
|
|
255
|
+
resource "hcloud_network_subnet" "env" {
|
|
256
|
+
network_id = local.network_id
|
|
257
|
+
type = "cloud"
|
|
258
|
+
network_zone = local.network_zone
|
|
259
|
+
ip_range = local.compute_subnet_cidr
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
# --- Firewall: the SG analog (CIDR-scoped, label-attached) -----------------
|
|
263
|
+
# One `rule` per (security_rules entry, applicable source), direction=in. Hetzner
|
|
264
|
+
# has no SG-to-SG, so sources are CIDRs (see local.fw_rules). The firewall
|
|
265
|
+
# attaches to THIS stack's servers via the `veltrix-stack` label selector — the
|
|
266
|
+
# per-stack isolation boundary. A firewall with rules is default-DENY inbound;
|
|
267
|
+
# outbound is unrestricted by default (mirrors the AWS "all egress" rule), so no
|
|
268
|
+
# explicit egress rule is needed. The resource is always created so
|
|
269
|
+
# security_group_id is stable (matching the AWS always-present node SG).
|
|
270
|
+
resource "hcloud_firewall" "node" {
|
|
271
|
+
name = "${local.host_prefix}-fw"
|
|
272
|
+
labels = local.labels_firewall
|
|
273
|
+
|
|
274
|
+
dynamic "rule" {
|
|
275
|
+
for_each = local.fw_rules
|
|
276
|
+
content {
|
|
277
|
+
direction = "in"
|
|
278
|
+
protocol = rule.value.protocol
|
|
279
|
+
port = tostring(rule.value.port)
|
|
280
|
+
source_ips = [rule.value.cidr]
|
|
281
|
+
description = substr(rule.value.description, 0, 255)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
apply_to {
|
|
286
|
+
label_selector = "veltrix-stack==${local.stack_label}"
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
# --- Compute: one hcloud_server per compute plan item ----------------------
|
|
291
|
+
# for_each keyed by plan_key => hcloud_server.node["data/indexer-1"] etc. The
|
|
292
|
+
# server keeps its default public interface (egress); its private IP comes from
|
|
293
|
+
# the hcloud_server_network attachment below. name = function short-label,
|
|
294
|
+
# prefixed with the stack for cross-stack uniqueness (a valid RFC1123 hostname).
|
|
295
|
+
resource "hcloud_server" "node" {
|
|
296
|
+
for_each = local.compute_nodes
|
|
297
|
+
|
|
298
|
+
name = "${local.host_prefix}-${local.node_short_labels[each.key]}"
|
|
299
|
+
image = local.resolved_image
|
|
300
|
+
server_type = coalesce(
|
|
301
|
+
lookup(var.server_types_by_kind, each.value.kind, null),
|
|
302
|
+
lookup(var.server_types, each.value.tier, null),
|
|
303
|
+
var.default_server_type,
|
|
304
|
+
)
|
|
305
|
+
location = local.location
|
|
306
|
+
ssh_keys = var.ssh_keys
|
|
307
|
+
labels = local.compute_labels[each.key]
|
|
308
|
+
|
|
309
|
+
public_net {
|
|
310
|
+
ipv4_enabled = true
|
|
311
|
+
ipv6_enabled = true
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
# Attach each server to the per-stack subnet, giving it a private IP.
|
|
316
|
+
resource "hcloud_server_network" "node" {
|
|
317
|
+
for_each = local.compute_nodes
|
|
318
|
+
server_id = hcloud_server.node[each.key].id
|
|
319
|
+
subnet_id = hcloud_network_subnet.env.id
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
# --- Storage: block volume (object storage is external — see gap notes) -----
|
|
323
|
+
# Hetzner Object Storage is S3-compatible but not in the hcloud provider, so a
|
|
324
|
+
# `storage` plan item provisions a block volume instead. The app's meaning of
|
|
325
|
+
# the storage is app-defined (InfraSpec.storage); the module just provisions it.
|
|
326
|
+
resource "hcloud_volume" "objstore" {
|
|
327
|
+
count = local.has_storage ? 1 : 0
|
|
328
|
+
name = "${local.host_prefix}-vol"
|
|
329
|
+
size = var.storage_volume_gb
|
|
330
|
+
location = local.location
|
|
331
|
+
format = "ext4"
|
|
332
|
+
labels = local.labels_storage
|
|
333
|
+
delete_protection = false
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
# --- TLS ------------------------------------------------------------------
|
|
337
|
+
# NO certificate resource is created here. A MANAGED hcloud certificate needs
|
|
338
|
+
# Hetzner DNS (out of the hcloud provider); an UPLOADED certificate is
|
|
339
|
+
# provisioned out-of-band. The LB references var.certificate_arn (an hcloud
|
|
340
|
+
# certificate id) on its HTTPS service when provided (see below).
|
|
341
|
+
|
|
342
|
+
# --- Load balancer: the app's web tier + HEC ingress ----------------------
|
|
343
|
+
# hcloud_load_balancer + a network attachment (private interface) + one service
|
|
344
|
+
# + per-server targets. NO WAF (Hetzner has none — waf_enabled is a no-op) and
|
|
345
|
+
# NO LB-level MFA (alb_auth is a no-op). Both vars are accepted for contract
|
|
346
|
+
# parity only; realizing either would need an external reverse-proxy/CDN tier.
|
|
347
|
+
resource "hcloud_load_balancer" "env" {
|
|
348
|
+
count = local.has_lb ? 1 : 0
|
|
349
|
+
name = "${local.host_prefix}-lb"
|
|
350
|
+
load_balancer_type = "lb11"
|
|
351
|
+
network_zone = local.network_zone
|
|
352
|
+
labels = local.labels_lb
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
# Attach the LB to the private network so it can reach nodes on their private IPs.
|
|
356
|
+
resource "hcloud_load_balancer_network" "env" {
|
|
357
|
+
count = local.has_lb ? 1 : 0
|
|
358
|
+
load_balancer_id = hcloud_load_balancer.env[0].id
|
|
359
|
+
network_id = local.network_id
|
|
360
|
+
depends_on = [hcloud_network_subnet.env]
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
# The web service: HTTPS (with the supplied cert + automatic HTTP->HTTPS
|
|
364
|
+
# redirect) when certificate_arn is set, else plain HTTP on the listener port.
|
|
365
|
+
# Port / health-check all come from the app's load_balancer spec. Gated on
|
|
366
|
+
# has_lb_spec (plan LB item + a spec), so var.load_balancer is non-null here.
|
|
367
|
+
resource "hcloud_load_balancer_service" "web" {
|
|
368
|
+
count = local.has_lb_spec ? 1 : 0
|
|
369
|
+
load_balancer_id = hcloud_load_balancer.env[0].id
|
|
370
|
+
protocol = local.lb_use_https ? "https" : "http"
|
|
371
|
+
listen_port = var.load_balancer.listener_port
|
|
372
|
+
destination_port = var.load_balancer.target_port
|
|
373
|
+
|
|
374
|
+
dynamic "http" {
|
|
375
|
+
for_each = local.lb_use_https ? [1] : []
|
|
376
|
+
content {
|
|
377
|
+
certificates = [tonumber(var.certificate_arn)]
|
|
378
|
+
redirect_http = true
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
health_check {
|
|
383
|
+
protocol = lower(var.load_balancer.health_check_protocol != "" ? var.load_balancer.health_check_protocol : var.load_balancer.target_protocol)
|
|
384
|
+
port = var.load_balancer.target_port
|
|
385
|
+
interval = 15
|
|
386
|
+
timeout = 10
|
|
387
|
+
retries = 3
|
|
388
|
+
|
|
389
|
+
http {
|
|
390
|
+
# Hetzner uses status-code patterns (e.g. "2??"), not the AWS matcher range
|
|
391
|
+
# "200-399"; "2??"/"3??" is the faithful equivalent of the default matcher.
|
|
392
|
+
path = var.load_balancer.health_check_path
|
|
393
|
+
status_codes = ["2??", "3??"]
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
# One target per web-serving node (the kinds the app named in the LB spec's
|
|
399
|
+
# target_kinds). Keyed by plan_key so the set tracks the compute for_each.
|
|
400
|
+
# use_private_ip requires the LB + servers to share the private network, hence
|
|
401
|
+
# the depends_on on both network attachments.
|
|
402
|
+
resource "hcloud_load_balancer_target" "web" {
|
|
403
|
+
for_each = local.has_lb_spec ? local.search_targets : {}
|
|
404
|
+
type = "server"
|
|
405
|
+
load_balancer_id = hcloud_load_balancer.env[0].id
|
|
406
|
+
server_id = hcloud_server.node[each.key].id
|
|
407
|
+
use_private_ip = true
|
|
408
|
+
|
|
409
|
+
depends_on = [
|
|
410
|
+
hcloud_load_balancer_network.env,
|
|
411
|
+
hcloud_server_network.node,
|
|
412
|
+
]
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
# --- WAF: NO-OP (Hetzner has no WAF) --------------------------------------
|
|
416
|
+
# `var.waf_enabled` is accepted for cross-cloud contract parity but realizes
|
|
417
|
+
# NOTHING here — Hetzner Cloud has no WAF primitive. Web-application filtering
|
|
418
|
+
# would require an external reverse proxy / CDN (e.g. a self-managed nginx+
|
|
419
|
+
# modsecurity server or a third-party CDN WAF) in front of the LB, provisioned
|
|
420
|
+
# as a separate out-of-band tier. Documented, not faked.
|
|
421
|
+
|
|
422
|
+
# --- MFA (alb_auth): NO-OP (no LB-level OIDC on Hetzner) ------------------
|
|
423
|
+
# `var.alb_auth` is accepted for contract parity but realizes NOTHING — Hetzner
|
|
424
|
+
# LBs have no Cognito/OIDC equivalent. Enforce MFA at the app (its own SAML/OIDC)
|
|
425
|
+
# or via an external identity-aware proxy. Documented, not faked.
|
|
426
|
+
|
|
427
|
+
# --- DNS records: NONE created (see gap notes at top) ---------------------
|
|
428
|
+
# The hcloud provider has no managed-DNS resource. Per-node FQDNs are computed
|
|
429
|
+
# (local.node_fqdns) and emitted as an output for the out-of-band bring-up layer
|
|
430
|
+
# (a `timohirt/hetznerdns` stack, or /etc/hosts built from node_fqdns +
|
|
431
|
+
# instance_private_ips). dns_mode / create_private_zone / private_zone_id are
|
|
432
|
+
# kept as accepted no-op variables so the worker passes a uniform var set.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# Hetzner Cloud environment module — outputs.
|
|
3
|
+
#
|
|
4
|
+
# `resource_refs` is THE contract with the status-back path: a map of
|
|
5
|
+
# plan_key -> external cloud ref. The CI apply reads this (via `tofu output
|
|
6
|
+
# -json`) and/or the parsed apply stream and emits `resource.status
|
|
7
|
+
# {planKey, status, externalRef}` per key, which the app's onEvent hook maps
|
|
8
|
+
# onto the matching BYOL-resource row. Output NAMES are identical to the
|
|
9
|
+
# AWS module; hcloud ids are numeric-in-state so they are tostring()'d to keep
|
|
10
|
+
# the ref map a uniform map(string).
|
|
11
|
+
# =============================================================================
|
|
12
|
+
|
|
13
|
+
locals {
|
|
14
|
+
# Foundation-tier refs, included only when the tier is in the plan.
|
|
15
|
+
# * secrets / license-file: NO ref — Hetzner has no secret store (the values
|
|
16
|
+
# live in the platform vault; reported via deployment.step, not a resource).
|
|
17
|
+
# * dns: NO ref — Hetzner creates no DNS records (see gap notes in main.tf).
|
|
18
|
+
# * tls: the supplied hcloud certificate id (not created here) when present.
|
|
19
|
+
# * hec: maps to the LB's public IP when there is an LB (else a post-config
|
|
20
|
+
# step, exactly like the AWS module).
|
|
21
|
+
infra_refs = merge(
|
|
22
|
+
{ "foundation/network" = tostring(local.network_id) },
|
|
23
|
+
local.has_storage ? { "foundation/storage" = tostring(hcloud_volume.objstore[0].id) } : {},
|
|
24
|
+
(local.has_tls && var.certificate_arn != "") ? { "foundation/tls" = var.certificate_arn } : {},
|
|
25
|
+
local.has_lb ? { "foundation/load-balancer" = tostring(hcloud_load_balancer.env[0].id) } : {},
|
|
26
|
+
local.has_hec && local.has_lb ? { "ingest/hec" = hcloud_load_balancer.env[0].ipv4 } : {},
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
output "resource_refs" {
|
|
31
|
+
description = "Map of plan_key -> external cloud reference (server id / volume id / cert id / LB id). Drives per-resource status-back."
|
|
32
|
+
value = merge(
|
|
33
|
+
{ for k, s in hcloud_server.node : k => tostring(s.id) },
|
|
34
|
+
local.infra_refs,
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
output "subnet_id" {
|
|
39
|
+
description = "Per-stack cloud subnet identifier. hcloud subnets have no standalone id, so this is the composite resource id (\"<network_id>-<ip_range>\")."
|
|
40
|
+
value = hcloud_network_subnet.env.id
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
output "security_group_id" {
|
|
44
|
+
description = "Id of the compute-node firewall (the hcloud SG analog)."
|
|
45
|
+
value = tostring(hcloud_firewall.node.id)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
output "network_id" {
|
|
49
|
+
description = "Id of the hcloud_network this stack runs in — the created network for dedicated, the looked-up network for shared/existing."
|
|
50
|
+
value = tostring(local.network_id)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
output "instance_ids" {
|
|
54
|
+
description = "plan_key -> hcloud_server id for compute nodes."
|
|
55
|
+
value = { for k, s in hcloud_server.node : k => tostring(s.id) }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
output "instance_private_ips" {
|
|
59
|
+
description = "plan_key -> private IP (from the per-stack subnet attachment) for compute nodes."
|
|
60
|
+
value = { for k, n in hcloud_server_network.node : k => n.ip }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
output "node_fqdns" {
|
|
64
|
+
description = <<-EOT
|
|
65
|
+
plan_key -> function FQDN (e.g. idx1.<dns_domain>, sh1.<dns_domain>) for each
|
|
66
|
+
compute node. Derived identically to the AWS module. On Hetzner NO DNS record
|
|
67
|
+
is created for these (the hcloud provider has no managed DNS); the bring-up
|
|
68
|
+
layer consumes this map to build /etc/hosts or a hetznerdns stack out-of-band.
|
|
69
|
+
Populated whenever dns_domain is set; empty otherwise.
|
|
70
|
+
EOT
|
|
71
|
+
value = local.node_fqdns
|
|
72
|
+
}
|