belt 0.1.8 → 0.1.9
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/lib/belt/cli/deploy_command.rb +40 -0
- data/lib/belt/cli/destroy_command.rb +8 -28
- data/lib/belt/cli/environment_command.rb +4 -12
- data/lib/belt/cli/frontend_command.rb +11 -13
- data/lib/belt/cli/frontend_setup_command.rb +14 -55
- data/lib/belt/cli/generate_command.rb +73 -10
- data/lib/belt/cli/new_command.rb +82 -33
- data/lib/belt/cli/setup_command.rb +2 -8
- data/lib/belt/cli/tables_command.rb +16 -21
- data/lib/belt/version.rb +1 -1
- data/lib/templates/environment/backend.tf.erb +2 -2
- data/lib/templates/environment/main.tf.erb +14 -18
- data/lib/templates/environment/outputs.tf.erb +28 -3
- data/lib/templates/environment/terraform.tfvars.erb +5 -0
- data/lib/templates/environment/variables.tf.erb +6 -0
- data/lib/templates/module/dns.tf.erb +83 -0
- data/lib/templates/module/frontend.tf.erb +179 -0
- data/lib/templates/module/main.tf.erb +42 -0
- data/lib/templates/module/outputs.tf.erb +24 -0
- data/lib/templates/module/variables.tf.erb +27 -0
- data/lib/templates/views/Form.jsx.erb +63 -7
- data/lib/templates/views/Index.jsx.erb +41 -10
- data/lib/templates/views/Show.jsx.erb +12 -2
- metadata +6 -1
|
@@ -61,7 +61,7 @@ module Belt
|
|
|
61
61
|
exit 1
|
|
62
62
|
end
|
|
63
63
|
|
|
64
|
-
#
|
|
64
|
+
# Resolve final bucket name now that we have credentials
|
|
65
65
|
@bucket_name = resolve_bucket_name unless @custom_bucket
|
|
66
66
|
|
|
67
67
|
@bucket_name = interactive_bucket_selection if @select_mode
|
|
@@ -142,13 +142,7 @@ module Belt
|
|
|
142
142
|
if @custom_bucket
|
|
143
143
|
@custom_bucket
|
|
144
144
|
else
|
|
145
|
-
|
|
146
|
-
"#{@app_name}-terraform-state-#{@env_name}"
|
|
147
|
-
else
|
|
148
|
-
"#{@app_name}-terraform-state"
|
|
149
|
-
end
|
|
150
|
-
base = "#{base}-#{@aws_account_id}-#{@region}" if @aws_account_id
|
|
151
|
-
s3_safe_name(base)
|
|
145
|
+
'belt-terraform-state'
|
|
152
146
|
end
|
|
153
147
|
end
|
|
154
148
|
|
|
@@ -9,6 +9,7 @@ module Belt
|
|
|
9
9
|
module CLI
|
|
10
10
|
class TablesCommand
|
|
11
11
|
SCHEMA_FILE = 'infrastructure/schema.tf.rb'
|
|
12
|
+
MODULE_DIR = 'infrastructure/modules/app'
|
|
12
13
|
|
|
13
14
|
include AppDetection
|
|
14
15
|
|
|
@@ -16,37 +17,32 @@ module Belt
|
|
|
16
17
|
env = EnvResolver.resolve(args)
|
|
17
18
|
|
|
18
19
|
if env.nil?
|
|
19
|
-
puts 'Usage: belt setup tables
|
|
20
|
-
puts "\nReads schema.tf.rb and generates dynamodb.tf in the
|
|
20
|
+
puts 'Usage: belt setup tables [environment]'
|
|
21
|
+
puts "\nReads schema.tf.rb and generates dynamodb.tf in the app module."
|
|
21
22
|
puts 'You can also set BELT_ENV to skip the environment argument.'
|
|
22
23
|
puts "\nExamples:"
|
|
23
|
-
puts ' belt setup tables
|
|
24
|
-
puts ' belt setup tables
|
|
25
|
-
puts ' BELT_ENV=
|
|
24
|
+
puts ' belt setup tables'
|
|
25
|
+
puts ' belt setup tables dev'
|
|
26
|
+
puts ' BELT_ENV=dev belt setup tables'
|
|
26
27
|
exit 1
|
|
27
28
|
end
|
|
28
29
|
|
|
29
30
|
new(env).run
|
|
30
31
|
end
|
|
31
32
|
|
|
32
|
-
# Automatically sync dynamodb.tf
|
|
33
|
+
# Automatically sync dynamodb.tf in the app module.
|
|
33
34
|
# Called by generators after updating schema.tf.rb.
|
|
34
35
|
def self.sync_all_environments
|
|
35
36
|
return unless File.exist?(SCHEMA_FILE)
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
environments.each do |env|
|
|
41
|
-
new(env, quiet: true).run
|
|
42
|
-
end
|
|
38
|
+
# With the module approach, we only need to generate once into modules/app/
|
|
39
|
+
new(nil, quiet: true).run
|
|
43
40
|
end
|
|
44
41
|
|
|
45
42
|
def initialize(env, quiet: false)
|
|
46
43
|
@env = env
|
|
47
44
|
@quiet = quiet
|
|
48
45
|
@app_name = detect_app_name
|
|
49
|
-
@env_dir = "infrastructure/#{@env}"
|
|
50
46
|
end
|
|
51
47
|
|
|
52
48
|
def run
|
|
@@ -67,11 +63,11 @@ module Belt
|
|
|
67
63
|
abort "Error: #{SCHEMA_FILE} not found. Run `belt generate resource` first." unless @quiet
|
|
68
64
|
return
|
|
69
65
|
end
|
|
70
|
-
return if Dir.exist?(
|
|
66
|
+
return if Dir.exist?(MODULE_DIR)
|
|
71
67
|
|
|
72
68
|
unless @quiet
|
|
73
|
-
abort "Error:
|
|
74
|
-
"
|
|
69
|
+
abort "Error: Module directory not found at #{MODULE_DIR}/.\n" \
|
|
70
|
+
"Run `belt new` to create a project with the correct structure."
|
|
75
71
|
end
|
|
76
72
|
end
|
|
77
73
|
|
|
@@ -82,7 +78,6 @@ module Belt
|
|
|
82
78
|
schema_content = File.read(SCHEMA_FILE)
|
|
83
79
|
|
|
84
80
|
# Replace DSL wrapper with direct parser call
|
|
85
|
-
# Belt.application.schema.draw do ... end → parser.instance_eval do ... end
|
|
86
81
|
inner = schema_content.sub(/\A(?:Belt\.application)\.schema\.draw do\n?/, '').sub(/\n?end\s*\z/, '')
|
|
87
82
|
inner.strip!
|
|
88
83
|
|
|
@@ -93,7 +88,7 @@ module Belt
|
|
|
93
88
|
end
|
|
94
89
|
|
|
95
90
|
def generate_dynamodb_tf(models)
|
|
96
|
-
dest = File.join(
|
|
91
|
+
dest = File.join(MODULE_DIR, 'dynamodb.tf')
|
|
97
92
|
existing_content = File.exist?(dest) ? File.read(dest) : nil
|
|
98
93
|
new_content = render_dynamodb(models)
|
|
99
94
|
|
|
@@ -108,7 +103,7 @@ module Belt
|
|
|
108
103
|
else
|
|
109
104
|
puts " create #{dest}"
|
|
110
105
|
puts "\n✓ Generated DynamoDB tables for #{models.size} model(s):"
|
|
111
|
-
models.each { |m| puts " • #{
|
|
106
|
+
models.each { |m| puts " • #{Belt::Inflector.pluralize(m[:name])}" }
|
|
112
107
|
puts "\nRun `belt deploy` to create them."
|
|
113
108
|
end
|
|
114
109
|
end
|
|
@@ -116,7 +111,7 @@ module Belt
|
|
|
116
111
|
def render_dynamodb(models)
|
|
117
112
|
blocks = models.map { |m| render_table(m) }
|
|
118
113
|
"# Auto-generated by Belt from schema.tf.rb\n" \
|
|
119
|
-
"# Do not edit manually — re-run `belt setup tables
|
|
114
|
+
"# Do not edit manually — re-run `belt setup tables`\n\n#{blocks.join("\n\n")}\n"
|
|
120
115
|
end
|
|
121
116
|
|
|
122
117
|
def render_table(model)
|
|
@@ -142,7 +137,7 @@ module Belt
|
|
|
142
137
|
end
|
|
143
138
|
|
|
144
139
|
def table_name(model_name)
|
|
145
|
-
"#{@app_name}
|
|
140
|
+
"#{@app_name}-${var.environment}-#{Belt::Inflector.pluralize(model_name)}"
|
|
146
141
|
end
|
|
147
142
|
|
|
148
143
|
# Minimal DSL parser for schema.tf.rb
|
data/lib/belt/version.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
terraform {
|
|
2
2
|
backend "s3" {
|
|
3
|
-
bucket = "
|
|
4
|
-
key = "<%= @env_name %>/terraform.tfstate"
|
|
3
|
+
bucket = "belt-terraform-state"
|
|
4
|
+
key = "<%= s3_safe_name(@app_name) %>/<%= @env_name %>/terraform.tfstate"
|
|
5
5
|
region = "us-east-1"
|
|
6
6
|
encrypt = true
|
|
7
7
|
}
|
|
@@ -32,25 +32,21 @@ provider "conveyor-belt" {
|
|
|
32
32
|
aws_region = var.aws_region
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
source = "${path.module}/../routes.tf.rb"
|
|
38
|
-
app_name = var.app_name
|
|
39
|
-
lambda_source_dir = "${path.module}/../../lambda"
|
|
40
|
-
lambda_shared_dirs = ["controllers", "helpers", "lib", "models", "views"]
|
|
41
|
-
frontend_urls = var.environment == "prod" ? [] : ["http://localhost:3000"]
|
|
42
|
-
friendly_errors = var.environment != "prod"
|
|
35
|
+
module "app" {
|
|
36
|
+
source = "../modules/app"
|
|
43
37
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
38
|
+
app_name = var.app_name
|
|
39
|
+
environment = var.environment
|
|
40
|
+
aws_region = var.aws_region
|
|
41
|
+
domain = var.domain
|
|
42
|
+
frontend_urls = concat(
|
|
43
|
+
var.domain != "" ? ["https://${var.environment == "prod" ? var.domain : "${var.environment}.${var.domain}"}"] : [],
|
|
44
|
+
var.environment == "prod" ? [] : ["http://localhost:3000"]
|
|
45
|
+
)
|
|
49
46
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
47
|
+
providers = {
|
|
48
|
+
aws = aws
|
|
49
|
+
random = random
|
|
50
|
+
conveyor-belt = conveyor-belt
|
|
55
51
|
}
|
|
56
52
|
}
|
|
@@ -1,14 +1,39 @@
|
|
|
1
1
|
output "api_url" {
|
|
2
2
|
description = "API Gateway base URL"
|
|
3
|
-
value =
|
|
3
|
+
value = module.app.api_url
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
output "api_urls" {
|
|
7
7
|
description = "Map of gateway names to API Gateway URLs"
|
|
8
|
-
value =
|
|
8
|
+
value = module.app.api_urls
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
output "lambda_functions" {
|
|
12
12
|
description = "Map of action names to Lambda function ARNs"
|
|
13
|
-
value =
|
|
13
|
+
value = module.app.lambda_functions
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
output "app_domain" {
|
|
17
|
+
description = "The application domain for this environment"
|
|
18
|
+
value = module.app.app_domain
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
output "name_servers" {
|
|
22
|
+
description = "NS records to configure at your domain registrar (only when domain is set)"
|
|
23
|
+
value = try(module.app.name_servers, [])
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
output "frontend_bucket_name" {
|
|
27
|
+
description = "S3 bucket for frontend assets"
|
|
28
|
+
value = try(module.app.frontend_bucket_name, "")
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
output "frontend_distribution_id" {
|
|
32
|
+
description = "CloudFront distribution ID"
|
|
33
|
+
value = try(module.app.frontend_distribution_id, "")
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
output "frontend_url" {
|
|
37
|
+
description = "Frontend URL"
|
|
38
|
+
value = try(module.app.frontend_url, "")
|
|
14
39
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# DNS configuration for Belt application
|
|
2
|
+
# Convention: prod → mydomain.com, other envs → <env>.mydomain.com
|
|
3
|
+
|
|
4
|
+
locals {
|
|
5
|
+
# Determine the domain for this environment
|
|
6
|
+
app_domain = var.domain != "" ? (
|
|
7
|
+
var.environment == "prod" ? var.domain : "${var.environment}.${var.domain}"
|
|
8
|
+
) : ""
|
|
9
|
+
|
|
10
|
+
# API subdomain: api.mydomain.com (prod) or api.<env>.mydomain.com (non-prod)
|
|
11
|
+
api_domain = var.domain != "" ? "api.${local.app_domain}" : ""
|
|
12
|
+
|
|
13
|
+
# Whether DNS is enabled
|
|
14
|
+
dns_enabled = var.domain != ""
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
# --- Route53 Hosted Zone ---
|
|
18
|
+
# One zone per environment subdomain (or root domain for prod)
|
|
19
|
+
resource "aws_route53_zone" "app" {
|
|
20
|
+
count = local.dns_enabled ? 1 : 0
|
|
21
|
+
name = local.app_domain
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
# --- ACM Certificate ---
|
|
25
|
+
# Wildcard cert covers the app domain + all subdomains (api.*, www.*, etc.)
|
|
26
|
+
resource "aws_acm_certificate" "app" {
|
|
27
|
+
count = local.dns_enabled ? 1 : 0
|
|
28
|
+
domain_name = local.app_domain
|
|
29
|
+
subject_alternative_names = ["*.${local.app_domain}"]
|
|
30
|
+
validation_method = "DNS"
|
|
31
|
+
|
|
32
|
+
lifecycle {
|
|
33
|
+
create_before_destroy = true
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# DNS validation record for the certificate
|
|
38
|
+
resource "aws_route53_record" "cert_validation" {
|
|
39
|
+
for_each = local.dns_enabled ? {
|
|
40
|
+
for dvo in aws_acm_certificate.app[0].domain_validation_options : dvo.domain_name => {
|
|
41
|
+
name = dvo.resource_record_name
|
|
42
|
+
type = dvo.resource_record_type
|
|
43
|
+
record = dvo.resource_record_value
|
|
44
|
+
}
|
|
45
|
+
} : {}
|
|
46
|
+
|
|
47
|
+
zone_id = aws_route53_zone.app[0].zone_id
|
|
48
|
+
name = each.value.name
|
|
49
|
+
type = each.value.type
|
|
50
|
+
ttl = 300
|
|
51
|
+
records = [each.value.record]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
resource "aws_acm_certificate_validation" "app" {
|
|
55
|
+
count = local.dns_enabled ? 1 : 0
|
|
56
|
+
certificate_arn = aws_acm_certificate.app[0].arn
|
|
57
|
+
validation_record_fqdns = [for record in aws_route53_record.cert_validation : record.fqdn]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# --- API Gateway Custom Domain ---
|
|
61
|
+
resource "aws_api_gateway_domain_name" "api" {
|
|
62
|
+
count = local.dns_enabled ? 1 : 0
|
|
63
|
+
domain_name = local.api_domain
|
|
64
|
+
regional_certificate_arn = aws_acm_certificate_validation.app[0].certificate_arn
|
|
65
|
+
|
|
66
|
+
endpoint_configuration {
|
|
67
|
+
types = ["REGIONAL"]
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# Route53 A record for api.<domain>
|
|
72
|
+
resource "aws_route53_record" "api" {
|
|
73
|
+
count = local.dns_enabled ? 1 : 0
|
|
74
|
+
zone_id = aws_route53_zone.app[0].zone_id
|
|
75
|
+
name = local.api_domain
|
|
76
|
+
type = "A"
|
|
77
|
+
|
|
78
|
+
alias {
|
|
79
|
+
name = aws_api_gateway_domain_name.api[0].regional_domain_name
|
|
80
|
+
zone_id = aws_api_gateway_domain_name.api[0].regional_zone_id
|
|
81
|
+
evaluate_target_health = false
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# Frontend hosting — S3 + CloudFront
|
|
2
|
+
# Custom domain attached when var.domain is set
|
|
3
|
+
|
|
4
|
+
resource "random_string" "frontend_suffix" {
|
|
5
|
+
length = 8
|
|
6
|
+
special = false
|
|
7
|
+
upper = false
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
# S3 bucket for frontend static assets
|
|
11
|
+
resource "aws_s3_bucket" "frontend" {
|
|
12
|
+
bucket = "${var.app_name}-frontend-${var.environment}-${random_string.frontend_suffix.result}"
|
|
13
|
+
|
|
14
|
+
lifecycle {
|
|
15
|
+
ignore_changes = [bucket]
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
resource "aws_s3_bucket_public_access_block" "frontend" {
|
|
20
|
+
bucket = aws_s3_bucket.frontend.id
|
|
21
|
+
|
|
22
|
+
block_public_acls = true
|
|
23
|
+
block_public_policy = true
|
|
24
|
+
ignore_public_acls = true
|
|
25
|
+
restrict_public_buckets = true
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
resource "aws_s3_bucket_server_side_encryption_configuration" "frontend" {
|
|
29
|
+
bucket = aws_s3_bucket.frontend.id
|
|
30
|
+
|
|
31
|
+
rule {
|
|
32
|
+
apply_server_side_encryption_by_default {
|
|
33
|
+
sse_algorithm = "AES256"
|
|
34
|
+
}
|
|
35
|
+
bucket_key_enabled = true
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# CloudFront OAC
|
|
40
|
+
resource "aws_cloudfront_origin_access_control" "frontend" {
|
|
41
|
+
name = "${var.app_name}-${var.environment}-frontend-oac"
|
|
42
|
+
description = "OAC for frontend bucket"
|
|
43
|
+
origin_access_control_origin_type = "s3"
|
|
44
|
+
signing_behavior = "always"
|
|
45
|
+
signing_protocol = "sigv4"
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# CloudFront distribution
|
|
49
|
+
resource "aws_cloudfront_distribution" "frontend" {
|
|
50
|
+
origin {
|
|
51
|
+
domain_name = aws_s3_bucket.frontend.bucket_regional_domain_name
|
|
52
|
+
origin_id = "S3-${aws_s3_bucket.frontend.id}"
|
|
53
|
+
origin_access_control_id = aws_cloudfront_origin_access_control.frontend.id
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
enabled = true
|
|
57
|
+
is_ipv6_enabled = true
|
|
58
|
+
default_root_object = "index.html"
|
|
59
|
+
|
|
60
|
+
# Custom domain aliases (if DNS configured)
|
|
61
|
+
aliases = local.dns_enabled ? compact([
|
|
62
|
+
local.app_domain,
|
|
63
|
+
var.environment == "prod" ? "www.${var.domain}" : ""
|
|
64
|
+
]) : []
|
|
65
|
+
|
|
66
|
+
default_cache_behavior {
|
|
67
|
+
allowed_methods = ["GET", "HEAD", "OPTIONS"]
|
|
68
|
+
cached_methods = ["GET", "HEAD"]
|
|
69
|
+
target_origin_id = "S3-${aws_s3_bucket.frontend.id}"
|
|
70
|
+
compress = true
|
|
71
|
+
viewer_protocol_policy = "redirect-to-https"
|
|
72
|
+
|
|
73
|
+
forwarded_values {
|
|
74
|
+
query_string = false
|
|
75
|
+
cookies {
|
|
76
|
+
forward = "none"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# SPA routing — serve index.html for 404/403
|
|
82
|
+
custom_error_response {
|
|
83
|
+
error_code = 404
|
|
84
|
+
response_code = 200
|
|
85
|
+
response_page_path = "/index.html"
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
custom_error_response {
|
|
89
|
+
error_code = 403
|
|
90
|
+
response_code = 200
|
|
91
|
+
response_page_path = "/index.html"
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
restrictions {
|
|
95
|
+
geo_restriction {
|
|
96
|
+
restriction_type = "none"
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
viewer_certificate {
|
|
101
|
+
cloudfront_default_certificate = !local.dns_enabled
|
|
102
|
+
acm_certificate_arn = local.dns_enabled ? aws_acm_certificate_validation.app[0].certificate_arn : null
|
|
103
|
+
ssl_support_method = local.dns_enabled ? "sni-only" : null
|
|
104
|
+
minimum_protocol_version = local.dns_enabled ? "TLSv1.2_2021" : null
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
tags = {
|
|
108
|
+
Name = "${var.app_name}-${var.environment}-frontend"
|
|
109
|
+
Environment = var.environment
|
|
110
|
+
ManagedBy = "Terraform"
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# S3 bucket policy — CloudFront OAC access only
|
|
115
|
+
resource "aws_s3_bucket_policy" "frontend" {
|
|
116
|
+
bucket = aws_s3_bucket.frontend.id
|
|
117
|
+
|
|
118
|
+
policy = jsonencode({
|
|
119
|
+
Version = "2012-10-17"
|
|
120
|
+
Statement = [
|
|
121
|
+
{
|
|
122
|
+
Sid = "AllowCloudFrontServicePrincipal"
|
|
123
|
+
Effect = "Allow"
|
|
124
|
+
Principal = {
|
|
125
|
+
Service = "cloudfront.amazonaws.com"
|
|
126
|
+
}
|
|
127
|
+
Action = "s3:GetObject"
|
|
128
|
+
Resource = "${aws_s3_bucket.frontend.arn}/*"
|
|
129
|
+
Condition = {
|
|
130
|
+
StringEquals = {
|
|
131
|
+
"AWS:SourceArn" = aws_cloudfront_distribution.frontend.arn
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
]
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
depends_on = [aws_s3_bucket_public_access_block.frontend]
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
# Route53 record for frontend (app domain → CloudFront)
|
|
142
|
+
resource "aws_route53_record" "frontend" {
|
|
143
|
+
count = local.dns_enabled ? 1 : 0
|
|
144
|
+
zone_id = aws_route53_zone.app[0].zone_id
|
|
145
|
+
name = local.app_domain
|
|
146
|
+
type = "A"
|
|
147
|
+
|
|
148
|
+
alias {
|
|
149
|
+
name = aws_cloudfront_distribution.frontend.domain_name
|
|
150
|
+
zone_id = aws_cloudfront_distribution.frontend.hosted_zone_id
|
|
151
|
+
evaluate_target_health = false
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
# www record for prod only
|
|
156
|
+
resource "aws_route53_record" "frontend_www" {
|
|
157
|
+
count = local.dns_enabled && var.environment == "prod" ? 1 : 0
|
|
158
|
+
zone_id = aws_route53_zone.app[0].zone_id
|
|
159
|
+
name = "www.${var.domain}"
|
|
160
|
+
type = "A"
|
|
161
|
+
|
|
162
|
+
alias {
|
|
163
|
+
name = aws_cloudfront_distribution.frontend.domain_name
|
|
164
|
+
zone_id = aws_cloudfront_distribution.frontend.hosted_zone_id
|
|
165
|
+
evaluate_target_health = false
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
output "frontend_bucket_name" {
|
|
170
|
+
value = aws_s3_bucket.frontend.id
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
output "frontend_distribution_id" {
|
|
174
|
+
value = aws_cloudfront_distribution.frontend.id
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
output "frontend_url" {
|
|
178
|
+
value = local.dns_enabled ? "https://${local.app_domain}" : "https://${aws_cloudfront_distribution.frontend.domain_name}"
|
|
179
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Belt application module — defines all shared infrastructure.
|
|
2
|
+
# Environments call this module with different variables.
|
|
3
|
+
|
|
4
|
+
terraform {
|
|
5
|
+
required_providers {
|
|
6
|
+
aws = {
|
|
7
|
+
source = "hashicorp/aws"
|
|
8
|
+
version = "~> 5.0"
|
|
9
|
+
}
|
|
10
|
+
random = {
|
|
11
|
+
source = "hashicorp/random"
|
|
12
|
+
version = "~> 3.0"
|
|
13
|
+
}
|
|
14
|
+
conveyor-belt = {
|
|
15
|
+
source = "stowzilla/conveyor-belt"
|
|
16
|
+
version = "~> 0.0.1"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
resource "conveyor_belt" "main" {
|
|
22
|
+
provider = conveyor-belt
|
|
23
|
+
|
|
24
|
+
source = "${path.module}/../../routes.tf.rb"
|
|
25
|
+
app_name = var.app_name
|
|
26
|
+
lambda_source_dir = "${path.module}/../../../lambda"
|
|
27
|
+
lambda_shared_dirs = ["controllers", "helpers", "lib", "models", "views"]
|
|
28
|
+
frontend_urls = var.frontend_urls
|
|
29
|
+
friendly_errors = var.environment != "prod"
|
|
30
|
+
|
|
31
|
+
custom_domain_name = local.dns_enabled ? local.api_domain : ""
|
|
32
|
+
|
|
33
|
+
# Per-lambda configuration: env vars, timeouts, memory
|
|
34
|
+
lambda_config = {
|
|
35
|
+
api = {
|
|
36
|
+
env_vars = {
|
|
37
|
+
WELCOME_TITLE = "Welcome to ${var.app_name}"
|
|
38
|
+
WELCOME_SUBTITLE = "API Gateway → Lambda → DynamoDB — all connected."
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
output "api_url" {
|
|
2
|
+
description = "API Gateway base URL (custom domain if configured, otherwise default)"
|
|
3
|
+
value = var.domain != "" ? "https://${local.api_domain}" : values(conveyor_belt.main.api_gateway_urls)[0]
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
output "api_urls" {
|
|
7
|
+
description = "Map of gateway names to API Gateway URLs"
|
|
8
|
+
value = conveyor_belt.main.api_gateway_urls
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
output "lambda_functions" {
|
|
12
|
+
description = "Map of action names to Lambda function ARNs"
|
|
13
|
+
value = conveyor_belt.main.lambda_functions
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
output "app_domain" {
|
|
17
|
+
description = "The application domain for this environment"
|
|
18
|
+
value = local.app_domain
|
|
19
|
+
}
|
|
20
|
+
|
|
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 : []
|
|
24
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
variable "app_name" {
|
|
2
|
+
description = "Name of the application"
|
|
3
|
+
type = string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
variable "environment" {
|
|
7
|
+
description = "Environment name (e.g., dev, prod, staging)"
|
|
8
|
+
type = string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
variable "aws_region" {
|
|
12
|
+
description = "AWS region"
|
|
13
|
+
type = string
|
|
14
|
+
default = "us-east-1"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
variable "domain" {
|
|
18
|
+
description = "Root domain for the application (e.g., myapp.com). Leave empty to skip DNS."
|
|
19
|
+
type = string
|
|
20
|
+
default = ""
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
variable "frontend_urls" {
|
|
24
|
+
description = "URLs allowed for CORS frontend access"
|
|
25
|
+
type = list(string)
|
|
26
|
+
default = []
|
|
27
|
+
}
|