@swovohq/fuel 0.2.2-alpha.9 → 0.2.2-alpha.91

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 (232) hide show
  1. package/README.md +173 -69
  2. package/dist/bin/fuel.js +77 -31
  3. package/dist/bin/fuel.js.map +1 -1
  4. package/dist/cleanup/finalize.d.ts.map +1 -1
  5. package/dist/cleanup/finalize.js +11 -3
  6. package/dist/cleanup/finalize.js.map +1 -1
  7. package/dist/commands/config-verify.d.ts.map +1 -1
  8. package/dist/commands/config-verify.js +14 -6
  9. package/dist/commands/config-verify.js.map +1 -1
  10. package/dist/commands/create-app.d.ts.map +1 -1
  11. package/dist/commands/create-app.js +41 -7
  12. package/dist/commands/create-app.js.map +1 -1
  13. package/dist/commands/infra-deploy.d.ts +15 -1
  14. package/dist/commands/infra-deploy.d.ts.map +1 -1
  15. package/dist/commands/infra-deploy.js +164 -18
  16. package/dist/commands/infra-deploy.js.map +1 -1
  17. package/dist/commands/infra-destroy.d.ts.map +1 -1
  18. package/dist/commands/infra-destroy.js +3 -3
  19. package/dist/commands/infra-destroy.js.map +1 -1
  20. package/dist/commands/infra-init.d.ts +91 -0
  21. package/dist/commands/infra-init.d.ts.map +1 -1
  22. package/dist/commands/infra-init.js +644 -113
  23. package/dist/commands/infra-init.js.map +1 -1
  24. package/dist/commands/infra-pull.d.ts +8 -0
  25. package/dist/commands/infra-pull.d.ts.map +1 -0
  26. package/dist/commands/infra-pull.js +81 -0
  27. package/dist/commands/infra-pull.js.map +1 -0
  28. package/dist/commands/list-modules.d.ts.map +1 -1
  29. package/dist/commands/list-modules.js +10 -3
  30. package/dist/commands/list-modules.js.map +1 -1
  31. package/dist/engines/convention.d.ts +2 -2
  32. package/dist/engines/convention.d.ts.map +1 -1
  33. package/dist/engines/convention.js +8 -6
  34. package/dist/engines/convention.js.map +1 -1
  35. package/dist/engines/dependencies.d.ts +2 -0
  36. package/dist/engines/dependencies.d.ts.map +1 -1
  37. package/dist/engines/dependencies.js.map +1 -1
  38. package/dist/engines/monorepo.d.ts.map +1 -1
  39. package/dist/engines/monorepo.js.map +1 -1
  40. package/dist/engines/template-source.d.ts.map +1 -1
  41. package/dist/engines/template-source.js +3 -1
  42. package/dist/engines/template-source.js.map +1 -1
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js.map +1 -1
  45. package/dist/infra/azure-defaults.d.ts +49 -0
  46. package/dist/infra/azure-defaults.d.ts.map +1 -0
  47. package/dist/infra/azure-defaults.js +52 -0
  48. package/dist/infra/azure-defaults.js.map +1 -0
  49. package/dist/infra/azure-keyvault-client.d.ts +4 -0
  50. package/dist/infra/azure-keyvault-client.d.ts.map +1 -0
  51. package/dist/infra/azure-keyvault-client.js +57 -0
  52. package/dist/infra/azure-keyvault-client.js.map +1 -0
  53. package/dist/infra/azure-rbac.d.ts +11 -0
  54. package/dist/infra/azure-rbac.d.ts.map +1 -0
  55. package/dist/infra/azure-rbac.js +49 -0
  56. package/dist/infra/azure-rbac.js.map +1 -0
  57. package/dist/infra/azure-state-storage.d.ts +20 -0
  58. package/dist/infra/azure-state-storage.d.ts.map +1 -0
  59. package/dist/infra/azure-state-storage.js +229 -0
  60. package/dist/infra/azure-state-storage.js.map +1 -0
  61. package/dist/infra/config-loader.d.ts.map +1 -1
  62. package/dist/infra/config-loader.js.map +1 -1
  63. package/dist/infra/config-writer.js +1 -1
  64. package/dist/infra/credentials.d.ts +2 -2
  65. package/dist/infra/credentials.d.ts.map +1 -1
  66. package/dist/infra/credentials.js +8 -4
  67. package/dist/infra/credentials.js.map +1 -1
  68. package/dist/infra/dependency-checker.d.ts.map +1 -1
  69. package/dist/infra/dependency-checker.js.map +1 -1
  70. package/dist/infra/deployment-detect.d.ts +9 -0
  71. package/dist/infra/deployment-detect.d.ts.map +1 -0
  72. package/dist/infra/deployment-detect.js +53 -0
  73. package/dist/infra/deployment-detect.js.map +1 -0
  74. package/dist/infra/git-client.d.ts.map +1 -1
  75. package/dist/infra/git-client.js +6 -10
  76. package/dist/infra/git-client.js.map +1 -1
  77. package/dist/infra/github-client.d.ts +7 -0
  78. package/dist/infra/github-client.d.ts.map +1 -1
  79. package/dist/infra/github-client.js +47 -7
  80. package/dist/infra/github-client.js.map +1 -1
  81. package/dist/infra/github-secrets-client.d.ts.map +1 -1
  82. package/dist/infra/github-secrets-client.js +2 -2
  83. package/dist/infra/github-secrets-client.js.map +1 -1
  84. package/dist/infra/github-token-link.d.ts +4 -0
  85. package/dist/infra/github-token-link.d.ts.map +1 -0
  86. package/dist/infra/github-token-link.js +32 -0
  87. package/dist/infra/github-token-link.js.map +1 -0
  88. package/dist/infra/orchestrator.d.ts +4 -1
  89. package/dist/infra/orchestrator.d.ts.map +1 -1
  90. package/dist/infra/orchestrator.js +218 -63
  91. package/dist/infra/orchestrator.js.map +1 -1
  92. package/dist/infra/port-utils.d.ts.map +1 -1
  93. package/dist/infra/port-utils.js.map +1 -1
  94. package/dist/infra/preflight.d.ts.map +1 -1
  95. package/dist/infra/preflight.js +25 -11
  96. package/dist/infra/preflight.js.map +1 -1
  97. package/dist/infra/remote-secret.d.ts +14 -0
  98. package/dist/infra/remote-secret.d.ts.map +1 -0
  99. package/dist/infra/remote-secret.js +42 -0
  100. package/dist/infra/remote-secret.js.map +1 -0
  101. package/dist/infra/s3-state-bucket.d.ts.map +1 -1
  102. package/dist/infra/s3-state-bucket.js +8 -4
  103. package/dist/infra/s3-state-bucket.js.map +1 -1
  104. package/dist/infra/secrets-manager-client.d.ts.map +1 -1
  105. package/dist/infra/secrets-manager-client.js +8 -5
  106. package/dist/infra/secrets-manager-client.js.map +1 -1
  107. package/dist/infra/subprocess.js +1 -1
  108. package/dist/infra/template-engine.d.ts +14 -5
  109. package/dist/infra/template-engine.d.ts.map +1 -1
  110. package/dist/infra/template-engine.js +157 -39
  111. package/dist/infra/template-engine.js.map +1 -1
  112. package/dist/infra/tofu-runner.d.ts +4 -3
  113. package/dist/infra/tofu-runner.d.ts.map +1 -1
  114. package/dist/infra/tofu-runner.js +40 -34
  115. package/dist/infra/tofu-runner.js.map +1 -1
  116. package/dist/infra/types.d.ts +241 -17
  117. package/dist/infra/types.d.ts.map +1 -1
  118. package/dist/infra/types.js +419 -22
  119. package/dist/infra/types.js.map +1 -1
  120. package/dist/infra-templates/TODO-azure-prod.md +127 -0
  121. package/dist/infra-templates/modules/azure-container-app/main.tf +124 -0
  122. package/dist/infra-templates/modules/azure-container-app/outputs.tf +24 -0
  123. package/dist/infra-templates/modules/azure-container-app/variables.tf +80 -0
  124. package/dist/infra-templates/modules/azure-container-apps-environment/main.tf +18 -0
  125. package/dist/infra-templates/modules/azure-container-apps-environment/outputs.tf +14 -0
  126. package/dist/infra-templates/modules/azure-container-apps-environment/variables.tf +24 -0
  127. package/dist/infra-templates/modules/azure-container-registry/main.tf +10 -0
  128. package/dist/infra-templates/modules/azure-container-registry/outputs.tf +14 -0
  129. package/dist/infra-templates/modules/azure-container-registry/variables.tf +20 -0
  130. package/dist/infra-templates/modules/azure-front-door/main.tf +80 -0
  131. package/dist/infra-templates/modules/azure-front-door/outputs.tf +20 -0
  132. package/dist/infra-templates/modules/azure-front-door/variables.tf +26 -0
  133. package/dist/infra-templates/modules/azure-github-oidc/main.tf +48 -0
  134. package/dist/infra-templates/modules/azure-github-oidc/outputs.tf +9 -0
  135. package/dist/infra-templates/modules/azure-github-oidc/variables.tf +69 -0
  136. package/dist/infra-templates/modules/azure-key-vault/main.tf +21 -0
  137. package/dist/infra-templates/modules/azure-key-vault/outputs.tf +11 -0
  138. package/dist/infra-templates/modules/azure-key-vault/variables.tf +43 -0
  139. package/dist/infra-templates/modules/azure-monitor/main.tf +29 -0
  140. package/dist/infra-templates/modules/azure-monitor/outputs.tf +17 -0
  141. package/dist/infra-templates/modules/azure-monitor/variables.tf +32 -0
  142. package/dist/infra-templates/modules/azure-network/main.tf +134 -0
  143. package/dist/infra-templates/modules/azure-network/outputs.tf +44 -0
  144. package/dist/infra-templates/modules/azure-network/variables.tf +56 -0
  145. package/dist/infra-templates/modules/azure-postgresql-flexible/main.tf +34 -0
  146. package/dist/infra-templates/modules/azure-postgresql-flexible/outputs.tf +20 -0
  147. package/dist/infra-templates/modules/azure-postgresql-flexible/variables.tf +54 -0
  148. package/dist/infra-templates/modules/azure-redis-cache/main.tf +32 -0
  149. package/dist/infra-templates/modules/azure-redis-cache/outputs.tf +15 -0
  150. package/dist/infra-templates/modules/azure-redis-cache/variables.tf +30 -0
  151. package/dist/infra-templates/modules/azure-resource-group/main.tf +5 -0
  152. package/dist/infra-templates/modules/azure-resource-group/outputs.tf +11 -0
  153. package/dist/infra-templates/modules/azure-resource-group/variables.tf +16 -0
  154. package/dist/infra-templates/modules/azure-sql-database/main.tf +63 -0
  155. package/dist/infra-templates/modules/azure-sql-database/outputs.tf +20 -0
  156. package/dist/infra-templates/modules/azure-sql-database/variables.tf +78 -0
  157. package/dist/infra-templates/modules/azure-storage-account/main.tf +13 -0
  158. package/dist/infra-templates/modules/azure-storage-account/outputs.tf +16 -0
  159. package/dist/infra-templates/modules/azure-storage-account/variables.tf +38 -0
  160. package/dist/infra-templates/modules/cloud-watch/event-bridge.tf +15 -0
  161. package/dist/infra-templates/modules/cloud-watch/lambda.tf +84 -0
  162. package/dist/infra-templates/modules/cloud-watch/lambda_function.zip +0 -0
  163. package/dist/infra-templates/modules/cloud-watch/lambda_function_deploy.zip +0 -0
  164. package/dist/infra-templates/modules/cloud-watch/main.tf +156 -0
  165. package/dist/infra-templates/modules/cloud-watch/sns.tf +3 -0
  166. package/dist/infra-templates/modules/cloud-watch/variables.tf +38 -0
  167. package/dist/infra-templates/modules/ecs-core/alb.tf +126 -0
  168. package/dist/infra-templates/modules/ecs-core/cloudwatch.tf +48 -0
  169. package/dist/infra-templates/modules/ecs-core/iam.tf +95 -0
  170. package/dist/infra-templates/modules/ecs-core/main.tf +12 -0
  171. package/dist/infra-templates/modules/ecs-core/outputs.tf +44 -0
  172. package/dist/infra-templates/modules/ecs-core/secrets.tf +9 -0
  173. package/dist/infra-templates/modules/ecs-core/security_groups.tf +73 -0
  174. package/dist/infra-templates/modules/ecs-core/variables.tf +96 -0
  175. package/dist/infra-templates/modules/ecs-service/ecs_autoscaling.tf +51 -0
  176. package/dist/infra-templates/modules/ecs-service/ecs_service.tf +39 -0
  177. package/dist/infra-templates/modules/ecs-service/ecs_task_definition.tf +56 -0
  178. package/dist/infra-templates/modules/ecs-service/variables.tf +170 -0
  179. package/dist/infra-templates/modules/elastic-container-registry/main.tf +32 -0
  180. package/dist/infra-templates/modules/elastic-container-registry/outputs.tf +9 -0
  181. package/dist/infra-templates/modules/elastic-container-registry/variables.tf +25 -0
  182. package/dist/infra-templates/modules/github-oidc-role/main.tf +46 -0
  183. package/dist/infra-templates/modules/github-oidc-role/outputs.tf +4 -0
  184. package/dist/infra-templates/modules/github-oidc-role/variables.tf +11 -0
  185. package/dist/infra-templates/modules/guardduty/main.tf +8 -0
  186. package/dist/infra-templates/modules/iam/main.tf +205 -0
  187. package/dist/infra-templates/modules/iam/outputs.tf +3 -0
  188. package/dist/infra-templates/modules/iam/variables.tf +4 -0
  189. package/dist/infra-templates/modules/random-key/main.tf +11 -0
  190. package/dist/infra-templates/modules/random-key/outputs.tf +4 -0
  191. package/dist/infra-templates/modules/random-key/variables.tf +10 -0
  192. package/dist/infra-templates/modules/redis/main.tf +47 -0
  193. package/dist/infra-templates/modules/redis/outputs.tf +8 -0
  194. package/dist/infra-templates/modules/redis/variables.tf +20 -0
  195. package/dist/infra-templates/modules/relational-database-service/main.tf +44 -0
  196. package/dist/infra-templates/modules/relational-database-service/outputs.tf +13 -0
  197. package/dist/infra-templates/modules/relational-database-service/security-group.tf +15 -0
  198. package/dist/infra-templates/modules/relational-database-service/variables.tf +50 -0
  199. package/dist/infra-templates/modules/secret/main.tf +14 -0
  200. package/dist/infra-templates/modules/secret/outputs.tf +3 -0
  201. package/dist/infra-templates/modules/secret/variables.tf +8 -0
  202. package/dist/infra-templates/modules/simple-storage-service/main.tf +28 -0
  203. package/dist/infra-templates/modules/simple-storage-service/variables.tf +4 -0
  204. package/dist/infra-templates/modules/vpc/data.tf +2 -0
  205. package/dist/infra-templates/modules/vpc/main.tf +76 -0
  206. package/dist/infra-templates/modules/vpc/outputs.tf +23 -0
  207. package/dist/infra-templates/modules/vpc/variables.tf +5 -0
  208. package/dist/infra-templates/package.json +6 -0
  209. package/dist/infra-templates/templates/azure/MANUAL_DEPLOY.md +168 -0
  210. package/dist/infra-templates/templates/azure/backend.tf +8 -0
  211. package/dist/infra-templates/templates/azure/data.tf +21 -0
  212. package/dist/infra-templates/templates/azure/default.tfvars +0 -0
  213. package/dist/infra-templates/templates/azure/locals.tf +116 -0
  214. package/dist/infra-templates/templates/azure/main.tf +421 -0
  215. package/dist/infra-templates/templates/azure/outputs.tf +40 -0
  216. package/dist/infra-templates/templates/azure/providers.tf +65 -0
  217. package/dist/infra-templates/templates/azure/variables.tf +69 -0
  218. package/dist/infra-templates/templates/azure/workflows/build_and_deploy.yml +155 -0
  219. package/dist/infra-templates/templates/azure/workflows/tests.yml +63 -0
  220. package/dist/infra-templates/templates/ecs/README.md +235 -0
  221. package/dist/infra-templates/templates/ecs/backend.tf +7 -0
  222. package/dist/infra-templates/templates/ecs/data.tf +6 -0
  223. package/dist/infra-templates/templates/ecs/default.tfvars +1 -0
  224. package/dist/infra-templates/templates/ecs/locals.tf +97 -0
  225. package/dist/infra-templates/templates/ecs/main.tf +185 -0
  226. package/dist/infra-templates/templates/ecs/outputs.tf +42 -0
  227. package/dist/infra-templates/templates/ecs/providers.tf +21 -0
  228. package/dist/infra-templates/templates/ecs/variables.tf +105 -0
  229. package/dist/infra-templates/templates/ecs/workflows/build_and_deploy.yml +188 -0
  230. package/dist/infra-templates/templates/ecs/workflows/tests.yml +65 -0
  231. package/package.json +10 -2
  232. package/schema/fuel.schema.json +570 -91
