@swovohq/fuel 0.2.2-alpha.88 → 0.2.2-alpha.89

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/README.md +65 -2
  2. package/dist/commands/create-app.d.ts.map +1 -1
  3. package/dist/commands/create-app.js +9 -3
  4. package/dist/commands/create-app.js.map +1 -1
  5. package/dist/commands/infra-deploy.d.ts.map +1 -1
  6. package/dist/commands/infra-deploy.js +3 -0
  7. package/dist/commands/infra-deploy.js.map +1 -1
  8. package/dist/commands/infra-init.d.ts.map +1 -1
  9. package/dist/commands/infra-init.js +63 -6
  10. package/dist/commands/infra-init.js.map +1 -1
  11. package/dist/infra/credentials.d.ts.map +1 -1
  12. package/dist/infra/credentials.js +5 -2
  13. package/dist/infra/credentials.js.map +1 -1
  14. package/dist/infra/github-client.d.ts +6 -0
  15. package/dist/infra/github-client.d.ts.map +1 -1
  16. package/dist/infra/github-client.js +30 -0
  17. package/dist/infra/github-client.js.map +1 -1
  18. package/dist/infra/github-token-link.d.ts +4 -0
  19. package/dist/infra/github-token-link.d.ts.map +1 -0
  20. package/dist/infra/github-token-link.js +28 -0
  21. package/dist/infra/github-token-link.js.map +1 -0
  22. package/dist/infra/orchestrator.d.ts +4 -1
  23. package/dist/infra/orchestrator.d.ts.map +1 -1
  24. package/dist/infra/orchestrator.js +16 -7
  25. package/dist/infra/orchestrator.js.map +1 -1
  26. package/dist/infra/preflight.d.ts.map +1 -1
  27. package/dist/infra/preflight.js +9 -1
  28. package/dist/infra/preflight.js.map +1 -1
  29. package/dist/infra/template-engine.d.ts +0 -4
  30. package/dist/infra/template-engine.d.ts.map +1 -1
  31. package/dist/infra/template-engine.js +36 -12
  32. package/dist/infra/template-engine.js.map +1 -1
  33. package/dist/infra/types.d.ts.map +1 -1
  34. package/dist/infra/types.js +58 -6
  35. package/dist/infra/types.js.map +1 -1
  36. package/dist/infra-templates/TODO-azure-prod.md +119 -0
  37. package/dist/infra-templates/modules/azure-container-app/main.tf +124 -0
  38. package/dist/infra-templates/modules/azure-container-app/outputs.tf +24 -0
  39. package/dist/infra-templates/modules/azure-container-app/variables.tf +80 -0
  40. package/dist/infra-templates/modules/azure-container-apps-environment/main.tf +18 -0
  41. package/dist/infra-templates/modules/azure-container-apps-environment/outputs.tf +14 -0
  42. package/dist/infra-templates/modules/azure-container-apps-environment/variables.tf +24 -0
  43. package/dist/infra-templates/modules/azure-container-registry/main.tf +10 -0
  44. package/dist/infra-templates/modules/azure-container-registry/outputs.tf +14 -0
  45. package/dist/infra-templates/modules/azure-container-registry/variables.tf +20 -0
  46. package/dist/infra-templates/modules/azure-front-door/main.tf +80 -0
  47. package/dist/infra-templates/modules/azure-front-door/outputs.tf +20 -0
  48. package/dist/infra-templates/modules/azure-front-door/variables.tf +26 -0
  49. package/dist/infra-templates/modules/azure-github-oidc/main.tf +48 -0
  50. package/dist/infra-templates/modules/azure-github-oidc/outputs.tf +9 -0
  51. package/dist/infra-templates/modules/azure-github-oidc/variables.tf +69 -0
  52. package/dist/infra-templates/modules/azure-key-vault/main.tf +21 -0
  53. package/dist/infra-templates/modules/azure-key-vault/outputs.tf +11 -0
  54. package/dist/infra-templates/modules/azure-key-vault/variables.tf +43 -0
  55. package/dist/infra-templates/modules/azure-monitor/main.tf +29 -0
  56. package/dist/infra-templates/modules/azure-monitor/outputs.tf +17 -0
  57. package/dist/infra-templates/modules/azure-monitor/variables.tf +32 -0
  58. package/dist/infra-templates/modules/azure-network/main.tf +134 -0
  59. package/dist/infra-templates/modules/azure-network/outputs.tf +44 -0
  60. package/dist/infra-templates/modules/azure-network/variables.tf +56 -0
  61. package/dist/infra-templates/modules/azure-postgresql-flexible/main.tf +34 -0
  62. package/dist/infra-templates/modules/azure-postgresql-flexible/outputs.tf +20 -0
  63. package/dist/infra-templates/modules/azure-postgresql-flexible/variables.tf +54 -0
  64. package/dist/infra-templates/modules/azure-redis-cache/main.tf +32 -0
  65. package/dist/infra-templates/modules/azure-redis-cache/outputs.tf +15 -0
  66. package/dist/infra-templates/modules/azure-redis-cache/variables.tf +30 -0
  67. package/dist/infra-templates/modules/azure-resource-group/main.tf +5 -0
  68. package/dist/infra-templates/modules/azure-resource-group/outputs.tf +11 -0
  69. package/dist/infra-templates/modules/azure-resource-group/variables.tf +16 -0
  70. package/dist/infra-templates/modules/azure-sql-database/main.tf +63 -0
  71. package/dist/infra-templates/modules/azure-sql-database/outputs.tf +20 -0
  72. package/dist/infra-templates/modules/azure-sql-database/variables.tf +78 -0
  73. package/dist/infra-templates/modules/azure-storage-account/main.tf +13 -0
  74. package/dist/infra-templates/modules/azure-storage-account/outputs.tf +16 -0
  75. package/dist/infra-templates/modules/azure-storage-account/variables.tf +38 -0
  76. package/dist/infra-templates/modules/cloud-watch/event-bridge.tf +15 -0
  77. package/dist/infra-templates/modules/cloud-watch/lambda.tf +84 -0
  78. package/dist/infra-templates/modules/cloud-watch/lambda_function.zip +0 -0
  79. package/dist/infra-templates/modules/cloud-watch/lambda_function_deploy.zip +0 -0
  80. package/dist/infra-templates/modules/cloud-watch/main.tf +156 -0
  81. package/dist/infra-templates/modules/cloud-watch/sns.tf +3 -0
  82. package/dist/infra-templates/modules/cloud-watch/variables.tf +38 -0
  83. package/dist/infra-templates/modules/ecs-core/alb.tf +126 -0
  84. package/dist/infra-templates/modules/ecs-core/cloudwatch.tf +48 -0
  85. package/dist/infra-templates/modules/ecs-core/iam.tf +95 -0
  86. package/dist/infra-templates/modules/ecs-core/main.tf +12 -0
  87. package/dist/infra-templates/modules/ecs-core/outputs.tf +44 -0
  88. package/dist/infra-templates/modules/ecs-core/secrets.tf +9 -0
  89. package/dist/infra-templates/modules/ecs-core/security_groups.tf +73 -0
  90. package/dist/infra-templates/modules/ecs-core/variables.tf +96 -0
  91. package/dist/infra-templates/modules/ecs-service/ecs_autoscaling.tf +51 -0
  92. package/dist/infra-templates/modules/ecs-service/ecs_service.tf +39 -0
  93. package/dist/infra-templates/modules/ecs-service/ecs_task_definition.tf +56 -0
  94. package/dist/infra-templates/modules/ecs-service/variables.tf +170 -0
  95. package/dist/infra-templates/modules/elastic-container-registry/main.tf +32 -0
  96. package/dist/infra-templates/modules/elastic-container-registry/outputs.tf +9 -0
  97. package/dist/infra-templates/modules/elastic-container-registry/variables.tf +25 -0
  98. package/dist/infra-templates/modules/github-oidc-role/main.tf +46 -0
  99. package/dist/infra-templates/modules/github-oidc-role/outputs.tf +4 -0
  100. package/dist/infra-templates/modules/github-oidc-role/variables.tf +11 -0
  101. package/dist/infra-templates/modules/guardduty/main.tf +8 -0
  102. package/dist/infra-templates/modules/iam/main.tf +205 -0
  103. package/dist/infra-templates/modules/iam/outputs.tf +3 -0
  104. package/dist/infra-templates/modules/iam/variables.tf +4 -0
  105. package/dist/infra-templates/modules/random-key/main.tf +11 -0
  106. package/dist/infra-templates/modules/random-key/outputs.tf +4 -0
  107. package/dist/infra-templates/modules/random-key/variables.tf +10 -0
  108. package/dist/infra-templates/modules/redis/main.tf +47 -0
  109. package/dist/infra-templates/modules/redis/outputs.tf +8 -0
  110. package/dist/infra-templates/modules/redis/variables.tf +20 -0
  111. package/dist/infra-templates/modules/relational-database-service/main.tf +44 -0
  112. package/dist/infra-templates/modules/relational-database-service/outputs.tf +13 -0
  113. package/dist/infra-templates/modules/relational-database-service/security-group.tf +15 -0
  114. package/dist/infra-templates/modules/relational-database-service/variables.tf +50 -0
  115. package/dist/infra-templates/modules/secret/main.tf +14 -0
  116. package/dist/infra-templates/modules/secret/outputs.tf +3 -0
  117. package/dist/infra-templates/modules/secret/variables.tf +8 -0
  118. package/dist/infra-templates/modules/simple-storage-service/main.tf +28 -0
  119. package/dist/infra-templates/modules/simple-storage-service/variables.tf +4 -0
  120. package/dist/infra-templates/modules/vpc/data.tf +2 -0
  121. package/dist/infra-templates/modules/vpc/main.tf +76 -0
  122. package/dist/infra-templates/modules/vpc/outputs.tf +23 -0
  123. package/dist/infra-templates/modules/vpc/variables.tf +5 -0
  124. package/dist/infra-templates/package.json +6 -0
  125. package/dist/infra-templates/templates/azure/MANUAL_DEPLOY.md +168 -0
  126. package/dist/infra-templates/templates/azure/backend.tf +8 -0
  127. package/dist/infra-templates/templates/azure/data.tf +21 -0
  128. package/dist/infra-templates/templates/azure/default.tfvars +0 -0
  129. package/dist/infra-templates/templates/azure/locals.tf +116 -0
  130. package/dist/infra-templates/templates/azure/main.tf +421 -0
  131. package/dist/infra-templates/templates/azure/outputs.tf +40 -0
  132. package/dist/infra-templates/templates/azure/providers.tf +65 -0
  133. package/dist/infra-templates/templates/azure/variables.tf +69 -0
  134. package/dist/infra-templates/templates/azure/workflows/build_and_deploy.yml +155 -0
  135. package/dist/infra-templates/templates/azure/workflows/tests.yml +63 -0
  136. package/dist/infra-templates/templates/ecs/README.md +225 -0
  137. package/dist/infra-templates/templates/ecs/backend.tf +7 -0
  138. package/dist/infra-templates/templates/ecs/data.tf +6 -0
  139. package/dist/infra-templates/templates/ecs/default.tfvars +1 -0
  140. package/dist/infra-templates/templates/ecs/locals.tf +97 -0
  141. package/dist/infra-templates/templates/ecs/main.tf +185 -0
  142. package/dist/infra-templates/templates/ecs/outputs.tf +42 -0
  143. package/dist/infra-templates/templates/ecs/providers.tf +21 -0
  144. package/dist/infra-templates/templates/ecs/variables.tf +105 -0
  145. package/dist/infra-templates/templates/ecs/workflows/build_and_deploy.yml +188 -0
  146. package/dist/infra-templates/templates/ecs/workflows/tests.yml +65 -0
  147. package/package.json +2 -2
  148. package/schema/fuel.schema.json +78 -44
