@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,920 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# Azure environment module — generic, tool-agnostic. A faithful translation of
|
|
3
|
+
# sdk/opentofu/modules/aws. Driven by an app's rendered InfraSpec; NOTHING here
|
|
4
|
+
# is tool-specific.
|
|
5
|
+
#
|
|
6
|
+
# Network mode (worker-set deployment var, NOT app InfraSpec):
|
|
7
|
+
# shared — Veltrix-hosted: data-source the shared VNet + create the
|
|
8
|
+
# allocated per-stack subnet.
|
|
9
|
+
# dedicated — BYOC: CREATE a fresh VNet + App Gateway/private subnets + a NAT
|
|
10
|
+
# gateway (private egress) in the customer's subscription.
|
|
11
|
+
# existing — BYOC: data-source a customer VNet + create subnets in it.
|
|
12
|
+
# DNS mode: managed (in-account public record; App Gateway consumes a Key Vault
|
|
13
|
+
# cert) / delegated (worker does the cross-account public DNS; module uses
|
|
14
|
+
# certificate_arn) / private-only.
|
|
15
|
+
#
|
|
16
|
+
# One compute VM per compute plan item (for_each keyed by plan_key) + storage /
|
|
17
|
+
# secrets / TLS / LB / DNS per topology tier. Isolation: per-stack subnet +
|
|
18
|
+
# NSG/ASG SG-to-SG. Cost/attribution: var.tags on every taggable resource.
|
|
19
|
+
#
|
|
20
|
+
# Azure <-> AWS primitive map: VNet=VPC, Subnet=Subnet, NSG+ASG=SG self-ref,
|
|
21
|
+
# App Gateway(+WAF policy)=ALB(+WAFv2), Private DNS zone=Route53 private zone,
|
|
22
|
+
# Storage Account=S3, Key Vault=Secrets Manager. See per-resource comments.
|
|
23
|
+
# =============================================================================
|
|
24
|
+
|
|
25
|
+
locals {
|
|
26
|
+
# Short, DNS/label-safe prefix. infrastructure_id is a UUID; 8 chars is enough
|
|
27
|
+
# to disambiguate within a customer while staying under Azure name limits.
|
|
28
|
+
name_prefix = "${var.app_id}-${substr(var.infrastructure_id, 0, 8)}"
|
|
29
|
+
|
|
30
|
+
# plan_key -> plan object, for compute nodes only. This map's keys ARE the
|
|
31
|
+
# azurerm_linux_virtual_machine.node[...] addresses, so status maps 1:1 back to
|
|
32
|
+
# resource rows. Tool-agnostic: an explicit compute_kinds allow-list wins;
|
|
33
|
+
# otherwise compute = any plan item whose kind is NOT a generic foundation kind.
|
|
34
|
+
compute_nodes = {
|
|
35
|
+
for r in var.plan : r.plan_key => r
|
|
36
|
+
if(length(var.compute_kinds) > 0
|
|
37
|
+
? contains(var.compute_kinds, r.kind)
|
|
38
|
+
: !contains(var.foundation_kinds, r.kind))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Presence flags for the optional foundation tiers (derived from the plan).
|
|
42
|
+
has_storage = length([for r in var.plan : r if r.kind == "storage"]) > 0
|
|
43
|
+
has_secrets = length([for r in var.plan : r if r.kind == "secrets"]) > 0
|
|
44
|
+
has_license_file = length([for r in var.plan : r if r.kind == "license-file"]) > 0
|
|
45
|
+
has_tls = length([for r in var.plan : r if r.kind == "tls"]) > 0
|
|
46
|
+
has_lb = length([for r in var.plan : r if r.kind == "load-balancer"]) > 0
|
|
47
|
+
has_dns = length([for r in var.plan : r if r.kind == "dns"]) > 0 && var.public_dns_zone_name != ""
|
|
48
|
+
has_hec = length([for r in var.plan : r if r.kind == "hec"]) > 0
|
|
49
|
+
|
|
50
|
+
# A single Key Vault backs both the secrets bundle and the BYOL license file.
|
|
51
|
+
has_key_vault = local.has_secrets || local.has_license_file
|
|
52
|
+
|
|
53
|
+
# A per-resource Name/plan_key is merged INTO the canonical tag set so every
|
|
54
|
+
# object still carries var.tags (incl. Veltrix:ManagedBy) verbatim.
|
|
55
|
+
base_tags = var.tags
|
|
56
|
+
|
|
57
|
+
# --- Network mode: hosted-shared vs BYOC dedicated/existing -----------
|
|
58
|
+
is_dedicated = var.network_mode == "dedicated"
|
|
59
|
+
lookup_network = var.network_mode == "shared" || var.network_mode == "existing"
|
|
60
|
+
|
|
61
|
+
# SSH key vs generated-password auth for the VMs.
|
|
62
|
+
use_ssh_key = var.admin_ssh_public_key != ""
|
|
63
|
+
|
|
64
|
+
# Resolved network + subnet sets, uniform across all three modes:
|
|
65
|
+
# dedicated -> the created VNet + its private (compute) / appgw subnets
|
|
66
|
+
# shared/existing -> the looked-up VNet + the single allocated subnet
|
|
67
|
+
# Azure subnets span the region (no per-AZ split like AWS), so each set is a
|
|
68
|
+
# single subnet — but we keep them as lists to mirror the AWS locals exactly.
|
|
69
|
+
network_id = local.is_dedicated ? azurerm_virtual_network.env[0].id : data.azurerm_virtual_network.shared[0].id
|
|
70
|
+
|
|
71
|
+
compute_subnet_ids = local.is_dedicated ? [azurerm_subnet.private[0].id] : [azurerm_subnet.env[0].id]
|
|
72
|
+
lb_subnet_id = local.is_dedicated ? azurerm_subnet.appgw[0].id : one(azurerm_subnet.appgw_shared[*].id)
|
|
73
|
+
|
|
74
|
+
# Compute nodes are spread round-robin across the available compute subnets
|
|
75
|
+
# (a single subnet on Azure, so all land on it — the loop mirrors AWS 1:1).
|
|
76
|
+
compute_subnet_for = {
|
|
77
|
+
for idx, k in sort(keys(local.compute_nodes)) : k =>
|
|
78
|
+
local.compute_subnet_ids[idx % length(local.compute_subnet_ids)]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# Plan-time-known CIDR of the App Gateway subnet, used as the "alb" source in
|
|
82
|
+
# NSG rules (the AWS module references the ALB SG id; on Azure we reference the
|
|
83
|
+
# gateway subnet prefix). dedicated -> carved from vpc_cidr; else the caller's
|
|
84
|
+
# appgw_subnet_cidr. Never a computed id, so it is safe in a for_each/rule set.
|
|
85
|
+
appgw_subnet_prefix = local.is_dedicated ? cidrsubnet(var.vpc_cidr, 4, 0) : var.appgw_subnet_cidr
|
|
86
|
+
|
|
87
|
+
# --- DNS mode: managed (in-account) / delegated (worker x-account) / none
|
|
88
|
+
dns_managed = var.dns_mode == "managed"
|
|
89
|
+
has_public_dns = var.dns_mode != "private-only"
|
|
90
|
+
|
|
91
|
+
# The App Gateway listener consumes a Key Vault certificate. Azure has no
|
|
92
|
+
# in-module public CA issuance (unlike AWS ACM), so BOTH managed and delegated
|
|
93
|
+
# reference var.certificate_arn (a KV cert secret id) here.
|
|
94
|
+
listener_cert_secret_id = var.certificate_arn
|
|
95
|
+
|
|
96
|
+
# --- Derived front-door / listener gates ------------------------------
|
|
97
|
+
# has_lb = the plan carries a load-balancer item.
|
|
98
|
+
# has_lb_spec = has_lb AND the app supplied a load_balancer spec. Unlike AWS
|
|
99
|
+
# (which can create a bare aws_lb on has_lb alone), an Azure Application
|
|
100
|
+
# Gateway REQUIRES a full backend/probe/listener/rule config, so the gateway
|
|
101
|
+
# is gated on has_lb_spec.
|
|
102
|
+
has_lb_spec = local.has_lb && var.load_balancer != null
|
|
103
|
+
# HTTPS listener needs BOTH a KV cert id AND a user-assigned identity that can
|
|
104
|
+
# read it. When either is missing the App Gateway serves HTTP only (so a
|
|
105
|
+
# partial plan still applies) — this is the documented Azure divergence from
|
|
106
|
+
# AWS's has_listener (AWS issues its own cert in managed mode).
|
|
107
|
+
has_listener = local.has_lb_spec && var.dns_domain != "" && var.certificate_arn != "" && var.appgw_identity_id != ""
|
|
108
|
+
# alb_auth (Cognito MFA) has NO Application Gateway equivalent — accepted but
|
|
109
|
+
# no-op'd (see the App Gateway block). Kept for tfvars uniformity.
|
|
110
|
+
alb_auth_enabled = var.alb_auth.enabled
|
|
111
|
+
|
|
112
|
+
# Azure protocol enum is title-case ("Http"/"Https"); the spec is upper-case.
|
|
113
|
+
# Guarded with a null check because locals are always evaluated (the App
|
|
114
|
+
# Gateway that consumes them only exists when has_lb_spec).
|
|
115
|
+
appgw_backend_protocol = var.load_balancer != null ? title(lower(var.load_balancer.target_protocol)) : "Http"
|
|
116
|
+
appgw_probe_protocol = (var.load_balancer != null && try(var.load_balancer.health_check_protocol, "") != "") ? (
|
|
117
|
+
title(lower(var.load_balancer.health_check_protocol))
|
|
118
|
+
) : local.appgw_backend_protocol
|
|
119
|
+
|
|
120
|
+
# Compute nodes that sit behind the front door — the kinds the app named as LB
|
|
121
|
+
# targets. try() keeps it null-safe when there is no spec.
|
|
122
|
+
lb_target_kinds = try(var.load_balancer.target_kinds, [])
|
|
123
|
+
search_targets = {
|
|
124
|
+
for k, r in local.compute_nodes : k => r
|
|
125
|
+
if contains(local.lb_target_kinds, r.kind)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
# --- Per-node function DNS labels (reused verbatim from the AWS module) ----
|
|
129
|
+
node_prefix = {
|
|
130
|
+
for k, r in local.compute_nodes : k => lookup(var.dns_prefixes, r.kind, r.kind)
|
|
131
|
+
}
|
|
132
|
+
node_keys_by_kind = {
|
|
133
|
+
for kind in distinct([for k, r in local.compute_nodes : r.kind]) : kind => sort([
|
|
134
|
+
for k, r in local.compute_nodes : k if r.kind == kind
|
|
135
|
+
])
|
|
136
|
+
}
|
|
137
|
+
node_short_labels = {
|
|
138
|
+
for k, r in local.compute_nodes : k => format(
|
|
139
|
+
"%s%d",
|
|
140
|
+
local.node_prefix[k],
|
|
141
|
+
index(local.node_keys_by_kind[r.kind], k) + 1,
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
# plan_key -> function FQDN in the private zone. Empty when no domain is set.
|
|
145
|
+
node_fqdns = var.dns_domain != "" ? {
|
|
146
|
+
for k, label in local.node_short_labels : k => "${label}.${var.dns_domain}"
|
|
147
|
+
} : {}
|
|
148
|
+
|
|
149
|
+
# --- Private DNS zone resolution --------------------------------------
|
|
150
|
+
# Either create a private zone for var.dns_domain (create_private_zone) or point
|
|
151
|
+
# at a caller-supplied one (private_zone_id != "" => reuse). want_private_dns is
|
|
152
|
+
# the plan-time-known intent used to gate the per-node record for_each. On Azure
|
|
153
|
+
# a private DNS record is addressed by zone NAME (= dns_domain) + resource group,
|
|
154
|
+
# not a zone id — so private_zone_id acts only as the "reuse existing" flag.
|
|
155
|
+
create_private_zone = var.create_private_zone && var.dns_domain != "" && var.private_zone_id == ""
|
|
156
|
+
want_private_dns = var.dns_domain != "" && (var.private_zone_id != "" || local.create_private_zone)
|
|
157
|
+
private_zone_name = var.dns_domain
|
|
158
|
+
private_zone_rg = local.create_private_zone ? azurerm_resource_group.env.name : var.private_zone_resource_group
|
|
159
|
+
|
|
160
|
+
# Public managed record name relative to public_dns_zone_name ("@" for apex).
|
|
161
|
+
public_record_name = var.dns_domain == var.public_dns_zone_name ? "@" : trimsuffix(var.dns_domain, ".${var.public_dns_zone_name}")
|
|
162
|
+
|
|
163
|
+
# --- Flattened NSG ingress rules (from the app's security_rules) --------
|
|
164
|
+
# Each (rule, source) pair becomes one azurerm_network_security_rule, keyed
|
|
165
|
+
# "<port>-<protocol>-<source>". "alb"-sourced rules are dropped when there is no
|
|
166
|
+
# load balancer (no gateway subnet to reference). This replaces any hardcoded,
|
|
167
|
+
# tool-specific port list — the app declares its ports in InfraSpec.securityRules.
|
|
168
|
+
sg_ingress = merge([
|
|
169
|
+
for r in var.security_rules : {
|
|
170
|
+
for s in r.sources : "${r.port}-${r.protocol}-${s}" => {
|
|
171
|
+
port = r.port
|
|
172
|
+
protocol = r.protocol
|
|
173
|
+
source = s
|
|
174
|
+
description = r.description != "" ? r.description : "port ${r.port} (${r.protocol}) from ${s}"
|
|
175
|
+
}
|
|
176
|
+
if !(s == "alb" && !local.has_lb)
|
|
177
|
+
}
|
|
178
|
+
]...)
|
|
179
|
+
|
|
180
|
+
# NSG rules need an ascending, unique priority. Derive it deterministically from
|
|
181
|
+
# the lexically-sorted key set (plan-time-known) so every apply is stable.
|
|
182
|
+
sg_ingress_keys = sort(keys(local.sg_ingress))
|
|
183
|
+
sg_rule_priority = { for i, k in local.sg_ingress_keys : k => 100 + i }
|
|
184
|
+
|
|
185
|
+
# App Gateway sub-resource names (referenced across multiple blocks).
|
|
186
|
+
appgw_frontend_ip_name = "frontend-ip"
|
|
187
|
+
appgw_http_port_name = "port-http"
|
|
188
|
+
appgw_https_port_name = "port-https"
|
|
189
|
+
appgw_backend_pool_name = "node-pool"
|
|
190
|
+
appgw_backend_http_name = "node-http-settings"
|
|
191
|
+
appgw_probe_name = "node-probe"
|
|
192
|
+
appgw_http_listener_name = "http-listener"
|
|
193
|
+
appgw_https_listener_name = "https-listener"
|
|
194
|
+
appgw_ssl_cert_name = "listener-cert"
|
|
195
|
+
appgw_gateway_ip_name = "gateway-ip-config"
|
|
196
|
+
appgw_redirect_name = "http-to-https"
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
# Current subscription/tenant/principal — for Key Vault tenant + access policy.
|
|
200
|
+
data "azurerm_client_config" "current" {}
|
|
201
|
+
|
|
202
|
+
# --- Per-stack resource group ---------------------------------------------
|
|
203
|
+
# Azure requires a resource group for every resource (AWS has no equivalent). We
|
|
204
|
+
# create ONE per stack so a stack's resources are grouped + destroyed together.
|
|
205
|
+
# In shared/existing mode the compute subnet still lands in the VNet's OWN RG
|
|
206
|
+
# (subnets belong to their VNet), so only that subnet lives outside this RG.
|
|
207
|
+
resource "azurerm_resource_group" "env" {
|
|
208
|
+
name = "${local.name_prefix}-rg"
|
|
209
|
+
location = var.region
|
|
210
|
+
|
|
211
|
+
tags = merge(local.base_tags, {
|
|
212
|
+
Name = "${local.name_prefix}-rg"
|
|
213
|
+
"Veltrix:PlanKey" = "foundation/network"
|
|
214
|
+
"Veltrix:Tier" = "foundation"
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
# --- Network lookup (network_mode = shared | existing) --------------------
|
|
219
|
+
# The VNet is data-sourced (never created) — the shared Veltrix VNet for hosted,
|
|
220
|
+
# or a customer-designated VNet for BYOC "existing". Absent when dedicated. Azure
|
|
221
|
+
# resolves a VNet by name + resource group (there is no by-id data source).
|
|
222
|
+
data "azurerm_virtual_network" "shared" {
|
|
223
|
+
count = local.lookup_network ? 1 : 0
|
|
224
|
+
name = var.network_ref
|
|
225
|
+
resource_group_name = var.network_resource_group
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
# The stack's single allocated compute subnet, in the looked-up VNet
|
|
229
|
+
# (shared|existing). Created into the VNet's RG. Azure subnets are not taggable.
|
|
230
|
+
resource "azurerm_subnet" "env" {
|
|
231
|
+
count = local.lookup_network ? 1 : 0
|
|
232
|
+
name = "${local.name_prefix}-subnet"
|
|
233
|
+
resource_group_name = data.azurerm_virtual_network.shared[0].resource_group_name
|
|
234
|
+
virtual_network_name = data.azurerm_virtual_network.shared[0].name
|
|
235
|
+
address_prefixes = [var.subnet_cidr]
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
# Dedicated App Gateway subnet in shared/existing mode. An Application Gateway
|
|
239
|
+
# MUST have its own empty subnet, so we carve one from appgw_subnet_cidr whenever
|
|
240
|
+
# there is a load balancer (the Azure analogue of AWS extra_lb_subnet_ids).
|
|
241
|
+
resource "azurerm_subnet" "appgw_shared" {
|
|
242
|
+
count = local.lookup_network && local.has_lb ? 1 : 0
|
|
243
|
+
name = "${local.name_prefix}-appgw-subnet"
|
|
244
|
+
resource_group_name = data.azurerm_virtual_network.shared[0].resource_group_name
|
|
245
|
+
virtual_network_name = data.azurerm_virtual_network.shared[0].name
|
|
246
|
+
address_prefixes = [var.appgw_subnet_cidr]
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
# --- Dedicated network fabric (network_mode = dedicated / BYOC) ------------
|
|
250
|
+
# A fresh, isolated VNet created in the DEPLOY subscription (the customer's, for
|
|
251
|
+
# BYOC): one App Gateway subnet + one private compute subnet + a NAT gateway for
|
|
252
|
+
# private egress. Nothing here runs in shared/existing mode.
|
|
253
|
+
resource "azurerm_virtual_network" "env" {
|
|
254
|
+
count = local.is_dedicated ? 1 : 0
|
|
255
|
+
name = "${local.name_prefix}-vnet"
|
|
256
|
+
location = var.region
|
|
257
|
+
resource_group_name = azurerm_resource_group.env.name
|
|
258
|
+
address_space = [var.vpc_cidr]
|
|
259
|
+
|
|
260
|
+
tags = merge(local.base_tags, {
|
|
261
|
+
Name = "${local.name_prefix}-vnet"
|
|
262
|
+
"Veltrix:PlanKey" = "foundation/network"
|
|
263
|
+
"Veltrix:Tier" = "foundation"
|
|
264
|
+
})
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
# App Gateway subnet (dedicated) — cidrsubnet(/16, 4, 0) => first /20.
|
|
268
|
+
resource "azurerm_subnet" "appgw" {
|
|
269
|
+
count = local.is_dedicated ? 1 : 0
|
|
270
|
+
name = "${local.name_prefix}-appgw-subnet"
|
|
271
|
+
resource_group_name = azurerm_resource_group.env.name
|
|
272
|
+
virtual_network_name = azurerm_virtual_network.env[0].name
|
|
273
|
+
address_prefixes = [cidrsubnet(var.vpc_cidr, 4, 0)]
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
# Private compute subnet (dedicated) — cidrsubnet(/16, 4, 1) => second /20.
|
|
277
|
+
resource "azurerm_subnet" "private" {
|
|
278
|
+
count = local.is_dedicated ? 1 : 0
|
|
279
|
+
name = "${local.name_prefix}-private-subnet"
|
|
280
|
+
resource_group_name = azurerm_resource_group.env.name
|
|
281
|
+
virtual_network_name = azurerm_virtual_network.env[0].name
|
|
282
|
+
address_prefixes = [cidrsubnet(var.vpc_cidr, 4, 1)]
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
# NAT gateway for private-subnet egress (peers, object storage, license/updates).
|
|
286
|
+
resource "azurerm_public_ip" "nat" {
|
|
287
|
+
count = local.is_dedicated ? 1 : 0
|
|
288
|
+
name = "${local.name_prefix}-nat-pip"
|
|
289
|
+
location = var.region
|
|
290
|
+
resource_group_name = azurerm_resource_group.env.name
|
|
291
|
+
allocation_method = "Static"
|
|
292
|
+
sku = "Standard"
|
|
293
|
+
|
|
294
|
+
tags = merge(local.base_tags, {
|
|
295
|
+
Name = "${local.name_prefix}-nat-pip"
|
|
296
|
+
"Veltrix:PlanKey" = "foundation/network"
|
|
297
|
+
"Veltrix:Tier" = "foundation"
|
|
298
|
+
})
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
resource "azurerm_nat_gateway" "env" {
|
|
302
|
+
count = local.is_dedicated ? 1 : 0
|
|
303
|
+
name = "${local.name_prefix}-nat"
|
|
304
|
+
location = var.region
|
|
305
|
+
resource_group_name = azurerm_resource_group.env.name
|
|
306
|
+
sku_name = "Standard"
|
|
307
|
+
|
|
308
|
+
tags = merge(local.base_tags, {
|
|
309
|
+
Name = "${local.name_prefix}-nat"
|
|
310
|
+
"Veltrix:PlanKey" = "foundation/network"
|
|
311
|
+
"Veltrix:Tier" = "foundation"
|
|
312
|
+
})
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
resource "azurerm_nat_gateway_public_ip_association" "env" {
|
|
316
|
+
count = local.is_dedicated ? 1 : 0
|
|
317
|
+
nat_gateway_id = azurerm_nat_gateway.env[0].id
|
|
318
|
+
public_ip_address_id = azurerm_public_ip.nat[0].id
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
# Route private-subnet egress through the NAT (the AWS private route table + NAT).
|
|
322
|
+
resource "azurerm_subnet_nat_gateway_association" "private" {
|
|
323
|
+
count = local.is_dedicated ? 1 : 0
|
|
324
|
+
subnet_id = azurerm_subnet.private[0].id
|
|
325
|
+
nat_gateway_id = azurerm_nat_gateway.env[0].id
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
# --- Security: SG-to-SG least privilege (NSG + Application Security Group) -----
|
|
329
|
+
# The AWS module self-references the node SG; on Azure "self" is expressed with an
|
|
330
|
+
# Application Security Group (ASG). Every compute NIC joins the node ASG, and
|
|
331
|
+
# NSG rules for "self" use it as source + destination — scoping intra-cluster
|
|
332
|
+
# traffic to THIS stack's instances, not a whole CIDR. "alb" => the App Gateway
|
|
333
|
+
# subnet prefix; "admin" => var.admin_cidr.
|
|
334
|
+
|
|
335
|
+
resource "azurerm_application_security_group" "node" {
|
|
336
|
+
name = "${local.name_prefix}-node-asg"
|
|
337
|
+
location = var.region
|
|
338
|
+
resource_group_name = azurerm_resource_group.env.name
|
|
339
|
+
|
|
340
|
+
tags = merge(local.base_tags, {
|
|
341
|
+
Name = "${local.name_prefix}-node-asg"
|
|
342
|
+
"Veltrix:PlanKey" = "foundation/network"
|
|
343
|
+
"Veltrix:Tier" = "foundation"
|
|
344
|
+
})
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
# Node NSG. Rules live in a single for_each'd standalone rule resource (below);
|
|
348
|
+
# do NOT add inline security_rule blocks here — mixing inline + standalone rules
|
|
349
|
+
# on one NSG makes them clobber each other (same caveat as the AWS module).
|
|
350
|
+
resource "azurerm_network_security_group" "node" {
|
|
351
|
+
name = "${local.name_prefix}-nsg"
|
|
352
|
+
location = var.region
|
|
353
|
+
resource_group_name = azurerm_resource_group.env.name
|
|
354
|
+
|
|
355
|
+
tags = merge(local.base_tags, {
|
|
356
|
+
Name = "${local.name_prefix}-nsg"
|
|
357
|
+
"Veltrix:PlanKey" = "foundation/network"
|
|
358
|
+
"Veltrix:Tier" = "foundation"
|
|
359
|
+
})
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
# One inbound rule per (security_rules entry, source), from local.sg_ingress.
|
|
363
|
+
# Exactly one source form is set per rule (the others are null/omitted):
|
|
364
|
+
# "self" -> source ASG; "alb" -> the App Gateway subnet prefix; "admin" ->
|
|
365
|
+
# var.admin_cidr. Destination is always the node ASG. NSG rules are not taggable.
|
|
366
|
+
resource "azurerm_network_security_rule" "node" {
|
|
367
|
+
for_each = local.sg_ingress
|
|
368
|
+
|
|
369
|
+
name = "allow-${each.key}"
|
|
370
|
+
priority = local.sg_rule_priority[each.key]
|
|
371
|
+
direction = "Inbound"
|
|
372
|
+
access = "Allow"
|
|
373
|
+
protocol = title(each.value.protocol)
|
|
374
|
+
source_port_range = "*"
|
|
375
|
+
destination_port_range = tostring(each.value.port)
|
|
376
|
+
description = each.value.description
|
|
377
|
+
resource_group_name = azurerm_resource_group.env.name
|
|
378
|
+
network_security_group_name = azurerm_network_security_group.node.name
|
|
379
|
+
|
|
380
|
+
source_application_security_group_ids = each.value.source == "self" ? [azurerm_application_security_group.node.id] : null
|
|
381
|
+
source_address_prefix = each.value.source == "admin" ? var.admin_cidr : (
|
|
382
|
+
each.value.source == "alb" ? local.appgw_subnet_prefix : null
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
destination_application_security_group_ids = [azurerm_application_security_group.node.id]
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
# Egress: Azure NSGs carry default rules that already permit outbound to the VNet
|
|
389
|
+
# and the Internet (AllowVnetOutBound / AllowInternetOutBound), so no explicit
|
|
390
|
+
# all-egress rule is needed (the AWS module adds one because AWS SGs deny egress
|
|
391
|
+
# by default). Nodes can therefore reach peers, object storage, and updates.
|
|
392
|
+
|
|
393
|
+
# --- Compute: one Linux VM (+ NIC) per compute plan item ------------------
|
|
394
|
+
# for_each keyed by plan_key => azurerm_linux_virtual_machine.node["data/indexer-1"].
|
|
395
|
+
# Hostname = the node's function short-label (idx1, sh1, ...).
|
|
396
|
+
|
|
397
|
+
resource "azurerm_network_interface" "node" {
|
|
398
|
+
for_each = local.compute_nodes
|
|
399
|
+
name = "${local.name_prefix}-${local.node_short_labels[each.key]}-nic"
|
|
400
|
+
location = var.region
|
|
401
|
+
resource_group_name = azurerm_resource_group.env.name
|
|
402
|
+
|
|
403
|
+
ip_configuration {
|
|
404
|
+
name = "internal"
|
|
405
|
+
subnet_id = local.compute_subnet_for[each.key]
|
|
406
|
+
private_ip_address_allocation = "Dynamic"
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
tags = merge(local.base_tags, {
|
|
410
|
+
Name = "${local.name_prefix}-${local.node_short_labels[each.key]}-nic"
|
|
411
|
+
"Veltrix:PlanKey" = each.key
|
|
412
|
+
"Veltrix:Tier" = each.value.tier
|
|
413
|
+
"Veltrix:Kind" = each.value.kind
|
|
414
|
+
"Veltrix:Role" = each.value.role
|
|
415
|
+
})
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
# Attach the node NSG to each compute NIC (closest analogue of AWS's per-instance
|
|
419
|
+
# vpc_security_group_ids). Associations are not taggable.
|
|
420
|
+
resource "azurerm_network_interface_security_group_association" "node" {
|
|
421
|
+
for_each = local.compute_nodes
|
|
422
|
+
network_interface_id = azurerm_network_interface.node[each.key].id
|
|
423
|
+
network_security_group_id = azurerm_network_security_group.node.id
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
# Join each compute NIC to the node ASG so "self" NSG rules scope to this stack.
|
|
427
|
+
resource "azurerm_network_interface_application_security_group_association" "node" {
|
|
428
|
+
for_each = local.compute_nodes
|
|
429
|
+
network_interface_id = azurerm_network_interface.node[each.key].id
|
|
430
|
+
application_security_group_id = azurerm_application_security_group.node.id
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
# When no SSH key is supplied, generate a complexity-compliant password so a
|
|
434
|
+
# minimal plan still applies (Azure requires SSH key OR password on a Linux VM).
|
|
435
|
+
resource "random_password" "vm" {
|
|
436
|
+
count = !local.use_ssh_key && length(local.compute_nodes) > 0 ? 1 : 0
|
|
437
|
+
length = 24
|
|
438
|
+
min_lower = 2
|
|
439
|
+
min_upper = 2
|
|
440
|
+
min_numeric = 2
|
|
441
|
+
min_special = 2
|
|
442
|
+
override_special = "!@#$%*()-_"
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
resource "azurerm_linux_virtual_machine" "node" {
|
|
446
|
+
for_each = local.compute_nodes
|
|
447
|
+
name = "${local.name_prefix}-${local.node_short_labels[each.key]}"
|
|
448
|
+
computer_name = local.node_short_labels[each.key]
|
|
449
|
+
location = var.region
|
|
450
|
+
resource_group_name = azurerm_resource_group.env.name
|
|
451
|
+
# Multi-AZ placement: pin to the node's availability zone when set (null = non-zonal).
|
|
452
|
+
zone = each.value.zone
|
|
453
|
+
size = coalesce(
|
|
454
|
+
lookup(var.vm_sizes_by_kind, each.value.kind, null),
|
|
455
|
+
lookup(var.vm_sizes, each.value.tier, null),
|
|
456
|
+
var.vm_size,
|
|
457
|
+
)
|
|
458
|
+
admin_username = var.admin_username
|
|
459
|
+
network_interface_ids = [azurerm_network_interface.node[each.key].id]
|
|
460
|
+
|
|
461
|
+
disable_password_authentication = local.use_ssh_key
|
|
462
|
+
admin_password = local.use_ssh_key ? null : one(random_password.vm[*].result)
|
|
463
|
+
|
|
464
|
+
dynamic "admin_ssh_key" {
|
|
465
|
+
for_each = local.use_ssh_key ? [1] : []
|
|
466
|
+
content {
|
|
467
|
+
username = var.admin_username
|
|
468
|
+
public_key = var.admin_ssh_public_key
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
os_disk {
|
|
473
|
+
caching = "ReadWrite"
|
|
474
|
+
storage_account_type = "StandardSSD_LRS"
|
|
475
|
+
disk_size_gb = var.os_disk_gb
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
# Custom image id when supplied; else the Ubuntu 22.04 LTS marketplace fallback
|
|
479
|
+
# (scaffold only — production must pass a tool-preinstalled image_ref).
|
|
480
|
+
source_image_id = var.image_ref != "" ? var.image_ref : null
|
|
481
|
+
|
|
482
|
+
dynamic "source_image_reference" {
|
|
483
|
+
for_each = var.image_ref == "" ? [1] : []
|
|
484
|
+
content {
|
|
485
|
+
publisher = "Canonical"
|
|
486
|
+
offer = "0001-com-ubuntu-server-jammy"
|
|
487
|
+
sku = "22_04-lts-gen2"
|
|
488
|
+
version = "latest"
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
tags = merge(local.base_tags, {
|
|
493
|
+
Name = "${local.name_prefix}-${local.node_short_labels[each.key]}"
|
|
494
|
+
"Veltrix:PlanKey" = each.key
|
|
495
|
+
"Veltrix:Tier" = each.value.tier
|
|
496
|
+
"Veltrix:Kind" = each.value.kind
|
|
497
|
+
"Veltrix:Role" = each.value.role
|
|
498
|
+
})
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
# --- Storage: object-storage account/container (SmartStore, warm/cold, etc.) --
|
|
502
|
+
# Generic private blob storage for the app's bulk/object storage. The tool's
|
|
503
|
+
# meaning is app-defined (InfraSpec.storage); the module just provisions it.
|
|
504
|
+
# Storage account names are globally unique, 3-24 chars, lowercase alphanumeric.
|
|
505
|
+
resource "random_string" "storage_suffix" {
|
|
506
|
+
count = local.has_storage ? 1 : 0
|
|
507
|
+
length = 6
|
|
508
|
+
upper = false
|
|
509
|
+
special = false
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
resource "azurerm_storage_account" "objstore" {
|
|
513
|
+
count = local.has_storage ? 1 : 0
|
|
514
|
+
name = "${substr("st${replace(lower(local.name_prefix), "/[^a-z0-9]/", "")}", 0, 18)}${random_string.storage_suffix[0].result}"
|
|
515
|
+
resource_group_name = azurerm_resource_group.env.name
|
|
516
|
+
location = var.region
|
|
517
|
+
account_tier = "Standard"
|
|
518
|
+
account_replication_type = "LRS"
|
|
519
|
+
min_tls_version = "TLS1_2"
|
|
520
|
+
allow_nested_items_to_be_public = false
|
|
521
|
+
|
|
522
|
+
tags = merge(local.base_tags, {
|
|
523
|
+
Name = "${local.name_prefix}-objstore"
|
|
524
|
+
"Veltrix:PlanKey" = "foundation/storage"
|
|
525
|
+
"Veltrix:Tier" = "foundation"
|
|
526
|
+
})
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
# Private container inside the account. Storage containers are not taggable.
|
|
530
|
+
resource "azurerm_storage_container" "objstore" {
|
|
531
|
+
count = local.has_storage ? 1 : 0
|
|
532
|
+
name = "objstore"
|
|
533
|
+
storage_account_name = azurerm_storage_account.objstore[0].name
|
|
534
|
+
container_access_type = "private"
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
# --- Secrets + BYOL license: a per-tenant Key Vault ------------------------
|
|
538
|
+
# One Key Vault backs the secrets bundle (admin seed / pass4SymmKey / etc.) AND
|
|
539
|
+
# the BYOL license file (each a secret). Created when EITHER tier is in the plan.
|
|
540
|
+
# An access policy grants the apply principal secret data-plane permissions.
|
|
541
|
+
resource "random_string" "kv_suffix" {
|
|
542
|
+
count = local.has_key_vault ? 1 : 0
|
|
543
|
+
length = 6
|
|
544
|
+
upper = false
|
|
545
|
+
special = false
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
resource "azurerm_key_vault" "secrets" {
|
|
549
|
+
count = local.has_key_vault ? 1 : 0
|
|
550
|
+
name = "${substr("kv${replace(lower(local.name_prefix), "/[^a-z0-9]/", "")}", 0, 18)}${random_string.kv_suffix[0].result}"
|
|
551
|
+
location = var.region
|
|
552
|
+
resource_group_name = azurerm_resource_group.env.name
|
|
553
|
+
tenant_id = data.azurerm_client_config.current.tenant_id
|
|
554
|
+
sku_name = "standard"
|
|
555
|
+
|
|
556
|
+
access_policy {
|
|
557
|
+
tenant_id = data.azurerm_client_config.current.tenant_id
|
|
558
|
+
object_id = data.azurerm_client_config.current.object_id
|
|
559
|
+
secret_permissions = ["Get", "List", "Set", "Delete", "Purge", "Recover"]
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
tags = merge(local.base_tags, {
|
|
563
|
+
Name = "${local.name_prefix}-kv"
|
|
564
|
+
"Veltrix:PlanKey" = "foundation/secrets"
|
|
565
|
+
"Veltrix:Tier" = "foundation"
|
|
566
|
+
})
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
# Seed secret for the secrets tier. Azure requires a value (AWS leaves the secret
|
|
570
|
+
# container empty), so we generate a real random seed; bring-up overwrites it,
|
|
571
|
+
# and ignore_changes keeps that rotation from showing as drift.
|
|
572
|
+
resource "random_password" "secret_seed" {
|
|
573
|
+
count = local.has_secrets ? 1 : 0
|
|
574
|
+
length = 32
|
|
575
|
+
min_lower = 2
|
|
576
|
+
min_upper = 2
|
|
577
|
+
min_numeric = 2
|
|
578
|
+
min_special = 2
|
|
579
|
+
override_special = "!@#$%*()-_"
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
resource "azurerm_key_vault_secret" "env" {
|
|
583
|
+
count = local.has_secrets ? 1 : 0
|
|
584
|
+
name = "env-secrets"
|
|
585
|
+
value = random_password.secret_seed[0].result
|
|
586
|
+
key_vault_id = azurerm_key_vault.secrets[0].id
|
|
587
|
+
|
|
588
|
+
tags = merge(local.base_tags, {
|
|
589
|
+
Name = "${local.name_prefix}-secrets"
|
|
590
|
+
"Veltrix:PlanKey" = "foundation/secrets"
|
|
591
|
+
"Veltrix:Tier" = "foundation"
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
lifecycle {
|
|
595
|
+
ignore_changes = [value]
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
# BYOL license file (stored as a secret; the real content is uploaded post-apply).
|
|
600
|
+
resource "azurerm_key_vault_secret" "license" {
|
|
601
|
+
count = local.has_license_file ? 1 : 0
|
|
602
|
+
name = "byol-license"
|
|
603
|
+
value = "REPLACE_WITH_LICENSE"
|
|
604
|
+
key_vault_id = azurerm_key_vault.secrets[0].id
|
|
605
|
+
|
|
606
|
+
tags = merge(local.base_tags, {
|
|
607
|
+
Name = "${local.name_prefix}-license"
|
|
608
|
+
"Veltrix:PlanKey" = "foundation/license-file"
|
|
609
|
+
"Veltrix:Tier" = "foundation"
|
|
610
|
+
})
|
|
611
|
+
|
|
612
|
+
lifecycle {
|
|
613
|
+
ignore_changes = [value]
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
# --- TLS certificate note --------------------------------------------------
|
|
618
|
+
# Azure has no in-module public CA issuance equivalent to AWS ACM. The App
|
|
619
|
+
# Gateway HTTPS listener consumes a Key Vault certificate referenced by
|
|
620
|
+
# var.certificate_arn (see the App Gateway ssl_certificate block). In dns_mode =
|
|
621
|
+
# managed the caller/worker provisions that KV cert (and the public A record is
|
|
622
|
+
# still created below); in delegated the worker also supplies certificate_arn.
|
|
623
|
+
# There is therefore NO azurerm resource here mirroring aws_acm_certificate.
|
|
624
|
+
|
|
625
|
+
# --- Front door: Application Gateway (+ WAF policy) ------------------------
|
|
626
|
+
# The Azure analogue of the ALB (+ WAFv2). Public IP + gateway-subnet IP config +
|
|
627
|
+
# backend pool (target-kind NICs, attached below) + backend HTTP settings + probe
|
|
628
|
+
# (from the app's load_balancer spec) + an HTTP listener, plus an HTTPS listener +
|
|
629
|
+
# HTTP->HTTPS redirect when a KV cert + identity are supplied. WAF_v2 SKU with an
|
|
630
|
+
# OWASP policy when waf_enabled, else Standard_v2. Gated on has_lb_spec.
|
|
631
|
+
|
|
632
|
+
resource "azurerm_public_ip" "appgw" {
|
|
633
|
+
count = local.has_lb_spec ? 1 : 0
|
|
634
|
+
name = "${local.name_prefix}-appgw-pip"
|
|
635
|
+
location = var.region
|
|
636
|
+
resource_group_name = azurerm_resource_group.env.name
|
|
637
|
+
allocation_method = "Static"
|
|
638
|
+
sku = "Standard"
|
|
639
|
+
|
|
640
|
+
tags = merge(local.base_tags, {
|
|
641
|
+
Name = "${local.name_prefix}-appgw-pip"
|
|
642
|
+
"Veltrix:PlanKey" = "foundation/load-balancer"
|
|
643
|
+
"Veltrix:Tier" = "foundation"
|
|
644
|
+
})
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
# OWASP-managed WAF policy, associated with the gateway when waf_enabled. Mirrors
|
|
648
|
+
# the AWS WAFv2 web ACL (managed rule groups). Default action is Prevention/block.
|
|
649
|
+
resource "azurerm_web_application_firewall_policy" "env" {
|
|
650
|
+
count = local.has_lb_spec && var.waf_enabled ? 1 : 0
|
|
651
|
+
name = "${local.name_prefix}-waf"
|
|
652
|
+
resource_group_name = azurerm_resource_group.env.name
|
|
653
|
+
location = var.region
|
|
654
|
+
|
|
655
|
+
policy_settings {
|
|
656
|
+
enabled = true
|
|
657
|
+
mode = "Prevention"
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
managed_rules {
|
|
661
|
+
managed_rule_set {
|
|
662
|
+
type = "OWASP"
|
|
663
|
+
version = "3.2"
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
tags = merge(local.base_tags, {
|
|
668
|
+
Name = "${local.name_prefix}-waf"
|
|
669
|
+
"Veltrix:PlanKey" = "foundation/load-balancer"
|
|
670
|
+
"Veltrix:Tier" = "foundation"
|
|
671
|
+
})
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
resource "azurerm_application_gateway" "env" {
|
|
675
|
+
count = local.has_lb_spec ? 1 : 0
|
|
676
|
+
name = "${local.name_prefix}-appgw"
|
|
677
|
+
location = var.region
|
|
678
|
+
resource_group_name = azurerm_resource_group.env.name
|
|
679
|
+
|
|
680
|
+
sku {
|
|
681
|
+
name = var.waf_enabled ? "WAF_v2" : "Standard_v2"
|
|
682
|
+
tier = var.waf_enabled ? "WAF_v2" : "Standard_v2"
|
|
683
|
+
capacity = 2
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
# OWASP WAF policy when enabled; null (and Standard_v2) otherwise.
|
|
687
|
+
firewall_policy_id = one(azurerm_web_application_firewall_policy.env[*].id)
|
|
688
|
+
|
|
689
|
+
gateway_ip_configuration {
|
|
690
|
+
name = local.appgw_gateway_ip_name
|
|
691
|
+
subnet_id = local.lb_subnet_id
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
frontend_ip_configuration {
|
|
695
|
+
name = local.appgw_frontend_ip_name
|
|
696
|
+
public_ip_address_id = azurerm_public_ip.appgw[0].id
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
frontend_port {
|
|
700
|
+
name = local.appgw_http_port_name
|
|
701
|
+
port = 80
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
dynamic "frontend_port" {
|
|
705
|
+
for_each = local.has_listener ? [1] : []
|
|
706
|
+
content {
|
|
707
|
+
name = local.appgw_https_port_name
|
|
708
|
+
port = var.load_balancer.listener_port
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
# Empty backend pool; target-kind NICs are attached via the association below
|
|
713
|
+
# (the Azure analogue of aws_lb_target_group_attachment, keyed by plan_key).
|
|
714
|
+
backend_address_pool {
|
|
715
|
+
name = local.appgw_backend_pool_name
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
# port/protocol/health check all come from the app's load_balancer spec.
|
|
719
|
+
backend_http_settings {
|
|
720
|
+
name = local.appgw_backend_http_name
|
|
721
|
+
cookie_based_affinity = "Disabled"
|
|
722
|
+
port = var.load_balancer.target_port
|
|
723
|
+
protocol = local.appgw_backend_protocol
|
|
724
|
+
request_timeout = 30
|
|
725
|
+
probe_name = local.appgw_probe_name
|
|
726
|
+
pick_host_name_from_backend_address = false
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
probe {
|
|
730
|
+
name = local.appgw_probe_name
|
|
731
|
+
protocol = local.appgw_probe_protocol
|
|
732
|
+
path = var.load_balancer.health_check_path
|
|
733
|
+
host = "127.0.0.1"
|
|
734
|
+
interval = 30
|
|
735
|
+
timeout = 5
|
|
736
|
+
unhealthy_threshold = 3
|
|
737
|
+
pick_host_name_from_backend_http_settings = false
|
|
738
|
+
|
|
739
|
+
match {
|
|
740
|
+
status_code = [var.load_balancer.health_check_matcher]
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
# Always-present HTTP listener on :80.
|
|
745
|
+
http_listener {
|
|
746
|
+
name = local.appgw_http_listener_name
|
|
747
|
+
frontend_ip_configuration_name = local.appgw_frontend_ip_name
|
|
748
|
+
frontend_port_name = local.appgw_http_port_name
|
|
749
|
+
protocol = "Http"
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
# HTTPS listener + KV cert + user-assigned identity, only when has_listener.
|
|
753
|
+
dynamic "http_listener" {
|
|
754
|
+
for_each = local.has_listener ? [1] : []
|
|
755
|
+
content {
|
|
756
|
+
name = local.appgw_https_listener_name
|
|
757
|
+
frontend_ip_configuration_name = local.appgw_frontend_ip_name
|
|
758
|
+
frontend_port_name = local.appgw_https_port_name
|
|
759
|
+
protocol = "Https"
|
|
760
|
+
ssl_certificate_name = local.appgw_ssl_cert_name
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
dynamic "ssl_certificate" {
|
|
765
|
+
for_each = local.has_listener ? [1] : []
|
|
766
|
+
content {
|
|
767
|
+
name = local.appgw_ssl_cert_name
|
|
768
|
+
key_vault_secret_id = local.listener_cert_secret_id
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
# The identity that reads the Key Vault cert. has_listener already requires
|
|
773
|
+
# appgw_identity_id to be non-empty.
|
|
774
|
+
dynamic "identity" {
|
|
775
|
+
for_each = local.has_listener ? [1] : []
|
|
776
|
+
content {
|
|
777
|
+
type = "UserAssigned"
|
|
778
|
+
identity_ids = [var.appgw_identity_id]
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
# HTTP -> HTTPS permanent redirect (the AWS HTTP(80) -> 301 listener), used by
|
|
783
|
+
# the http-redirect routing rule below.
|
|
784
|
+
dynamic "redirect_configuration" {
|
|
785
|
+
for_each = local.has_listener ? [1] : []
|
|
786
|
+
content {
|
|
787
|
+
name = local.appgw_redirect_name
|
|
788
|
+
redirect_type = "Permanent"
|
|
789
|
+
target_listener_name = local.appgw_https_listener_name
|
|
790
|
+
include_path = true
|
|
791
|
+
include_query_string = true
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
# No TLS cert -> the HTTP listener forwards straight to the backend.
|
|
796
|
+
dynamic "request_routing_rule" {
|
|
797
|
+
for_each = local.has_listener ? [] : [1]
|
|
798
|
+
content {
|
|
799
|
+
name = "http-forward"
|
|
800
|
+
rule_type = "Basic"
|
|
801
|
+
http_listener_name = local.appgw_http_listener_name
|
|
802
|
+
backend_address_pool_name = local.appgw_backend_pool_name
|
|
803
|
+
backend_http_settings_name = local.appgw_backend_http_name
|
|
804
|
+
priority = 100
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
# TLS cert present -> HTTPS forwards to the backend; HTTP redirects to HTTPS.
|
|
809
|
+
dynamic "request_routing_rule" {
|
|
810
|
+
for_each = local.has_listener ? [1] : []
|
|
811
|
+
content {
|
|
812
|
+
name = "https-forward"
|
|
813
|
+
rule_type = "Basic"
|
|
814
|
+
http_listener_name = local.appgw_https_listener_name
|
|
815
|
+
backend_address_pool_name = local.appgw_backend_pool_name
|
|
816
|
+
backend_http_settings_name = local.appgw_backend_http_name
|
|
817
|
+
priority = 100
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
dynamic "request_routing_rule" {
|
|
822
|
+
for_each = local.has_listener ? [1] : []
|
|
823
|
+
content {
|
|
824
|
+
name = "http-redirect"
|
|
825
|
+
rule_type = "Basic"
|
|
826
|
+
http_listener_name = local.appgw_http_listener_name
|
|
827
|
+
redirect_configuration_name = local.appgw_redirect_name
|
|
828
|
+
priority = 200
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
# NOTE (documented gap): AWS enforces optional Cognito/OIDC MFA at the ALB
|
|
833
|
+
# listener (authenticate-cognito). Azure Application Gateway has NO equivalent
|
|
834
|
+
# built-in auth action — the Azure path is Azure AD Application Proxy / Front
|
|
835
|
+
# Door + Entra ID, a follow-on. var.alb_auth (alb_auth_enabled) is therefore
|
|
836
|
+
# ACCEPTED but NOT wired here. Do NOT fake it.
|
|
837
|
+
|
|
838
|
+
tags = merge(local.base_tags, {
|
|
839
|
+
Name = "${local.name_prefix}-appgw"
|
|
840
|
+
"Veltrix:PlanKey" = "foundation/load-balancer"
|
|
841
|
+
"Veltrix:Tier" = "foundation"
|
|
842
|
+
})
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
# One backend-pool membership per web-serving node (the kinds named in the LB
|
|
846
|
+
# spec's target_kinds). Keyed by plan_key so the set tracks the compute for_each —
|
|
847
|
+
# the Azure analogue of aws_lb_target_group_attachment. Associations are untagged.
|
|
848
|
+
resource "azurerm_network_interface_application_gateway_backend_address_pool_association" "node" {
|
|
849
|
+
for_each = local.has_lb_spec ? local.search_targets : {}
|
|
850
|
+
network_interface_id = azurerm_network_interface.node[each.key].id
|
|
851
|
+
ip_configuration_name = "internal"
|
|
852
|
+
backend_address_pool_id = one([for p in azurerm_application_gateway.env[0].backend_address_pool : p.id if p.name == local.appgw_backend_pool_name])
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
# --- Public DNS record ----------------------------------------------------
|
|
856
|
+
# Created in-account only for dns_mode = managed. delegated => the worker writes
|
|
857
|
+
# it cross-account into Veltrix's zone; private-only => no public record. An alias
|
|
858
|
+
# A record (target_resource_id) points at the App Gateway public IP — the Azure
|
|
859
|
+
# analogue of the AWS alias to the ALB.
|
|
860
|
+
resource "azurerm_dns_a_record" "env" {
|
|
861
|
+
count = local.dns_managed && local.has_dns && local.has_lb_spec ? 1 : 0
|
|
862
|
+
name = local.public_record_name
|
|
863
|
+
zone_name = var.public_dns_zone_name
|
|
864
|
+
resource_group_name = var.public_dns_rg
|
|
865
|
+
ttl = 60
|
|
866
|
+
target_resource_id = azurerm_public_ip.appgw[0].id
|
|
867
|
+
|
|
868
|
+
tags = merge(local.base_tags, {
|
|
869
|
+
Name = "${local.name_prefix}-public-dns"
|
|
870
|
+
"Veltrix:PlanKey" = "foundation/dns"
|
|
871
|
+
"Veltrix:Tier" = "foundation"
|
|
872
|
+
})
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
# --- Private DNS: intra-cluster function FQDNs ----------------------------
|
|
876
|
+
# A Private DNS zone (linked to the deploy VNet) gives every node a stable
|
|
877
|
+
# function FQDN (idx1.<domain>, sh1.<domain>, ...). The bring-up layer uses
|
|
878
|
+
# node_fqdns (see outputs) to build its inventory. The PUBLIC record above is
|
|
879
|
+
# unaffected. Either create the zone here (create_private_zone) or reuse a
|
|
880
|
+
# caller-supplied one (private_zone_id != "").
|
|
881
|
+
resource "azurerm_private_dns_zone" "env" {
|
|
882
|
+
count = local.create_private_zone ? 1 : 0
|
|
883
|
+
name = var.dns_domain
|
|
884
|
+
resource_group_name = azurerm_resource_group.env.name
|
|
885
|
+
|
|
886
|
+
tags = merge(local.base_tags, {
|
|
887
|
+
Name = "${local.name_prefix}-private-zone"
|
|
888
|
+
"Veltrix:PlanKey" = "foundation/dns"
|
|
889
|
+
"Veltrix:Tier" = "foundation"
|
|
890
|
+
})
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
resource "azurerm_private_dns_zone_virtual_network_link" "env" {
|
|
894
|
+
count = local.create_private_zone ? 1 : 0
|
|
895
|
+
name = "${local.name_prefix}-dns-link"
|
|
896
|
+
resource_group_name = azurerm_resource_group.env.name
|
|
897
|
+
private_dns_zone_name = azurerm_private_dns_zone.env[0].name
|
|
898
|
+
virtual_network_id = local.network_id
|
|
899
|
+
registration_enabled = false
|
|
900
|
+
|
|
901
|
+
tags = merge(local.base_tags, {
|
|
902
|
+
Name = "${local.name_prefix}-dns-link"
|
|
903
|
+
"Veltrix:PlanKey" = "foundation/dns"
|
|
904
|
+
"Veltrix:Tier" = "foundation"
|
|
905
|
+
})
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
# One A record per compute node -> its private IP, keyed by plan_key so the set
|
|
909
|
+
# tracks the compute for_each. Gated on want_private_dns (plan-time-known). The
|
|
910
|
+
# record NAME is the relative label (idx1), the zone NAME is dns_domain. zone_name
|
|
911
|
+
# resolves via the created zone when present (establishing the dependency), else
|
|
912
|
+
# the reused zone name.
|
|
913
|
+
resource "azurerm_private_dns_a_record" "node" {
|
|
914
|
+
for_each = local.want_private_dns ? local.node_short_labels : {}
|
|
915
|
+
name = each.value
|
|
916
|
+
zone_name = coalesce(one(azurerm_private_dns_zone.env[*].name), local.private_zone_name)
|
|
917
|
+
resource_group_name = local.private_zone_rg
|
|
918
|
+
ttl = 60
|
|
919
|
+
records = [azurerm_network_interface.node[each.key].private_ip_address]
|
|
920
|
+
}
|