@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,76 @@
1
+ module "vpc" {
2
+ source = "terraform-aws-modules/vpc/aws"
3
+ version = "5.7.1"
4
+
5
+ name = var.project_name
6
+ cidr = "10.0.0.0/16"
7
+
8
+ azs = data.aws_availability_zones.available.names
9
+ private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
10
+ public_subnets = ["10.0.4.0/24", "10.0.5.0/24"]
11
+
12
+ enable_nat_gateway = true
13
+ single_nat_gateway = true
14
+ enable_dns_hostnames = true
15
+
16
+ default_security_group_egress = [{
17
+ cidr_blocks = "0.0.0.0/0"
18
+ }]
19
+
20
+ default_security_group_ingress = [
21
+ {
22
+ description = "Allow inbound traffic from the same security group"
23
+ from_port = 0
24
+ to_port = 65535
25
+ protocol = "tcp"
26
+ self = true
27
+ },
28
+ {
29
+ description = "Allow inbound traffic from the same security group (UDP)"
30
+ from_port = 0
31
+ to_port = 65535
32
+ protocol = "udp"
33
+ self = true
34
+ },
35
+ ]
36
+
37
+ tags = {
38
+ Terraform = "true"
39
+ Environment = var.project_name
40
+ }
41
+ }
42
+
43
+ resource "aws_iam_role" "flow_logs_role" {
44
+ name = "${var.project_name}-flow-logs-role"
45
+
46
+ assume_role_policy = jsonencode({
47
+ Version = "2012-10-17"
48
+ Statement = [{
49
+ Effect = "Allow"
50
+ Principal = {
51
+ Service = "vpc-flow-logs.amazonaws.com"
52
+ }
53
+ Action = "sts:AssumeRole"
54
+ }]
55
+ })
56
+ }
57
+
58
+ resource "aws_iam_role_policy" "flow_logs_policy" {
59
+ name = "${var.project_name}-flow-logs-policy"
60
+ role = aws_iam_role.flow_logs_role.id
61
+
62
+ policy = jsonencode({
63
+ Version = "2012-10-17"
64
+ Statement = [{
65
+ Effect = "Allow"
66
+ Action = [
67
+ "logs:CreateLogGroup",
68
+ "logs:CreateLogStream",
69
+ "logs:PutLogEvents",
70
+ "logs:DescribeLogGroups",
71
+ "logs:DescribeLogStreams"
72
+ ]
73
+ Resource = "*"
74
+ }]
75
+ })
76
+ }
@@ -0,0 +1,23 @@
1
+ output "default_security_group_id" {
2
+ value = module.vpc.default_security_group_id
3
+ }
4
+
5
+ output "vpc_id" {
6
+ value = module.vpc.vpc_id
7
+ }
8
+
9
+ output "private_subnets" {
10
+ value = module.vpc.private_subnets
11
+ }
12
+
13
+ output "public_subnets" {
14
+ value = module.vpc.public_subnets
15
+ }
16
+
17
+ output "igw_id" {
18
+ value = module.vpc.igw_id
19
+ }
20
+
21
+ output "vpc_cidr_block" {
22
+ value = module.vpc.vpc_cidr_block
23
+ }
@@ -0,0 +1,5 @@
1
+ variable "project_name" {
2
+ description = "A unique name for your project. This will be used as part of resource names."
3
+ default = ""
4
+ }
5
+
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "@fuel/infra",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "description": "AWS (ECS) and Azure (Container Apps) infrastructure templates and modules for FUEL CLI"
6
+ }
@@ -0,0 +1,168 @@
1
+ # Manual deploy (without the `fuel` CLI)
2
+
3
+ `fuel infra:deploy` is a thin wrapper around Azure SDK calls (bootstrap) + `tofu`
4
+ (everything else). This is the manual equivalent, reverse-engineered from
5
+ `packages/cli/src/infra/{orchestrator,tofu-runner,azure-state-storage}.ts` — useful
6
+ on a machine without Node/`fuel` installed (e.g. CI runner, teammate's laptop).
7
+
8
+ Everything below assumes this `infra/` folder was already generated once by
9
+ `fuel infra:init` (so `backend.tf`/`default.tfvars` are already patched with real
10
+ names, not the `{placeholder}` tokens from the template source). If that never ran,
11
+ you don't have a project to deploy yet — this doc only replaces the _deploy_ step.
12
+
13
+ ## 0. Prerequisites
14
+
15
+ - A service principal's `AZURE_CLIENT_ID`/`AZURE_CLIENT_SECRET`/`AZURE_TENANT_ID`/
16
+ `AZURE_SUBSCRIPTION_ID` (same ones in `.fuel-credentials` at the project root,
17
+ gitignored). `az login` alone isn't enough — the bootstrap steps below and the
18
+ Tofu provider (`providers.tf`) are wired to these four values explicitly, not to
19
+ whatever identity `az` is currently logged in as.
20
+ - `tofu` (OpenTofu) installed.
21
+ - Your `fuel.yml` fully filled out (apps, `variables:`, etc.) — this is what gets
22
+ uploaded as the bootstrap secret in step 3.
23
+
24
+ Compute the naming suffix once — it's deterministic, derived from the project name
25
+ (`deriveSuffix` in `s3-state-bucket.ts`):
26
+
27
+ ```bash
28
+ PROJECT_NAME=<your fuel.yml project name>
29
+ SUFFIX=$(echo -n "$PROJECT_NAME" | shasum -a 256 | cut -c1-8)
30
+ LOCATION=<your fuel.yml location, e.g. eastus>
31
+
32
+ RESOURCE_GROUP="${PROJECT_NAME}-rg"
33
+ STORAGE_ACCOUNT=$(echo -n "st${PROJECT_NAME}${SUFFIX}" | tr -cd 'a-zA-Z0-9' | tr 'A-Z' 'a-z' | cut -c1-24)
34
+ KEY_VAULT_NAME=$(echo -n "kv-${PROJECT_NAME}-${SUFFIX}" | cut -c1-24)
35
+ ```
36
+
37
+ These must match what's already baked into your `infra/backend.tf` and
38
+ `infra/default.tfvars` — diff against those files if unsure instead of recomputing.
39
+
40
+ ## 1. Bootstrap resource group, state storage, Key Vault
41
+
42
+ Skip any sub-step whose resource already exists (all of this is idempotent in the
43
+ CLI — `create` commands below aren't, so check first if unsure):
44
+
45
+ ```bash
46
+ az group create --name "$RESOURCE_GROUP" --location "$LOCATION"
47
+
48
+ az storage account create \
49
+ --name "$STORAGE_ACCOUNT" \
50
+ --resource-group "$RESOURCE_GROUP" \
51
+ --location "$LOCATION" \
52
+ --sku Standard_LRS \
53
+ --kind StorageV2 \
54
+ --min-tls-version TLS1_2 \
55
+ --allow-blob-public-access false
56
+
57
+ az storage container create \
58
+ --name tfstate \
59
+ --account-name "$STORAGE_ACCOUNT" \
60
+ --auth-mode login
61
+
62
+ az keyvault create \
63
+ --name "$KEY_VAULT_NAME" \
64
+ --resource-group "$RESOURCE_GROUP" \
65
+ --location "$LOCATION"
66
+ ```
67
+
68
+ ## 2. Grant the deploying identity access to the bootstrap vault
69
+
70
+ The bootstrap vault uses access policies (not RBAC) — see the security discussion
71
+ in `TODO-azure-prod.md` for why that's a known gap. Grant secrets access to
72
+ whichever identity runs `tofu apply` (service principal shown here; swap
73
+ `--spn`/`--upn`/`--object-id` for whoever that is):
74
+
75
+ ```bash
76
+ az keyvault set-policy \
77
+ --name "$KEY_VAULT_NAME" \
78
+ --resource-group "$RESOURCE_GROUP" \
79
+ --secret-permissions get list set delete \
80
+ --spn "$AZURE_CLIENT_ID"
81
+ ```
82
+
83
+ Also confirm that identity can create role assignments (Terraform creates several —
84
+ GitHub OIDC → ACR push, per-app Key Vault RBAC, etc.). `Contributor` alone is NOT
85
+ enough — it explicitly excludes `Microsoft.Authorization/roleAssignments/write`:
86
+
87
+ ```bash
88
+ az role assignment create \
89
+ --assignee "$AZURE_CLIENT_ID" \
90
+ --role "User Access Administrator" \
91
+ --scope "/subscriptions/$AZURE_SUBSCRIPTION_ID"
92
+ ```
93
+
94
+ ## 3. Upload fuel.yml as the bootstrap secret
95
+
96
+ Terraform's `data.azurerm_key_vault_secret.config` (see `data.tf`) expects a secret
97
+ literally named `infrastructure-variables` holding your entire `fuel.yml` as YAML:
98
+
99
+ ```bash
100
+ az keyvault secret set \
101
+ --vault-name "$KEY_VAULT_NAME" \
102
+ --name infrastructure-variables \
103
+ --file fuel.yml
104
+ ```
105
+
106
+ (The CLI also injects a random `seed` and `github_org` key into this payload before
107
+ uploading — neither is currently read by any `.tf` file in this template, so
108
+ uploading the raw `fuel.yml` as-is is equivalent today.)
109
+
110
+ ## 4. Run Tofu
111
+
112
+ From the `infra/` folder:
113
+
114
+ ```bash
115
+ export ARM_SUBSCRIPTION_ID="$AZURE_SUBSCRIPTION_ID"
116
+ export ARM_TENANT_ID="$AZURE_TENANT_ID"
117
+ export ARM_CLIENT_ID="$AZURE_CLIENT_ID"
118
+ export ARM_CLIENT_SECRET="$AZURE_CLIENT_SECRET"
119
+
120
+ # The provider is wired to var.azure_client_secret directly (see providers.tf), so it still
121
+ # needs to be supplied as a Tofu variable — but never via -var, which would leave it readable
122
+ # in process listings (ps, /proc). TF_VAR_* is how Tofu reads variables from the environment.
123
+ export TF_VAR_azure_client_secret="$AZURE_CLIENT_SECRET"
124
+
125
+ tofu init
126
+
127
+ tofu plan \
128
+ -var-file=default.tfvars \
129
+ -var="azure_subscription_id=$AZURE_SUBSCRIPTION_ID" \
130
+ -var="azure_tenant_id=$AZURE_TENANT_ID" \
131
+ -var="azure_client_id=$AZURE_CLIENT_ID" \
132
+ -var="github_organization=<your GitHub org>" \
133
+ -var="db_username=<slugified project name>" \
134
+ -var="db_name=<slugified project name>" \
135
+ -var="github_repo_name=<repo name>"
136
+
137
+ # same -var flags, swap plan for apply -auto-approve when ready
138
+ tofu apply -auto-approve \
139
+ -var-file=default.tfvars \
140
+ -var="azure_subscription_id=$AZURE_SUBSCRIPTION_ID" \
141
+ -var="azure_tenant_id=$AZURE_TENANT_ID" \
142
+ -var="azure_client_id=$AZURE_CLIENT_ID" \
143
+ -var="github_organization=<your GitHub org>" \
144
+ -var="db_username=<slugified project name>" \
145
+ -var="db_name=<slugified project name>" \
146
+ -var="github_repo_name=<repo name>"
147
+ ```
148
+
149
+ `<slugified project name>` = your project name, lowercased, non-alphanumeric chars
150
+ replaced with `_` (see `slugify()` in `orchestrator.ts`).
151
+
152
+ ## 5. What `fuel infra:deploy` does after `tofu apply` (manual equivalents)
153
+
154
+ Not infra, but part of the same flow if you want full parity:
155
+
156
+ - Reads `tofu output -json` for `acr_login_server`, `github_oidc_client_id`,
157
+ `front_door_endpoint_hostname`, etc.
158
+ - Sets GitHub Actions repo secrets (`AZURE_TENANT_ID`, `AZURE_SUBSCRIPTION_ID`,
159
+ `AZURE_CLIENT_ID` from the OIDC output, `PROJECT_NAME`, `ACR_LOGIN_SERVER`,
160
+ `RESOURCE_GROUP`) via `gh secret set <name> --repo <org>/<repo> --body <value>`.
161
+ - Creates the GitHub repo (`gh repo create`) if it doesn't exist, sets branch
162
+ protection, pushes the local commit.
163
+ - Prints any custom-domain DNS validation records from
164
+ `custom_domain_validation` output (TXT + CNAME) for you to add at your DNS
165
+ provider.
166
+
167
+ None of this is required for the Azure resources themselves to exist — only for
168
+ CI/CD to work against them afterward.
@@ -0,0 +1,8 @@
1
+ terraform {
2
+ backend "azurerm" {
3
+ resource_group_name = "{resource_group_name}"
4
+ storage_account_name = "{storage_account_name}"
5
+ container_name = "{container_name}"
6
+ key = "terraform.tfstate"
7
+ }
8
+ }
@@ -0,0 +1,21 @@
1
+ data "azurerm_resource_group" "this" {
2
+ name = var.resource_group_name
3
+ }
4
+
5
+ data "azurerm_key_vault" "bootstrap" {
6
+ name = var.key_vault_name
7
+ resource_group_name = var.resource_group_name
8
+ }
9
+
10
+ data "azurerm_key_vault_secret" "config" {
11
+ name = "infrastructure-variables"
12
+ key_vault_id = data.azurerm_key_vault.bootstrap.id
13
+ }
14
+
15
+ # azurerm_container_app's ip_security_restriction.ip_address_range is validated client-side as a
16
+ # strict CIDR — unlike NSG rules or `az containerapp ingress access-restriction set`, it doesn't
17
+ # accept the "AzureFrontDoor.Backend" service tag directly. Resolve it to real CIDRs instead.
18
+ data "azurerm_network_service_tags" "front_door_backend" {
19
+ location = var.location
20
+ service = "AzureFrontDoor.Backend"
21
+ }
@@ -0,0 +1,116 @@
1
+ locals {
2
+ credentials = yamldecode(data.azurerm_key_vault_secret.config.value)
3
+ apps = yamldecode(data.azurerm_key_vault_secret.config.value).apps
4
+
5
+ # Sensitive variables per app — kept separate so they never leak into plan output
6
+ app_variables = { for i in local.apps : nonsensitive(i.name) => try(i.variables, {}) }
7
+ app_build_env = { for i in local.apps : nonsensitive(i.name) => try(i.build_env, {}) }
8
+
9
+ worker_list = nonsensitive(flatten([for i in local.apps : [
10
+ [for worker in try(i.workers, []) : {
11
+ app_name = i.name
12
+ technology = i.technology
13
+ dockerfile = worker.dockerfile
14
+ worker_name = worker.name
15
+ branch = i.branch
16
+ }]
17
+ ]]))
18
+
19
+ app_list = nonsensitive([for i in local.apps :
20
+ {
21
+ app_name = i.name
22
+ technology = i.technology
23
+ port = i.port
24
+ domain = i.domain
25
+ health_check_path = i.health_check_path
26
+ dockerfile = i.dockerfile
27
+ branch = i.branch
28
+
29
+ database_enabled = try(i.database.enabled, false)
30
+ database_engine = try(i.database.engine, "mssql")
31
+ database_sku = try(i.database.sku_name, "GP_S_Gen5_4")
32
+ database_storage_type = try(i.database.storage_account_type, "Local")
33
+ database_pitr_days = try(i.database.pitr_retention_days, 20)
34
+ database_ltr = try(i.database.ltr_weekly_retention, "P21W")
35
+ database_min_capacity = try(i.database.min_capacity, 0.5)
36
+ database_storage_mb = try(i.database.allocated_storage_mb, 32768)
37
+
38
+ redis_enabled = try(i.redis.enabled, false)
39
+ redis_sku = try(i.redis.sku_name, "Balanced_B0")
40
+
41
+ cpu = try(i.container_app.cpu, 1)
42
+ memory = try(i.container_app.memory, "2Gi")
43
+ concurrent_requests = try(i.container_app.concurrent_requests, 20)
44
+ min_replicas = try(i.container_app.min_replicas, 0)
45
+ max_replicas = try(i.container_app.max_replicas, 3)
46
+ }
47
+ ])
48
+
49
+ # Every app (nest or react/Next.js) runs as a Container App — matches how ECS always ran them.
50
+ container_apps = local.app_list
51
+
52
+ # Keyed lookup so container_app_worker (for_each'd by worker_name) can read its parent app's
53
+ # flags (redis_enabled, database_enabled, ...) without going through the shared `env_vars` trap below.
54
+ app_list_by_name = { for a in local.app_list : a.app_name => a }
55
+
56
+ # Keyed lookup so worker_key_vault_secrets_user (for_each'd by worker_name via
57
+ # module.container_app_worker) can find its parent app's name.
58
+ worker_list_by_name = { for w in local.worker_list : w.worker_name => w }
59
+
60
+ # Just a feature flag, not a real secret — stripped so it can be used in count/for_each.
61
+ github_oidc_enabled = nonsensitive(try(local.credentials.github_oidc.enabled, true))
62
+
63
+ # An Azure AD app client ID/object ID is a public identifier, not a secret — stripped so it
64
+ # doesn't poison the github_oidc_client_id output (var.enabled had this same issue, above).
65
+ github_oidc_existing_client_id = nonsensitive(try(local.credentials.github_oidc.existing_client_id, null))
66
+ github_oidc_existing_principal_id = nonsensitive(try(local.credentials.github_oidc.existing_principal_id, null))
67
+ # Just a feature flag, not a real secret — same treatment as github_oidc_enabled above.
68
+ github_oidc_manage_role_assignment = nonsensitive(try(local.credentials.github_oidc.manage_role_assignment, true))
69
+
70
+ # Container Apps' ip_security_restriction rejects IPv6 ranges (IpRestrictionsAddressEnteredInvalid)
71
+ # even though the AzureFrontDoor.Backend service tag includes them — IPv4-only until that's fixed.
72
+ front_door_backend_ipv4_cidrs = [
73
+ for cidr in data.azurerm_network_service_tags.front_door_backend.address_prefixes : cidr
74
+ if !strcontains(cidr, ":")
75
+ ]
76
+
77
+ # Seeds every Container App on first create. The real ACR tag never exists yet at that
78
+ # point — the CI pipeline that pushes it depends on this same apply having already
79
+ # created the GitHub OIDC identity/repo secrets. module.container_app/container_app_worker
80
+ # ignore_changes on this attribute, so CI's first real push takes over permanently.
81
+ bootstrap_container_image = "mcr.microsoft.com/azuredocs/containerapps-helloworld:latest"
82
+
83
+ # Drives which private DNS zones/subnets module.network actually provisions —
84
+ # no point creating a postgres zone when every app uses mssql, etc.
85
+ network_needs_postgres = anytrue([for a in local.app_list : a.database_enabled && a.database_engine == "postgres"])
86
+ network_needs_mssql = anytrue([for a in local.app_list : a.database_enabled && a.database_engine == "mssql"])
87
+ network_needs_redis = anytrue([for a in local.app_list : a.redis_enabled])
88
+
89
+ storage_account_name = substr(lower(replace("${var.project_name}app", "/[^a-zA-Z0-9]/", "")), 0, 24)
90
+
91
+ # One Key Vault per app (mirrors the ECS template's one-Secrets-Manager-secret-per-app
92
+ # pattern) — each app's Container App identity only gets RBAC on its own vault, not a
93
+ # shared one every app could read every other app's secrets from.
94
+ #
95
+ # Key Vault names are globally unique and capped at 24 chars, so truncating the naive
96
+ # "kv" + project_name + app_name concatenation can wipe out app_name entirely for long
97
+ # project names, making every app in the project collide on the same vault name. Reserve
98
+ # the last 8 chars of the (slugified) app_name as a fixed suffix and only truncate the
99
+ # project_name portion, so distinct apps always produce distinct names.
100
+ app_name_suffixes = {
101
+ for a in local.app_list : a.app_name => substr(lower(replace(a.app_name, "/[^a-zA-Z0-9]/", "")), -8, 8)
102
+ }
103
+ app_key_vault_names = {
104
+ for a in local.app_list : a.app_name => "kv${substr(lower(replace(var.project_name, "/[^a-zA-Z0-9]/", "")), 0, max(0, 24 - 2 - length(local.app_name_suffixes[a.app_name])))}${local.app_name_suffixes[a.app_name]}"
105
+ }
106
+
107
+ # Whoever runs `tofu apply` (local machine, not CI) needs a data-plane network path to
108
+ # write app_variables into each vault — see data.http.tofu_operator_ip in main.tf.
109
+ tofu_operator_ip_cidr = "${chomp(data.http.tofu_operator_ip.response_body)}/32"
110
+
111
+ # NOTE: env vars are computed inline inside each `module.container_app`/`container_app_worker`
112
+ # instance (main.tf), not as a single shared local here. A `for` over all apps producing one map
113
+ # value can't be partially resolved — if any one app's database/redis module fails, HCL can't
114
+ # tell Terraform "the other keys are fine," so the whole map (and everything reading from it)
115
+ # blocks. Computing it per for_each instance keeps each app's Container App creation independent.
116
+ }