belt 0.3.29 → 0.3.31

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 84353c2ac395a3d2855e5df323c6d90305eda791b58906c41c89571e842325bb
4
- data.tar.gz: d1364d5d4a87087431b67782f2919ffcc2ab37779d30ff779c2a7c9a8ced58ce
3
+ metadata.gz: b3545e62c7890adc16931218212e284d8e68994814be13ae0e231d5aee89b2ec
4
+ data.tar.gz: 89b19c24b919d441bffbbb8cd426697e656b8726596c24c58cf5f03d79233c81
5
5
  SHA512:
6
- metadata.gz: bcea273ea44bfd7410ad5bc249fd21206ee050bf78a7e31ad841e51196a3930e7646e5dd237fb80ecd49253b8fdd9a1982cc5972e46cee43ddfbef4441a8baaa
7
- data.tar.gz: 7cf36fd36c683e8ca9694f27943401a909793a8f64757f03bb44a7f985bd6229670a9876446d4f8ba9b60ee021e700fa2e70d08f120ae72bcec4f4bd2bc512db
6
+ metadata.gz: 7df1d7c18c7208d54cb0e37cbea0cf6c998f9b502950651c7e3e304f2786c3a750d664894d5f7f09cbc8848b550f365d7ebe415a5bcb701da45ef115fb7d28bb
7
+ data.tar.gz: '05380ceae6d72a8874e66f52f616b527baa69f7101513842a6240765d2b1b6644bcd7b6d469acffeab246d18a152eabfb618a52c74753f5e61fd8cbbb663143a'
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.31
4
+
5
+ ### Bug Fix
6
+
7
+ - **`belt destroy environment` now uses the environment's AWS profile**: The
8
+ terraform destroy path (and remote-state detection) previously inherited
9
+ whatever `AWS_PROFILE` was in the shell, which could point at the wrong
10
+ account and fail with "No valid credential sources found". It now loads
11
+ `infrastructure/<env>/belt.rb` and applies the configured `aws_profile` and
12
+ env vars before running terraform — matching `belt deploy` and
13
+ `belt terraform`.
14
+
3
15
  ## 0.3.16
4
16
 
5
17
  ### Enhancement
@@ -6,6 +6,7 @@ require_relative 'auth_command'
6
6
  require_relative 'frontend_registry'
7
7
  require_relative 'generator_registry'
8
8
  require_relative 'tables_command'
9
+ require_relative 'environment_config'
9
10
  require_relative '../inflector'
10
11
 
11
12
  module Belt
@@ -194,6 +195,33 @@ module Belt
194
195
  exit 1
195
196
  end
196
197
 
198
+ # Check if any environments are nested under this one
199
+ nested_children = find_nested_children(@name)
200
+ if nested_children.any?
201
+ puts "⚠ The following environments are nested under '#{@name}':"
202
+ nested_children.each { |child| puts " - #{child}" }
203
+ puts ''
204
+ puts ' Destroying the parent will leave them with broken DNS references.'
205
+ puts ' Consider destroying nested environments first:'
206
+ nested_children.each { |child| puts " belt destroy environment #{child}" }
207
+ puts ''
208
+ unless @force
209
+ print ' Continue anyway? [y/N] '
210
+ response = $stdin.gets&.strip&.downcase
211
+ unless response&.start_with?('y')
212
+ puts 'Cancelled.'
213
+ exit 0
214
+ end
215
+ end
216
+ end
217
+
218
+ # Apply the environment's AWS profile + env vars from infrastructure/<env>/belt.rb
219
+ # BEFORE any terraform/aws calls, so both remote-state detection and the
220
+ # subsequent destroy authenticate against the correct account. Without this,
221
+ # terraform inherits whatever AWS_PROFILE is in the shell, which may point at
222
+ # the wrong account (mirrors `belt deploy` / `belt terraform`).
223
+ apply_env_config! unless @skip_terraform
224
+
197
225
  # Check if terraform state exists (infra may still be live)
198
226
  if !@skip_terraform && terraform_state_exists?(dir)
