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 +4 -4
- data/CHANGELOG.md +12 -0
- data/lib/belt/cli/destroy_command.rb +90 -0
- data/lib/belt/cli/environment_command.rb +87 -12
- data/lib/belt/cli/environment_config.rb +2 -0
- data/lib/belt/version.rb +1 -1
- data/lib/templates/environment/main.tf.erb +7 -6
- data/lib/templates/environment/terraform.tfvars.erb +3 -0
- data/lib/templates/environment/variables.tf.erb +6 -0
- data/lib/templates/module/dns.tf.erb +75 -17
- data/lib/templates/module/frontend.tf.erb +4 -4
- data/lib/templates/module/outputs.tf.erb +2 -2
- data/lib/templates/module/variables.tf.erb +6 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b3545e62c7890adc16931218212e284d8e68994814be13ae0e231d5aee89b2ec
|
|
4
|
+
data.tar.gz: 89b19c24b919d441bffbbb8cd426697e656b8726596c24c58cf5f03d79233c81
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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*"([^"]+)"/)
|
data/lib/belt/version.rb
CHANGED
|
@@ -35,14 +35,15 @@ provider "conveyor-belt" {
|
|
|
35
35
|
module "app" {
|
|
36
36
|
source = "../modules/app"
|
|
37
37
|
|
|
38
|
-
app_name
|
|
39
|
-
environment
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
|
@@ -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:
|
|
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
|
-
|
|
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:
|
|
11
|
-
|
|
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
|
|
53
|
-
name
|
|
54
|
-
type
|
|
55
|
-
ttl
|
|
56
|
-
records
|
|
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
|
|
68
|
-
domain_name
|
|
69
|
-
|
|
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
|