@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.
- package/README.md +65 -2
- package/dist/commands/create-app.d.ts.map +1 -1
- package/dist/commands/create-app.js +9 -3
- package/dist/commands/create-app.js.map +1 -1
- package/dist/commands/infra-deploy.d.ts.map +1 -1
- package/dist/commands/infra-deploy.js +3 -0
- package/dist/commands/infra-deploy.js.map +1 -1
- package/dist/commands/infra-init.d.ts.map +1 -1
- package/dist/commands/infra-init.js +63 -6
- package/dist/commands/infra-init.js.map +1 -1
- package/dist/infra/credentials.d.ts.map +1 -1
- package/dist/infra/credentials.js +5 -2
- package/dist/infra/credentials.js.map +1 -1
- package/dist/infra/github-client.d.ts +6 -0
- package/dist/infra/github-client.d.ts.map +1 -1
- package/dist/infra/github-client.js +30 -0
- package/dist/infra/github-client.js.map +1 -1
- package/dist/infra/github-token-link.d.ts +4 -0
- package/dist/infra/github-token-link.d.ts.map +1 -0
- package/dist/infra/github-token-link.js +28 -0
- package/dist/infra/github-token-link.js.map +1 -0
- package/dist/infra/orchestrator.d.ts +4 -1
- package/dist/infra/orchestrator.d.ts.map +1 -1
- package/dist/infra/orchestrator.js +16 -7
- package/dist/infra/orchestrator.js.map +1 -1
- package/dist/infra/preflight.d.ts.map +1 -1
- package/dist/infra/preflight.js +9 -1
- package/dist/infra/preflight.js.map +1 -1
- package/dist/infra/template-engine.d.ts +0 -4
- package/dist/infra/template-engine.d.ts.map +1 -1
- package/dist/infra/template-engine.js +36 -12
- package/dist/infra/template-engine.js.map +1 -1
- package/dist/infra/types.d.ts.map +1 -1
- package/dist/infra/types.js +58 -6
- package/dist/infra/types.js.map +1 -1
- package/dist/infra-templates/TODO-azure-prod.md +119 -0
- package/dist/infra-templates/modules/azure-container-app/main.tf +124 -0
- package/dist/infra-templates/modules/azure-container-app/outputs.tf +24 -0
- package/dist/infra-templates/modules/azure-container-app/variables.tf +80 -0
- package/dist/infra-templates/modules/azure-container-apps-environment/main.tf +18 -0
- package/dist/infra-templates/modules/azure-container-apps-environment/outputs.tf +14 -0
- package/dist/infra-templates/modules/azure-container-apps-environment/variables.tf +24 -0
- package/dist/infra-templates/modules/azure-container-registry/main.tf +10 -0
- package/dist/infra-templates/modules/azure-container-registry/outputs.tf +14 -0
- package/dist/infra-templates/modules/azure-container-registry/variables.tf +20 -0
- package/dist/infra-templates/modules/azure-front-door/main.tf +80 -0
- package/dist/infra-templates/modules/azure-front-door/outputs.tf +20 -0
- package/dist/infra-templates/modules/azure-front-door/variables.tf +26 -0
- package/dist/infra-templates/modules/azure-github-oidc/main.tf +48 -0
- package/dist/infra-templates/modules/azure-github-oidc/outputs.tf +9 -0
- package/dist/infra-templates/modules/azure-github-oidc/variables.tf +69 -0
- package/dist/infra-templates/modules/azure-key-vault/main.tf +21 -0
- package/dist/infra-templates/modules/azure-key-vault/outputs.tf +11 -0
- package/dist/infra-templates/modules/azure-key-vault/variables.tf +43 -0
- package/dist/infra-templates/modules/azure-monitor/main.tf +29 -0
- package/dist/infra-templates/modules/azure-monitor/outputs.tf +17 -0
- package/dist/infra-templates/modules/azure-monitor/variables.tf +32 -0
- package/dist/infra-templates/modules/azure-network/main.tf +134 -0
- package/dist/infra-templates/modules/azure-network/outputs.tf +44 -0
- package/dist/infra-templates/modules/azure-network/variables.tf +56 -0
- package/dist/infra-templates/modules/azure-postgresql-flexible/main.tf +34 -0
- package/dist/infra-templates/modules/azure-postgresql-flexible/outputs.tf +20 -0
- package/dist/infra-templates/modules/azure-postgresql-flexible/variables.tf +54 -0
- package/dist/infra-templates/modules/azure-redis-cache/main.tf +32 -0
- package/dist/infra-templates/modules/azure-redis-cache/outputs.tf +15 -0
- package/dist/infra-templates/modules/azure-redis-cache/variables.tf +30 -0
- package/dist/infra-templates/modules/azure-resource-group/main.tf +5 -0
- package/dist/infra-templates/modules/azure-resource-group/outputs.tf +11 -0
- package/dist/infra-templates/modules/azure-resource-group/variables.tf +16 -0
- package/dist/infra-templates/modules/azure-sql-database/main.tf +63 -0
- package/dist/infra-templates/modules/azure-sql-database/outputs.tf +20 -0
- package/dist/infra-templates/modules/azure-sql-database/variables.tf +78 -0
- package/dist/infra-templates/modules/azure-storage-account/main.tf +13 -0
- package/dist/infra-templates/modules/azure-storage-account/outputs.tf +16 -0
- package/dist/infra-templates/modules/azure-storage-account/variables.tf +38 -0
- package/dist/infra-templates/modules/cloud-watch/event-bridge.tf +15 -0
- package/dist/infra-templates/modules/cloud-watch/lambda.tf +84 -0
- package/dist/infra-templates/modules/cloud-watch/lambda_function.zip +0 -0
- package/dist/infra-templates/modules/cloud-watch/lambda_function_deploy.zip +0 -0
- package/dist/infra-templates/modules/cloud-watch/main.tf +156 -0
- package/dist/infra-templates/modules/cloud-watch/sns.tf +3 -0
- package/dist/infra-templates/modules/cloud-watch/variables.tf +38 -0
- package/dist/infra-templates/modules/ecs-core/alb.tf +126 -0
- package/dist/infra-templates/modules/ecs-core/cloudwatch.tf +48 -0
- package/dist/infra-templates/modules/ecs-core/iam.tf +95 -0
- package/dist/infra-templates/modules/ecs-core/main.tf +12 -0
- package/dist/infra-templates/modules/ecs-core/outputs.tf +44 -0
- package/dist/infra-templates/modules/ecs-core/secrets.tf +9 -0
- package/dist/infra-templates/modules/ecs-core/security_groups.tf +73 -0
- package/dist/infra-templates/modules/ecs-core/variables.tf +96 -0
- package/dist/infra-templates/modules/ecs-service/ecs_autoscaling.tf +51 -0
- package/dist/infra-templates/modules/ecs-service/ecs_service.tf +39 -0
- package/dist/infra-templates/modules/ecs-service/ecs_task_definition.tf +56 -0
- package/dist/infra-templates/modules/ecs-service/variables.tf +170 -0
- package/dist/infra-templates/modules/elastic-container-registry/main.tf +32 -0
- package/dist/infra-templates/modules/elastic-container-registry/outputs.tf +9 -0
- package/dist/infra-templates/modules/elastic-container-registry/variables.tf +25 -0
- package/dist/infra-templates/modules/github-oidc-role/main.tf +46 -0
- package/dist/infra-templates/modules/github-oidc-role/outputs.tf +4 -0
- package/dist/infra-templates/modules/github-oidc-role/variables.tf +11 -0
- package/dist/infra-templates/modules/guardduty/main.tf +8 -0
- package/dist/infra-templates/modules/iam/main.tf +205 -0
- package/dist/infra-templates/modules/iam/outputs.tf +3 -0
- package/dist/infra-templates/modules/iam/variables.tf +4 -0
- package/dist/infra-templates/modules/random-key/main.tf +11 -0
- package/dist/infra-templates/modules/random-key/outputs.tf +4 -0
- package/dist/infra-templates/modules/random-key/variables.tf +10 -0
- package/dist/infra-templates/modules/redis/main.tf +47 -0
- package/dist/infra-templates/modules/redis/outputs.tf +8 -0
- package/dist/infra-templates/modules/redis/variables.tf +20 -0
- package/dist/infra-templates/modules/relational-database-service/main.tf +44 -0
- package/dist/infra-templates/modules/relational-database-service/outputs.tf +13 -0
- package/dist/infra-templates/modules/relational-database-service/security-group.tf +15 -0
- package/dist/infra-templates/modules/relational-database-service/variables.tf +50 -0
- package/dist/infra-templates/modules/secret/main.tf +14 -0
- package/dist/infra-templates/modules/secret/outputs.tf +3 -0
- package/dist/infra-templates/modules/secret/variables.tf +8 -0
- package/dist/infra-templates/modules/simple-storage-service/main.tf +28 -0
- package/dist/infra-templates/modules/simple-storage-service/variables.tf +4 -0
- package/dist/infra-templates/modules/vpc/data.tf +2 -0
- package/dist/infra-templates/modules/vpc/main.tf +76 -0
- package/dist/infra-templates/modules/vpc/outputs.tf +23 -0
- package/dist/infra-templates/modules/vpc/variables.tf +5 -0
- package/dist/infra-templates/package.json +6 -0
- package/dist/infra-templates/templates/azure/MANUAL_DEPLOY.md +168 -0
- package/dist/infra-templates/templates/azure/backend.tf +8 -0
- package/dist/infra-templates/templates/azure/data.tf +21 -0
- package/dist/infra-templates/templates/azure/default.tfvars +0 -0
- package/dist/infra-templates/templates/azure/locals.tf +116 -0
- package/dist/infra-templates/templates/azure/main.tf +421 -0
- package/dist/infra-templates/templates/azure/outputs.tf +40 -0
- package/dist/infra-templates/templates/azure/providers.tf +65 -0
- package/dist/infra-templates/templates/azure/variables.tf +69 -0
- package/dist/infra-templates/templates/azure/workflows/build_and_deploy.yml +155 -0
- package/dist/infra-templates/templates/azure/workflows/tests.yml +63 -0
- package/dist/infra-templates/templates/ecs/README.md +225 -0
- package/dist/infra-templates/templates/ecs/backend.tf +7 -0
- package/dist/infra-templates/templates/ecs/data.tf +6 -0
- package/dist/infra-templates/templates/ecs/default.tfvars +1 -0
- package/dist/infra-templates/templates/ecs/locals.tf +97 -0
- package/dist/infra-templates/templates/ecs/main.tf +185 -0
- package/dist/infra-templates/templates/ecs/outputs.tf +42 -0
- package/dist/infra-templates/templates/ecs/providers.tf +21 -0
- package/dist/infra-templates/templates/ecs/variables.tf +105 -0
- package/dist/infra-templates/templates/ecs/workflows/build_and_deploy.yml +188 -0
- package/dist/infra-templates/templates/ecs/workflows/tests.yml +65 -0
- package/package.json +2 -2
- package/schema/fuel.schema.json +78 -44
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
output "db_hostname" {
|
|
2
|
+
value = module.db.db_instance_endpoint
|
|
3
|
+
description = "The database hostname (endpoint) for staging. Will be 'N/A' if not created."
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
output "db_password" {
|
|
7
|
+
value = local.effective_db_password
|
|
8
|
+
sensitive = true
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
output "app_name" {
|
|
12
|
+
value = var.app_name
|
|
13
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
resource "aws_security_group" "this" {
|
|
2
|
+
name = "${var.app_name}-rds-security-group"
|
|
3
|
+
description = "Security group for RDS instance"
|
|
4
|
+
vpc_id = var.vpc_id
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
resource "aws_security_group_rule" "ingress" {
|
|
8
|
+
count = var.allowed_ip_list != "" ? 1 : 0
|
|
9
|
+
type = "ingress"
|
|
10
|
+
from_port = 5432
|
|
11
|
+
to_port = 5432
|
|
12
|
+
protocol = "tcp"
|
|
13
|
+
cidr_blocks = split(",", var.allowed_ip_list)
|
|
14
|
+
security_group_id = aws_security_group.this.id
|
|
15
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
variable "app_name" {
|
|
2
|
+
description = "A unique name for your project. This will be used as part of resource names."
|
|
3
|
+
default = ""
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
variable "allowed_ip_list" {
|
|
7
|
+
description = "The list of IP which can get access into database"
|
|
8
|
+
default = ""
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
variable "vpc_id" {
|
|
12
|
+
description = "The id of environment VPC"
|
|
13
|
+
default = ""
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
variable "subnet_ids" {
|
|
17
|
+
description = "Public subnet IDs"
|
|
18
|
+
default = ""
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
variable "vpc_security_group_default" {
|
|
22
|
+
description = "The id of VPC where create security group for RDS access"
|
|
23
|
+
default = ""
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
variable "db_password" {
|
|
27
|
+
description = "Master database password. Leave empty to auto-generate."
|
|
28
|
+
default = ""
|
|
29
|
+
sensitive = true
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
variable "db_username" {
|
|
33
|
+
description = "Username for db"
|
|
34
|
+
default = ""
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
variable "db_name" {
|
|
38
|
+
description = "Name of db"
|
|
39
|
+
default = ""
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
variable "db_instance_type" {
|
|
43
|
+
description = "Database instance type"
|
|
44
|
+
default = "db.t3.small"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
variable "db_allocated_storage" {
|
|
48
|
+
description = "Database allocated storage"
|
|
49
|
+
default = 30
|
|
50
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
resource "random_id" "secret_suffix" {
|
|
2
|
+
byte_length = 4
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
resource "aws_secretsmanager_secret" "app" {
|
|
6
|
+
name = "${var.name}-${random_id.secret_suffix.hex}"
|
|
7
|
+
description = "This is a ${var.name} secret"
|
|
8
|
+
recovery_window_in_days = 0
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
resource "aws_secretsmanager_secret_version" "app" {
|
|
12
|
+
secret_id = aws_secretsmanager_secret.app.id
|
|
13
|
+
secret_string = jsonencode(var.env_vars)
|
|
14
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
resource "random_id" "bucket_suffix" {
|
|
2
|
+
byte_length = 4
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
module "s3_bucket" {
|
|
6
|
+
source = "terraform-aws-modules/s3-bucket/aws"
|
|
7
|
+
version = "5.9.1"
|
|
8
|
+
|
|
9
|
+
bucket = "${var.project_name}-${random_id.bucket_suffix.hex}"
|
|
10
|
+
acl = "private"
|
|
11
|
+
|
|
12
|
+
control_object_ownership = true
|
|
13
|
+
object_ownership = "ObjectWriter"
|
|
14
|
+
|
|
15
|
+
versioning = {
|
|
16
|
+
enabled = true
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
cors_rule = [
|
|
20
|
+
{
|
|
21
|
+
allowed_headers = ["Content-Type"]
|
|
22
|
+
allowed_methods = ["PUT", "POST", "GET"]
|
|
23
|
+
allowed_origins = ["*"]
|
|
24
|
+
expose_headers = ["ETag"]
|
|
25
|
+
max_age_seconds = 3000
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
@@ -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,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,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
|
+
}
|
|
File without changes
|
|
@@ -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
|
+
}
|