199
227
  puts "⚠ Environment '#{@name}' appears to have active infrastructure."
@@ -241,6 +269,21 @@ module Belt
241
269
  warn_about_dns_delegation(@name)
242
270
  end
243
271
 
272
+ def find_nested_children(env_name)
273
+ nested_children = []
274
+ Dir.glob('infrastructure/*/terraform.tfvars').each do |tfvars_file|
275
+ next if tfvars_file.include?('/dns/')
276
+ next if tfvars_file.include?('/modules/')
277
+
278
+ content = File.read(tfvars_file)
279
+ next unless content =~ /^\s*parent_environment\s*=\s*"#{Regexp.escape(env_name)}"/
280
+
281
+ child_env = File.basename(File.dirname(tfvars_file))
282
+ nested_children << child_env
283
+ end
284
+ nested_children
285
+ end
286
+
244
287
  def terraform_state_exists?(dir)
245
288
  # Check for local .terraform directory (initialized state)
246
289
  return true if Dir.exist?(File.join(dir, '.terraform'))
@@ -262,6 +305,23 @@ module Belt
262
305
  end
263
306
 
264
307
  def warn_about_dns_delegation(env_name)
308
+ # Check if this is a nested environment (DNS records are in parent's zone, handled by terraform)
309
+ tfvars_file = "infrastructure/#{env_name}/terraform.tfvars"
310
+ if File.exist?(tfvars_file)
311
+ tfvars_content = File.read(tfvars_file)
312
+ if tfvars_content =~ /^\s*parent_environment\s*=\s*"([^"]+)"/
313
+ parent_env = ::Regexp.last_match(1)
314
+ puts ''
315
+ puts "ℹ This was a nested environment under '#{parent_env}'."
316
+ puts ' The DNS A record in the parent zone was deleted by terraform destroy.'
317
+ return
318
+ end
319
+ end
320
+
321
+ # Check if any other environments are nested under this one
322
+ warn_about_nested_children(env_name)
323
+
324
+ # Check for standalone environment DNS delegation
265
325
  dns_tfvars = 'infrastructure/dns/terraform.tfvars'
266
326
  return unless File.exist?(dns_tfvars)
267
327
 
@@ -279,6 +339,27 @@ module Belt
279
339
  puts ' belt dns deploy'
280
340
  end
281
341
 
342
+ def warn_about_nested_children(env_name)
343
+ nested_children = []
344
+ Dir.glob('infrastructure/*/terraform.tfvars').each do |tfvars_file|
345
+ next if tfvars_file.include?('/dns/')
346
+
347
+ content = File.read(tfvars_file)
348
+ next unless content =~ /^\s*parent_environment\s*=\s*"#{Regexp.escape(env_name)}"/
349
+
350
+ child_env = File.basename(File.dirname(tfvars_file))
351
+ nested_children << child_env
352
+ end
353
+
354
+ return if nested_children.empty?
355
+
356
+ puts ''
357
+ puts '⚠ The following environments were nested under this one:'
358
+ nested_children.each { |child| puts " - #{child}" }
359
+ puts ' They may have dangling DNS references. Consider destroying them first,'
360
+ puts ' or manually cleaning up their Route53 records.'
361
+ end
362
+
282
363
  def run_terraform_destroy(dir)
283
364
  puts "\n━━━ terraform destroy (#{@name}) ━━━"
284
365
 
@@ -307,6 +388,15 @@ module Belt
307
388
  puts ' ✓ Infrastructure destroyed.'
308
389
  end
309
390
 
391
+ # Load infrastructure/<env>/belt.rb and apply its aws_profile + env vars to
392
+ # the current process so terraform (and any aws CLI calls during destroy)
393
+ # authenticate against the right account. Mirrors DeployCommand / TerraformCommand.
394
+ def apply_env_config!
395
+ env_config = EnvironmentConfig.load(@name)
396
+ env_config.apply!
397
+ puts " 🔑 Using AWS profile: #{env_config.aws_profile}" if env_config.aws_profile?
398
+ end
399
+
310
400
  def empty_s3_buckets_in_state
