thinkwork-cli 0.1.1 → 0.2.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/dist/cli.js +132 -11
- package/dist/terraform/examples/greenfield/main.tf +190 -0
- package/dist/terraform/examples/greenfield/terraform.tfvars.example +28 -0
- package/dist/terraform/modules/_internal/workspace-guard/main.tf +29 -0
- package/dist/terraform/modules/app/agentcore-runtime/main.tf +217 -0
- package/dist/terraform/modules/app/appsync-subscriptions/main.tf +122 -0
- package/dist/terraform/modules/app/appsync-subscriptions/outputs.tf +20 -0
- package/dist/terraform/modules/app/appsync-subscriptions/variables.tf +31 -0
- package/dist/terraform/modules/app/crons/main.tf +55 -0
- package/dist/terraform/modules/app/hindsight-memory/README.md +66 -0
- package/dist/terraform/modules/app/hindsight-memory/main.tf +331 -0
- package/dist/terraform/modules/app/job-triggers/main.tf +70 -0
- package/dist/terraform/modules/app/lambda-api/.build/placeholder.zip +0 -0
- package/dist/terraform/modules/app/lambda-api/handlers.tf +311 -0
- package/dist/terraform/modules/app/lambda-api/main.tf +245 -0
- package/dist/terraform/modules/app/lambda-api/outputs.tf +24 -0
- package/dist/terraform/modules/app/lambda-api/variables.tf +153 -0
- package/dist/terraform/modules/app/ses-email/main.tf +51 -0
- package/dist/terraform/modules/app/static-site/main.tf +176 -0
- package/dist/terraform/modules/data/aurora-postgres/README.md +92 -0
- package/dist/terraform/modules/data/aurora-postgres/main.tf +185 -0
- package/dist/terraform/modules/data/aurora-postgres/outputs.tf +30 -0
- package/dist/terraform/modules/data/aurora-postgres/variables.tf +114 -0
- package/dist/terraform/modules/data/bedrock-knowledge-base/main.tf +102 -0
- package/dist/terraform/modules/data/s3-buckets/main.tf +91 -0
- package/dist/terraform/modules/foundation/cognito/main.tf +377 -0
- package/dist/terraform/modules/foundation/cognito/outputs.tf +29 -0
- package/dist/terraform/modules/foundation/cognito/variables.tf +124 -0
- package/dist/terraform/modules/foundation/dns/main.tf +49 -0
- package/dist/terraform/modules/foundation/kms/main.tf +49 -0
- package/dist/terraform/modules/foundation/vpc/main.tf +137 -0
- package/dist/terraform/modules/foundation/vpc/outputs.tf +14 -0
- package/dist/terraform/modules/foundation/vpc/variables.tf +40 -0
- package/dist/terraform/modules/thinkwork/main.tf +212 -0
- package/dist/terraform/modules/thinkwork/outputs.tf +87 -0
- package/dist/terraform/modules/thinkwork/variables.tf +241 -0
- package/dist/terraform/schema.graphql +199 -0
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -83,7 +83,19 @@ function resolveTierDir(terraformDir, stage, tier) {
|
|
|
83
83
|
if (existsSync(envDir)) {
|
|
84
84
|
return envDir;
|
|
85
85
|
}
|
|
86
|
-
|
|
86
|
+
const greenfield = path.join(terraformDir, "examples", "greenfield");
|
|
87
|
+
if (existsSync(greenfield)) {
|
|
88
|
+
return greenfield;
|
|
89
|
+
}
|
|
90
|
+
const flat = path.join(terraformDir);
|
|
91
|
+
if (existsSync(path.join(flat, "main.tf"))) {
|
|
92
|
+
return flat;
|
|
93
|
+
}
|
|
94
|
+
const cwdTf = path.join(process.cwd(), "terraform");
|
|
95
|
+
if (existsSync(path.join(cwdTf, "main.tf"))) {
|
|
96
|
+
return cwdTf;
|
|
97
|
+
}
|
|
98
|
+
return terraformDir;
|
|
87
99
|
}
|
|
88
100
|
async function ensureWorkspace(cwd, stage) {
|
|
89
101
|
const list = await runTerraformRaw(cwd, ["workspace", "list"]);
|
|
@@ -709,11 +721,13 @@ function registerLoginCommand(program2) {
|
|
|
709
721
|
}
|
|
710
722
|
|
|
711
723
|
// src/commands/init.ts
|
|
712
|
-
import { existsSync as existsSync3, mkdirSync, writeFileSync as writeFileSync2 } from "fs";
|
|
713
|
-
import { resolve as resolve2, join } from "path";
|
|
724
|
+
import { existsSync as existsSync3, mkdirSync, writeFileSync as writeFileSync2, cpSync } from "fs";
|
|
725
|
+
import { resolve as resolve2, join, dirname } from "path";
|
|
714
726
|
import { execSync as execSync4 } from "child_process";
|
|
727
|
+
import { fileURLToPath } from "url";
|
|
715
728
|
import { createInterface as createInterface3 } from "readline";
|
|
716
729
|
import chalk3 from "chalk";
|
|
730
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
717
731
|
function ask2(prompt, defaultVal = "") {
|
|
718
732
|
const rl = createInterface3({ input: process.stdin, output: process.stdout });
|
|
719
733
|
const suffix = defaultVal ? chalk3.dim(` [${defaultVal}]`) : "";
|
|
@@ -736,6 +750,15 @@ function generateSecret(length = 32) {
|
|
|
736
750
|
for (const b of bytes) result += chars[b % chars.length];
|
|
737
751
|
return result;
|
|
738
752
|
}
|
|
753
|
+
function findBundledTerraform() {
|
|
754
|
+
const bundled = resolve2(__dirname, "..", "terraform");
|
|
755
|
+
if (existsSync3(join(bundled, "modules"))) return bundled;
|
|
756
|
+
const repoTf = resolve2(__dirname, "..", "..", "..", "..", "terraform");
|
|
757
|
+
if (existsSync3(join(repoTf, "modules"))) return repoTf;
|
|
758
|
+
throw new Error(
|
|
759
|
+
"Terraform modules not found. The CLI package may be incomplete.\nTry reinstalling: npm install -g thinkwork-cli@latest"
|
|
760
|
+
);
|
|
761
|
+
}
|
|
739
762
|
function buildTfvars(config) {
|
|
740
763
|
const lines = [
|
|
741
764
|
`# Thinkwork \u2014 ${config.stage} stage`,
|
|
@@ -782,7 +805,7 @@ function buildTfvars(config) {
|
|
|
782
805
|
return lines.join("\n");
|
|
783
806
|
}
|
|
784
807
|
function registerInitCommand(program2) {
|
|
785
|
-
program2.command("init").description("Initialize a new Thinkwork environment").requiredOption("-s, --stage <name>", "Stage name (e.g. dev, staging, prod)").option("-d, --dir <path>", "
|
|
808
|
+
program2.command("init").description("Initialize a new Thinkwork environment").requiredOption("-s, --stage <name>", "Stage name (e.g. dev, staging, prod)").option("-d, --dir <path>", "Target directory", ".").option("--defaults", "Skip interactive prompts, use all defaults").action(async (opts) => {
|
|
786
809
|
const stageCheck = validateStage(opts.stage);
|
|
787
810
|
if (!stageCheck.valid) {
|
|
788
811
|
printError(stageCheck.error);
|
|
@@ -794,8 +817,8 @@ function registerInitCommand(program2) {
|
|
|
794
817
|
printError("AWS credentials not configured. Run `thinkwork login` first.");
|
|
795
818
|
process.exit(1);
|
|
796
819
|
}
|
|
797
|
-
const
|
|
798
|
-
const tfDir = join(
|
|
820
|
+
const targetDir = resolve2(opts.dir);
|
|
821
|
+
const tfDir = join(targetDir, "terraform");
|
|
799
822
|
const tfvarsPath = join(tfDir, "terraform.tfvars");
|
|
800
823
|
if (existsSync3(tfvarsPath)) {
|
|
801
824
|
printWarning(`terraform.tfvars already exists at ${tfvarsPath}`);
|
|
@@ -851,13 +874,110 @@ function registerInitCommand(program2) {
|
|
|
851
874
|
console.log(chalk3.dim(` DB password: ${config.db_password.slice(0, 8)}...`));
|
|
852
875
|
console.log(chalk3.dim(` API auth secret: ${config.api_auth_secret.slice(0, 16)}...`));
|
|
853
876
|
}
|
|
854
|
-
|
|
855
|
-
|
|
877
|
+
console.log("");
|
|
878
|
+
console.log(" Scaffolding Terraform modules...");
|
|
879
|
+
let bundledTf;
|
|
880
|
+
try {
|
|
881
|
+
bundledTf = findBundledTerraform();
|
|
882
|
+
} catch (err) {
|
|
883
|
+
printError(String(err));
|
|
884
|
+
process.exit(1);
|
|
885
|
+
}
|
|
886
|
+
mkdirSync(tfDir, { recursive: true });
|
|
887
|
+
const copyDirs = ["modules", "examples"];
|
|
888
|
+
for (const dir of copyDirs) {
|
|
889
|
+
const src = join(bundledTf, dir);
|
|
890
|
+
const dst = join(tfDir, dir);
|
|
891
|
+
if (existsSync3(src) && !existsSync3(dst)) {
|
|
892
|
+
cpSync(src, dst, { recursive: true });
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
const schemaPath = join(bundledTf, "schema.graphql");
|
|
896
|
+
if (existsSync3(schemaPath) && !existsSync3(join(tfDir, "schema.graphql"))) {
|
|
897
|
+
cpSync(schemaPath, join(tfDir, "schema.graphql"));
|
|
856
898
|
}
|
|
857
899
|
const tfvars = buildTfvars(config);
|
|
858
900
|
writeFileSync2(tfvarsPath, tfvars);
|
|
859
|
-
|
|
860
|
-
|
|
901
|
+
const mainTfPath = join(tfDir, "main.tf");
|
|
902
|
+
if (!existsSync3(mainTfPath)) {
|
|
903
|
+
writeFileSync2(mainTfPath, `################################################################################
|
|
904
|
+
# Thinkwork \u2014 ${config.stage}
|
|
905
|
+
# Generated by: thinkwork init -s ${config.stage}
|
|
906
|
+
################################################################################
|
|
907
|
+
|
|
908
|
+
terraform {
|
|
909
|
+
required_version = ">= 1.5"
|
|
910
|
+
|
|
911
|
+
required_providers {
|
|
912
|
+
aws = {
|
|
913
|
+
source = "hashicorp/aws"
|
|
914
|
+
version = "~> 5.0"
|
|
915
|
+
}
|
|
916
|
+
archive = {
|
|
917
|
+
source = "hashicorp/archive"
|
|
918
|
+
version = "~> 2.0"
|
|
919
|
+
}
|
|
920
|
+
null = {
|
|
921
|
+
source = "hashicorp/null"
|
|
922
|
+
version = "~> 3.0"
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
provider "aws" {
|
|
928
|
+
region = var.region
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
variable "stage" { type = string }
|
|
932
|
+
variable "region" { type = string; default = "us-east-1" }
|
|
933
|
+
variable "account_id" { type = string }
|
|
934
|
+
variable "db_password" { type = string; sensitive = true }
|
|
935
|
+
variable "database_engine" { type = string; default = "aurora-serverless" }
|
|
936
|
+
variable "memory_engine" { type = string; default = "managed" }
|
|
937
|
+
variable "google_oauth_client_id" { type = string; default = "" }
|
|
938
|
+
variable "google_oauth_client_secret" { type = string; sensitive = true; default = "" }
|
|
939
|
+
variable "pre_signup_lambda_zip" { type = string; default = "" }
|
|
940
|
+
variable "lambda_zips_dir" { type = string; default = "" }
|
|
941
|
+
variable "api_auth_secret" { type = string; sensitive = true; default = "" }
|
|
942
|
+
variable "admin_callback_urls" { type = list(string); default = ["http://localhost:5174", "http://localhost:5174/auth/callback"] }
|
|
943
|
+
variable "admin_logout_urls" { type = list(string); default = ["http://localhost:5174"] }
|
|
944
|
+
variable "mobile_callback_urls" { type = list(string); default = ["exp://localhost:8081", "thinkwork://", "thinkwork://auth/callback"] }
|
|
945
|
+
variable "mobile_logout_urls" { type = list(string); default = ["exp://localhost:8081", "thinkwork://"] }
|
|
946
|
+
|
|
947
|
+
module "thinkwork" {
|
|
948
|
+
source = "./modules/thinkwork"
|
|
949
|
+
|
|
950
|
+
stage = var.stage
|
|
951
|
+
region = var.region
|
|
952
|
+
account_id = var.account_id
|
|
953
|
+
|
|
954
|
+
db_password = var.db_password
|
|
955
|
+
database_engine = var.database_engine
|
|
956
|
+
memory_engine = var.memory_engine
|
|
957
|
+
google_oauth_client_id = var.google_oauth_client_id
|
|
958
|
+
google_oauth_client_secret = var.google_oauth_client_secret
|
|
959
|
+
pre_signup_lambda_zip = var.pre_signup_lambda_zip
|
|
960
|
+
lambda_zips_dir = var.lambda_zips_dir
|
|
961
|
+
api_auth_secret = var.api_auth_secret
|
|
962
|
+
admin_callback_urls = var.admin_callback_urls
|
|
963
|
+
admin_logout_urls = var.admin_logout_urls
|
|
964
|
+
mobile_callback_urls = var.mobile_callback_urls
|
|
965
|
+
mobile_logout_urls = var.mobile_logout_urls
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
output "api_endpoint" { value = module.thinkwork.api_endpoint }
|
|
969
|
+
output "user_pool_id" { value = module.thinkwork.user_pool_id }
|
|
970
|
+
output "admin_client_id" { value = module.thinkwork.admin_client_id }
|
|
971
|
+
output "mobile_client_id" { value = module.thinkwork.mobile_client_id }
|
|
972
|
+
output "bucket_name" { value = module.thinkwork.bucket_name }
|
|
973
|
+
output "db_cluster_endpoint" { value = module.thinkwork.db_cluster_endpoint }
|
|
974
|
+
output "db_secret_arn" { value = module.thinkwork.db_secret_arn; sensitive = true }
|
|
975
|
+
output "ecr_repository_url" { value = module.thinkwork.ecr_repository_url }
|
|
976
|
+
output "memory_engine" { value = module.thinkwork.memory_engine }
|
|
977
|
+
output "hindsight_endpoint" { value = module.thinkwork.hindsight_endpoint }
|
|
978
|
+
`);
|
|
979
|
+
}
|
|
980
|
+
console.log(` Wrote ${chalk3.cyan(tfDir + "/")}`);
|
|
861
981
|
console.log("");
|
|
862
982
|
console.log(chalk3.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
863
983
|
console.log(` ${chalk3.bold("Stage:")} ${config.stage}`);
|
|
@@ -866,6 +986,7 @@ function registerInitCommand(program2) {
|
|
|
866
986
|
console.log(` ${chalk3.bold("Database:")} ${config.database_engine}`);
|
|
867
987
|
console.log(` ${chalk3.bold("Memory:")} ${config.memory_engine}`);
|
|
868
988
|
console.log(` ${chalk3.bold("Google OAuth:")} ${config.google_oauth_client_id ? "enabled" : "disabled"}`);
|
|
989
|
+
console.log(` ${chalk3.bold("Directory:")} ${tfDir}`);
|
|
869
990
|
console.log(chalk3.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
870
991
|
console.log("\n Initializing Terraform...\n");
|
|
871
992
|
try {
|
|
@@ -878,7 +999,7 @@ function registerInitCommand(program2) {
|
|
|
878
999
|
console.log("");
|
|
879
1000
|
console.log(" Next steps:");
|
|
880
1001
|
console.log(` ${chalk3.cyan("1.")} thinkwork plan -s ${opts.stage} ${chalk3.dim("# Review infrastructure plan")}`);
|
|
881
|
-
console.log(` ${chalk3.cyan("2.")} thinkwork deploy -s ${opts.stage} ${chalk3.dim("# Deploy to AWS")}`);
|
|
1002
|
+
console.log(` ${chalk3.cyan("2.")} thinkwork deploy -s ${opts.stage} ${chalk3.dim("# Deploy to AWS (~5 min)")}`);
|
|
882
1003
|
console.log(` ${chalk3.cyan("3.")} thinkwork bootstrap -s ${opts.stage} ${chalk3.dim("# Seed workspace files + skills")}`);
|
|
883
1004
|
console.log(` ${chalk3.cyan("4.")} thinkwork outputs -s ${opts.stage} ${chalk3.dim("# Show API URL, Cognito IDs, etc.")}`);
|
|
884
1005
|
console.log("");
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
################################################################################
|
|
2
|
+
# Greenfield Example
|
|
3
|
+
#
|
|
4
|
+
# Creates everything from scratch in a fresh AWS account.
|
|
5
|
+
# Copy this directory to start a new Thinkwork deployment.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# cd terraform/examples/greenfield
|
|
9
|
+
# cp terraform.tfvars.example terraform.tfvars # edit with your values
|
|
10
|
+
# terraform init
|
|
11
|
+
# terraform workspace new dev # or your stage name
|
|
12
|
+
# terraform plan -var-file=terraform.tfvars
|
|
13
|
+
# terraform apply -var-file=terraform.tfvars
|
|
14
|
+
################################################################################
|
|
15
|
+
|
|
16
|
+
terraform {
|
|
17
|
+
required_version = ">= 1.5"
|
|
18
|
+
|
|
19
|
+
required_providers {
|
|
20
|
+
aws = {
|
|
21
|
+
source = "hashicorp/aws"
|
|
22
|
+
version = "~> 5.0"
|
|
23
|
+
}
|
|
24
|
+
archive = {
|
|
25
|
+
source = "hashicorp/archive"
|
|
26
|
+
version = "~> 2.0"
|
|
27
|
+
}
|
|
28
|
+
null = {
|
|
29
|
+
source = "hashicorp/null"
|
|
30
|
+
version = "~> 3.0"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# For the example, use local state. Production deployments should
|
|
35
|
+
# use S3 + DynamoDB backend — see the docs for configuration.
|
|
36
|
+
# backend "s3" { ... }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
provider "aws" {
|
|
40
|
+
region = var.region
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
variable "stage" {
|
|
44
|
+
description = "Deployment stage — must match the Terraform workspace name"
|
|
45
|
+
type = string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
variable "region" {
|
|
49
|
+
description = "AWS region"
|
|
50
|
+
type = string
|
|
51
|
+
default = "us-east-1"
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
variable "account_id" {
|
|
55
|
+
description = "AWS account ID"
|
|
56
|
+
type = string
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
variable "db_password" {
|
|
60
|
+
description = "Master password for the Aurora cluster"
|
|
61
|
+
type = string
|
|
62
|
+
sensitive = true
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
variable "database_engine" {
|
|
66
|
+
description = "Database engine: 'aurora-serverless' (production) or 'rds-postgres' (dev/test, cheaper)"
|
|
67
|
+
type = string
|
|
68
|
+
default = "aurora-serverless"
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
variable "memory_engine" {
|
|
72
|
+
description = "Memory engine: 'managed' (AgentCore built-in, default) or 'hindsight' (ECS+ALB, opt-in)"
|
|
73
|
+
type = string
|
|
74
|
+
default = "managed"
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
variable "google_oauth_client_id" {
|
|
78
|
+
description = "Google OAuth client ID (optional — leave empty to skip Google login)"
|
|
79
|
+
type = string
|
|
80
|
+
default = ""
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
variable "google_oauth_client_secret" {
|
|
84
|
+
description = "Google OAuth client secret"
|
|
85
|
+
type = string
|
|
86
|
+
sensitive = true
|
|
87
|
+
default = ""
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
variable "pre_signup_lambda_zip" {
|
|
91
|
+
description = "Path to the Cognito pre-signup Lambda zip"
|
|
92
|
+
type = string
|
|
93
|
+
default = ""
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
variable "lambda_zips_dir" {
|
|
97
|
+
description = "Local directory containing Lambda zip artifacts (from pnpm build:lambdas)"
|
|
98
|
+
type = string
|
|
99
|
+
default = ""
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
variable "api_auth_secret" {
|
|
103
|
+
description = "Shared secret for inter-service API authentication"
|
|
104
|
+
type = string
|
|
105
|
+
sensitive = true
|
|
106
|
+
default = ""
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module "thinkwork" {
|
|
110
|
+
source = "../../modules/thinkwork"
|
|
111
|
+
|
|
112
|
+
stage = var.stage
|
|
113
|
+
region = var.region
|
|
114
|
+
account_id = var.account_id
|
|
115
|
+
|
|
116
|
+
db_password = var.db_password
|
|
117
|
+
database_engine = var.database_engine
|
|
118
|
+
memory_engine = var.memory_engine
|
|
119
|
+
google_oauth_client_id = var.google_oauth_client_id
|
|
120
|
+
google_oauth_client_secret = var.google_oauth_client_secret
|
|
121
|
+
pre_signup_lambda_zip = var.pre_signup_lambda_zip
|
|
122
|
+
lambda_zips_dir = var.lambda_zips_dir
|
|
123
|
+
api_auth_secret = var.api_auth_secret
|
|
124
|
+
|
|
125
|
+
# Greenfield: create everything (all defaults are true)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
################################################################################
|
|
129
|
+
# Outputs
|
|
130
|
+
################################################################################
|
|
131
|
+
|
|
132
|
+
output "api_endpoint" {
|
|
133
|
+
description = "API Gateway endpoint URL"
|
|
134
|
+
value = module.thinkwork.api_endpoint
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
output "appsync_realtime_url" {
|
|
138
|
+
description = "AppSync realtime WebSocket URL (for frontend subscription clients)"
|
|
139
|
+
value = module.thinkwork.appsync_realtime_url
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
output "user_pool_id" {
|
|
143
|
+
description = "Cognito user pool ID"
|
|
144
|
+
value = module.thinkwork.user_pool_id
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
output "admin_client_id" {
|
|
148
|
+
description = "Cognito app client ID for web admin"
|
|
149
|
+
value = module.thinkwork.admin_client_id
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
output "mobile_client_id" {
|
|
153
|
+
description = "Cognito app client ID for mobile"
|
|
154
|
+
value = module.thinkwork.mobile_client_id
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
output "ecr_repository_url" {
|
|
158
|
+
description = "ECR repository URL for the AgentCore container"
|
|
159
|
+
value = module.thinkwork.ecr_repository_url
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
output "bucket_name" {
|
|
163
|
+
description = "Primary S3 bucket"
|
|
164
|
+
value = module.thinkwork.bucket_name
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
output "db_cluster_endpoint" {
|
|
168
|
+
description = "Aurora cluster endpoint"
|
|
169
|
+
value = module.thinkwork.db_cluster_endpoint
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
output "db_secret_arn" {
|
|
173
|
+
description = "Secrets Manager ARN for database credentials"
|
|
174
|
+
value = module.thinkwork.db_secret_arn
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
output "database_name" {
|
|
178
|
+
description = "Database name"
|
|
179
|
+
value = module.thinkwork.database_name
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
output "memory_engine" {
|
|
183
|
+
description = "Active memory engine (managed or hindsight)"
|
|
184
|
+
value = module.thinkwork.memory_engine
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
output "hindsight_endpoint" {
|
|
188
|
+
description = "Hindsight API endpoint (null when memory_engine = managed)"
|
|
189
|
+
value = module.thinkwork.hindsight_endpoint
|
|
190
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Thinkwork Greenfield Deployment
|
|
2
|
+
#
|
|
3
|
+
# Copy this file to terraform.tfvars and fill in your values.
|
|
4
|
+
# DO NOT commit terraform.tfvars to version control.
|
|
5
|
+
|
|
6
|
+
stage = "dev"
|
|
7
|
+
region = "us-east-1"
|
|
8
|
+
account_id = "123456789012" # your AWS account ID
|
|
9
|
+
|
|
10
|
+
# Database engine:
|
|
11
|
+
# "aurora-serverless" — Aurora Serverless v2 (production, auto-scaling, deletion protection on)
|
|
12
|
+
# "rds-postgres" — Standard RDS PostgreSQL (dev/test, cheaper, deletion protection off)
|
|
13
|
+
database_engine = "rds-postgres"
|
|
14
|
+
|
|
15
|
+
# Memory engine:
|
|
16
|
+
# "managed" — AgentCore built-in long-term memory (default, no extra infra)
|
|
17
|
+
# "hindsight" — Hindsight ECS+ALB service (opt-in, adds retain/recall/reflect tools)
|
|
18
|
+
memory_engine = "managed"
|
|
19
|
+
|
|
20
|
+
# Database master password — use a strong password
|
|
21
|
+
db_password = "CHANGE_ME_strong_password_here"
|
|
22
|
+
|
|
23
|
+
# Google OAuth (optional — leave empty to skip Google social login)
|
|
24
|
+
# google_oauth_client_id = ""
|
|
25
|
+
# google_oauth_client_secret = ""
|
|
26
|
+
|
|
27
|
+
# Pre-signup Lambda (optional — leave empty if not using custom pre-signup logic)
|
|
28
|
+
# pre_signup_lambda_zip = "./lambdas/pre-signup.zip"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
################################################################################
|
|
2
|
+
# Workspace Guard
|
|
3
|
+
#
|
|
4
|
+
# Prevents applying the wrong var file to the wrong Terraform workspace.
|
|
5
|
+
# Every tier (foundation, data, app) should consume this module.
|
|
6
|
+
#
|
|
7
|
+
# History: on 2026-04-05, running `terraform apply -var-file=prod.tfvars` in
|
|
8
|
+
# the dev workspace destroyed dev infrastructure. This guard prevents that
|
|
9
|
+
# class of incident by failing the plan before any damage occurs.
|
|
10
|
+
################################################################################
|
|
11
|
+
|
|
12
|
+
variable "stage" {
|
|
13
|
+
description = "The deployment stage (must match the Terraform workspace name)"
|
|
14
|
+
type = string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
resource "null_resource" "workspace_guard" {
|
|
18
|
+
triggers = {
|
|
19
|
+
stage = var.stage
|
|
20
|
+
workspace = terraform.workspace
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
lifecycle {
|
|
24
|
+
precondition {
|
|
25
|
+
condition = var.stage == terraform.workspace
|
|
26
|
+
error_message = "SAFETY: stage '${var.stage}' does not match workspace '${terraform.workspace}'. You are applying the wrong var file!"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
################################################################################
|
|
2
|
+
# AgentCore Runtime — App Module
|
|
3
|
+
#
|
|
4
|
+
# Creates ECR repository, IAM roles, and container build infrastructure
|
|
5
|
+
# for the Strands-based agent runtime. Full implementation ported in Phase 3.
|
|
6
|
+
# Phase 1 creates the ECR repo and IAM scaffolding only.
|
|
7
|
+
################################################################################
|
|
8
|
+
|
|
9
|
+
variable "stage" {
|
|
10
|
+
description = "Deployment stage"
|
|
11
|
+
type = string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
variable "account_id" {
|
|
15
|
+
description = "AWS account ID"
|
|
16
|
+
type = string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
variable "region" {
|
|
20
|
+
description = "AWS region"
|
|
21
|
+
type = string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
variable "bucket_name" {
|
|
25
|
+
description = "Primary S3 bucket for skills and workspace files"
|
|
26
|
+
type = string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
variable "memory_engine" {
|
|
30
|
+
description = "Memory engine: 'managed' or 'hindsight'. Passed as MEMORY_ENGINE env var to the container."
|
|
31
|
+
type = string
|
|
32
|
+
default = "managed"
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
variable "hindsight_endpoint" {
|
|
36
|
+
description = "Hindsight API endpoint (only used when memory_engine = 'hindsight')"
|
|
37
|
+
type = string
|
|
38
|
+
default = ""
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
variable "agentcore_memory_id" {
|
|
42
|
+
description = "AgentCore Memory resource ID (only used when memory_engine = 'managed')"
|
|
43
|
+
type = string
|
|
44
|
+
default = ""
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
################################################################################
|
|
48
|
+
# ECR Repository
|
|
49
|
+
################################################################################
|
|
50
|
+
|
|
51
|
+
resource "aws_ecr_repository" "agentcore" {
|
|
52
|
+
name = "thinkwork-${var.stage}-agentcore"
|
|
53
|
+
image_tag_mutability = "MUTABLE"
|
|
54
|
+
force_delete = true
|
|
55
|
+
|
|
56
|
+
image_scanning_configuration {
|
|
57
|
+
scan_on_push = true
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
tags = {
|
|
61
|
+
Name = "thinkwork-${var.stage}-agentcore"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
resource "aws_ecr_lifecycle_policy" "agentcore" {
|
|
66
|
+
repository = aws_ecr_repository.agentcore.name
|
|
67
|
+
|
|
68
|
+
policy = jsonencode({
|
|
69
|
+
rules = [{
|
|
70
|
+
rulePriority = 1
|
|
71
|
+
description = "Keep last 10 images"
|
|
72
|
+
selection = {
|
|
73
|
+
tagStatus = "any"
|
|
74
|
+
countType = "imageCountMoreThan"
|
|
75
|
+
countNumber = 10
|
|
76
|
+
}
|
|
77
|
+
action = {
|
|
78
|
+
type = "expire"
|
|
79
|
+
}
|
|
80
|
+
}]
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
################################################################################
|
|
85
|
+
# Execution Role
|
|
86
|
+
################################################################################
|
|
87
|
+
|
|
88
|
+
resource "aws_iam_role" "agentcore" {
|
|
89
|
+
name = "thinkwork-${var.stage}-agentcore-role"
|
|
90
|
+
|
|
91
|
+
assume_role_policy = jsonencode({
|
|
92
|
+
Version = "2012-10-17"
|
|
93
|
+
Statement = [{
|
|
94
|
+
Effect = "Allow"
|
|
95
|
+
Principal = { Service = ["ecs-tasks.amazonaws.com", "lambda.amazonaws.com"] }
|
|
96
|
+
Action = "sts:AssumeRole"
|
|
97
|
+
}]
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
resource "aws_iam_role_policy" "agentcore" {
|
|
102
|
+
name = "agentcore-permissions"
|
|
103
|
+
role = aws_iam_role.agentcore.id
|
|
104
|
+
|
|
105
|
+
policy = jsonencode({
|
|
106
|
+
Version = "2012-10-17"
|
|
107
|
+
Statement = [
|
|
108
|
+
{
|
|
109
|
+
Sid = "S3Access"
|
|
110
|
+
Effect = "Allow"
|
|
111
|
+
Action = ["s3:GetObject", "s3:PutObject", "s3:ListBucket"]
|
|
112
|
+
Resource = [
|
|
113
|
+
"arn:aws:s3:::${var.bucket_name}",
|
|
114
|
+
"arn:aws:s3:::${var.bucket_name}/*",
|
|
115
|
+
]
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
Sid = "BedrockInvoke"
|
|
119
|
+
Effect = "Allow"
|
|
120
|
+
Action = ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream", "bedrock:InvokeAgent"]
|
|
121
|
+
Resource = "*"
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
Sid = "CloudWatchLogs"
|
|
125
|
+
Effect = "Allow"
|
|
126
|
+
Action = ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"]
|
|
127
|
+
Resource = "arn:aws:logs:${var.region}:${var.account_id}:*"
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
Sid = "ECRPull"
|
|
131
|
+
Effect = "Allow"
|
|
132
|
+
Action = ["ecr:GetDownloadUrlForLayer", "ecr:BatchGetImage", "ecr:GetAuthorizationToken"]
|
|
133
|
+
Resource = "*"
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
Sid = "SSMParameterAccess"
|
|
137
|
+
Effect = "Allow"
|
|
138
|
+
Action = ["ssm:GetParameter", "ssm:PutParameter"]
|
|
139
|
+
Resource = "arn:aws:ssm:${var.region}:${var.account_id}:parameter/thinkwork/${var.stage}/agentcore/*"
|
|
140
|
+
},
|
|
141
|
+
]
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
################################################################################
|
|
146
|
+
# CloudWatch Log Group
|
|
147
|
+
################################################################################
|
|
148
|
+
|
|
149
|
+
resource "aws_cloudwatch_log_group" "agentcore" {
|
|
150
|
+
name = "/thinkwork/${var.stage}/agentcore"
|
|
151
|
+
retention_in_days = 30
|
|
152
|
+
|
|
153
|
+
tags = {
|
|
154
|
+
Name = "thinkwork-${var.stage}-agentcore-logs"
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
################################################################################
|
|
159
|
+
# Lambda Container Image
|
|
160
|
+
################################################################################
|
|
161
|
+
|
|
162
|
+
resource "aws_lambda_function" "agentcore" {
|
|
163
|
+
function_name = "thinkwork-${var.stage}-agentcore"
|
|
164
|
+
role = aws_iam_role.agentcore.arn
|
|
165
|
+
package_type = "Image"
|
|
166
|
+
image_uri = "${aws_ecr_repository.agentcore.repository_url}:latest"
|
|
167
|
+
timeout = 900
|
|
168
|
+
memory_size = 2048
|
|
169
|
+
|
|
170
|
+
environment {
|
|
171
|
+
variables = {
|
|
172
|
+
PORT = "8080"
|
|
173
|
+
AWS_LWA_PORT = "8080"
|
|
174
|
+
MEMORY_ENGINE = var.memory_engine
|
|
175
|
+
AGENTCORE_MEMORY_ID = var.agentcore_memory_id
|
|
176
|
+
AGENTCORE_FILES_BUCKET = var.bucket_name
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
logging_config {
|
|
181
|
+
log_group = aws_cloudwatch_log_group.agentcore.name
|
|
182
|
+
log_format = "Text"
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
tags = {
|
|
186
|
+
Name = "thinkwork-${var.stage}-agentcore"
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
resource "aws_lambda_function_url" "agentcore" {
|
|
191
|
+
function_name = aws_lambda_function.agentcore.function_name
|
|
192
|
+
authorization_type = "NONE"
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
################################################################################
|
|
196
|
+
# Outputs
|
|
197
|
+
################################################################################
|
|
198
|
+
|
|
199
|
+
output "ecr_repository_url" {
|
|
200
|
+
description = "ECR repository URL for the AgentCore container"
|
|
201
|
+
value = aws_ecr_repository.agentcore.repository_url
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
output "execution_role_arn" {
|
|
205
|
+
description = "IAM role ARN for AgentCore execution"
|
|
206
|
+
value = aws_iam_role.agentcore.arn
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
output "agentcore_invoke_url" {
|
|
210
|
+
description = "Lambda Function URL for the AgentCore container"
|
|
211
|
+
value = aws_lambda_function_url.agentcore.function_url
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
output "agentcore_function_name" {
|
|
215
|
+
description = "AgentCore Lambda function name (for direct SDK invoke)"
|
|
216
|
+
value = aws_lambda_function.agentcore.function_name
|
|
217
|
+
}
|