thinkwork-cli 0.5.3 → 0.6.0
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 +4 -0
- package/dist/cli.js +220 -81
- package/dist/terraform/examples/greenfield/main.tf +156 -2
- package/dist/terraform/examples/greenfield/terraform.tfvars.example +10 -0
- package/dist/terraform/modules/app/agentcore-runtime/main.tf +33 -0
- package/dist/terraform/modules/app/job-triggers/main.tf +21 -0
- package/dist/terraform/modules/app/lambda-api/.build/placeholder.zip +0 -0
- package/dist/terraform/modules/app/lambda-api/handlers.tf +66 -16
- package/dist/terraform/modules/app/lambda-api/main.tf +120 -2
- package/dist/terraform/modules/app/lambda-api/outputs.tf +20 -0
- package/dist/terraform/modules/app/lambda-api/variables.tf +22 -0
- package/dist/terraform/modules/app/ses-email/main.tf +173 -10
- package/dist/terraform/modules/app/static-site/main.tf +37 -14
- package/dist/terraform/modules/app/www-dns/README.md +39 -0
- package/dist/terraform/modules/app/www-dns/main.tf +245 -0
- package/dist/terraform/modules/app/www-dns/outputs.tf +14 -0
- package/dist/terraform/modules/app/www-dns/variables.tf +43 -0
- package/dist/terraform/modules/thinkwork/main.tf +52 -9
- package/dist/terraform/modules/thinkwork/outputs.tf +32 -0
- package/dist/terraform/modules/thinkwork/variables.tf +57 -3
- package/package.json +1 -1
|
@@ -33,6 +33,12 @@ variable "certificate_arn" {
|
|
|
33
33
|
default = ""
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
variable "is_spa" {
|
|
37
|
+
description = "When true, configure CloudFront for a single-page app: drop the directory-rewrite function and fall back to /index.html with 200 on 403/404 so the client router can handle deep links."
|
|
38
|
+
type = bool
|
|
39
|
+
default = false
|
|
40
|
+
}
|
|
41
|
+
|
|
36
42
|
################################################################################
|
|
37
43
|
# S3 Bucket
|
|
38
44
|
################################################################################
|
|
@@ -74,6 +80,11 @@ resource "aws_cloudfront_origin_access_control" "site" {
|
|
|
74
80
|
#
|
|
75
81
|
# S3 with OAC doesn't auto-serve index.html for subdirectory requests.
|
|
76
82
|
# /getting-started/ → /getting-started/index.html
|
|
83
|
+
#
|
|
84
|
+
# Always created (so flipping is_spa on an existing distribution doesn't hit
|
|
85
|
+
# CloudFront's "can't delete a function still associated with a distribution"
|
|
86
|
+
# error), but the viewer-request association is only wired up for non-SPA
|
|
87
|
+
# sites. For SPAs we rely on the 403/404 → /index.html fallback below.
|
|
77
88
|
################################################################################
|
|
78
89
|
|
|
79
90
|
resource "aws_cloudfront_function" "rewrite" {
|
|
@@ -94,6 +105,19 @@ resource "aws_cloudfront_function" "rewrite" {
|
|
|
94
105
|
EOF
|
|
95
106
|
}
|
|
96
107
|
|
|
108
|
+
locals {
|
|
109
|
+
# For SPAs, 403/404 from S3 means "not a real asset" — serve index.html with
|
|
110
|
+
# a 200 so the client router can resolve the route. For directory-style
|
|
111
|
+
# static sites, surface a real 404 page.
|
|
112
|
+
error_responses = var.is_spa ? [
|
|
113
|
+
{ error_code = 403, response_code = 200, response_page_path = "/index.html" },
|
|
114
|
+
{ error_code = 404, response_code = 200, response_page_path = "/index.html" },
|
|
115
|
+
] : [
|
|
116
|
+
{ error_code = 404, response_code = 404, response_page_path = "/404.html" },
|
|
117
|
+
{ error_code = 403, response_code = 404, response_page_path = "/404.html" },
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
|
|
97
121
|
################################################################################
|
|
98
122
|
# CloudFront Distribution
|
|
99
123
|
################################################################################
|
|
@@ -117,9 +141,12 @@ resource "aws_cloudfront_distribution" "site" {
|
|
|
117
141
|
cached_methods = ["GET", "HEAD"]
|
|
118
142
|
compress = true
|
|
119
143
|
|
|
120
|
-
function_association {
|
|
121
|
-
|
|
122
|
-
|
|
144
|
+
dynamic "function_association" {
|
|
145
|
+
for_each = var.is_spa ? [] : [1]
|
|
146
|
+
content {
|
|
147
|
+
event_type = "viewer-request"
|
|
148
|
+
function_arn = aws_cloudfront_function.rewrite.arn
|
|
149
|
+
}
|
|
123
150
|
}
|
|
124
151
|
|
|
125
152
|
forwarded_values {
|
|
@@ -130,17 +157,13 @@ resource "aws_cloudfront_distribution" "site" {
|
|
|
130
157
|
}
|
|
131
158
|
}
|
|
132
159
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
custom_error_response {
|
|
141
|
-
error_code = 403
|
|
142
|
-
response_code = 404
|
|
143
|
-
response_page_path = "/404.html"
|
|
160
|
+
dynamic "custom_error_response" {
|
|
161
|
+
for_each = local.error_responses
|
|
162
|
+
content {
|
|
163
|
+
error_code = custom_error_response.value.error_code
|
|
164
|
+
response_code = custom_error_response.value.response_code
|
|
165
|
+
response_page_path = custom_error_response.value.response_page_path
|
|
166
|
+
}
|
|
144
167
|
}
|
|
145
168
|
|
|
146
169
|
restrictions {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# www-dns
|
|
2
|
+
|
|
3
|
+
DNS + TLS wiring for the public website, using a Cloudflare-managed zone and an AWS ACM certificate.
|
|
4
|
+
|
|
5
|
+
## What it creates
|
|
6
|
+
|
|
7
|
+
1. ACM certificate in `us-east-1` covering the apex and `www` SAN, DNS-validated via Cloudflare records.
|
|
8
|
+
2. Apex CNAME in Cloudflare pointing at the primary www CloudFront distribution (DNS-only, grey-cloud).
|
|
9
|
+
3. S3 website-redirect bucket + a second CloudFront distribution that 301s `www.<domain>` → `https://<domain>`, plus its Cloudflare CNAME.
|
|
10
|
+
|
|
11
|
+
## Why a second CloudFront distribution instead of a Cloudflare page rule
|
|
12
|
+
|
|
13
|
+
The apex and www records must be **DNS-only** so CloudFront can terminate TLS with the ACM cert. DNS-only traffic bypasses Cloudflare's proxy, so Cloudflare page rules and transform rules never run. The redirect has to live at AWS. An S3 website bucket with `redirect_all_requests_to` is the cheapest, simplest thing that works.
|
|
14
|
+
|
|
15
|
+
## Required environment
|
|
16
|
+
|
|
17
|
+
- `CLOUDFLARE_API_TOKEN` — exported in the shell or CI environment. **Never** committed to tfvars. Rotate after anyone outside the deploy path sees it.
|
|
18
|
+
|
|
19
|
+
## Usage (from greenfield example)
|
|
20
|
+
|
|
21
|
+
```hcl
|
|
22
|
+
provider "cloudflare" {
|
|
23
|
+
# token read from CLOUDFLARE_API_TOKEN env var
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module "www_dns" {
|
|
27
|
+
source = "../../modules/app/www-dns"
|
|
28
|
+
stage = var.stage
|
|
29
|
+
domain = var.www_domain
|
|
30
|
+
cloudflare_zone_id = var.cloudflare_zone_id
|
|
31
|
+
cloudfront_domain_name = module.thinkwork.www_distribution_domain
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Then pass `module.www_dns.certificate_arn` back into `module "thinkwork"` as `www_certificate_arn`.
|
|
36
|
+
|
|
37
|
+
## First-apply ordering
|
|
38
|
+
|
|
39
|
+
On a fresh apply, Terraform has to create the primary CloudFront distribution (via `module.thinkwork.www_site`) before this module can reference its domain name. Terraform's dependency graph handles that automatically. If you're applying after a `terraform destroy`, two `apply` passes are fine — the first resolves the cert + distributions, the second binds the apex CNAME once the CloudFront distribution has deployed.
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
################################################################################
|
|
2
|
+
# Public Website DNS + TLS (Cloudflare zone, AWS ACM cert, www→apex 301)
|
|
3
|
+
#
|
|
4
|
+
# Responsibilities:
|
|
5
|
+
# 1. ACM certificate in us-east-1 covering apex + www
|
|
6
|
+
# (+ optional docs + optional admin).
|
|
7
|
+
# 2. Cloudflare DNS records for ACM DNS validation.
|
|
8
|
+
# 3. Apex CNAME in Cloudflare → primary CloudFront distribution (DNS-only).
|
|
9
|
+
# 4. Second CloudFront distribution fronting an S3 website-redirect bucket
|
|
10
|
+
# that 301s www.<domain> → https://<domain>, plus its Cloudflare CNAME.
|
|
11
|
+
# 5. Optional docs.<domain> CNAME → docs CloudFront distribution.
|
|
12
|
+
# 6. Optional admin.<domain> CNAME → admin CloudFront distribution.
|
|
13
|
+
#
|
|
14
|
+
# Cloudflare records MUST be DNS-only (grey cloud). CloudFront terminates TLS
|
|
15
|
+
# with the ACM cert and needs the real Host header.
|
|
16
|
+
################################################################################
|
|
17
|
+
|
|
18
|
+
terraform {
|
|
19
|
+
required_providers {
|
|
20
|
+
aws = {
|
|
21
|
+
source = "hashicorp/aws"
|
|
22
|
+
version = "~> 5.0"
|
|
23
|
+
}
|
|
24
|
+
cloudflare = {
|
|
25
|
+
source = "cloudflare/cloudflare"
|
|
26
|
+
version = "~> 4.0"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
locals {
|
|
32
|
+
apex = var.domain
|
|
33
|
+
www = "www.${var.domain}"
|
|
34
|
+
docs = "docs.${var.domain}"
|
|
35
|
+
admin = "admin.${var.domain}"
|
|
36
|
+
name_id = replace(var.domain, ".", "-")
|
|
37
|
+
|
|
38
|
+
# ACM SANs: always include www, conditionally include docs and admin.
|
|
39
|
+
# Gated on plain bool vars (not on CloudFront outputs) to keep the
|
|
40
|
+
# dependency graph acyclic — distributions depend on the cert, so
|
|
41
|
+
# the cert mustn't depend on distribution outputs.
|
|
42
|
+
cert_sans = concat(
|
|
43
|
+
[local.www],
|
|
44
|
+
var.include_docs ? [local.docs] : [],
|
|
45
|
+
var.include_admin ? [local.admin] : [],
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# CNAME records can only be created when we have the distribution
|
|
49
|
+
# domain to point at. Those inputs come after the cert is done, so
|
|
50
|
+
# they don't participate in the cert's dependency graph.
|
|
51
|
+
create_docs_record = var.include_docs && var.docs_cloudfront_domain_name != ""
|
|
52
|
+
create_admin_record = var.include_admin && var.admin_cloudfront_domain_name != ""
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
################################################################################
|
|
56
|
+
# ACM certificate (us-east-1, covers apex + www [+ docs])
|
|
57
|
+
################################################################################
|
|
58
|
+
|
|
59
|
+
resource "aws_acm_certificate" "www" {
|
|
60
|
+
domain_name = local.apex
|
|
61
|
+
subject_alternative_names = local.cert_sans
|
|
62
|
+
validation_method = "DNS"
|
|
63
|
+
|
|
64
|
+
lifecycle {
|
|
65
|
+
create_before_destroy = true
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
tags = {
|
|
69
|
+
Name = "thinkwork-${var.stage}-${local.name_id}"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
resource "cloudflare_record" "acm_validation" {
|
|
74
|
+
for_each = {
|
|
75
|
+
for dvo in aws_acm_certificate.www.domain_validation_options : dvo.domain_name => {
|
|
76
|
+
name = dvo.resource_record_name
|
|
77
|
+
value = dvo.resource_record_value
|
|
78
|
+
type = dvo.resource_record_type
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
zone_id = var.cloudflare_zone_id
|
|
83
|
+
name = trimsuffix(each.value.name, ".")
|
|
84
|
+
content = trimsuffix(each.value.value, ".")
|
|
85
|
+
type = each.value.type
|
|
86
|
+
ttl = 60
|
|
87
|
+
proxied = false
|
|
88
|
+
comment = "ACM DNS validation for ${each.key}"
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
resource "aws_acm_certificate_validation" "www" {
|
|
92
|
+
certificate_arn = aws_acm_certificate.www.arn
|
|
93
|
+
validation_record_fqdns = [for r in cloudflare_record.acm_validation : r.hostname]
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
################################################################################
|
|
97
|
+
# Apex DNS → primary CloudFront distribution
|
|
98
|
+
#
|
|
99
|
+
# Cloudflare flattens apex CNAMEs automatically. proxied=false is required so
|
|
100
|
+
# CloudFront sees the real Host header and the ACM cert matches.
|
|
101
|
+
################################################################################
|
|
102
|
+
|
|
103
|
+
resource "cloudflare_record" "apex" {
|
|
104
|
+
zone_id = var.cloudflare_zone_id
|
|
105
|
+
name = local.apex
|
|
106
|
+
content = var.cloudfront_domain_name
|
|
107
|
+
type = "CNAME"
|
|
108
|
+
ttl = 300
|
|
109
|
+
proxied = false
|
|
110
|
+
comment = "thinkwork-${var.stage} apex → CloudFront (www)"
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
################################################################################
|
|
114
|
+
# www.<domain> → apex 301 redirect
|
|
115
|
+
#
|
|
116
|
+
# Uses an S3 website-redirect bucket (website endpoint, not REST). Fronted by
|
|
117
|
+
# its own CloudFront distribution with the www alias and the same ACM cert.
|
|
118
|
+
################################################################################
|
|
119
|
+
|
|
120
|
+
resource "aws_s3_bucket" "www_redirect" {
|
|
121
|
+
bucket = "thinkwork-${var.stage}-www-redirect"
|
|
122
|
+
|
|
123
|
+
tags = {
|
|
124
|
+
Name = "thinkwork-${var.stage}-www-redirect"
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
resource "aws_s3_bucket_public_access_block" "www_redirect" {
|
|
129
|
+
bucket = aws_s3_bucket.www_redirect.id
|
|
130
|
+
|
|
131
|
+
block_public_acls = true
|
|
132
|
+
block_public_policy = true
|
|
133
|
+
ignore_public_acls = true
|
|
134
|
+
restrict_public_buckets = true
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
resource "aws_s3_bucket_website_configuration" "www_redirect" {
|
|
138
|
+
bucket = aws_s3_bucket.www_redirect.id
|
|
139
|
+
|
|
140
|
+
redirect_all_requests_to {
|
|
141
|
+
host_name = local.apex
|
|
142
|
+
protocol = "https"
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
resource "aws_cloudfront_distribution" "www_redirect" {
|
|
147
|
+
enabled = true
|
|
148
|
+
aliases = [local.www]
|
|
149
|
+
price_class = "PriceClass_100"
|
|
150
|
+
is_ipv6_enabled = true
|
|
151
|
+
comment = "thinkwork-${var.stage}-www-redirect → ${local.apex}"
|
|
152
|
+
|
|
153
|
+
origin {
|
|
154
|
+
domain_name = aws_s3_bucket_website_configuration.www_redirect.website_endpoint
|
|
155
|
+
origin_id = "s3-website-${aws_s3_bucket.www_redirect.id}"
|
|
156
|
+
|
|
157
|
+
custom_origin_config {
|
|
158
|
+
http_port = 80
|
|
159
|
+
https_port = 443
|
|
160
|
+
origin_protocol_policy = "http-only"
|
|
161
|
+
origin_ssl_protocols = ["TLSv1.2"]
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
default_cache_behavior {
|
|
166
|
+
target_origin_id = "s3-website-${aws_s3_bucket.www_redirect.id}"
|
|
167
|
+
viewer_protocol_policy = "redirect-to-https"
|
|
168
|
+
allowed_methods = ["GET", "HEAD"]
|
|
169
|
+
cached_methods = ["GET", "HEAD"]
|
|
170
|
+
compress = true
|
|
171
|
+
|
|
172
|
+
forwarded_values {
|
|
173
|
+
query_string = false
|
|
174
|
+
cookies {
|
|
175
|
+
forward = "none"
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
min_ttl = 0
|
|
180
|
+
default_ttl = 3600
|
|
181
|
+
max_ttl = 86400
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
restrictions {
|
|
185
|
+
geo_restriction {
|
|
186
|
+
restriction_type = "none"
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
viewer_certificate {
|
|
191
|
+
acm_certificate_arn = aws_acm_certificate_validation.www.certificate_arn
|
|
192
|
+
ssl_support_method = "sni-only"
|
|
193
|
+
minimum_protocol_version = "TLSv1.2_2021"
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
tags = {
|
|
197
|
+
Name = "thinkwork-${var.stage}-www-redirect"
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
resource "cloudflare_record" "www_redirect" {
|
|
202
|
+
zone_id = var.cloudflare_zone_id
|
|
203
|
+
name = local.www
|
|
204
|
+
content = aws_cloudfront_distribution.www_redirect.domain_name
|
|
205
|
+
type = "CNAME"
|
|
206
|
+
ttl = 300
|
|
207
|
+
proxied = false
|
|
208
|
+
comment = "thinkwork-${var.stage} www → redirect distribution"
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
################################################################################
|
|
212
|
+
# docs.<domain> → docs CloudFront distribution (optional)
|
|
213
|
+
#
|
|
214
|
+
# Created only when the greenfield stack wires docs_cloudfront_domain_name.
|
|
215
|
+
# The docs CloudFront alias picks up the same ACM cert via certificate_arn
|
|
216
|
+
# on the thinkwork module's docs_site.
|
|
217
|
+
################################################################################
|
|
218
|
+
|
|
219
|
+
resource "cloudflare_record" "docs" {
|
|
220
|
+
count = local.create_docs_record ? 1 : 0
|
|
221
|
+
|
|
222
|
+
zone_id = var.cloudflare_zone_id
|
|
223
|
+
name = local.docs
|
|
224
|
+
content = var.docs_cloudfront_domain_name
|
|
225
|
+
type = "CNAME"
|
|
226
|
+
ttl = 300
|
|
227
|
+
proxied = false
|
|
228
|
+
comment = "thinkwork-${var.stage} docs → CloudFront"
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
################################################################################
|
|
232
|
+
# admin.<domain> → admin CloudFront distribution (optional)
|
|
233
|
+
################################################################################
|
|
234
|
+
|
|
235
|
+
resource "cloudflare_record" "admin" {
|
|
236
|
+
count = local.create_admin_record ? 1 : 0
|
|
237
|
+
|
|
238
|
+
zone_id = var.cloudflare_zone_id
|
|
239
|
+
name = local.admin
|
|
240
|
+
content = var.admin_cloudfront_domain_name
|
|
241
|
+
type = "CNAME"
|
|
242
|
+
ttl = 300
|
|
243
|
+
proxied = false
|
|
244
|
+
comment = "thinkwork-${var.stage} admin → CloudFront"
|
|
245
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
output "certificate_arn" {
|
|
2
|
+
description = "ACM certificate ARN (us-east-1) covering apex + www. Pass this to the static-site module via www_certificate_arn."
|
|
3
|
+
value = aws_acm_certificate_validation.www.certificate_arn
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
output "www_redirect_distribution_id" {
|
|
7
|
+
description = "CloudFront distribution ID for the www→apex redirect"
|
|
8
|
+
value = aws_cloudfront_distribution.www_redirect.id
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
output "www_redirect_distribution_domain" {
|
|
12
|
+
description = "CloudFront distribution domain for the www→apex redirect"
|
|
13
|
+
value = aws_cloudfront_distribution.www_redirect.domain_name
|
|
14
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
variable "stage" {
|
|
2
|
+
description = "Deployment stage (used for resource naming)"
|
|
3
|
+
type = string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
variable "domain" {
|
|
7
|
+
description = "Apex domain served by the public website (e.g. thinkwork.ai)"
|
|
8
|
+
type = string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
variable "cloudflare_zone_id" {
|
|
12
|
+
description = "Cloudflare zone ID for the apex domain. Non-secret; lives in tfvars."
|
|
13
|
+
type = string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
variable "cloudfront_domain_name" {
|
|
17
|
+
description = "CloudFront distribution domain name for the primary www site (e.g. d123.cloudfront.net). Passed in from the static-site module output."
|
|
18
|
+
type = string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
variable "include_docs" {
|
|
22
|
+
description = "When true, add docs.<domain> to the ACM cert SANs and create a Cloudflare CNAME for it. Separated from docs_cloudfront_domain_name to avoid a Terraform dependency cycle (distribution depends on cert, so the cert can't depend on the distribution output)."
|
|
23
|
+
type = bool
|
|
24
|
+
default = false
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
variable "docs_cloudfront_domain_name" {
|
|
28
|
+
description = "CloudFront distribution domain name for the docs site. Used as the target for the docs.<domain> Cloudflare CNAME when include_docs is true."
|
|
29
|
+
type = string
|
|
30
|
+
default = ""
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
variable "include_admin" {
|
|
34
|
+
description = "When true, add admin.<domain> to the ACM cert SANs and create a Cloudflare CNAME for it. Same cycle-avoidance rationale as include_docs."
|
|
35
|
+
type = bool
|
|
36
|
+
default = false
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
variable "admin_cloudfront_domain_name" {
|
|
40
|
+
description = "CloudFront distribution domain name for the admin SPA. Used as the target for the admin.<domain> Cloudflare CNAME when include_admin is true."
|
|
41
|
+
type = string
|
|
42
|
+
default = ""
|
|
43
|
+
}
|
|
@@ -13,10 +13,22 @@ locals {
|
|
|
13
13
|
bucket_name = var.bucket_name != "" ? var.bucket_name : "thinkwork-${var.stage}-storage"
|
|
14
14
|
|
|
15
15
|
# Hindsight is an optional add-on. Preferred toggle: var.enable_hindsight.
|
|
16
|
-
# For one release we also honor the
|
|
17
|
-
# so existing tfvars keep working.
|
|
18
|
-
# between the two. Remove the legacy branch in a future release.
|
|
16
|
+
# For one release we also honor the legacy var.memory_engine == "hindsight"
|
|
17
|
+
# so existing tfvars keep working.
|
|
19
18
|
hindsight_enabled = var.enable_hindsight || var.memory_engine == "hindsight"
|
|
19
|
+
|
|
20
|
+
# Canonical long-term memory engine for this deployment. Exactly one engine
|
|
21
|
+
# is active per deployment for recall/inspect/export. Auto-selects from
|
|
22
|
+
# enable_hindsight when var.memory_engine is left empty so existing deploys
|
|
23
|
+
# keep working without config changes. Legacy value "managed" maps to
|
|
24
|
+
# "agentcore".
|
|
25
|
+
resolved_memory_engine = (
|
|
26
|
+
var.memory_engine == "hindsight" || var.memory_engine == "agentcore"
|
|
27
|
+
? var.memory_engine
|
|
28
|
+
: var.memory_engine == "managed"
|
|
29
|
+
? "agentcore"
|
|
30
|
+
: local.hindsight_enabled ? "hindsight" : "agentcore"
|
|
31
|
+
)
|
|
20
32
|
}
|
|
21
33
|
|
|
22
34
|
################################################################################
|
|
@@ -66,11 +78,13 @@ module "cognito" {
|
|
|
66
78
|
|
|
67
79
|
admin_callback_urls = concat(
|
|
68
80
|
var.admin_callback_urls,
|
|
69
|
-
["https://${module.admin_site.distribution_domain}", "https://${module.admin_site.distribution_domain}/auth/callback"]
|
|
81
|
+
["https://${module.admin_site.distribution_domain}", "https://${module.admin_site.distribution_domain}/auth/callback"],
|
|
82
|
+
var.admin_domain != "" ? ["https://${var.admin_domain}", "https://${var.admin_domain}/auth/callback"] : []
|
|
70
83
|
)
|
|
71
84
|
admin_logout_urls = concat(
|
|
72
85
|
var.admin_logout_urls,
|
|
73
|
-
["https://${module.admin_site.distribution_domain}"]
|
|
86
|
+
["https://${module.admin_site.distribution_domain}"],
|
|
87
|
+
var.admin_domain != "" ? ["https://${var.admin_domain}"] : []
|
|
74
88
|
)
|
|
75
89
|
mobile_callback_urls = var.mobile_callback_urls
|
|
76
90
|
mobile_logout_urls = var.mobile_logout_urls
|
|
@@ -174,10 +188,13 @@ module "api" {
|
|
|
174
188
|
agentcore_function_arn = module.agentcore.agentcore_function_arn
|
|
175
189
|
hindsight_endpoint = local.hindsight_enabled ? module.hindsight[0].hindsight_endpoint : ""
|
|
176
190
|
agentcore_memory_id = module.agentcore_memory.memory_id
|
|
191
|
+
memory_engine = local.resolved_memory_engine
|
|
177
192
|
admin_url = "https://${module.admin_site.distribution_domain}"
|
|
178
193
|
docs_url = "https://${module.docs_site.distribution_domain}"
|
|
179
194
|
appsync_realtime_url = module.appsync.graphql_realtime_url
|
|
180
195
|
ecr_repository_url = module.agentcore.ecr_repository_url
|
|
196
|
+
job_scheduler_role_arn = module.job_triggers.job_scheduler_role_arn
|
|
197
|
+
lastmile_tasks_api_url = var.lastmile_tasks_api_url
|
|
181
198
|
}
|
|
182
199
|
|
|
183
200
|
################################################################################
|
|
@@ -206,6 +223,7 @@ module "agentcore" {
|
|
|
206
223
|
|
|
207
224
|
hindsight_endpoint = local.hindsight_enabled ? module.hindsight[0].hindsight_endpoint : ""
|
|
208
225
|
agentcore_memory_id = module.agentcore_memory.memory_id
|
|
226
|
+
memory_engine = local.resolved_memory_engine
|
|
209
227
|
}
|
|
210
228
|
|
|
211
229
|
module "crons" {
|
|
@@ -239,8 +257,16 @@ module "hindsight" {
|
|
|
239
257
|
module "ses" {
|
|
240
258
|
source = "../app/ses-email"
|
|
241
259
|
|
|
242
|
-
stage
|
|
243
|
-
account_id
|
|
260
|
+
stage = var.stage
|
|
261
|
+
account_id = var.account_id
|
|
262
|
+
region = var.region
|
|
263
|
+
email_domain = var.ses_inbound_domain
|
|
264
|
+
|
|
265
|
+
inbound_bucket_name = module.s3.bucket_name
|
|
266
|
+
email_inbound_fn_arn = module.api.email_inbound_fn_arn
|
|
267
|
+
email_inbound_fn_name = module.api.email_inbound_fn_name
|
|
268
|
+
|
|
269
|
+
manage_active_rule_set = var.ses_manage_active_rule_set
|
|
244
270
|
}
|
|
245
271
|
|
|
246
272
|
################################################################################
|
|
@@ -250,8 +276,11 @@ module "ses" {
|
|
|
250
276
|
module "admin_site" {
|
|
251
277
|
source = "../app/static-site"
|
|
252
278
|
|
|
253
|
-
stage
|
|
254
|
-
site_name
|
|
279
|
+
stage = var.stage
|
|
280
|
+
site_name = "admin"
|
|
281
|
+
is_spa = true
|
|
282
|
+
custom_domain = var.admin_domain
|
|
283
|
+
certificate_arn = var.admin_certificate_arn
|
|
255
284
|
}
|
|
256
285
|
|
|
257
286
|
################################################################################
|
|
@@ -266,3 +295,17 @@ module "docs_site" {
|
|
|
266
295
|
custom_domain = var.docs_domain
|
|
267
296
|
certificate_arn = var.docs_certificate_arn
|
|
268
297
|
}
|
|
298
|
+
|
|
299
|
+
################################################################################
|
|
300
|
+
# Public Website (www)
|
|
301
|
+
################################################################################
|
|
302
|
+
|
|
303
|
+
module "www_site" {
|
|
304
|
+
source = "../app/static-site"
|
|
305
|
+
|
|
306
|
+
stage = var.stage
|
|
307
|
+
site_name = "www"
|
|
308
|
+
custom_domain = var.www_domain
|
|
309
|
+
certificate_arn = var.www_certificate_arn
|
|
310
|
+
# is_spa defaults to false — SSG output, directory URIs get rewritten to index.html
|
|
311
|
+
}
|
|
@@ -127,3 +127,35 @@ output "docs_bucket_name" {
|
|
|
127
127
|
description = "S3 bucket for docs site assets"
|
|
128
128
|
value = module.docs_site.bucket_name
|
|
129
129
|
}
|
|
130
|
+
|
|
131
|
+
# Public website (www)
|
|
132
|
+
output "www_distribution_id" {
|
|
133
|
+
description = "CloudFront distribution ID for the public website"
|
|
134
|
+
value = module.www_site.distribution_id
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
output "www_distribution_domain" {
|
|
138
|
+
description = "CloudFront domain for the public website"
|
|
139
|
+
value = module.www_site.distribution_domain
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
output "www_bucket_name" {
|
|
143
|
+
description = "S3 bucket for the public website assets"
|
|
144
|
+
value = module.www_site.bucket_name
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
# SES inbound email
|
|
148
|
+
output "ses_inbound_zone_id" {
|
|
149
|
+
description = "Route53 hosted zone ID for the email subdomain (null when ses_inbound_domain is not set)"
|
|
150
|
+
value = module.ses.zone_id
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
output "ses_inbound_name_servers" {
|
|
154
|
+
description = "Name servers for the delegated email subzone. Paste these as NS records at the registrar that hosts the parent domain (e.g. Google Domains) before SES can verify."
|
|
155
|
+
value = module.ses.name_servers
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
output "ses_inbound_mx_target" {
|
|
159
|
+
description = "MX target host for the email subdomain. Terraform already writes this into the subzone — this output is informational."
|
|
160
|
+
value = module.ses.mx_target
|
|
161
|
+
}
|
|
@@ -145,13 +145,13 @@ variable "enable_hindsight" {
|
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
variable "memory_engine" {
|
|
148
|
-
description = "
|
|
148
|
+
description = "Active long-term memory engine for canonical recall/inspect/export. Exactly one engine is authoritative per deployment. Accepted values: 'hindsight' (requires enable_hindsight = true), 'agentcore' (uses the always-on AgentCore managed memory). Legacy value 'managed' maps to 'agentcore'. Empty = auto-select: 'hindsight' when enable_hindsight = true, otherwise 'agentcore'."
|
|
149
149
|
type = string
|
|
150
150
|
default = ""
|
|
151
151
|
|
|
152
152
|
validation {
|
|
153
|
-
condition = var.memory_engine == "" || contains(["managed", "hindsight"], var.memory_engine)
|
|
154
|
-
error_message = "memory_engine
|
|
153
|
+
condition = var.memory_engine == "" || contains(["managed", "hindsight", "agentcore"], var.memory_engine)
|
|
154
|
+
error_message = "memory_engine must be empty, 'hindsight', 'agentcore', or legacy 'managed'."
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
157
|
|
|
@@ -261,3 +261,57 @@ variable "docs_certificate_arn" {
|
|
|
261
261
|
type = string
|
|
262
262
|
default = ""
|
|
263
263
|
}
|
|
264
|
+
|
|
265
|
+
# ---------------------------------------------------------------------------
|
|
266
|
+
# Public website (custom domain — optional)
|
|
267
|
+
# ---------------------------------------------------------------------------
|
|
268
|
+
|
|
269
|
+
variable "www_domain" {
|
|
270
|
+
description = "Custom domain for the public website (e.g. thinkwork.ai). Leave empty for CloudFront default."
|
|
271
|
+
type = string
|
|
272
|
+
default = ""
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
variable "www_certificate_arn" {
|
|
276
|
+
description = "ACM certificate ARN for the www domain (us-east-1, required for CloudFront custom domains). Covers both the apex and www subdomain."
|
|
277
|
+
type = string
|
|
278
|
+
default = ""
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
# ---------------------------------------------------------------------------
|
|
282
|
+
# Admin site (custom domain — optional)
|
|
283
|
+
# ---------------------------------------------------------------------------
|
|
284
|
+
|
|
285
|
+
variable "admin_domain" {
|
|
286
|
+
description = "Custom domain for the admin SPA (e.g. admin.thinkwork.ai). Leave empty for CloudFront default."
|
|
287
|
+
type = string
|
|
288
|
+
default = ""
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
variable "admin_certificate_arn" {
|
|
292
|
+
description = "ACM certificate ARN for the admin domain (us-east-1, required for CloudFront custom domains)."
|
|
293
|
+
type = string
|
|
294
|
+
default = ""
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
# ---------------------------------------------------------------------------
|
|
298
|
+
# SES inbound email (delegated subzone — Option A)
|
|
299
|
+
# ---------------------------------------------------------------------------
|
|
300
|
+
|
|
301
|
+
variable "ses_inbound_domain" {
|
|
302
|
+
description = "Subdomain used for agent email (e.g. agents.thinkwork.ai). Terraform creates a delegated Route53 hosted zone for this name, manages the SES domain identity + DKIM CNAMEs + MX in that zone, and wires an SES receipt rule that stores inbound mail in S3 and invokes the email-inbound Lambda. Leave empty to skip all SES inbound resources. After first apply, paste the `ses_inbound_name_servers` output as NS records at whatever hosts the parent domain."
|
|
303
|
+
type = string
|
|
304
|
+
default = ""
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
variable "ses_manage_active_rule_set" {
|
|
308
|
+
description = "Activate the SES receipt rule set. Only ONE rule set can be active per region per AWS account; set false on secondary stages that share an account so they don't fight over activation."
|
|
309
|
+
type = bool
|
|
310
|
+
default = true
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
variable "lastmile_tasks_api_url" {
|
|
314
|
+
description = "Base URL of the LastMile Tasks REST API (e.g. https://api-dev.lastmile-tei.com for develop). Feature-flags the outbound task sync — leave blank to keep mobile-created tasks in sync_status='local'."
|
|
315
|
+
type = string
|
|
316
|
+
default = ""
|
|
317
|
+
}
|