311
401
  output = `terraform state list 2>/dev/null`
312
402
  return unless Process.last_status.success?
@@ -3,6 +3,7 @@
3
3
  require 'fileutils'
4
4
  require 'erb'
5
5
  require_relative 'app_detection'
6
+ require_relative 'environment_config'
6
7
  require_relative 'frontend_registry'
7
8
  require_relative 'frontend_setup_command'
8
9
 
@@ -15,26 +16,49 @@ module Belt
15
16
 
16
17
  def self.run(args)
17
18
  env_name = args.shift
19
+ parent_env = args.shift # Optional: parent environment for nested subdomains
18
20
 
19
21
  if env_name.nil? || env_name.empty?
20
- puts 'Usage: belt generate environment <name>'
22
+ puts 'Usage: belt generate environment <name> [parent]'
21
23
  puts "\nExamples:"
22
24
  puts ' belt generate environment dev01'
23
25
  puts ' belt generate environment staging'
24
26
  puts ' belt generate environment prod'
27
+ puts ''
28
+ puts 'Nested environments (subdomain of existing environment):'
29
+ puts ' belt generate environment fizzy123 dev'
30
+ puts ' → Creates fizzy123.dev.example.com using dev\'s wildcard cert'
25
31
  exit 1
26
32
  end
27
33
 
28
- new(env_name).generate
34
+ # Validate parent environment exists if provided
35
+ if parent_env && !parent_env.empty?
36
+ parent_dir = "infrastructure/#{parent_env}"
37
+ unless Dir.exist?(parent_dir)
38
+ puts "✗ Parent environment '#{parent_env}' not found at #{parent_dir}/"
39
+ puts ' The parent environment must exist before creating a nested environment.'
40
+ exit 1
41
+ end
42
+ end
43
+
44
+ new(env_name, parent_environment: parent_env).generate
29
45
  end
30
46
 
31
- def initialize(env_name, quiet: false, domain: nil, announce: true)
47
+ def initialize(env_name, quiet: false, domain: nil, announce: true, parent_environment: nil)
32
48
  @env_name = env_name.downcase.gsub(/[^a-z0-9_-]/, '')
33
49
  @app_name = detect_app_name
34
- @domain = domain
35
50
  @quiet = quiet
36
51
  @announce = announce
52
+ @parent_environment = parent_environment&.downcase&.gsub(/[^a-z0-9_-]/, '')
37
53
  @state_bucket = resolve_state_bucket
54
+
55
+ # For nested envs, inherit domain and aws_profile from parent
56
+ if @parent_environment
57
+ load_parent_config
58
+ @domain = domain || @parent_domain
59
+ else
60
+ @domain = domain
61
+ end
38
62
  end
39
63
 
40
64
  def generate
@@ -46,6 +70,7 @@ module Belt
46
70
  end
47
71
 
48
72
  puts "Creating environment: #{@env_name}" unless @quiet
73
+ puts " (nested under #{@parent_environment})" if @parent_environment && !@quiet
49
74
  FileUtils.mkdir_p(dest_dir)
50
75
 
51
76
  templates.each do |template_name, dest_file|
@@ -54,19 +79,40 @@ module Belt
54
79
  puts " create #{dest_path}" unless @quiet
55
80
  end
56
81
 
82
+ # Generate belt.rb if we have an inherited aws_profile
83
+ if @parent_aws_profile
84
+ belt_rb_path = File.join(dest_dir, 'belt.rb')
85
+ write_belt_rb(belt_rb_path)
86
+ puts " create #{belt_rb_path}" unless @quiet
87
+ end
88
+
57
89
  append_extra_frontend_outputs(File.join(dest_dir, 'outputs.tf'))
58
90
 
59
91
  return if @quiet || !@announce
60
92
 
61
93
  puts "\n✓ Environment '#{@env_name}' created!"