@@ -0,0 +1,421 @@
1
+ module "random_key" {
2
+ source = "../../modules/random-key"
3
+ name = "nest_encryption_password"
4
+ }
5
+
6
+ module "acr" {
7
+ source = "../../modules/azure-container-registry"
8
+ name = var.acr_name
9
+ resource_group_name = data.azurerm_resource_group.this.name
10
+ location = var.location
11
+ }
12
+
13
+ module "monitor" {
14
+ source = "../../modules/azure-monitor"
15
+ project_name = var.project_name
16
+ resource_group_name = data.azurerm_resource_group.this.name
17
+ location = var.location
18
+ daily_data_cap_gb = try(local.credentials.monitor.daily_data_cap_gb, 1)
19
+ retention_days = try(local.credentials.monitor.retention_days, 90)
20
+ dashboard_count = try(local.credentials.monitor.dashboard_count, 0)
21
+ }
22
+
23
+ module "network" {
24
+ source = "../../modules/azure-network"
25
+ project_name = var.project_name
26
+ resource_group_name = data.azurerm_resource_group.this.name
27
+ location = var.location
28
+ postgres_needed = local.network_needs_postgres
29
+ mssql_needed = local.network_needs_mssql
30
+ redis_needed = local.network_needs_redis
31
+ }
32
+
33
+ module "container_env" {
34
+ source = "../../modules/azure-container-apps-environment"
35
+ name = "${var.project_name}-cae"
36
+ resource_group_name = data.azurerm_resource_group.this.name
37
+ location = var.location
38
+ log_analytics_workspace_id = module.monitor.log_analytics_workspace_id
39
+ infrastructure_subnet_id = module.network.container_apps_subnet_id
40
+ }
41
+
42
+ module "github_oidc" {
43
+ source = "../../modules/azure-github-oidc"
44
+ project_name = var.project_name
45
+ github_organization = var.github_organization
46
+ github_repo_name = var.github_repo_name
47
+ branches = distinct([for a in local.app_list : a.branch])
48
+ role_assignment_scope = data.azurerm_resource_group.this.id
49
+ existing_client_id = local.github_oidc_existing_client_id
50
+ existing_principal_id = local.github_oidc_existing_principal_id
51
+ manage_role_assignment = local.github_oidc_manage_role_assignment
52
+ enabled = local.github_oidc_enabled
53
+ }
54
+
55
+ resource "azurerm_role_assignment" "oidc_acr_push" {
56
+ count = local.github_oidc_enabled && local.github_oidc_manage_role_assignment ? 1 : 0
57
+
58
+ scope = module.acr.id
59
+ role_definition_name = "AcrPush"
60
+ principal_id = module.github_oidc.principal_id
61
+ }
62
+
63
+ module "storage_account" {
64
+ source = "../../modules/azure-storage-account"
65
+ name = local.storage_account_name
66
+ resource_group_name = data.azurerm_resource_group.this.name
67
+ location = var.location
68
+ account_kind = try(local.credentials.storage_account.account_kind, "BlockBlobStorage")
69
+ account_tier = try(local.credentials.storage_account.account_tier, "Premium")
70
+ account_replication_type = try(local.credentials.storage_account.account_replication_type, "LRS")
71
+ access_tier = try(local.credentials.storage_account.access_tier, "Hot")
72
+ }
73
+
74
+ module "key_vault" {
75
+ for_each = { for a in local.app_list : a.app_name => a }
76
+
77
+ source = "../../modules/azure-key-vault"
78
+ name = local.app_key_vault_names[each.key]
79
+ resource_group_name = data.azurerm_resource_group.this.name
80
+ location = var.location
81
+ tenant_id = var.azure_tenant_id
82
+ sku_name = try(local.credentials.key_vault.sku_name, "standard")
83
+ ip_rules = [local.tofu_operator_ip_cidr]
84
+ }
85
+
86
+ # Each app can only read secrets from its own vault — not every app's.
87
+ resource "azurerm_role_assignment" "app_key_vault_secrets_user" {
88
+ for_each = module.container_app
89
+
90
+ scope = module.key_vault[each.key].id
91
+ role_definition_name = "Key Vault Secrets User"
92
+ principal_id = each.value.principal_id
93
+ }
94
+
95
+ # object_id of whoever is authenticated to run Tofu — resolved via ARM, not Graph, so it
96
+ # works with a bare service-principal client_id/secret and needs no extra Graph permission
97
+ # (unlike module.github_oidc's existing_principal_id, which looks up ANOTHER principal).
98
+ data "azurerm_client_config" "current" {}
99
+
100
+ # Public IP of the machine running `tofu apply`, allowlisted on each vault's firewall below —
101
+ # Key Vault ignores network_acls entirely while public access is off, and writing secrets is
102
+ # a data-plane call the ARM-level Contributor role on the resource group doesn't cover.
103
+ data "http" "tofu_operator_ip" {
104
+ url = "https://api.ipify.org?format=text"
105
+ }
106
+
107
+ # Data-plane RBAC to write secrets — Contributor (assigned to the deploying SP elsewhere)
108
+ # only covers ARM/control-plane operations on the vault resource itself.
109
+ resource "azurerm_role_assignment" "tofu_key_vault_secrets_officer" {
110
+ for_each = module.key_vault
111
+
112
+ scope = each.value.id
113
+ role_definition_name = "Key Vault Secrets Officer"
114
+ principal_id = data.azurerm_client_config.current.object_id
115
+ }
116
+
117
+ # Azure RBAC role assignments take time to propagate — writing the secret in the same apply
118
+ # that grants the role can 403 without this buffer.
119
+ resource "time_sleep" "key_vault_rbac_propagation" {
120
+ depends_on = [azurerm_role_assignment.tofu_key_vault_secrets_officer]
121
+ create_duration = "30s"
122
+ }
123
+
124
+ # Pre-populates each app's vault with the SAME fully-merged env var set the Container App
125
+ # itself gets (fuel.yml `variables:` + DB creds + Redis + defaults) — mirrors the ECS
126
+ # template's module.secret_manager, which stores local.env_vars (the merged map, not just
127
+ # the raw fuel.yml variables) as one JSON blob per app. Duplicated here rather than shared
128
+ # via a single locals map for the same reason module.container_app computes it inline (see
129
+ # locals.tf note): a shared map can't partially resolve if one app's DB/Redis module errors.
130
+ resource "azurerm_key_vault_secret" "app_variables" {
131
+ for_each = { for a in local.app_list : a.app_name => a }
132
+
133
+ name = "app-variables"
134
+ value = jsonencode(merge(
135
+ {
136
+ APPLICATIONINSIGHTS_CONNECTION_STRING = module.monitor.connection_string
137
+ },
138
+
139
+ each.value.technology == "nest" ? {
140
+ NODE_TLS_REJECT_UNAUTHORIZED = "0"
141
+ API_KEY_HEADER = "x-api-key"
142
+ ENCRYPTION_PASSWORD = module.random_key.value
143
+ HOST_URL = try(local.app_variables[each.key].host_url, "https://${each.value.domain}")
144
+ FRONT_END_URL = try(local.app_variables[each.key].front_end_url, "https://${each.key}")
145
+ APP_NAME = "Fuel App"
146
+ APP_COMPANY = try(local.app_variables[each.key].app_company, "Swovo")
147
+ } : {},
148
+
149
+ each.value.technology == "react" ? {
150
+ NEXT_PUBLIC_API_HOST = try(local.app_build_env[each.key]["NEXT_PUBLIC_API_HOST"], "")
151
+ } : {},
152
+
153
+ each.value.redis_enabled ? {
154
+ REDIS_HOST = module.redis[each.key].hostname
155
+ REDIS_PORT = module.redis[each.key].ssl_port
156
+ REDIS_URL = "rediss://:${module.redis[each.key].primary_access_key}@${module.redis[each.key].hostname}:${module.redis[each.key].ssl_port}/0"
157
+ } : {},
158
+
159
+ each.value.database_enabled && each.value.database_engine == "mssql" ? {
160
+ DATABASE_HOST = module.sql_database[each.key].fqdn
161
+ DATABASE_USERNAME = module.sql_database[each.key].admin_username
162
+ DATABASE_NAME = module.sql_database[each.key].database_name
163
+ DATABASE_PASSWORD = module.sql_database[each.key].admin_password
164
+ DATABASE_URL = "sqlserver://${module.sql_database[each.key].fqdn}:1433;database=${module.sql_database[each.key].database_name};user=${module.sql_database[each.key].admin_username};password=${module.sql_database[each.key].admin_password};encrypt=true"
165
+ } : {},
166
+
167
+ each.value.database_enabled && each.value.database_engine == "postgres" ? {
168
+ DATABASE_HOST = module.postgres_database[each.key].fqdn
169
+ DATABASE_USERNAME = module.postgres_database[each.key].admin_username
170
+ DATABASE_NAME = module.postgres_database[each.key].database_name
171
+ DATABASE_PASSWORD = module.postgres_database[each.key].admin_password
172
+ DATABASE_URL = "postgresql://${module.postgres_database[each.key].admin_username}:${module.postgres_database[each.key].admin_password}@${module.postgres_database[each.key].fqdn}/${module.postgres_database[each.key].database_name}"
173
+ } : {},
174
+
175
+ length(try(local.app_variables[each.key], {})) > 0 ? {
176
+ for k, v in local.app_variables[each.key] : upper(k) => v
177
+ } : {}
178
+ ))
179
+ key_vault_id = module.key_vault[each.key].id
180
+
181
+ depends_on = [time_sleep.key_vault_rbac_propagation]
182
+ }
183
+
184
+ module "sql_database" {
185
+ for_each = { for app in local.app_list : app.app_name => app if app.database_enabled && app.database_engine == "mssql" }
186
+
187
+ source = "../../modules/azure-sql-database"
188
+ server_name = "${var.project_name}-${each.value.app_name}-sql"
189
+ database_name = var.db_name
190
+ resource_group_name = data.azurerm_resource_group.this.name
191
+ location = var.location
192
+ sku_name = each.value.database_sku
193
+ min_capacity = each.value.database_min_capacity
194
+ storage_account_type = each.value.database_storage_type
195
+ pitr_retention_days = each.value.database_pitr_days
196
+ ltr_weekly_retention = each.value.database_ltr
197
+ admin_username = var.db_username
198
+ private_endpoint_subnet_id = module.network.private_endpoints_subnet_id
199
+ private_dns_zone_id = module.network.mssql_dns_zone_id
200
+ }
201
+
202
+ module "postgres_database" {
203
+ for_each = { for app in local.app_list : app.app_name => app if app.database_enabled && app.database_engine == "postgres" }
204
+
205
+ source = "../../modules/azure-postgresql-flexible"
206
+ server_name = "${var.project_name}-${each.value.app_name}-psql"
207
+ database_name = var.db_name
208
+ resource_group_name = data.azurerm_resource_group.this.name
209
+ location = var.location
210
+ storage_mb = each.value.database_storage_mb
211
+ admin_username = var.db_username
212
+ delegated_subnet_id = module.network.postgres_subnet_id
213
+ private_dns_zone_id = module.network.postgres_dns_zone_id
214
+
215
+ # Postgres Flexible Server's VNet integration requires the private DNS zone to already
216
+ # be linked to the VNet — depend on the whole network module, not just its outputs.
217
+ depends_on = [module.network]
218
+ }
219
+
220
+ module "redis" {
221
+ for_each = { for app in local.app_list : app.app_name => app if app.redis_enabled }
222
+
223
+ source = "../../modules/azure-redis-cache"
224
+ name = "${each.value.app_name}-redis"
225
+ resource_group_name = data.azurerm_resource_group.this.name
226
+ location = var.location
227
+ sku_name = each.value.redis_sku
228
+ private_endpoint_subnet_id = module.network.private_endpoints_subnet_id
229
+ private_dns_zone_id = module.network.redis_dns_zone_id
230
+ }
231
+
232
+ module "container_app" {
233
+ for_each = { for app in local.container_apps : app.app_name => app }
234
+
235
+ source = "../../modules/azure-container-app"
236
+ name = each.value.app_name
237
+ resource_group_name = data.azurerm_resource_group.this.name
238
+ container_app_environment_id = module.container_env.id
239
+ image = local.bootstrap_container_image
240
+ target_port = each.value.port
241
+ external_ingress = true
242
+ front_door_backend_cidrs = local.front_door_backend_ipv4_cidrs
243
+ cpu = each.value.cpu
244
+ memory = each.value.memory
245
+ concurrent_requests = each.value.concurrent_requests
246
+ min_replicas = each.value.min_replicas
247
+ max_replicas = each.value.max_replicas
248
+
249
+ # Computed inline (not via a shared `local.env_vars` map) so this instance's dependency on
250
+ # module.sql_database/module.redis is scoped to THIS app only — see locals.tf note above.
251
+ env_vars = merge(
252
+ {
253
+ APPLICATIONINSIGHTS_CONNECTION_STRING = module.monitor.connection_string
254
+ KEY_VAULT_URI = module.key_vault[each.key].vault_uri
255
+ },
256
+
257
+ each.value.technology == "nest" ? {
258
+ NODE_TLS_REJECT_UNAUTHORIZED = "0"
259
+ API_KEY_HEADER = "x-api-key"
260
+ ENCRYPTION_PASSWORD = module.random_key.value
261
+ HOST_URL = try(local.app_variables[each.key].host_url, "https://${each.value.domain}")
262
+ FRONT_END_URL = try(local.app_variables[each.key].front_end_url, "https://${each.key}")
263
+ APP_NAME = "Fuel App"
264
+ APP_COMPANY = try(local.app_variables[each.key].app_company, "Swovo")
265
+ } : {},
266
+
267
+ each.value.technology == "react" ? {
268
+ NEXT_PUBLIC_API_HOST = try(local.app_build_env[each.key]["NEXT_PUBLIC_API_HOST"], "")
269
+ } : {},
270
+
271
+ each.value.redis_enabled ? {
272
+ REDIS_HOST = module.redis[each.key].hostname
273
+ REDIS_PORT = module.redis[each.key].ssl_port
274
+ } : {},
275
+
276
+ each.value.database_enabled && each.value.database_engine == "mssql" ? {
277
+ DATABASE_HOST = module.sql_database[each.key].fqdn
278
+ DATABASE_USERNAME = module.sql_database[each.key].admin_username
279
+ DATABASE_NAME = module.sql_database[each.key].database_name
280
+ } : {},
281
+
282
+ each.value.database_enabled && each.value.database_engine == "postgres" ? {
283
+ DATABASE_HOST = module.postgres_database[each.key].fqdn
284
+ DATABASE_USERNAME = module.postgres_database[each.key].admin_username
285
+ DATABASE_NAME = module.postgres_database[each.key].database_name
286
+ } : {},
287
+
288
+ length(try(local.app_variables[each.key], {})) > 0 ? {
289
+ for k, v in local.app_variables[each.key] : upper(k) => v
290
+ } : {}
291
+ )
292
+
293
+ # DB passwords / Redis keys as Container App secrets (secretRef), never plaintext env values.
294
+ secrets = merge(
295
+ each.value.redis_enabled ? {
296
+ REDIS_URL = "rediss://:${module.redis[each.key].primary_access_key}@${module.redis[each.key].hostname}:${module.redis[each.key].ssl_port}/0"
297
+ } : {},
298
+
299
+ each.value.database_enabled && each.value.database_engine == "mssql" ? {
300
+ DATABASE_PASSWORD = module.sql_database[each.key].admin_password
301
+ DATABASE_URL = "sqlserver://${module.sql_database[each.key].fqdn}:1433;database=${module.sql_database[each.key].database_name};user=${module.sql_database[each.key].admin_username};password=${module.sql_database[each.key].admin_password};encrypt=true"
302
+ } : {},
303
+
304
+ each.value.database_enabled && each.value.database_engine == "postgres" ? {
305
+ DATABASE_PASSWORD = module.postgres_database[each.key].admin_password
306
+ DATABASE_URL = "postgresql://${module.postgres_database[each.key].admin_username}:${module.postgres_database[each.key].admin_password}@${module.postgres_database[each.key].fqdn}/${module.postgres_database[each.key].database_name}"
307
+ } : {}
308
+ )
309
+ }
310
+
311
+ resource "azurerm_role_assignment" "container_app_acr_pull" {
312
+ for_each = module.container_app
313
+
314
+ scope = module.acr.id
315
+ role_definition_name = "AcrPull"
316
+ principal_id = each.value.principal_id
317
+ }
318
+
319
+ module "container_app_worker" {
320
+ for_each = { for w in local.worker_list : w.worker_name => w }
321
+
322
+ source = "../../modules/azure-container-app"
323
+ name = each.value.worker_name
324
+ resource_group_name = data.azurerm_resource_group.this.name
325
+ container_app_environment_id = module.container_env.id
326
+ image = local.bootstrap_container_image
327
+ external_ingress = false
328
+
329
+ # Same inline computation as module.container_app above, keyed off the PARENT app's flags
330
+ # (worker's own each.value only has app_name/dockerfile/worker_name/branch).
331
+ env_vars = merge(
332
+ {
333
+ APPLICATIONINSIGHTS_CONNECTION_STRING = module.monitor.connection_string
334
+ KEY_VAULT_URI = module.key_vault[each.value.app_name].vault_uri
335
+ },
336
+
337
+ local.app_list_by_name[each.value.app_name].technology == "nest" ? {
338
+ NODE_TLS_REJECT_UNAUTHORIZED = "0"
339
+ API_KEY_HEADER = "x-api-key"
340
+ ENCRYPTION_PASSWORD = module.random_key.value
341
+ HOST_URL = try(local.app_variables[each.value.app_name].host_url, "https://${local.app_list_by_name[each.value.app_name].domain}")
342
+ FRONT_END_URL = try(local.app_variables[each.value.app_name].front_end_url, "https://${each.value.app_name}")
343
+ APP_NAME = "Fuel App"
344
+ APP_COMPANY = try(local.app_variables[each.value.app_name].app_company, "Swovo")
345
+ } : {},
346
+
347
+ local.app_list_by_name[each.value.app_name].technology == "react" ? {
348
+ NEXT_PUBLIC_API_HOST = try(local.app_build_env[each.value.app_name]["NEXT_PUBLIC_API_HOST"], "")
349
+ } : {},
350
+
351
+ local.app_list_by_name[each.value.app_name].redis_enabled ? {
352
+ REDIS_HOST = module.redis[each.value.app_name].hostname
353
+ REDIS_PORT = module.redis[each.value.app_name].ssl_port
354
+ } : {},
355
+
356
+ local.app_list_by_name[each.value.app_name].database_enabled && local.app_list_by_name[each.value.app_name].database_engine == "mssql" ? {
357
+ DATABASE_HOST = module.sql_database[each.value.app_name].fqdn
358
+ DATABASE_USERNAME = module.sql_database[each.value.app_name].admin_username
359
+ DATABASE_NAME = module.sql_database[each.value.app_name].database_name
360
+ } : {},
361
+
362
+ local.app_list_by_name[each.value.app_name].database_enabled && local.app_list_by_name[each.value.app_name].database_engine == "postgres" ? {
363
+ DATABASE_HOST = module.postgres_database[each.value.app_name].fqdn
364
+ DATABASE_USERNAME = module.postgres_database[each.value.app_name].admin_username
365
+ DATABASE_NAME = module.postgres_database[each.value.app_name].database_name
366
+ } : {},
367
+
368
+ length(try(local.app_variables[each.value.app_name], {})) > 0 ? {
369
+ for k, v in local.app_variables[each.value.app_name] : upper(k) => v
370
+ } : {}
371
+ )
372
+
373
+ secrets = merge(
374
+ local.app_list_by_name[each.value.app_name].redis_enabled ? {
375
+ REDIS_URL = "rediss://:${module.redis[each.value.app_name].primary_access_key}@${module.redis[each.value.app_name].hostname}:${module.redis[each.value.app_name].ssl_port}/0"
376
+ } : {},
377
+
378
+ local.app_list_by_name[each.value.app_name].database_enabled && local.app_list_by_name[each.value.app_name].database_engine == "mssql" ? {
379
+ DATABASE_PASSWORD = module.sql_database[each.value.app_name].admin_password
380
+ DATABASE_URL = "sqlserver://${module.sql_database[each.value.app_name].fqdn}:1433;database=${module.sql_database[each.value.app_name].database_name};user=${module.sql_database[each.value.app_name].admin_username};password=${module.sql_database[each.value.app_name].admin_password};encrypt=true"
381
+ } : {},
382
+
383
+ local.app_list_by_name[each.value.app_name].database_enabled && local.app_list_by_name[each.value.app_name].database_engine == "postgres" ? {
384
+ DATABASE_PASSWORD = module.postgres_database[each.value.app_name].admin_password
385
+ DATABASE_URL = "postgresql://${module.postgres_database[each.value.app_name].admin_username}:${module.postgres_database[each.value.app_name].admin_password}@${module.postgres_database[each.value.app_name].fqdn}/${module.postgres_database[each.value.app_name].database_name}"
386
+ } : {}
387
+ )
388
+ }
389
+
390
+ resource "azurerm_role_assignment" "worker_acr_pull" {
391
+ for_each = module.container_app_worker
392
+
393
+ scope = module.acr.id
394
+ role_definition_name = "AcrPull"
395
+ principal_id = each.value.principal_id
396
+ }
397
+
398
+ # Worker has its own identity (distinct from its parent app's) — reads from the same
399
+ # per-app vault as its parent, since it's the same logical app.
400
+ resource "azurerm_role_assignment" "worker_key_vault_secrets_user" {
401
+ for_each = module.container_app_worker
402
+
403
+ scope = module.key_vault[local.worker_list_by_name[each.key].app_name].id
404
+ role_definition_name = "Key Vault Secrets User"
405
+ principal_id = each.value.principal_id
406
+ }
407
+
408
+
409
+ module "front_door" {
410
+ source = "../../modules/azure-front-door"
411
+ name = "${var.project_name}-fd"
412
+ resource_group_name = data.azurerm_resource_group.this.name
413
+ sku_name = try(local.credentials.front_door.sku_name, "Standard_AzureFrontDoor")
414
+
415
+ origins = {
416
+ for app in local.container_apps : app.app_name => {
417
+ host_name = module.container_app[app.app_name].fqdn
418
+ custom_domain = app.domain
419
+ }
420
+ }
421
+ }
@@ -0,0 +1,40 @@
1
+ output "github_oidc_client_id" {
2
+ value = module.github_oidc.client_id
3
+ }
4
+
5
+ output "acr_login_server" {
6
+ value = module.acr.login_server
7
+ }
8
+
9
+ output "front_door_endpoint_hostname" {
10
+ value = module.front_door.endpoint_hostname
11
+ }
12
+
13
+ output "custom_domain_validation" {
14
+ description = "DNS records to create at the domain's own DNS provider so Azure can validate ownership and issue the managed TLS certificate."
15
+ value = module.front_door.custom_domain_validation
16
+ }
17
+
18
+ output "github_http_repo_url" {
19
+ value = "https://github.com/${var.github_organization}/${var.github_repo_name}.git"
20
+ sensitive = true
21
+ }
22
+
23
+ output "resource_group_name" {
24
+ value = data.azurerm_resource_group.this.name
25
+ }
26
+
27
+ output "container_apps_outbound_ip" {
28
+ description = "Static outbound IP for all Container Apps in this project — use for allowlisting against third-party APIs/firewalls."
29
+ value = module.network.container_apps_outbound_ip
30
+ }
31
+
32
+ output "apps" {
33
+ value = local.app_list
34
+ sensitive = true
35
+ }
36
+
37
+ output "workers" {
38
+ value = local.worker_list
39
+ sensitive = true
40
+ }
@@ -0,0 +1,65 @@
1
+ terraform {
2
+ required_version = ">= 1.5.0"
3
+ required_providers {
4
+ azurerm = {
5
+ source = "hashicorp/azurerm"
6
+ # >= 4.0 required for resource_provider_registrations/resource_providers_to_register below.
7
+ # < 5.0 pinned: v5 renames/removes arguments on several resources (e.g.
8
+ # azurerm_private_dns_zone_virtual_network_link) this template set hasn't been migrated to yet.
9
+ version = ">= 4.0.0, < 5.0.0"
10
+ }
11
+ azuread = {
12
+ source = "hashicorp/azuread"
13
+ version = ">= 2.53.0"
14
+ }
15
+ random = {
16
+ source = "hashicorp/random"
17
+ version = ">= 3.5.1"
18
+ }
19
+ http = {
20
+ source = "hashicorp/http"
21
+ version = ">= 3.4.0"
22
+ }
23
+ time = {
24
+ source = "hashicorp/time"
25
+ version = ">= 0.11.0"
26
+ }
27
+ }
28
+ }
29
+
30
+ provider "azurerm" {
31
+ features {}
32
+ subscription_id = var.azure_subscription_id
33
+ tenant_id = var.azure_tenant_id
34
+ client_id = var.azure_client_id
35
+ client_secret = var.azure_client_secret
36
+
37
+ # azurerm v4 only auto-registers a curated "core" set of resource providers before
38
+ # apply — Microsoft.App (Container Apps) isn't in it, which surfaces as a 409
39
+ # MissingSubscriptionRegistration on a subscription that has never used that
40
+ # namespace before. Register everything this template set actually provisions,
41
+ # explicitly, so `tofu apply` is self-sufficient (no external registration step,
42
+ # CLI or otherwise, has to run first).
43
+ resource_provider_registrations = "none"
44
+ resource_providers_to_register = [
45
+ "Microsoft.App",
46
+ "Microsoft.Sql",
47
+ "Microsoft.DBforPostgreSQL",
48
+ "Microsoft.Cache",
49
+ "Microsoft.ContainerRegistry",
50
+ "Microsoft.KeyVault",
51
+ "Microsoft.Network",
52
+ "Microsoft.Cdn",
53
+ "Microsoft.Storage",
54
+ "Microsoft.OperationalInsights",
55
+ "Microsoft.Insights",
56
+ "Microsoft.Portal",
57
+ "Microsoft.Authorization",
58
+ ]
59
+ }
60
+
61
+ provider "azuread" {
62
+ tenant_id = var.azure_tenant_id
63
+ client_id = var.azure_client_id
64
+ client_secret = var.azure_client_secret
65
+ }
@@ -0,0 +1,69 @@
1
+ variable "project_name" {
2
+ description = "A unique name for your project. Used as part of most resource names."
3
+ default = ""
4
+ }
5
+
6
+ variable "location" {
7
+ description = "Azure region for all resources"
8
+ default = "eastus"
9
+ }
10
+
11
+ variable "resource_group_name" {
12
+ description = "Name of the resource group (created by the CLI before `tofu init`)"
13
+ default = ""
14
+ }
15
+
16
+ variable "key_vault_name" {
17
+ description = "Name of the bootstrap Key Vault holding the fuel.yml infrastructure secret (created by the CLI)"
18
+ default = ""
19
+ }
20
+
21
+ variable "acr_name" {
22
+ description = "Globally unique name for the Azure Container Registry"
23
+ default = ""
24
+ }
25
+
26
+ variable "github_organization" {
27
+ description = "GitHub organization where the repository is created"
28
+ default = ""
29
+ }
30
+
31
+ variable "github_repo_name" {
32
+ description = "Name of the GitHub repository (created by the CLI)"
33
+ type = string
34
+ default = ""
35
+ }
36
+
37
+ variable "azure_subscription_id" {
38
+ description = "Azure subscription ID"
39
+ default = ""
40
+ sensitive = true
41
+ }
42
+
43
+ variable "azure_tenant_id" {
44
+ description = "Azure AD tenant ID"
45
+ default = ""
46
+ sensitive = true
47
+ }
48
+
49
+ variable "azure_client_id" {
50
+ description = "Client ID of the service principal used to run Tofu"
51
+ default = ""
52
+ sensitive = true
53
+ }
54
+
55
+ variable "azure_client_secret" {
56
+ description = "Client secret of the service principal used to run Tofu"
57
+ default = ""
58
+ sensitive = true
59
+ }
60
+
61
+ variable "db_username" {
62
+ description = "Administrator username for provisioned databases"
63
+ default = ""
64
+ }
65
+
66
+ variable "db_name" {
67
+ description = "Database name for provisioned databases"
68
+ default = ""
69
+ }