thinkwork-cli 0.8.2 → 0.9.1
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/LICENSE +202 -0
- package/README.md +18 -2
- package/dist/cli.js +3004 -215
- package/dist/terraform/examples/greenfield/main.tf +325 -19
- package/dist/terraform/examples/greenfield/terraform.tfvars.example +14 -0
- package/dist/terraform/modules/app/agentcore-code-interpreter/Dockerfile.sandbox-base +61 -0
- package/dist/terraform/modules/app/agentcore-code-interpreter/README.md +54 -0
- package/dist/terraform/modules/app/agentcore-code-interpreter/main.tf +197 -0
- package/dist/terraform/modules/app/agentcore-code-interpreter/scripts/build_and_push_sandbox_base.sh +70 -0
- package/dist/terraform/modules/app/agentcore-flue/README.md +58 -0
- package/dist/terraform/modules/app/agentcore-flue/main.tf +322 -0
- package/dist/terraform/modules/app/agentcore-flue/outputs.tf +23 -0
- package/dist/terraform/modules/app/agentcore-flue/variables.tf +91 -0
- package/dist/terraform/modules/app/agentcore-memory/scripts/create_or_find_memory.sh +0 -0
- package/dist/terraform/modules/app/agentcore-runtime/main.tf +204 -4
- package/dist/terraform/modules/app/appsync-subscriptions/main.tf +4 -0
- package/dist/terraform/modules/app/appsync-subscriptions/outputs.tf +5 -0
- package/dist/terraform/modules/app/computer-runtime/README.md +15 -0
- package/dist/terraform/modules/app/computer-runtime/main.tf +406 -0
- package/dist/terraform/modules/app/computer-runtime/outputs.tf +75 -0
- package/dist/terraform/modules/app/computer-runtime/variables.tf +66 -0
- package/dist/terraform/modules/app/hindsight-memory/main.tf +6 -0
- package/dist/terraform/modules/app/lambda-api/eval-fanout.tf +128 -0
- package/dist/terraform/modules/app/lambda-api/handlers.tf +1557 -42
- package/dist/terraform/modules/app/lambda-api/main.tf +299 -15
- package/dist/terraform/modules/app/lambda-api/mcp-oauth.tf +118 -0
- package/dist/terraform/modules/app/lambda-api/oauth-secrets.tf +49 -0
- package/dist/terraform/modules/app/lambda-api/outputs.tf +38 -0
- package/dist/terraform/modules/app/lambda-api/slack-app-secrets.tf +43 -0
- package/dist/terraform/modules/app/lambda-api/stripe-secrets.tf +53 -0
- package/dist/terraform/modules/app/lambda-api/variables.tf +349 -2
- package/dist/terraform/modules/app/lambda-api/workspace-events.tf +125 -0
- package/dist/terraform/modules/app/routines-stepfunctions/main.tf +453 -0
- package/dist/terraform/modules/app/sandbox-log-scrubber/README.md +66 -0
- package/dist/terraform/modules/app/sandbox-log-scrubber/main.tf +200 -0
- package/dist/terraform/modules/app/static-site/main.tf +146 -5
- package/dist/terraform/modules/app/www-dns/main.tf +118 -15
- package/dist/terraform/modules/app/www-dns/outputs.tf +10 -0
- package/dist/terraform/modules/app/www-dns/variables.tf +42 -0
- package/dist/terraform/modules/data/aurora-postgres/main.tf +164 -3
- package/dist/terraform/modules/data/aurora-postgres/outputs.tf +34 -0
- package/dist/terraform/modules/data/aurora-postgres/variables.tf +16 -0
- package/dist/terraform/modules/data/compliance-audit-bucket/README.md +145 -0
- package/dist/terraform/modules/data/compliance-audit-bucket/main.tf +573 -0
- package/dist/terraform/modules/data/compliance-audit-bucket/outputs.tf +43 -0
- package/dist/terraform/modules/data/compliance-audit-bucket/variables.tf +93 -0
- package/dist/terraform/modules/data/compliance-exports-bucket/main.tf +269 -0
- package/dist/terraform/modules/data/compliance-exports-bucket/outputs.tf +23 -0
- package/dist/terraform/modules/data/compliance-exports-bucket/variables.tf +50 -0
- package/dist/terraform/modules/data/s3-backups-bucket/main.tf +123 -0
- package/dist/terraform/modules/data/s3-buckets/main.tf +13 -0
- package/dist/terraform/modules/foundation/cognito/variables.tf +5 -2
- package/dist/terraform/modules/thinkwork/main.tf +439 -21
- package/dist/terraform/modules/thinkwork/outputs.tf +121 -0
- package/dist/terraform/modules/thinkwork/variables.tf +165 -6
- package/dist/terraform/schema.graphql +45 -0
- package/package.json +15 -14
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
################################################################################
|
|
2
|
+
# Sandbox Log Scrubber — App Module
|
|
3
|
+
#
|
|
4
|
+
# Secondary (backstop) R13 layer for the AgentCore Code Interpreter sandbox
|
|
5
|
+
# (plan Unit 12). Pattern-redacts known-shape OAuth tokens in AgentCore
|
|
6
|
+
# APPLICATION_LOGS before they land in the long-term CloudWatch tier.
|
|
7
|
+
#
|
|
8
|
+
# **This is not the primary R13 layer.** The primary layer is the base-image
|
|
9
|
+
# sitecustomize.py stdio wrapper (plan Unit 4, terraform/modules/app/
|
|
10
|
+
# agentcore-code-interpreter) which redacts by *value* using the session-
|
|
11
|
+
# scoped token set. That layer can catch any token the preamble registered,
|
|
12
|
+
# regardless of shape.
|
|
13
|
+
#
|
|
14
|
+
# This backstop redacts by *pattern* — Authorization: Bearer, JWTs, and
|
|
15
|
+
# known OAuth prefixes (gh[opsru]_, xox[abep]-, ya29.). It does not have
|
|
16
|
+
# access to session token values so it cannot catch arbitrary leaks. It
|
|
17
|
+
# exists to mitigate stdio-bypass classes (subprocess env dumps, os.write,
|
|
18
|
+
# C-extension direct writes, multiprocessing workers) whose bytes carry a
|
|
19
|
+
# recognizable token prefix.
|
|
20
|
+
#
|
|
21
|
+
# If the scrubber Lambda fails, source log events remain in the original
|
|
22
|
+
# CloudWatch group. S3 tier is delayed, data is not lost.
|
|
23
|
+
################################################################################
|
|
24
|
+
|
|
25
|
+
terraform {
|
|
26
|
+
required_providers {
|
|
27
|
+
aws = {
|
|
28
|
+
source = "hashicorp/aws"
|
|
29
|
+
version = ">= 5.0"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
variable "stage" {
|
|
35
|
+
description = "Deployment stage (dev, prod, etc.)."
|
|
36
|
+
type = string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
variable "region" {
|
|
40
|
+
description = "AWS region."
|
|
41
|
+
type = string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
variable "account_id" {
|
|
45
|
+
description = "AWS account ID."
|
|
46
|
+
type = string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
variable "source_log_group_name" {
|
|
50
|
+
description = "Source CloudWatch log group to subscribe to. Typically the AgentCore runtime group, e.g. /aws/bedrock-agentcore/runtimes/<runtime-name>."
|
|
51
|
+
type = string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
variable "lambda_zip_path" {
|
|
55
|
+
description = "Path to the built Lambda zip. Produced by scripts/build-lambdas.sh sandbox-log-scrubber."
|
|
56
|
+
type = string
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
variable "lambda_zip_hash" {
|
|
60
|
+
description = "source_code_hash (base64 SHA-256) for the Lambda zip. Triggers function update when the bundle changes."
|
|
61
|
+
type = string
|
|
62
|
+
default = ""
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
variable "retention_days" {
|
|
66
|
+
description = "CloudWatch retention on the scrubbed output log group. Matches standard runtime retention."
|
|
67
|
+
type = number
|
|
68
|
+
default = 90
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
################################################################################
|
|
72
|
+
# Output log group — where scrubbed events land
|
|
73
|
+
################################################################################
|
|
74
|
+
|
|
75
|
+
resource "aws_cloudwatch_log_group" "scrubbed" {
|
|
76
|
+
name = "/thinkwork/${var.stage}/sandbox/scrubbed"
|
|
77
|
+
retention_in_days = var.retention_days
|
|
78
|
+
|
|
79
|
+
tags = {
|
|
80
|
+
Stage = var.stage
|
|
81
|
+
Purpose = "sandbox-log-scrubber-output"
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
################################################################################
|
|
86
|
+
# Lambda execution role + policy
|
|
87
|
+
################################################################################
|
|
88
|
+
|
|
89
|
+
resource "aws_iam_role" "scrubber" {
|
|
90
|
+
name = "thinkwork-${var.stage}-sandbox-log-scrubber"
|
|
91
|
+
|
|
92
|
+
assume_role_policy = jsonencode({
|
|
93
|
+
Version = "2012-10-17"
|
|
94
|
+
Statement = [{
|
|
95
|
+
Effect = "Allow"
|
|
96
|
+
Principal = { Service = "lambda.amazonaws.com" }
|
|
97
|
+
Action = "sts:AssumeRole"
|
|
98
|
+
}]
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
resource "aws_iam_role_policy" "scrubber" {
|
|
103
|
+
name = "sandbox-log-scrubber"
|
|
104
|
+
role = aws_iam_role.scrubber.id
|
|
105
|
+
|
|
106
|
+
policy = jsonencode({
|
|
107
|
+
Version = "2012-10-17"
|
|
108
|
+
Statement = [
|
|
109
|
+
{
|
|
110
|
+
Sid = "WriteScrubbedEvents"
|
|
111
|
+
Effect = "Allow"
|
|
112
|
+
Action = [
|
|
113
|
+
"logs:CreateLogStream",
|
|
114
|
+
"logs:PutLogEvents",
|
|
115
|
+
"logs:DescribeLogStreams",
|
|
116
|
+
]
|
|
117
|
+
Resource = [
|
|
118
|
+
aws_cloudwatch_log_group.scrubbed.arn,
|
|
119
|
+
"${aws_cloudwatch_log_group.scrubbed.arn}:*",
|
|
120
|
+
]
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
# Lambda's own execution logs (separate group managed by AWS).
|
|
124
|
+
Sid = "SelfLogs"
|
|
125
|
+
Effect = "Allow"
|
|
126
|
+
Action = [
|
|
127
|
+
"logs:CreateLogGroup",
|
|
128
|
+
"logs:CreateLogStream",
|
|
129
|
+
"logs:PutLogEvents",
|
|
130
|
+
]
|
|
131
|
+
Resource = "arn:aws:logs:${var.region}:${var.account_id}:log-group:/aws/lambda/thinkwork-${var.stage}-sandbox-log-scrubber:*"
|
|
132
|
+
},
|
|
133
|
+
]
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
################################################################################
|
|
138
|
+
# Lambda function
|
|
139
|
+
################################################################################
|
|
140
|
+
|
|
141
|
+
resource "aws_lambda_function" "scrubber" {
|
|
142
|
+
function_name = "thinkwork-${var.stage}-sandbox-log-scrubber"
|
|
143
|
+
role = aws_iam_role.scrubber.arn
|
|
144
|
+
handler = "index.handler"
|
|
145
|
+
runtime = "nodejs20.x"
|
|
146
|
+
filename = var.lambda_zip_path
|
|
147
|
+
source_code_hash = var.lambda_zip_hash
|
|
148
|
+
timeout = 30
|
|
149
|
+
memory_size = 256
|
|
150
|
+
|
|
151
|
+
environment {
|
|
152
|
+
variables = {
|
|
153
|
+
OUTPUT_LOG_GROUP = aws_cloudwatch_log_group.scrubbed.name
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
tags = {
|
|
158
|
+
Stage = var.stage
|
|
159
|
+
Purpose = "sandbox-log-scrubber"
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
resource "aws_lambda_permission" "allow_cloudwatch" {
|
|
164
|
+
statement_id = "AllowExecutionFromCloudWatchLogs"
|
|
165
|
+
action = "lambda:InvokeFunction"
|
|
166
|
+
function_name = aws_lambda_function.scrubber.function_name
|
|
167
|
+
principal = "logs.${var.region}.amazonaws.com"
|
|
168
|
+
source_arn = "arn:aws:logs:${var.region}:${var.account_id}:log-group:${var.source_log_group_name}:*"
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
################################################################################
|
|
172
|
+
# Subscription filter — fan source events into the Lambda
|
|
173
|
+
################################################################################
|
|
174
|
+
|
|
175
|
+
resource "aws_cloudwatch_log_subscription_filter" "source" {
|
|
176
|
+
name = "thinkwork-${var.stage}-sandbox-scrubber"
|
|
177
|
+
log_group_name = var.source_log_group_name
|
|
178
|
+
filter_pattern = "" # deliver every event; the Lambda decides
|
|
179
|
+
destination_arn = aws_lambda_function.scrubber.arn
|
|
180
|
+
depends_on = [aws_lambda_permission.allow_cloudwatch]
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
################################################################################
|
|
184
|
+
# Outputs
|
|
185
|
+
################################################################################
|
|
186
|
+
|
|
187
|
+
output "scrubbed_log_group_name" {
|
|
188
|
+
description = "CloudWatch log group receiving scrubbed events."
|
|
189
|
+
value = aws_cloudwatch_log_group.scrubbed.name
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
output "scrubber_function_name" {
|
|
193
|
+
description = "Scrubber Lambda function name."
|
|
194
|
+
value = aws_lambda_function.scrubber.function_name
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
output "scrubber_role_arn" {
|
|
198
|
+
description = "Execution role ARN."
|
|
199
|
+
value = aws_iam_role.scrubber.arn
|
|
200
|
+
}
|
|
@@ -39,12 +39,85 @@ variable "is_spa" {
|
|
|
39
39
|
default = false
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
# Response-headers policy (optional, plan-012 U3)
|
|
44
|
+
#
|
|
45
|
+
# Two opt-in forms, both backwards compatible — existing callers
|
|
46
|
+
# (computer_site, admin_site, docs_site, www_site) leave both unset and the
|
|
47
|
+
# distribution is created with no response-headers policy attached:
|
|
48
|
+
#
|
|
49
|
+
# - Pass `response_headers_policy_id` to attach an existing policy by id.
|
|
50
|
+
# Useful if a sibling module already minted the policy and you want to
|
|
51
|
+
# share it across distributions.
|
|
52
|
+
#
|
|
53
|
+
# - Pass `inline_response_headers` (an object describing the CSP + other
|
|
54
|
+
# response headers) to have this module mint a fresh policy and attach
|
|
55
|
+
# it. Used by `computer_sandbox_site` to ship the iframe-side CSP
|
|
56
|
+
# (script-src 'self' blob:; connect-src 'none'; frame-ancestors ... etc.)
|
|
57
|
+
# alongside the dedicated sandbox distribution.
|
|
58
|
+
#
|
|
59
|
+
# Passing both is an error — the inline policy would be unused.
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
variable "response_headers_policy_id" {
|
|
63
|
+
description = "ID of an existing aws_cloudfront_response_headers_policy to attach. Mutually exclusive with inline_response_headers."
|
|
64
|
+
type = string
|
|
65
|
+
default = ""
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
variable "inline_response_headers" {
|
|
69
|
+
description = "When set, this module mints a new response-headers policy and attaches it. Pass null to skip. Fields: content_security_policy (string), content_type_options_override (bool), strict_transport_security (object: max_age_sec, include_subdomains, preload, override), cors (object: allow_origins, allow_methods, allow_headers, allow_credentials, max_age_sec, origin_override). Mutually exclusive with response_headers_policy_id."
|
|
70
|
+
type = object({
|
|
71
|
+
content_security_policy = optional(string)
|
|
72
|
+
content_type_options_override = optional(bool, true)
|
|
73
|
+
strict_transport_security = optional(object({
|
|
74
|
+
max_age_sec = number
|
|
75
|
+
include_subdomains = bool
|
|
76
|
+
preload = bool
|
|
77
|
+
override = bool
|
|
78
|
+
}))
|
|
79
|
+
cors = optional(object({
|
|
80
|
+
allow_origins = list(string)
|
|
81
|
+
allow_methods = optional(list(string), ["GET", "HEAD", "OPTIONS"])
|
|
82
|
+
allow_headers = optional(list(string), ["*"])
|
|
83
|
+
allow_credentials = optional(bool, false)
|
|
84
|
+
max_age_sec = optional(number, 600)
|
|
85
|
+
origin_override = optional(bool, true)
|
|
86
|
+
}))
|
|
87
|
+
})
|
|
88
|
+
default = null
|
|
89
|
+
}
|
|
90
|
+
|
|
42
91
|
################################################################################
|
|
43
92
|
# S3 Bucket
|
|
44
93
|
################################################################################
|
|
45
94
|
|
|
46
95
|
locals {
|
|
47
96
|
bucket_name = var.bucket_name != "" ? var.bucket_name : "thinkwork-${var.stage}-${var.site_name}"
|
|
97
|
+
|
|
98
|
+
# Mutually-exclusive validator — surface as an error during plan.
|
|
99
|
+
_conflicting_policy_inputs = var.response_headers_policy_id != "" && var.inline_response_headers != null
|
|
100
|
+
|
|
101
|
+
inline_policy_enabled = var.inline_response_headers != null
|
|
102
|
+
|
|
103
|
+
# Final policy id wired into the cache behavior. Empty string means no
|
|
104
|
+
# policy (CloudFront default). Terraform's resource attribute treats ""
|
|
105
|
+
# the same as null because we condition on it below.
|
|
106
|
+
effective_response_headers_policy_id = (
|
|
107
|
+
var.response_headers_policy_id != ""
|
|
108
|
+
? var.response_headers_policy_id
|
|
109
|
+
: (local.inline_policy_enabled
|
|
110
|
+
? aws_cloudfront_response_headers_policy.inline[0].id
|
|
111
|
+
: ""
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
check "policy_inputs_are_mutually_exclusive" {
|
|
117
|
+
assert {
|
|
118
|
+
condition = !local._conflicting_policy_inputs
|
|
119
|
+
error_message = "static-site: response_headers_policy_id and inline_response_headers are mutually exclusive."
|
|
120
|
+
}
|
|
48
121
|
}
|
|
49
122
|
|
|
50
123
|
resource "aws_s3_bucket" "site" {
|
|
@@ -64,6 +137,68 @@ resource "aws_s3_bucket_public_access_block" "site" {
|
|
|
64
137
|
restrict_public_buckets = true
|
|
65
138
|
}
|
|
66
139
|
|
|
140
|
+
################################################################################
|
|
141
|
+
# CloudFront Response-Headers Policy (optional, inline-minted variant)
|
|
142
|
+
################################################################################
|
|
143
|
+
|
|
144
|
+
resource "aws_cloudfront_response_headers_policy" "inline" {
|
|
145
|
+
count = local.inline_policy_enabled ? 1 : 0
|
|
146
|
+
|
|
147
|
+
name = "thinkwork-${var.stage}-${var.site_name}-headers"
|
|
148
|
+
comment = "Response-headers policy for thinkwork-${var.stage}-${var.site_name}"
|
|
149
|
+
|
|
150
|
+
dynamic "security_headers_config" {
|
|
151
|
+
for_each = var.inline_response_headers.content_security_policy != null || var.inline_response_headers.content_type_options_override != false || var.inline_response_headers.strict_transport_security != null ? [1] : []
|
|
152
|
+
content {
|
|
153
|
+
dynamic "content_security_policy" {
|
|
154
|
+
for_each = var.inline_response_headers.content_security_policy != null ? [1] : []
|
|
155
|
+
content {
|
|
156
|
+
content_security_policy = var.inline_response_headers.content_security_policy
|
|
157
|
+
override = true
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
dynamic "content_type_options" {
|
|
162
|
+
for_each = var.inline_response_headers.content_type_options_override == true ? [1] : []
|
|
163
|
+
content {
|
|
164
|
+
override = true
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
dynamic "strict_transport_security" {
|
|
169
|
+
for_each = var.inline_response_headers.strict_transport_security != null ? [1] : []
|
|
170
|
+
content {
|
|
171
|
+
access_control_max_age_sec = var.inline_response_headers.strict_transport_security.max_age_sec
|
|
172
|
+
include_subdomains = var.inline_response_headers.strict_transport_security.include_subdomains
|
|
173
|
+
preload = var.inline_response_headers.strict_transport_security.preload
|
|
174
|
+
override = var.inline_response_headers.strict_transport_security.override
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
dynamic "cors_config" {
|
|
181
|
+
for_each = var.inline_response_headers.cors != null ? [var.inline_response_headers.cors] : []
|
|
182
|
+
content {
|
|
183
|
+
access_control_allow_credentials = cors_config.value.allow_credentials
|
|
184
|
+
access_control_max_age_sec = cors_config.value.max_age_sec
|
|
185
|
+
origin_override = cors_config.value.origin_override
|
|
186
|
+
|
|
187
|
+
access_control_allow_headers {
|
|
188
|
+
items = cors_config.value.allow_headers
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
access_control_allow_methods {
|
|
192
|
+
items = cors_config.value.allow_methods
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
access_control_allow_origins {
|
|
196
|
+
items = cors_config.value.allow_origins
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
67
202
|
################################################################################
|
|
68
203
|
# CloudFront OAC
|
|
69
204
|
################################################################################
|
|
@@ -135,11 +270,12 @@ resource "aws_cloudfront_distribution" "site" {
|
|
|
135
270
|
}
|
|
136
271
|
|
|
137
272
|
default_cache_behavior {
|
|
138
|
-
target_origin_id
|
|
139
|
-
viewer_protocol_policy
|
|
140
|
-
allowed_methods
|
|
141
|
-
cached_methods
|
|
142
|
-
compress
|
|
273
|
+
target_origin_id = "s3-${local.bucket_name}"
|
|
274
|
+
viewer_protocol_policy = "redirect-to-https"
|
|
275
|
+
allowed_methods = ["GET", "HEAD", "OPTIONS"]
|
|
276
|
+
cached_methods = ["GET", "HEAD"]
|
|
277
|
+
compress = true
|
|
278
|
+
response_headers_policy_id = local.effective_response_headers_policy_id != "" ? local.effective_response_headers_policy_id : null
|
|
143
279
|
|
|
144
280
|
dynamic "function_association" {
|
|
145
281
|
for_each = var.is_spa ? [] : [1]
|
|
@@ -228,3 +364,8 @@ output "bucket_name" {
|
|
|
228
364
|
description = "S3 bucket name for the site"
|
|
229
365
|
value = aws_s3_bucket.site.id
|
|
230
366
|
}
|
|
367
|
+
|
|
368
|
+
output "response_headers_policy_id" {
|
|
369
|
+
description = "ID of the response-headers policy attached to the default cache behavior. Empty when no policy is attached."
|
|
370
|
+
value = local.effective_response_headers_policy_id
|
|
371
|
+
}
|
|
@@ -29,27 +29,38 @@ terraform {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
locals {
|
|
32
|
-
apex
|
|
33
|
-
www
|
|
34
|
-
docs
|
|
35
|
-
admin
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
#
|
|
32
|
+
apex = var.domain
|
|
33
|
+
www = "www.${var.domain}"
|
|
34
|
+
docs = "docs.${var.domain}"
|
|
35
|
+
admin = "admin.${var.domain}"
|
|
36
|
+
computer = "computer.${var.domain}"
|
|
37
|
+
sandbox = "sandbox.${var.domain}"
|
|
38
|
+
api = "api.${var.domain}"
|
|
39
|
+
name_id = replace(var.domain, ".", "-")
|
|
40
|
+
|
|
41
|
+
# ACM SANs: always include www, conditionally include docs, admin, computer,
|
|
42
|
+
# and api. The Computer iframe sandbox uses its own certificate so adding or
|
|
43
|
+
# rotating the sandbox host never forces a replacement of this shared
|
|
44
|
+
# production certificate.
|
|
45
|
+
# Gated on plain bool vars (not on CloudFront/API Gateway outputs) to keep
|
|
46
|
+
# the dependency graph acyclic — distributions / custom domain names
|
|
47
|
+
# depend on the cert, so the cert mustn't depend on those outputs.
|
|
42
48
|
cert_sans = concat(
|
|
43
49
|
[local.www],
|
|
44
50
|
var.include_docs ? [local.docs] : [],
|
|
45
51
|
var.include_admin ? [local.admin] : [],
|
|
52
|
+
var.include_computer ? [local.computer] : [],
|
|
53
|
+
var.include_api ? [local.api] : [],
|
|
46
54
|
)
|
|
47
55
|
|
|
48
|
-
# CNAME records
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
|
|
52
|
-
|
|
56
|
+
# Existing CNAME records stay gated on non-empty targets because their target
|
|
57
|
+
# outputs are already known in the deployed stack. Newly bootstrapped records
|
|
58
|
+
# such as the sandbox CNAME must gate only on an explicit boolean so Terraform
|
|
59
|
+
# can plan the resource count before the new CloudFront distribution exists.
|
|
60
|
+
create_docs_record = var.include_docs && var.docs_cloudfront_domain_name != ""
|
|
61
|
+
create_admin_record = var.include_admin && var.admin_cloudfront_domain_name != ""
|
|
62
|
+
create_computer_record = var.include_computer && var.computer_cloudfront_domain_name != ""
|
|
63
|
+
create_api_record = var.include_api && var.api_gateway_id != ""
|
|
53
64
|
}
|
|
54
65
|
|
|
55
66
|
################################################################################
|
|
@@ -86,6 +97,17 @@ resource "cloudflare_record" "acm_validation" {
|
|
|
86
97
|
ttl = 60
|
|
87
98
|
proxied = false
|
|
88
99
|
comment = "ACM DNS validation for ${each.key}"
|
|
100
|
+
|
|
101
|
+
# When the cert SAN list changes (adding admin/computer/api/etc.), ACM may
|
|
102
|
+
# reissue with new validation tokens. With create_before_destroy on the cert,
|
|
103
|
+
# Terraform creates the new cert + validation records before destroying the
|
|
104
|
+
# old ones — and the Cloudflare provider rejects with "expected DNS record to
|
|
105
|
+
# not already be present but already exists" when names collide. allow_overwrite
|
|
106
|
+
# tells the provider to take ownership of an existing record by name instead
|
|
107
|
+
# of failing. Safe here because the validation records are fully managed by
|
|
108
|
+
# this resource — anything else writing _acm-challenge records on this zone
|
|
109
|
+
# would already be a conflict we'd want to overwrite.
|
|
110
|
+
allow_overwrite = true
|
|
89
111
|
}
|
|
90
112
|
|
|
91
113
|
resource "aws_acm_certificate_validation" "www" {
|
|
@@ -243,3 +265,84 @@ resource "cloudflare_record" "admin" {
|
|
|
243
265
|
proxied = false
|
|
244
266
|
comment = "thinkwork-${var.stage} admin → CloudFront"
|
|
245
267
|
}
|
|
268
|
+
|
|
269
|
+
################################################################################
|
|
270
|
+
# computer.<domain> → computer CloudFront distribution (optional)
|
|
271
|
+
################################################################################
|
|
272
|
+
|
|
273
|
+
resource "cloudflare_record" "computer" {
|
|
274
|
+
count = local.create_computer_record ? 1 : 0
|
|
275
|
+
|
|
276
|
+
zone_id = var.cloudflare_zone_id
|
|
277
|
+
name = local.computer
|
|
278
|
+
content = var.computer_cloudfront_domain_name
|
|
279
|
+
type = "CNAME"
|
|
280
|
+
ttl = 300
|
|
281
|
+
proxied = false
|
|
282
|
+
comment = "thinkwork-${var.stage} computer → CloudFront"
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
################################################################################
|
|
286
|
+
# sandbox.<domain> → Computer iframe sandbox CloudFront distribution (optional)
|
|
287
|
+
################################################################################
|
|
288
|
+
|
|
289
|
+
resource "cloudflare_record" "computer_sandbox" {
|
|
290
|
+
count = var.include_computer_sandbox ? 1 : 0
|
|
291
|
+
|
|
292
|
+
zone_id = var.cloudflare_zone_id
|
|
293
|
+
name = local.sandbox
|
|
294
|
+
content = var.computer_sandbox_cloudfront_domain_name
|
|
295
|
+
type = "CNAME"
|
|
296
|
+
ttl = 300
|
|
297
|
+
proxied = false
|
|
298
|
+
comment = "thinkwork-${var.stage} sandbox → Computer iframe sandbox CloudFront"
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
################################################################################
|
|
302
|
+
# api.<domain> → HTTP API Gateway (optional)
|
|
303
|
+
#
|
|
304
|
+
# API Gateway v2 HTTP APIs support regional custom domains. The cert lives in
|
|
305
|
+
# the same region as the API (us-east-1 here) and the domain name maps the
|
|
306
|
+
# target stage under the root base path ("") so routes defined on the API
|
|
307
|
+
# (/api/stripe/webhook, /graphql, etc.) are reachable at the vanity domain.
|
|
308
|
+
#
|
|
309
|
+
# Cloudflare must stay DNS-only (proxied=false). API Gateway presents the
|
|
310
|
+
# ACM cert directly; a proxied CNAME would mess with the TLS handshake.
|
|
311
|
+
################################################################################
|
|
312
|
+
|
|
313
|
+
resource "aws_apigatewayv2_domain_name" "api" {
|
|
314
|
+
count = var.include_api ? 1 : 0
|
|
315
|
+
|
|
316
|
+
domain_name = local.api
|
|
317
|
+
|
|
318
|
+
domain_name_configuration {
|
|
319
|
+
certificate_arn = aws_acm_certificate_validation.www.certificate_arn
|
|
320
|
+
endpoint_type = "REGIONAL"
|
|
321
|
+
security_policy = "TLS_1_2"
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
tags = {
|
|
325
|
+
Name = "thinkwork-${var.stage}-api-custom-domain"
|
|
326
|
+
Stage = var.stage
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
resource "aws_apigatewayv2_api_mapping" "api" {
|
|
331
|
+
count = local.create_api_record ? 1 : 0
|
|
332
|
+
|
|
333
|
+
api_id = var.api_gateway_id
|
|
334
|
+
domain_name = aws_apigatewayv2_domain_name.api[0].id
|
|
335
|
+
stage = var.api_gateway_stage_name
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
resource "cloudflare_record" "api" {
|
|
339
|
+
count = local.create_api_record ? 1 : 0
|
|
340
|
+
|
|
341
|
+
zone_id = var.cloudflare_zone_id
|
|
342
|
+
name = local.api
|
|
343
|
+
content = aws_apigatewayv2_domain_name.api[0].domain_name_configuration[0].target_domain_name
|
|
344
|
+
type = "CNAME"
|
|
345
|
+
ttl = 300
|
|
346
|
+
proxied = false
|
|
347
|
+
comment = "thinkwork-${var.stage} api → API Gateway v2 regional domain"
|
|
348
|
+
}
|
|
@@ -12,3 +12,13 @@ output "www_redirect_distribution_domain" {
|
|
|
12
12
|
description = "CloudFront distribution domain for the www→apex redirect"
|
|
13
13
|
value = aws_cloudfront_distribution.www_redirect.domain_name
|
|
14
14
|
}
|
|
15
|
+
|
|
16
|
+
output "api_custom_domain_name" {
|
|
17
|
+
description = "Custom domain name for the HTTP API (e.g. api.thinkwork.ai). Empty string when include_api is false."
|
|
18
|
+
value = var.include_api ? aws_apigatewayv2_domain_name.api[0].domain_name : ""
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
output "api_custom_domain_target" {
|
|
22
|
+
description = "API Gateway regional target domain to CNAME to (useful for external DNS configuration). Empty string when include_api is false."
|
|
23
|
+
value = var.include_api ? aws_apigatewayv2_domain_name.api[0].domain_name_configuration[0].target_domain_name : ""
|
|
24
|
+
}
|
|
@@ -41,3 +41,45 @@ variable "admin_cloudfront_domain_name" {
|
|
|
41
41
|
type = string
|
|
42
42
|
default = ""
|
|
43
43
|
}
|
|
44
|
+
|
|
45
|
+
variable "include_computer" {
|
|
46
|
+
description = "When true, add computer.<domain> to the ACM cert SANs and create a Cloudflare CNAME for it. Same cycle-avoidance rationale as include_docs."
|
|
47
|
+
type = bool
|
|
48
|
+
default = false
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
variable "computer_cloudfront_domain_name" {
|
|
52
|
+
description = "CloudFront distribution domain name for the computer SPA. Used as the target for the computer.<domain> Cloudflare CNAME when include_computer is true."
|
|
53
|
+
type = string
|
|
54
|
+
default = ""
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
variable "include_computer_sandbox" {
|
|
58
|
+
description = "When true, add sandbox.<domain> to the ACM cert SANs and create a Cloudflare CNAME for it. Same cycle-avoidance rationale as include_docs."
|
|
59
|
+
type = bool
|
|
60
|
+
default = false
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
variable "computer_sandbox_cloudfront_domain_name" {
|
|
64
|
+
description = "CloudFront distribution domain name for the Computer iframe sandbox host. Used as the target for the sandbox.<domain> Cloudflare CNAME when include_computer_sandbox is true."
|
|
65
|
+
type = string
|
|
66
|
+
default = ""
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
variable "include_api" {
|
|
70
|
+
description = "When true, add api.<domain> to the ACM cert SANs, create an API Gateway v2 custom domain name + base-path mapping, and create a Cloudflare CNAME pointing api.<domain> at the API Gateway regional domain. The API Gateway must be in the same region as this module since regional custom domains require a cert in the same region."
|
|
71
|
+
type = bool
|
|
72
|
+
default = false
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
variable "api_gateway_id" {
|
|
76
|
+
description = "aws_apigatewayv2_api.id of the HTTP API to expose at api.<domain>. Required when include_api is true."
|
|
77
|
+
type = string
|
|
78
|
+
default = ""
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
variable "api_gateway_stage_name" {
|
|
82
|
+
description = "aws_apigatewayv2_stage.name to map under the root base path on the custom domain. Defaults to the auto-deployed `$default` stage."
|
|
83
|
+
type = string
|
|
84
|
+
default = "$default"
|
|
85
|
+
}
|