62
- puts "\nReview your settings in #{dest_dir}/terraform.tfvars:"
63
- puts ' environment = "..." # environment name (defaults to directory name)'
64
- puts ' domain = "..." # your domain (e.g., "myapp.com")'
65
- puts "\nThen deploy:"
66
- puts " belt deploy #{@env_name}"
67
- puts "\nIf using multiple environments with a custom domain, run:"
68
- puts ' belt dns generate'
69
- puts 'to manage root domain delegation to per-environment zones.'
94
+
95
+ if @parent_environment
96
+ puts "\nThis is a nested environment under '#{@parent_environment}'."
97
+ puts "It will use the parent's wildcard certificate and hosted zone."
98
+ if @domain
99
+ puts "Domain will be: api.#{@env_name}.#{@parent_environment}.#{@domain}"
100
+ else
101
+ puts "Domain will be: #{@env_name}.#{@parent_environment}.<your-domain>"
102
+ end
103
+ puts "\nDeploy with:"
104
+ puts " belt deploy #{@env_name}"
105
+ puts "\nNo DNS delegation needed — the parent environment handles DNS."
106
+ else
107
+ puts "\nReview your settings in #{dest_dir}/terraform.tfvars:"
108
+ puts ' environment = "..." # environment name (defaults to directory name)'
109
+ puts ' domain = "..." # your domain (e.g., "myapp.com")'
110
+ puts "\nThen deploy:"
111
+ puts " belt deploy #{@env_name}"
112
+ puts "\nIf using multiple environments with a custom domain, run:"
113
+ puts ' belt dns generate'
114
+ puts 'to manage root domain delegation to per-environment zones.'
115
+ end
70
116
  end
71
117
 
72
118
  private
@@ -101,6 +147,35 @@ module Belt
101
147
  bucket_from_sibling || bucket_from_aws || 'belt-terraform-state'
102
148
  end
103
149
 
150
+ # Load domain and aws_profile from parent environment for nested envs
151
+ def load_parent_config
152
+ parent_dir = "infrastructure/#{@parent_environment}"
153
+
154
+ # Load domain from parent's terraform.tfvars
155
+ tfvars_path = File.join(parent_dir, 'terraform.tfvars')
156
+ if File.exist?(tfvars_path)
157
+ content = File.read(tfvars_path)
158
+ domain_match = content.match(/^\s*domain\s*=\s*"([^"]+)"/)
159
+ @parent_domain = domain_match[1] if domain_match
160
+ end
161
+
162
+ # Load aws_profile from parent's belt.rb
163
+ parent_config = EnvironmentConfig.load(@parent_environment)
164
+ @parent_aws_profile = parent_config.aws_profile if parent_config.aws_profile?
165
+ end
166
+
167
+ # Generate belt.rb with inherited aws_profile from parent
168
+ def write_belt_rb(path)
169
+ content = <<~RUBY
170
+ # frozen_string_literal: true
171
+
172
+ Belt.configure do |config|
173
+ config.aws_profile = "#{@parent_aws_profile}"
174
+ end
175
+ RUBY
176
+ File.write(path, content)
177
+ end
178
+
104
179
  def bucket_from_sibling
105
180
  Dir.glob('infrastructure/*/backend.tf').each do |f|
106
181
  match = File.read(f).match(/bucket\s*=\s*"([^"]+)"/)
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'backup_config'
4
+
3
5
  module Belt
4
6
  module CLI
5
7
  class EnvironmentConfig
data/lib/belt/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Belt
4
- VERSION = '0.3.29'
4
+ VERSION = '0.3.31'
5
5
  end