@@ -0,0 +1,124 @@
1
+ # NOTE on `registry` + identity auth: a resource cannot reference its own
2
+ # computed attributes (e.g. azurerm_container_app.this.identity[0].principal_id)
3
+ # from within its own configuration - that's a circular reference and won't
4
+ # plan. `registry.identity = "System"` is the documented way to tell the
5
+ # platform "use this container app's own system-assigned identity" (same
6
+ # convention as `secret.identity`). The ACR pull role assignment (created by
7
+ # the parent template, against this module's `principal_id` output) necessarily
8
+ # happens AFTER this resource exists (it needs the identity's principal ID).
9
+ # Configuring `registry` here anyway made Container Apps try to validate that
10
+ # credential during the very first create, before the role assignment exists —
11
+ # the provisioning call hangs and times out ("Operation expired") regardless of
12
+ # which image is actually being pulled. So: no `registry` block here at all: CI
13
+ # wires it up with `az containerapp registry set` once the role assignment is
14
+ # already in place (see build_and_deploy.yml), and ignore_changes below stops
15
+ # `tofu apply` from ever trying to remove what CI configured.
16
+ locals {
17
+ # Container App secret names must be lowercase alphanumeric + '-'; env var names keep
18
+ # the caller's original casing (e.g. STRIPE_SECRET_KEY) via secret_name lookup below.
19
+ secret_names = { for k, v in var.secrets : k => lower(replace(k, "_", "-")) }
20
+ }
21
+
22
+ resource "azurerm_container_app" "this" {
23
+ name = var.name
24
+ resource_group_name = var.resource_group_name
25
+ container_app_environment_id = var.container_app_environment_id
26
+ revision_mode = "Single"
27
+ # Matches the "Consumption" profile module.azure-container-apps-environment always declares —
28
+ # explicit so `plan` doesn't perpetually diff against what Azure reports back.
29
+ workload_profile_name = "Consumption"
30
+
31
+ identity {
32
+ type = "SystemAssigned"
33
+ }
34
+
35
+ template {
36
+ min_replicas = var.min_replicas
37
+ max_replicas = var.max_replicas
38
+
39
+ container {
40
+ name = var.name
41
+ image = var.image
42
+ cpu = var.cpu
43
+ memory = var.memory
44
+
45
+ dynamic "env" {
46
+ for_each = var.env_vars
47
+ content {
48
+ name = env.key
49
+ value = env.value
50
+ }
51
+ }
52
+
53
+ dynamic "env" {
54
+ for_each = var.secrets
55
+ content {
56
+ name = env.key
57
+ secret_name = local.secret_names[env.key]
58
+ }
59
+ }
60
+ }
61
+
62
+ # HTTP concurrency-based autoscaling only makes sense for apps that
63
+ # actually receive ingress traffic; workers have no ingress to scale on.
64
+ dynamic "http_scale_rule" {
65
+ for_each = var.external_ingress ? [1] : []
66
+ content {
67
+ name = "${var.name}-http-scale"
68
+ concurrent_requests = var.concurrent_requests
69
+ }
70
+ }
71
+ }
72
+
73
+ dynamic "secret" {
74
+ for_each = var.secrets
75
+ content {
76
+ name = local.secret_names[secret.key]
77
+ value = secret.value
78
+ }
79
+ }
80
+
81
+ # Workers (external_ingress = false) get no ingress block at all.
82
+ dynamic "ingress" {
83
+ for_each = var.external_ingress ? [1] : []
84
+ content {
85
+ external_enabled = true
86
+ target_port = var.target_port
87
+
88
+ traffic_weight {
89
+ latest_revision = true
90
+ percentage = 100
91
+ }
92
+
93
+ # Every public app is fronted by Front Door (custom domain + eventually WAF) — direct
94
+ # hits to the *.azurecontainerapps.io hostname would bypass it entirely. Restrict inbound
95
+ # to Front Door's published backend range (Microsoft's documented pattern for this;
96
+ # resolved to real CIDRs by the caller since this field validates as strict CIDR, not a
97
+ # service tag name). Doesn't stop another tenant's Front Door from proxying through (same
98
+ # shared IP range) — the app itself should also check the X-Azure-FDID header against
99
+ # this project's Front Door ID for that.
100
+ dynamic "ip_security_restriction" {
101
+ for_each = var.front_door_backend_cidrs
102
+ content {
103
+ name = "allow-front-door-${replace(ip_security_restriction.value, "/", "-")}"
104
+ action = "Allow"
105
+ ip_address_range = ip_security_restriction.value
106
+ }
107
+ }
108
+ }
109
+ }
110
+
111
+ # var.image is only ever meaningful on the very first create — the ACR tag it
112
+ # points at doesn't exist yet (the CI pipeline that pushes it depends on this same
113
+ # apply having already created the GitHub OIDC identity/repo secrets). Ignore drift
114
+ # on it so a later `az containerapp update` (done by CI once it has a real image)
115
+ # never gets clobbered back by a subsequent `tofu apply`/`--regenerate`.
116
+ lifecycle {
117
+ ignore_changes = [template[0].container[0].image, registry]
118
+
119
+ precondition {
120
+ condition = !var.external_ingress || length(var.front_door_backend_cidrs) > 0
121
+ error_message = "front_door_backend_cidrs must not be empty when external_ingress is true — an empty list removes every ip_security_restriction rule, exposing this Container App directly to the internet instead of only through Front Door."
122
+ }
123
+ }
124
+ }
@@ -0,0 +1,24 @@
1
+ output "id" {
2
+ value = azurerm_container_app.this.id
3
+ description = "The ID of the Container App"
4
+ }
5
+
6
+ output "name" {
7
+ value = azurerm_container_app.this.name
8
+ description = "The name of the Container App"
9
+ }
10
+
11
+ output "fqdn" {
12
+ value = try(azurerm_container_app.this.ingress[0].fqdn, "")
13
+ description = "The FQDN of the Container App ingress, or an empty string when external_ingress is false"
14
+ }
15
+
16
+ output "principal_id" {
17
+ value = azurerm_container_app.this.identity[0].principal_id
18
+ description = "Principal ID of the system-assigned managed identity, used by the parent template to grant AcrPull"
19
+ }
20
+
21
+ output "latest_revision_name" {
22
+ value = azurerm_container_app.this.latest_revision_name
23
+ description = "The name of the latest Container App revision"
24
+ }
@@ -0,0 +1,80 @@
1
+ variable "name" {
2
+ type = string
3
+ description = "Name of the Container App"
4
+ }
5
+
6
+ variable "resource_group_name" {
7
+ type = string
8
+ description = "Name of the resource group in which to create the Container App"
9
+ }
10
+
11
+ variable "container_app_environment_id" {
12
+ type = string
13
+ description = "ID of the (Consumption) Container App Environment this app belongs to"
14
+ }
15
+
16
+ variable "image" {
17
+ type = string
18
+ description = "Full image reference to run, e.g. \"myacr.azurecr.io/app:latest\""
19
+ }
20
+
21
+ variable "target_port" {
22
+ type = number
23
+ description = "Port the container listens on"
24
+ default = 8080
25
+ }
26
+
27
+ variable "external_ingress" {
28
+ type = bool
29
+ description = "Whether to expose external ingress. Set to false for workers (no ingress block is created)"
30
+ default = true
31
+ }
32
+
33
+ variable "cpu" {
34
+ type = number
35
+ description = "vCPU allocated to the container"
36
+ default = 1
37
+ }
38
+
39
+ variable "memory" {
40
+ type = string
41
+ description = "Memory allocated to the container, e.g. \"2Gi\""
42
+ default = "2Gi"
43
+ }
44
+
45
+ variable "min_replicas" {
46
+ type = number
47
+ description = "Minimum number of replicas"
48
+ default = 0
49
+ }
50
+
51
+ variable "max_replicas" {
52
+ type = number
53
+ description = "Maximum number of replicas"
54
+ default = 3
55
+ }
56
+
57
+ variable "concurrent_requests" {
58
+ type = number
59
+ description = "Number of concurrent HTTP requests that triggers scaling. Only relevant when external_ingress is true"
60
+ default = 20
61
+ }
62
+
63
+ variable "env_vars" {
64
+ type = map(string)
65
+ description = "Plain (non-secret) environment variables for the container"
66
+ default = {}
67
+ }
68
+
69
+ variable "secrets" {
70
+ type = map(string)
71
+ description = "Name/value pairs exposed as Container App secrets, referenceable via secretRef in env vars"
72
+ default = {}
73
+ sensitive = true
74
+ }
75
+
76
+ variable "front_door_backend_cidrs" {
77
+ type = list(string)
78
+ description = "CIDR ranges to allow through ingress (Front Door's backend range) when external_ingress is true. Empty list means no restriction is applied."
79
+ default = []
80
+ }
@@ -0,0 +1,18 @@
1
+ resource "azurerm_container_app_environment" "this" {
2
+ name = var.name
3
+ resource_group_name = var.resource_group_name
4
+ location = var.location
5
+ log_analytics_workspace_id = var.log_analytics_workspace_id
6
+ infrastructure_subnet_id = var.infrastructure_subnet_id
7
+
8
+ # Azure implicitly attaches this default profile to every environment regardless of what's
9
+ # declared here (there's no more "Consumption-only, no workload profile at all" environment
10
+ # type) — leaving it undeclared just meant every plan showed a perpetual diff against the
11
+ # real state. Declare it explicitly so plan reflects reality. Adding a Dedicated/GP profile
12
+ # here later doesn't change this app's plan — that's controlled per-app via
13
+ # azurerm_container_app.workload_profile_name.
14
+ workload_profile {
15
+ name = "Consumption"
16
+ workload_profile_type = "Consumption"
17
+ }
18
+ }
@@ -0,0 +1,14 @@
1
+ output "id" {
2
+ value = azurerm_container_app_environment.this.id
3
+ description = "ID of the Container Apps Environment"
4
+ }
5
+
6
+ output "name" {
7
+ value = azurerm_container_app_environment.this.name
8
+ description = "Name of the Container Apps Environment"
9
+ }
10
+
11
+ output "default_domain" {
12
+ value = azurerm_container_app_environment.this.default_domain
13
+ description = "Default domain of the Container Apps Environment"
14
+ }
@@ -0,0 +1,24 @@
1
+ variable "name" {
2
+ description = "Name of the Container Apps Environment"
3
+ type = string
4
+ }
5
+
6
+ variable "resource_group_name" {
7
+ description = "Name of the resource group where the environment will be created"
8
+ type = string
9
+ }
10
+
11
+ variable "location" {
12
+ description = "Azure region where the environment will be created"
13
+ type = string
14
+ }
15
+
16
+ variable "log_analytics_workspace_id" {
17
+ description = "ID of the Log Analytics workspace used for Container Apps diagnostics"
18
+ type = string
19
+ }
20
+
21
+ variable "infrastructure_subnet_id" {
22
+ description = "Delegated subnet for VNet integration. Egress-only: ingress stays public so Front Door can still reach apps directly."
23
+ type = string
24
+ }
@@ -0,0 +1,10 @@
1
+ resource "azurerm_container_registry" "this" {
2
+ name = var.name
3
+ resource_group_name = var.resource_group_name
4
+ location = var.location
5
+ sku = var.sku
6
+
7
+ # Admin credentials are disabled on purpose: pulls are done via managed
8
+ # identity + role assignment (AcrPull), configured by the parent template.
9
+ admin_enabled = false
10
+ }
@@ -0,0 +1,14 @@
1
+ output "id" {
2
+ value = azurerm_container_registry.this.id
3
+ description = "The ID of the Container Registry"
4
+ }
5
+
6
+ output "login_server" {
7
+ value = azurerm_container_registry.this.login_server
8
+ description = "The URL used to log into the Container Registry"
9
+ }
10
+
11
+ output "name" {
12
+ value = azurerm_container_registry.this.name
13
+ description = "The name of the Container Registry"
14
+ }
@@ -0,0 +1,20 @@
1
+ variable "name" {
2
+ type = string
3
+ description = "Globally unique name for the Container Registry (5-50 alphanumeric characters). Uniqueness is the caller's responsibility."
4
+ }
5
+
6
+ variable "resource_group_name" {
7
+ type = string
8
+ description = "Name of the resource group in which to create the Container Registry"
9
+ }
10
+
11
+ variable "location" {
12
+ type = string
13
+ description = "Azure region in which to create the Container Registry"
14
+ }
15
+
16
+ variable "sku" {
17
+ type = string
18
+ description = "SKU for the Container Registry"
19
+ default = "Basic"
20
+ }
@@ -0,0 +1,80 @@
1
+ resource "azurerm_cdn_frontdoor_profile" "this" {
2
+ name = var.name
3
+ resource_group_name = var.resource_group_name
4
+ sku_name = var.sku_name
5
+ }
6
+
7
+ resource "azurerm_cdn_frontdoor_endpoint" "this" {
8
+ name = "${var.name}-endpoint"
9
+ cdn_frontdoor_profile_id = azurerm_cdn_frontdoor_profile.this.id
10
+ }
11
+
12
+ resource "azurerm_cdn_frontdoor_origin_group" "this" {
13
+ for_each = var.origins
14
+
15
+ name = "${each.key}-og"
16
+ cdn_frontdoor_profile_id = azurerm_cdn_frontdoor_profile.this.id
17
+
18
+ load_balancing {
19
+ sample_size = 4
20
+ successful_samples_required = 3
21
+ }
22
+
23
+ health_probe {
24
+ protocol = "Https"
25
+ request_type = "HEAD"
26
+ path = "/"
27
+ interval_in_seconds = 100
28
+ }
29
+ }
30
+
31
+ resource "azurerm_cdn_frontdoor_origin" "this" {
32
+ for_each = var.origins
33
+
34
+ name = "${each.key}-origin"
35
+ cdn_frontdoor_origin_group_id = azurerm_cdn_frontdoor_origin_group.this[each.key].id
36
+
37
+ host_name = each.value.host_name
38
+ # Without this, Front Door forwards the client's original Host header (the custom domain)
39
+ # straight through to the origin. Container Apps routes ingress by Host header and has no
40
+ # idea about the custom domain (only Front Door's custom domain resource knows it) — it
41
+ # responds "This Container App is stopped or does not exist" for an unrecognized host.
42
+ # Rewrite to the origin's own *.azurecontainerapps.io hostname so it always matches.
43
+ origin_host_header = each.value.host_name
44
+ certificate_name_check_enabled = true
45
+ http_port = each.value.http_port
46
+ https_port = each.value.https_port
47
+ }
48
+
49
+ resource "azurerm_cdn_frontdoor_custom_domain" "this" {
50
+ for_each = { for k, v in var.origins : k => v if v.custom_domain != null }
51
+
52
+ name = "${each.key}-domain"
53
+ cdn_frontdoor_profile_id = azurerm_cdn_frontdoor_profile.this.id
54
+ host_name = each.value.custom_domain
55
+
56
+ tls {
57
+ certificate_type = "ManagedCertificate"
58
+ }
59
+ }
60
+
61
+ resource "azurerm_cdn_frontdoor_route" "this" {
62
+ for_each = var.origins
63
+
64
+ name = "${each.key}-route"
65
+ cdn_frontdoor_endpoint_id = azurerm_cdn_frontdoor_endpoint.this.id
66
+ cdn_frontdoor_origin_group_id = azurerm_cdn_frontdoor_origin_group.this[each.key].id
67
+ cdn_frontdoor_origin_ids = [azurerm_cdn_frontdoor_origin.this[each.key].id]
68
+ cdn_frontdoor_custom_domain_ids = each.value.custom_domain != null ? [azurerm_cdn_frontdoor_custom_domain.this[each.key].id] : []
69
+
70
+ supported_protocols = ["Http", "Https"]
71
+ patterns_to_match = ["/*"]
72
+ forwarding_protocol = "HttpsOnly"
73
+ https_redirect_enabled = true
74
+
75
+ # All routes share one Front Door endpoint/profile — every route linking to the shared
76
+ # default *.azurefd.net domain with the same "/*" pattern collides the moment a second
77
+ # app is added ("this combination already exists in Route <other-app>-route"). Only fall
78
+ # back to the default domain when an app has no custom domain of its own to route on.
79
+ link_to_default_domain = each.value.custom_domain == null
80
+ }
@@ -0,0 +1,20 @@
1
+ output "endpoint_hostname" {
2
+ value = azurerm_cdn_frontdoor_endpoint.this.host_name
3
+ description = "Hostname of the Front Door endpoint shared by all apps/origins"
4
+ }
5
+
6
+ output "profile_id" {
7
+ value = azurerm_cdn_frontdoor_profile.this.id
8
+ description = "ID of the Front Door profile"
9
+ }
10
+
11
+ output "custom_domain_validation" {
12
+ description = "Per-domain DNS records the domain owner must create: a TXT record at _dnsauth.<host_name> with value validation_token (proves ownership so Azure can issue the managed certificate), and a CNAME from host_name to the shared endpoint_hostname above."
13
+ value = {
14
+ for k, d in azurerm_cdn_frontdoor_custom_domain.this : d.host_name => {
15
+ txt_record_name = "_dnsauth.${d.host_name}"
16
+ txt_record_value = d.validation_token
17
+ cname_target = azurerm_cdn_frontdoor_endpoint.this.host_name
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,26 @@
1
+ variable "name" {
2
+ description = "Name of the Front Door profile"
3
+ type = string
4
+ }
5
+
6
+ variable "resource_group_name" {
7
+ description = "Name of the resource group where the Front Door profile will be created"
8
+ type = string
9
+ }
10
+
11
+ variable "sku_name" {
12
+ description = "SKU for the Front Door profile"
13
+ type = string
14
+ default = "Standard_AzureFrontDoor"
15
+ }
16
+
17
+ variable "origins" {
18
+ description = "Map of apps/origins to expose behind the shared Front Door profile/endpoint. The map key is used as the app name and as a suffix for the origin group/origin/route resources of that app."
19
+ type = map(object({
20
+ host_name = string
21
+ http_port = optional(number, 80)
22
+ https_port = optional(number, 443)
23
+ custom_domain = optional(string)
24
+ }))
25
+ default = {}
26
+ }
@@ -0,0 +1,48 @@
1
+ resource "azuread_application" "this" {
2
+ count = var.enabled && var.existing_client_id == null ? 1 : 0
3
+
4
+ display_name = "${var.project_name}-github-actions"
5
+ }
6
+
7
+ resource "azuread_service_principal" "this" {
8
+ count = var.enabled && var.existing_client_id == null ? 1 : 0
9
+
10
+ client_id = azuread_application.this[0].client_id
11
+ }
12
+
13
+ resource "azuread_application_federated_identity_credential" "this" {
14
+ for_each = var.enabled && var.existing_client_id == null ? toset(var.branches) : toset([])
15
+
16
+ application_id = azuread_application.this[0].id
17
+ display_name = "github-${each.value}"
18
+ audiences = ["api://AzureADTokenExchange"]
19
+ issuer = "https://token.actions.githubusercontent.com"
20
+ subject = "repo:${var.github_organization}/${var.github_repo_name}:ref:refs/heads/${each.value}"
21
+ }
22
+
23
+ # Looked up (not created) when the app/SP/federated credential were set up out-of-band by a
24
+ # tenant admin — see variables.tf for why. Skipped entirely when the caller already supplies
25
+ # existing_principal_id: the lookup needs Directory.Read.All/Application.Read.All on the
26
+ # deploying SP, which a lot of tenants won't grant just to resolve one known object ID.
27
+ data "azuread_service_principal" "existing" {
28
+ count = var.enabled && var.existing_client_id != null && var.existing_principal_id == null ? 1 : 0
29
+
30
+ client_id = var.existing_client_id
31
+ }
32
+
33
+ locals {
34
+ # null when disabled — the parent template must guard any reference to outputs on `var.enabled`.
35
+ principal_id = var.enabled ? (
36
+ var.existing_principal_id != null ? var.existing_principal_id : (
37
+ var.existing_client_id == null ? azuread_service_principal.this[0].object_id : data.azuread_service_principal.existing[0].object_id
38
+ )
39
+ ) : null
40
+ }
41
+
42
+ resource "azurerm_role_assignment" "contributor" {
43
+ count = var.enabled && var.manage_role_assignment ? 1 : 0
44
+
45
+ scope = var.role_assignment_scope
46
+ role_definition_name = "Contributor"
47
+ principal_id = local.principal_id
48
+ }
@@ -0,0 +1,9 @@
1
+ output "client_id" {
2
+ value = var.enabled ? (var.existing_client_id != null ? var.existing_client_id : azuread_application.this[0].client_id) : null
3
+ description = "Client ID of the Azure AD application used by GitHub Actions to authenticate via OIDC. Null when disabled."
4
+ }
5
+
6
+ output "principal_id" {
7
+ value = local.principal_id
8
+ description = "Object ID of the service principal, used by the parent template to grant additional roles (e.g. AcrPush/AcrPull). Null when disabled."
9
+ }
@@ -0,0 +1,69 @@
1
+ variable "project_name" {
2
+ description = "Project name used as prefix for resource names"
3
+ type = string
4
+ }
5
+
6
+ variable "github_organization" {
7
+ description = "GitHub organization name for the OIDC trust subject"
8
+ type = string
9
+ }
10
+
11
+ variable "github_repo_name" {
12
+ description = "GitHub repository name for the OIDC trust subject"
13
+ type = string
14
+ }
15
+
16
+ variable "branches" {
17
+ description = "Branches allowed to assume the Azure AD application via OIDC. One federated credential is created per branch."
18
+ type = list(string)
19
+ default = ["main"]
20
+ }
21
+
22
+ variable "role_assignment_scope" {
23
+ description = "Scope for the Contributor role assignment, typically the resource group ID"
24
+ type = string
25
+ }
26
+
27
+ variable "existing_client_id" {
28
+ description = <<-EOT
29
+ Client ID of an app registration + service principal + federated identity credential(s)
30
+ already created out-of-band by a tenant admin. Set this when the deploying SP lacks the
31
+ Entra ID "Application Administrator" role (creating an app registration requires it,
32
+ regardless of subscription-level RBAC) — the module then looks up and reuses that identity
33
+ instead of creating a new one. The Contributor role assignment is still managed by this
34
+ module either way, since that only needs "User Access Administrator" on the subscription.
35
+ EOT
36
+ type = string
37
+ default = null
38
+ }
39
+
40
+ variable "existing_principal_id" {
41
+ description = <<-EOT
42
+ Object ID (principal ID) of the service principal identified by existing_client_id, when
43
+ that's already known. Set this to skip looking it up via Microsoft Graph entirely — the
44
+ lookup (data.azuread_service_principal.existing) needs Directory.Read.All or
45
+ Application.Read.All on the deploying SP, which many tenant admins won't grant just to
46
+ resolve one known object. Whoever created the app out-of-band can get this by running
47
+ `az ad sp show --id <existing_client_id> --query id -o tsv`.
48
+ EOT
49
+ type = string
50
+ default = null
51
+ }
52
+
53
+ variable "manage_role_assignment" {
54
+ description = <<-EOT
55
+ Whether Terraform should create/manage the Contributor role assignment for this identity.
56
+ Set to false when it was already granted out-of-band alongside existing_client_id (e.g. a
57
+ tenant admin's onboarding runbook already ran `az role assignment create ... --role
58
+ Contributor`) — otherwise Terraform tries to create it again on every apply and fails with
59
+ RoleAssignmentExists.
60
+ EOT
61
+ type = bool
62
+ default = true
63
+ }
64
+
65
+ variable "enabled" {
66
+ description = "Set to false to skip this module entirely (no app, no SP, no role assignment). Use to unblock testing while waiting on a tenant admin for Application Administrator or an existing_client_id."
67
+ type = bool
68
+ default = true
69
+ }
@@ -0,0 +1,21 @@
1
+ # Application Key Vault (RBAC-based), used by apps to read secrets at runtime.
2
+ # Distinct from the bootstrap vault created by the CLI via SDK.
3
+ resource "azurerm_key_vault" "this" {
4
+ name = var.name
5
+ resource_group_name = var.resource_group_name
6
+ location = var.location
7
+ tenant_id = var.tenant_id
8
+ sku_name = var.sku_name
9
+
10
+ rbac_authorization_enabled = true
11
+ purge_protection_enabled = false
12
+ soft_delete_retention_days = 7
13
+
14
+ public_network_access_enabled = length(var.ip_rules) > 0 ? true : var.public_network_access_enabled
15
+
16
+ network_acls {
17
+ bypass = "AzureServices"
18
+ default_action = var.network_acls_default_action
19
+ ip_rules = var.ip_rules
20
+ }
21
+ }
@@ -0,0 +1,11 @@
1
+ output "id" {
2
+ value = azurerm_key_vault.this.id
3
+ }
4
+
5
+ output "vault_uri" {
6
+ value = azurerm_key_vault.this.vault_uri
7
+ }
8
+
9
+ output "name" {
10
+ value = azurerm_key_vault.this.name
11
+ }
@@ -0,0 +1,43 @@
1
+ variable "name" {
2
+ type = string
3
+ description = "Key Vault name"
4
+ }
5
+
6
+ variable "resource_group_name" {
7
+ type = string
8
+ description = "Resource group where the Key Vault is created"
9
+ }
10
+
11
+ variable "location" {
12
+ type = string
13
+ description = "Azure region"
14
+ }
15
+
16
+ variable "tenant_id" {
17
+ type = string
18
+ description = "Azure AD tenant ID"
19
+ }
20
+
21
+ variable "sku_name" {
22
+ type = string
23
+ description = "Key Vault SKU"
24
+ default = "standard"
25
+ }
26
+
27
+ variable "public_network_access_enabled" {
28
+ type = bool
29
+ description = "Allow access from the public internet. Defaults to false: this vault isn't read by anything at runtime today (no VNet/private-endpoint consumer wired up yet), so there's no reason to expose it publicly."
30
+ default = false
31
+ }
32
+
33
+ variable "network_acls_default_action" {
34
+ type = string
35
+ description = "Default action for requests not matched by an IP/VNet rule. Defaults to \"Deny\" — flip to \"Allow\" (or add a private endpoint) only once something outside the vault's own resource group needs to read from it."
36
+ default = "Deny"
37
+ }
38
+
39
+ variable "ip_rules" {
40
+ type = list(string)
41
+ description = "Public IPs/CIDRs allowed through the firewall (e.g. the operator machine running `tofu apply`, which needs data-plane access to write secrets). Forces public_network_access_enabled on when non-empty, since Azure ignores network_acls entirely while public access is off."
42
+ default = []
43
+ }