@@ -35,14 +35,15 @@ provider "conveyor-belt" {
35
35
  module "app" {
36
36
  source = "../modules/app"
37
37
 
38
- app_name = var.app_name
39
- environment = var.environment
40
- aws_region = var.aws_region
41
- domain = var.domain
42
- enable_pitr = var.enable_pitr
38
+ app_name = var.app_name
39
+ environment = var.environment
40
+ parent_environment = var.parent_environment
41
+ aws_region = var.aws_region
42
+ domain = var.domain
43
+ enable_pitr = var.enable_pitr
43
44
  deletion_protection = var.deletion_protection
44
45
  frontend_urls = concat(
45
- var.domain != "" ? ["https://${var.environment == "prod" ? var.domain : "${var.environment}.${var.domain}"}"] : [],
46
+ var.domain != "" ? ["https://${var.parent_environment != "" ? "${var.environment}.${var.parent_environment}.${var.domain}" : (var.environment == "prod" ? var.domain : "${var.environment}.${var.domain}")}"] : [],
46
47
  var.environment == "prod" ? [] : ["http://localhost:3000"]
47
48
  )
48
49
 
@@ -1,4 +1,7 @@
1
1
  environment = "<%= @env_name %>"
2
+ <% if @parent_environment && !@parent_environment.empty? -%>
3
+ parent_environment = "<%= @parent_environment %>"
4
+ <% end -%>
2
5
  <% if @domain && !@domain.empty? -%>
3
6
  domain = "<%= @domain %>"
4
7
  <% else -%>
@@ -9,6 +9,12 @@ variable "environment" {
9
9
  type = string
10
10
  }
11
11
 
12
+ variable "parent_environment" {
13
+ description = "Parent environment for nested subdomains (e.g., 'dev' creates fizzy123.dev.example.com). Leave empty for standalone environments."
14
+ type = string
15
+ default = ""
16
+ }
17
+
12
18
  variable "aws_region" {
13
19
  description = "AWS region"
14
20
  type = string
@@ -1,30 +1,58 @@
1
1
  # DNS configuration for Belt application
2
- # Convention: prod → mydomain.com, other envs → <env>.mydomain.com
2
+ # Convention:
3
+ # prod → api.mydomain.com (standalone, creates own zone + cert)
4
+ # dev → api.dev.mydomain.com (standalone, creates own zone + cert)
5
+ # fizzy123 (dev) → api-fizzy123.dev.mydomain.com (nested, uses parent zone + cert)
6
+ #
7
+ # Note: Nested envs use api-<env> prefix (not api.<env>) because SSL wildcards are
8
+ # single-level. The parent's *.dev.mydomain.com covers api-fizzy123.dev.mydomain.com
9
+ # but NOT api.fizzy123.dev.mydomain.com.
3
10
 
4
11
  locals {
12
+ # S3 bucket names follow DNS rules — underscores are not allowed.
13
+ s3_safe_name = replace(var.app_name, "_", "-")
14
+
15
+ # Is this a nested environment?
16
+ is_nested = var.parent_environment != ""
17
+
18
+ # Parent domain for nested envs (e.g., "dev.featureparity.dev")
19
+ parent_domain = local.is_nested ? "${var.parent_environment}.${var.domain}" : ""
20
+
5
21
  # Determine the domain for this environment
22
+ # Nested: fizzy123.dev.mydomain.com
23
+ # Standalone: dev.mydomain.com (or mydomain.com for prod)
6
24
  app_domain = var.domain != "" ? (
7
- var.environment == "prod" ? var.domain : "${var.environment}.${var.domain}"
25
+ local.is_nested ? "${var.environment}.${local.parent_domain}" : (
26
+ var.environment == "prod" ? var.domain : "${var.environment}.${var.domain}"
27
+ )
8
28
  ) : ""
9
29
 
10
- # API subdomain: api.mydomain.com (prod) or api.<env>.mydomain.com (non-prod)
11
- api_domain = var.domain != "" ? "api.${local.app_domain}" : ""
30
+ # API subdomain:
31
+ # Standalone: api.dev.mydomain.com (covered by *.dev.mydomain.com)
32
+ # Nested: api-fizzy123.dev.mydomain.com (api- prefix keeps it single-level under wildcard)
33
+ api_domain = var.domain != "" ? (
34
+ local.is_nested ? "api-${var.environment}.${local.parent_domain}" : "api.${local.app_domain}"
35
+ ) : ""
12
36
 
13
37
  # Whether DNS is enabled
14
38
  dns_enabled = var.domain != ""
15
39
  }
16
40
 
41
+ # =============================================================================
42
+ # STANDALONE ENVIRONMENTS (no parent)
43
+ # =============================================================================
44
+
17
45
  # --- Route53 Hosted Zone ---
18
46
  # One zone per environment subdomain (or root domain for prod)
19
47
  resource "aws_route53_zone" "app" {
20
- count = local.dns_enabled ? 1 : 0
48
+ count = local.dns_enabled && !local.is_nested ? 1 : 0
21
49
  name = local.app_domain
22
50
  }
23
51
 
24
52
  # --- ACM Certificate ---
25
53
  # Wildcard cert covers the app domain + all subdomains (api.*, www.*, etc.)
26
54
  resource "aws_acm_certificate" "app" {
27
- count = local.dns_enabled ? 1 : 0
55
+ count = local.dns_enabled && !local.is_nested ? 1 : 0
28
56
  domain_name = local.app_domain
29
57
  subject_alternative_names = ["*.${local.app_domain}"]
30
58
  validation_method = "DNS"
@@ -41,7 +69,7 @@ resource "aws_acm_certificate" "app" {
41
69
  # Use ellipsis (...) to group duplicates, then take the first element since
42
70
  # they're identical anyway.
43
71
  resource "aws_route53_record" "cert_validation" {
44
- for_each = local.dns_enabled ? {
72
+ for_each = local.dns_enabled && !local.is_nested ? {
45
73
  for dvo in aws_acm_certificate.app[0].domain_validation_options : dvo.resource_record_name => {
46
74
  name = dvo.resource_record_name
47
75
  type = dvo.resource_record_type
@@ -49,24 +77,53 @@ resource "aws_route53_record" "cert_validation" {
49
77
  }...
50
78
  } : {}
51
79
 
52
- zone_id = aws_route53_zone.app[0].zone_id
53
- name = each.value[0].name
54
- type = each.value[0].type
55
- ttl = 300
56
- records = [each.value[0].record]
80
+ zone_id = aws_route53_zone.app[0].zone_id
81
+ name = each.value[0].name
82
+ type = each.value[0].type
83
+ ttl = 300
84
+ records = [each.value[0].record]
85
+ allow_overwrite = true
57
86
  }
58
87
 
59
88
  resource "aws_acm_certificate_validation" "app" {
60
- count = local.dns_enabled ? 1 : 0
89
+ count = local.dns_enabled && !local.is_nested ? 1 : 0
61
90
  certificate_arn = aws_acm_certificate.app[0].arn
62
91
  validation_record_fqdns = [for record in aws_route53_record.cert_validation : record.fqdn]
63
92
  }
64
93
 
94
+ # =============================================================================
95
+ # NESTED ENVIRONMENTS (uses parent zone + wildcard cert)
96
+ # =============================================================================
97
+
98
+ # Look up parent's hosted zone
99
+ data "aws_route53_zone" "parent" {
100
+ count = local.dns_enabled && local.is_nested ? 1 : 0
101
+ name = local.parent_domain
102
+ }
103
+
104
+ # Look up parent's wildcard certificate
105
+ data "aws_acm_certificate" "parent" {
106
+ count = local.dns_enabled && local.is_nested ? 1 : 0
107
+ domain = local.parent_domain
108
+ statuses = ["ISSUED"]
109
+ most_recent = true
110
+ }
111
+
112
+ # =============================================================================
113
+ # API GATEWAY (shared logic, uses appropriate cert/zone)
114
+ # =============================================================================
115
+
65
116
  # --- API Gateway Custom Domain ---
66
117
  resource "aws_api_gateway_domain_name" "api" {
67
- count = local.dns_enabled ? 1 : 0
68
- domain_name = local.api_domain
69
- regional_certificate_arn = aws_acm_certificate_validation.app[0].certificate_arn
118
+ count = local.dns_enabled ? 1 : 0
119
+ domain_name = local.api_domain
120
+
121
+ # Use parent's cert for nested, own cert for standalone
122
+ regional_certificate_arn = local.is_nested ? (
123
+ data.aws_acm_certificate.parent[0].arn
124
+ ) : (
125
+ aws_acm_certificate_validation.app[0].certificate_arn
126
+ )
70
127
 
71
128
  endpoint_configuration {
72
129
  types = ["REGIONAL"]
@@ -74,9 +131,10 @@ resource "aws_api_gateway_domain_name" "api" {
74
131
  }
75
132
 
76
133
  # Route53 A record for api.<domain>
134
+ # For nested envs, this goes in the parent zone
77
135
  resource "aws_route53_record" "api" {
78
136
  count = local.dns_enabled ? 1 : 0
79
- zone_id = aws_route53_zone.app[0].zone_id
137
+ zone_id = local.is_nested ? data.aws_route53_zone.parent[0].zone_id : aws_route53_zone.app[0].zone_id
80
138
  name = local.api_domain
81
139
  type = "A"
82
140
 
@@ -102,7 +102,7 @@ resource "aws_cloudfront_distribution" "<%= @tf_name %>" {
102
102
 
103
103
  viewer_certificate {
104
104
  cloudfront_default_certificate = <%= @include_dns ? '!local.dns_enabled' : 'true' %>
105
- acm_certificate_arn = <%= @include_dns ? 'local.dns_enabled ? aws_acm_certificate_validation.app[0].certificate_arn : null' : 'null' %>
105
+ acm_certificate_arn = <%= @include_dns ? 'local.dns_enabled ? (local.is_nested ? data.aws_acm_certificate.parent[0].arn : aws_acm_certificate_validation.app[0].certificate_arn) : null' : 'null' %>
106
106
  ssl_support_method = <%= @include_dns ? 'local.dns_enabled ? "sni-only" : null' : 'null' %>
107
107
  minimum_protocol_version = <%= @include_dns ? 'local.dns_enabled ? "TLSv1.2_2021" : null' : 'null' %>
108
108
  }
@@ -144,7 +144,7 @@ resource "aws_s3_bucket_policy" "<%= @tf_name %>" {
144
144
  # Route53 record for frontend (app domain → CloudFront)
145
145
  resource "aws_route53_record" "<%= @tf_name %>" {
146
146
  count = local.dns_enabled ? 1 : 0
147
- zone_id = aws_route53_zone.app[0].zone_id
147
+ zone_id = local.is_nested ? data.aws_route53_zone.parent[0].zone_id : aws_route53_zone.app[0].zone_id
148
148
  name = local.app_domain
149
149
  type = "A"
150
150
 
@@ -155,9 +155,9 @@ resource "aws_route53_record" "<%= @tf_name %>" {
155
155
  }
156
156
  }
157
157
 
158
- # www record for prod only
158
+ # www record for prod only (standalone envs only — nested envs don't need www)
159
159
  resource "aws_route53_record" "<%= @tf_name %>_www" {
160
- count = local.dns_enabled && var.environment == "prod" ? 1 : 0
160
+ count = local.dns_enabled && var.environment == "prod" && !local.is_nested ? 1 : 0
161
161
  zone_id = aws_route53_zone.app[0].zone_id
162
162
  name = "www.${var.domain}"
163
163
  type = "A"
@@ -19,6 +19,6 @@ output "app_domain" {
19
19
  }
20
20
 
21
21
  output "name_servers" {
22
- description = "NS records to configure at your registrar"
23
- value = local.dns_enabled ? aws_route53_zone.app[0].name_servers : []
22
+ description = "NS records to configure at your registrar (standalone envs only)"
23
+ value = local.dns_enabled && !local.is_nested ? aws_route53_zone.app[0].name_servers : []
24
24
  }
@@ -8,6 +8,12 @@ variable "environment" {
8
8
  type = string
9
9
  }
10
10
 
11
+ variable "parent_environment" {
12
+ description = "Parent environment for nested subdomains (e.g., 'dev' creates fizzy123.dev.example.com). Leave empty for standalone environments."
13
+ type = string
14
+ default = ""
15
+ }
16
+
11
17
  variable "aws_region" {
12
18
  description = "AWS region"
13
19
  type = string
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: belt
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.29
4
+ version: 0.3.31
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stowzilla