From f027aa2526b9dc34321762d413c5441d1a8ca390 Mon Sep 17 00:00:00 2001 From: James Willis Date: Mon, 13 Oct 2025 13:56:18 +0100 Subject: [PATCH 1/4] add terraform --- platform/.dockerignore | 37 +++ platform/live-frame/yarn.lock | 38 +-- platform/wab/package.json | 1 + platform/wab/rsbuild.config.ts | 2 + platform/wab/yarn.lock | 41 +-- terraform/.gitignore | 40 +++ terraform/generate-configs.sh | 65 +++++ terraform/modules/backend-service/alb.tf | 58 ++++ terraform/modules/backend-service/iam.tf | 38 +++ terraform/modules/backend-service/main.tf | 120 ++++++++ terraform/modules/backend-service/outputs.tf | 34 +++ .../modules/backend-service/variables.tf | 173 +++++++++++ .../projects/database/.terraform.lock.hcl | 25 ++ .../config/integration-backend.tfvars.example | 4 + .../config/integration.tfvars.example | 13 + terraform/projects/database/main.tf | 67 +++++ terraform/projects/database/outputs.tf | 35 +++ terraform/projects/database/providers.tf | 24 ++ terraform/projects/database/remote-state.tf | 15 + terraform/projects/database/vars.tf | 53 ++++ .../projects/dynamodb/.terraform.lock.hcl | 25 ++ .../config/integration-backend.tfvars.example | 4 + .../config/integration.tfvars.example | 5 + terraform/projects/dynamodb/main.tf | 26 ++ terraform/projects/dynamodb/outputs.tf | 14 + terraform/projects/dynamodb/terraform.tf | 24 ++ terraform/projects/dynamodb/variables.tf | 16 + terraform/projects/ecr/.terraform.lock.hcl | 25 ++ .../ecr/config/shared-backend.tfvars.example | 4 + .../projects/ecr/config/shared.tfvars.example | 5 + terraform/projects/ecr/main.tf | 41 +++ terraform/projects/ecr/outputs.tf | 14 + terraform/projects/ecr/providers.tf | 24 ++ terraform/projects/ecr/vars.tf | 16 + .../projects/ecs-cluster/.terraform.lock.hcl | 44 +++ terraform/projects/ecs-cluster/alb.tf | 89 ++++++ .../config/integration-backend.tfvars.example | 4 + .../config/integration.tfvars.example | 4 + .../ecs-cluster/database-uri-secret.tf | 15 + terraform/projects/ecs-cluster/iam.tf | 45 +++ terraform/projects/ecs-cluster/main.tf | 38 +++ terraform/projects/ecs-cluster/outputs.tf | 55 ++++ terraform/projects/ecs-cluster/providers.tf | 24 ++ .../projects/ecs-cluster/remote-state.tf | 31 ++ .../projects/ecs-cluster/security-groups.tf | 89 ++++++ terraform/projects/ecs-cluster/vars.tf | 16 + .../projects/frontend/.terraform.lock.hcl | 25 ++ .../config/integration-backend.tfvars.example | 4 + .../config/integration.tfvars.example | 2 + terraform/projects/frontend/main.tf | 232 +++++++++++++++ terraform/projects/frontend/outputs.tf | 39 +++ terraform/projects/frontend/providers.tf | 26 ++ terraform/projects/frontend/variables.tf | 9 + .../projects/s3-assets/.terraform.lock.hcl | 25 ++ .../config/integration-backend.tfvars.example | 4 + .../config/integration.tfvars.example | 9 + terraform/projects/s3-assets/main.tf | 58 ++++ terraform/projects/s3-assets/outputs.tf | 9 + terraform/projects/s3-assets/terraform.tf | 24 ++ terraform/projects/s3-assets/variables.tf | 16 + .../projects/s3-clips/.terraform.lock.hcl | 25 ++ .../config/integration-backend.tfvars.example | 4 + .../config/integration.tfvars.example | 2 + terraform/projects/s3-clips/main.tf | 45 +++ terraform/projects/s3-clips/outputs.tf | 9 + terraform/projects/s3-clips/terraform.tf | 24 ++ terraform/projects/s3-clips/variables.tf | 10 + .../s3-site-assets/.terraform.lock.hcl | 25 ++ .../config/integration-backend.tfvars.example | 4 + .../config/integration.tfvars.example | 10 + terraform/projects/s3-site-assets/main.tf | 65 +++++ terraform/projects/s3-site-assets/outputs.tf | 14 + .../projects/s3-site-assets/terraform.tf | 24 ++ .../projects/s3-site-assets/variables.tf | 16 + .../projects/secrets/.terraform.lock.hcl | 25 ++ .../config/integration-backend.tfvars.example | 4 + .../secrets/config/integration.tfvars.example | 15 + terraform/projects/secrets/main.tf | 113 +++++++ terraform/projects/secrets/outputs.tf | 29 ++ terraform/projects/secrets/terraform.tf | 24 ++ terraform/projects/secrets/variables.tf | 64 ++++ .../services/codegen/.terraform.lock.hcl | 25 ++ .../config/integration-backend.tfvars.example | 4 + .../codegen/config/integration.tfvars.example | 26 ++ terraform/projects/services/codegen/main.tf | 99 +++++++ .../projects/services/codegen/outputs.tf | 29 ++ .../projects/services/codegen/providers.tf | 27 ++ .../projects/services/codegen/remote-state.tf | 52 ++++ terraform/projects/services/codegen/vars.tf | 72 +++++ .../services/copilot/.terraform.lock.hcl | 25 ++ .../config/integration-backend.tfvars.example | 4 + .../copilot/config/integration.tfvars.example | 26 ++ terraform/projects/services/copilot/main.tf | 123 ++++++++ .../projects/services/copilot/outputs.tf | 29 ++ .../projects/services/copilot/providers.tf | 27 ++ .../projects/services/copilot/remote-state.tf | 76 +++++ terraform/projects/services/copilot/vars.tf | 79 +++++ .../services/data/.terraform.lock.hcl | 25 ++ .../config/integration-backend.tfvars.example | 4 + .../data/config/integration.tfvars.example | 23 ++ terraform/projects/services/data/main.tf | 78 +++++ terraform/projects/services/data/outputs.tf | 29 ++ terraform/projects/services/data/providers.tf | 27 ++ .../projects/services/data/remote-state.tf | 54 ++++ terraform/projects/services/data/vars.tf | 62 ++++ .../projects/services/wab/.terraform.lock.hcl | 25 ++ .../projects/services/wab/build-and-push.sh | 74 +++++ .../config/integration-backend.tfvars.example | 4 + .../wab/config/integration.tfvars.example | 28 ++ terraform/projects/services/wab/main.tf | 119 ++++++++ terraform/projects/services/wab/outputs.tf | 30 ++ terraform/projects/services/wab/providers.tf | 24 ++ .../projects/services/wab/remote-state.tf | 66 +++++ terraform/projects/services/wab/vars.tf | 70 +++++ terraform/projects/vpc/.terraform.lock.hcl | 25 ++ .../config/integration-backend.tfvars.example | 4 + .../vpc/config/integration.tfvars.example | 7 + terraform/projects/vpc/main.tf | 149 ++++++++++ terraform/projects/vpc/outputs.tf | 34 +++ terraform/projects/vpc/providers.tf | 24 ++ terraform/projects/vpc/vars.tf | 27 ++ terraform/scripts/bootstrap.sh | 92 ++++++ terraform/scripts/create-secret.sh | 64 ++++ terraform/scripts/create-secrets.sh | 82 ++++++ terraform/scripts/deploy-all.sh | 259 +++++++++++++++++ terraform/scripts/deploy-frontend.sh | 275 ++++++++++++++++++ 126 files changed, 5072 insertions(+), 48 deletions(-) create mode 100644 platform/.dockerignore create mode 100644 terraform/.gitignore create mode 100755 terraform/generate-configs.sh create mode 100644 terraform/modules/backend-service/alb.tf create mode 100644 terraform/modules/backend-service/iam.tf create mode 100644 terraform/modules/backend-service/main.tf create mode 100644 terraform/modules/backend-service/outputs.tf create mode 100644 terraform/modules/backend-service/variables.tf create mode 100644 terraform/projects/database/.terraform.lock.hcl create mode 100644 terraform/projects/database/config/integration-backend.tfvars.example create mode 100644 terraform/projects/database/config/integration.tfvars.example create mode 100644 terraform/projects/database/main.tf create mode 100644 terraform/projects/database/outputs.tf create mode 100644 terraform/projects/database/providers.tf create mode 100644 terraform/projects/database/remote-state.tf create mode 100644 terraform/projects/database/vars.tf create mode 100644 terraform/projects/dynamodb/.terraform.lock.hcl create mode 100644 terraform/projects/dynamodb/config/integration-backend.tfvars.example create mode 100644 terraform/projects/dynamodb/config/integration.tfvars.example create mode 100644 terraform/projects/dynamodb/main.tf create mode 100644 terraform/projects/dynamodb/outputs.tf create mode 100644 terraform/projects/dynamodb/terraform.tf create mode 100644 terraform/projects/dynamodb/variables.tf create mode 100644 terraform/projects/ecr/.terraform.lock.hcl create mode 100644 terraform/projects/ecr/config/shared-backend.tfvars.example create mode 100644 terraform/projects/ecr/config/shared.tfvars.example create mode 100644 terraform/projects/ecr/main.tf create mode 100644 terraform/projects/ecr/outputs.tf create mode 100644 terraform/projects/ecr/providers.tf create mode 100644 terraform/projects/ecr/vars.tf create mode 100644 terraform/projects/ecs-cluster/.terraform.lock.hcl create mode 100644 terraform/projects/ecs-cluster/alb.tf create mode 100644 terraform/projects/ecs-cluster/config/integration-backend.tfvars.example create mode 100644 terraform/projects/ecs-cluster/config/integration.tfvars.example create mode 100644 terraform/projects/ecs-cluster/database-uri-secret.tf create mode 100644 terraform/projects/ecs-cluster/iam.tf create mode 100644 terraform/projects/ecs-cluster/main.tf create mode 100644 terraform/projects/ecs-cluster/outputs.tf create mode 100644 terraform/projects/ecs-cluster/providers.tf create mode 100644 terraform/projects/ecs-cluster/remote-state.tf create mode 100644 terraform/projects/ecs-cluster/security-groups.tf create mode 100644 terraform/projects/ecs-cluster/vars.tf create mode 100644 terraform/projects/frontend/.terraform.lock.hcl create mode 100644 terraform/projects/frontend/config/integration-backend.tfvars.example create mode 100644 terraform/projects/frontend/config/integration.tfvars.example create mode 100644 terraform/projects/frontend/main.tf create mode 100644 terraform/projects/frontend/outputs.tf create mode 100644 terraform/projects/frontend/providers.tf create mode 100644 terraform/projects/frontend/variables.tf create mode 100644 terraform/projects/s3-assets/.terraform.lock.hcl create mode 100644 terraform/projects/s3-assets/config/integration-backend.tfvars.example create mode 100644 terraform/projects/s3-assets/config/integration.tfvars.example create mode 100644 terraform/projects/s3-assets/main.tf create mode 100644 terraform/projects/s3-assets/outputs.tf create mode 100644 terraform/projects/s3-assets/terraform.tf create mode 100644 terraform/projects/s3-assets/variables.tf create mode 100644 terraform/projects/s3-clips/.terraform.lock.hcl create mode 100644 terraform/projects/s3-clips/config/integration-backend.tfvars.example create mode 100644 terraform/projects/s3-clips/config/integration.tfvars.example create mode 100644 terraform/projects/s3-clips/main.tf create mode 100644 terraform/projects/s3-clips/outputs.tf create mode 100644 terraform/projects/s3-clips/terraform.tf create mode 100644 terraform/projects/s3-clips/variables.tf create mode 100644 terraform/projects/s3-site-assets/.terraform.lock.hcl create mode 100644 terraform/projects/s3-site-assets/config/integration-backend.tfvars.example create mode 100644 terraform/projects/s3-site-assets/config/integration.tfvars.example create mode 100644 terraform/projects/s3-site-assets/main.tf create mode 100644 terraform/projects/s3-site-assets/outputs.tf create mode 100644 terraform/projects/s3-site-assets/terraform.tf create mode 100644 terraform/projects/s3-site-assets/variables.tf create mode 100644 terraform/projects/secrets/.terraform.lock.hcl create mode 100644 terraform/projects/secrets/config/integration-backend.tfvars.example create mode 100644 terraform/projects/secrets/config/integration.tfvars.example create mode 100644 terraform/projects/secrets/main.tf create mode 100644 terraform/projects/secrets/outputs.tf create mode 100644 terraform/projects/secrets/terraform.tf create mode 100644 terraform/projects/secrets/variables.tf create mode 100644 terraform/projects/services/codegen/.terraform.lock.hcl create mode 100644 terraform/projects/services/codegen/config/integration-backend.tfvars.example create mode 100644 terraform/projects/services/codegen/config/integration.tfvars.example create mode 100644 terraform/projects/services/codegen/main.tf create mode 100644 terraform/projects/services/codegen/outputs.tf create mode 100644 terraform/projects/services/codegen/providers.tf create mode 100644 terraform/projects/services/codegen/remote-state.tf create mode 100644 terraform/projects/services/codegen/vars.tf create mode 100644 terraform/projects/services/copilot/.terraform.lock.hcl create mode 100644 terraform/projects/services/copilot/config/integration-backend.tfvars.example create mode 100644 terraform/projects/services/copilot/config/integration.tfvars.example create mode 100644 terraform/projects/services/copilot/main.tf create mode 100644 terraform/projects/services/copilot/outputs.tf create mode 100644 terraform/projects/services/copilot/providers.tf create mode 100644 terraform/projects/services/copilot/remote-state.tf create mode 100644 terraform/projects/services/copilot/vars.tf create mode 100644 terraform/projects/services/data/.terraform.lock.hcl create mode 100644 terraform/projects/services/data/config/integration-backend.tfvars.example create mode 100644 terraform/projects/services/data/config/integration.tfvars.example create mode 100644 terraform/projects/services/data/main.tf create mode 100644 terraform/projects/services/data/outputs.tf create mode 100644 terraform/projects/services/data/providers.tf create mode 100644 terraform/projects/services/data/remote-state.tf create mode 100644 terraform/projects/services/data/vars.tf create mode 100644 terraform/projects/services/wab/.terraform.lock.hcl create mode 100755 terraform/projects/services/wab/build-and-push.sh create mode 100644 terraform/projects/services/wab/config/integration-backend.tfvars.example create mode 100644 terraform/projects/services/wab/config/integration.tfvars.example create mode 100644 terraform/projects/services/wab/main.tf create mode 100644 terraform/projects/services/wab/outputs.tf create mode 100644 terraform/projects/services/wab/providers.tf create mode 100644 terraform/projects/services/wab/remote-state.tf create mode 100644 terraform/projects/services/wab/vars.tf create mode 100644 terraform/projects/vpc/.terraform.lock.hcl create mode 100644 terraform/projects/vpc/config/integration-backend.tfvars.example create mode 100644 terraform/projects/vpc/config/integration.tfvars.example create mode 100644 terraform/projects/vpc/main.tf create mode 100644 terraform/projects/vpc/outputs.tf create mode 100644 terraform/projects/vpc/providers.tf create mode 100644 terraform/projects/vpc/vars.tf create mode 100755 terraform/scripts/bootstrap.sh create mode 100755 terraform/scripts/create-secret.sh create mode 100755 terraform/scripts/create-secrets.sh create mode 100755 terraform/scripts/deploy-all.sh create mode 100755 terraform/scripts/deploy-frontend.sh diff --git a/platform/.dockerignore b/platform/.dockerignore new file mode 100644 index 000000000..333408209 --- /dev/null +++ b/platform/.dockerignore @@ -0,0 +1,37 @@ +# Dependencies - will be installed inside the container +**/node_modules +**/bower_components + +# Build artifacts +**/dist +**/build +**/.next +**/.cache + +# Development files +**/.git +**/.gitignore +**/.env +**/.env.* + +# IDE files +**/.vscode +**/.idea +**/*.swp +**/*.swo +**/.DS_Store + +# Logs +**/npm-debug.log* +**/yarn-debug.log* +**/yarn-error.log* +**/lerna-debug.log* + +# Testing +**/coverage +**/.nyc_output + +# Temp files +**/tmp +**/temp +**/*.log \ No newline at end of file diff --git a/platform/live-frame/yarn.lock b/platform/live-frame/yarn.lock index 15354044e..5c613c8b9 100644 --- a/platform/live-frame/yarn.lock +++ b/platform/live-frame/yarn.lock @@ -112,21 +112,21 @@ resolved "https://registry.yarnpkg.com/@plasmicapp/data-sources-context/-/data-sources-context-0.1.22.tgz#03ee66b603df6bea52ff0f4708a6cd019e57c73a" integrity sha512-FxXHCZj/pVysamgBhbeVKP14xTfilQI+2peZixrY09gKCz+C2iVqKZCKuwhuXodFdMfCveiMSzWg8Hqz9xJRqQ== -"@plasmicapp/data-sources@0.1.188": - version "0.1.188" - resolved "https://registry.yarnpkg.com/@plasmicapp/data-sources/-/data-sources-0.1.188.tgz#5ac1613f8781deeb5c05f875b2f0118925180fab" - integrity sha512-MFynj9ZWGi99wQldotfgx5afgEcQ7MXBU7bblF2b9B5WapZtiXWR62l9JBs/VMU1yRfVkm7mZVXBBCHRAayp8A== +"@plasmicapp/data-sources@0.1.190": + version "0.1.190" + resolved "https://registry.yarnpkg.com/@plasmicapp/data-sources/-/data-sources-0.1.190.tgz#44e6b669d00367c003eb9ab0abb3025cfb2e5ce3" + integrity sha512-oivcCp+F4GS8DTDieng8qTlnUcRa7lni+GINCn6egRURwA9LV2CtMv/z7m80dNC8myuNpEi7U3x7/XIs/f9FTg== dependencies: "@plasmicapp/data-sources-context" "0.1.22" - "@plasmicapp/host" "1.0.224" + "@plasmicapp/host" "1.0.226" "@plasmicapp/isomorphic-unfetch" "1.0.3" "@plasmicapp/query" "0.1.80" fast-stringify "^2.0.0" -"@plasmicapp/host@1.0.224": - version "1.0.224" - resolved "https://registry.yarnpkg.com/@plasmicapp/host/-/host-1.0.224.tgz#e37ac84107ca786a266dcbb495127c59e8448c75" - integrity sha512-vvyTVIPUjjfG148RyKuRYylpzpJPb0YNdlYNlaQGoxIdO3xjTGSS1v0ouI9zG80CKI3MqtYRgfoCFQ1RycbSUg== +"@plasmicapp/host@1.0.226": + version "1.0.226" + resolved "https://registry.yarnpkg.com/@plasmicapp/host/-/host-1.0.226.tgz#3807c401d2ac3c126d18973684b42db23abeba75" + integrity sha512-8tCtx2FqRaPkKREkFbwfuoVmhCmUl/BS7/7sE/xO1IlZXrKWh4qQV4t2iXQLaz4QmectkNaFm38BUjlnnpKYvA== dependencies: "@plasmicapp/query" "0.1.80" csstype "^3.1.2" @@ -139,17 +139,17 @@ dependencies: unfetch "^4.2.0" -"@plasmicapp/loader-splits@1.0.64": - version "1.0.64" - resolved "https://registry.yarnpkg.com/@plasmicapp/loader-splits/-/loader-splits-1.0.64.tgz#07a68e66927eb97aa6814a794754c0abe7110641" - integrity sha512-PrZNSokH7aedwXtFD0tWn/P7yL+h1oDpEqKDm7zD0d0tjq6spL90I61lBU8MJlc5/dngLBrovLPyzO51EmPLqg== +"@plasmicapp/loader-splits@1.0.65": + version "1.0.65" + resolved "https://registry.yarnpkg.com/@plasmicapp/loader-splits/-/loader-splits-1.0.65.tgz#c167efe15b46c2505abdd4d13f45926c99d11e99" + integrity sha512-kU+5Ky157i9JUu5cSCp+yIohQKF2lGNr4mQcaV7SHpE7aexQ4WDIr36kyZehwh4EcFTIHHc3H2g+DzAt64AIDA== dependencies: json-logic-js "^2.0.2" -"@plasmicapp/nextjs-app-router@1.0.17": - version "1.0.17" - resolved "https://registry.yarnpkg.com/@plasmicapp/nextjs-app-router/-/nextjs-app-router-1.0.17.tgz#bee6242f48e83f516a44901e70a63351154d3150" - integrity sha512-aIYkQZoFunwDGo9Xf+zeJagHasVyORa41RtD17RbDvk6CSQM3rtTEwvz/xua0KZsF1DTfE8bMUP1pCRdJGQrbA== +"@plasmicapp/nextjs-app-router@1.0.18": + version "1.0.18" + resolved "https://registry.yarnpkg.com/@plasmicapp/nextjs-app-router/-/nextjs-app-router-1.0.18.tgz#86ed48fabbfc1790b4aa4b41ce95cd00500b1c1b" + integrity sha512-Rai/CrOfOzr6nmuDvNS18Rfl4Qsn2fk+vYYgNWUlcp3S76XW0RFJPBt0iDpBo+f1ysHfkI+L0P8hz+aSPUVL6w== dependencies: "@plasmicapp/prepass" "1.0.20" "@plasmicapp/query" "0.1.80" @@ -1684,7 +1684,7 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= -loader-utils@1.4.2, loader-utils@^1.1.0: +loader-utils@^1.1.0: version "1.4.2" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.2.tgz#29a957f3a63973883eb684f10ffd3d151fec01a3" integrity sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg== @@ -1737,7 +1737,7 @@ minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" -minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.8: +minimist@^1.2.0, minimist@^1.2.5: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== diff --git a/platform/wab/package.json b/platform/wab/package.json index c270cfb1d..038424df0 100644 --- a/platform/wab/package.json +++ b/platform/wab/package.json @@ -168,6 +168,7 @@ "pegjs": "~0.10.0", "pegjs-coffee-plugin": "~0.3.0", "prando": "^6.0.1", + "raw-loader": "^4.0.2", "storybook": "^7.6.20", "sucrase": "^3.35.0", "ts-node": "^10.9.2", diff --git a/platform/wab/rsbuild.config.ts b/platform/wab/rsbuild.config.ts index 8980a457a..23bd974f4 100644 --- a/platform/wab/rsbuild.config.ts +++ b/platform/wab/rsbuild.config.ts @@ -233,6 +233,8 @@ export default defineConfig({ SENTRY_ORG_ID: OPTIONAL_VAR, SENTRY_PROJECT_ID: OPTIONAL_VAR, STRIPE_PUBLISHABLE_KEY: OPTIONAL_VAR, + + REACT_APP_DEFAULT_HOST_URL: REQUIRED_VAR, }) ), new MonacoWebpackPlugin(), diff --git a/platform/wab/yarn.lock b/platform/wab/yarn.lock index 7edc10e0c..1ebde0931 100644 --- a/platform/wab/yarn.lock +++ b/platform/wab/yarn.lock @@ -15580,7 +15580,7 @@ loader-utils@^1.1.0: emojis-list "^3.0.0" json5 "^1.0.1" -loader-utils@^2.0.2, loader-utils@^2.0.4: +loader-utils@^2.0.0, loader-utils@^2.0.2, loader-utils@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c" integrity sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw== @@ -18256,6 +18256,14 @@ raw-body@2.5.2: iconv-lite "0.4.24" unpipe "1.0.0" +raw-loader@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-4.0.2.tgz#1aac6b7d1ad1501e66efdac1522c73e59a584eb6" + integrity sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA== + dependencies: + loader-utils "^2.0.0" + schema-utils "^3.0.0" + rc-align@^4.0.0: version "4.0.9" resolved "https://registry.yarnpkg.com/rc-align/-/rc-align-4.0.9.tgz#46d8801c4a139ff6a65ad1674e8efceac98f85f2" @@ -20961,16 +20969,7 @@ string-length@^5.0.1: char-regex "^2.0.0" strip-ansi "^7.0.1" -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -21020,7 +21019,7 @@ stringify-object@^3.2.1: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -21034,13 +21033,6 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2" @@ -22709,7 +22701,7 @@ workerpool@^6.1.4: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.1.4.tgz#6a972b6df82e38d50248ee2820aa98e2d0ad3090" integrity sha512-jGWPzsUqzkow8HoAvqaPWTUPCrlPJaJ5tY8Iz7n1uCz3tTp6s3CDG0FF1NsX42WNlkRSW6Mr+CDZGnNoSsKa7g== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -22736,15 +22728,6 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" diff --git a/terraform/.gitignore b/terraform/.gitignore new file mode 100644 index 000000000..801e3048a --- /dev/null +++ b/terraform/.gitignore @@ -0,0 +1,40 @@ +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log +crash.*.log + +# Exclude all plan files +*.tfplan +plans/*.tfplan + +# Ignore override files +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Ignore CLI configuration files +.terraformrc +terraform.rc + +# Ignore log files +*.log +*tunnel.log + +# Generated files +.generated/* + +# Environment-specific config files (contain sensitive values) +# Copy from .tfvars.example and fill in your values +**/config/*.tfvars +**/config/*-backend.tfvars +*.tfvars.bak + +# Keep example files for documentation +!**/config/*.tfvars.example diff --git a/terraform/generate-configs.sh b/terraform/generate-configs.sh new file mode 100755 index 000000000..76474ab58 --- /dev/null +++ b/terraform/generate-configs.sh @@ -0,0 +1,65 @@ +#!/bin/bash +# Generate .tfvars files from .example files using environment variables +# +# Usage: +# Local: Export variables then run ./generate-configs.sh +# CI: Variables are already exported by GitHub Actions + +set -e + +# Required environment variables +REQUIRED_VARS=( + "AWS_ACCOUNT_ID" + "TERRAFORM_STATE_BUCKET" + "TERRAFORM_LOCKS_TABLE" +) + +# Optional variables with defaults +: "${ALB_DNS_NAME:=placeholder-alb.us-east-2.elb.amazonaws.com}" +: "${CLOUDFRONT_DISTRIBUTION_URL:=placeholder.cloudfront.net}" +: "${INTERNAL_DOMAIN:=example.com}" +: "${LOADER_ASSETS_BUCKET:=placeholder-bucket}" +: "${DB_USERNAME:=plasmicadmin}" + +# Check required variables +missing=() +for var in "${REQUIRED_VARS[@]}"; do + if [ -z "${!var}" ]; then + missing+=("$var") + fi +done + +if [ ${#missing[@]} -gt 0 ]; then + echo "โŒ Error: Missing required environment variables:" + printf ' - %s\n' "${missing[@]}" + echo "" + echo "Example usage:" + echo " export AWS_ACCOUNT_ID=123456789012" + echo " export TERRAFORM_STATE_BUCKET=my-terraform-state" + echo " export TERRAFORM_LOCKS_TABLE=my-terraform-locks" + echo " ./generate-configs.sh" + exit 1 +fi + +echo "Generating config files from examples..." +echo " AWS Account: ${AWS_ACCOUNT_ID}" +echo " Region: us-east-2" +echo "" + +count=0 +find projects -name "*.tfvars.example" -type f | while read -r example_file; do + output_file="${example_file%.example}" + + sed -e "s||${AWS_ACCOUNT_ID}|g" \ + -e "s||${TERRAFORM_STATE_BUCKET}|g" \ + -e "s||${TERRAFORM_LOCKS_TABLE}|g" \ + -e "s||${ALB_DNS_NAME}|g" \ + -e "s||${CLOUDFRONT_DISTRIBUTION_URL}|g" \ + -e "s||${INTERNAL_DOMAIN}|g" \ + -e "s||${LOADER_ASSETS_BUCKET}|g" \ + -e "s||${DB_USERNAME}|g" \ + "$example_file" > "$output_file" + + echo "Generated: $output_file" + count=$((count + 1)) +done diff --git a/terraform/modules/backend-service/alb.tf b/terraform/modules/backend-service/alb.tf new file mode 100644 index 000000000..c822746c1 --- /dev/null +++ b/terraform/modules/backend-service/alb.tf @@ -0,0 +1,58 @@ +# Target Group +resource "aws_lb_target_group" "service" { + name = "plasmic-${var.environment}-${var.service_name}-tg" + port = var.container_port + protocol = "HTTP" + vpc_id = var.vpc_id + target_type = "ip" + + health_check { + enabled = true + healthy_threshold = 2 + unhealthy_threshold = 3 + timeout = 5 + interval = 30 + path = var.health_check_path + protocol = "HTTP" + matcher = "200" + } + + deregistration_delay = 30 + + tags = { + Name = "plasmic-${var.environment}-${var.service_name}-tg" + } +} + +# Listener Rule +resource "aws_lb_listener_rule" "service" { + listener_arn = var.alb_listener_arn + priority = var.listener_rule_priority + + action { + type = "forward" + target_group_arn = aws_lb_target_group.service.arn + } + + dynamic "condition" { + for_each = var.host_header != null ? [1] : [] + content { + host_header { + values = [var.host_header] + } + } + } + + dynamic "condition" { + for_each = var.path_pattern != null ? [1] : [] + content { + path_pattern { + values = [var.path_pattern] + } + } + } + + tags = { + Name = "plasmic-${var.environment}-${var.service_name}-rule" + } +} diff --git a/terraform/modules/backend-service/iam.tf b/terraform/modules/backend-service/iam.tf new file mode 100644 index 000000000..291715d92 --- /dev/null +++ b/terraform/modules/backend-service/iam.tf @@ -0,0 +1,38 @@ +# Task Role +resource "aws_iam_role" "task" { + count = var.create_task_role ? 1 : 0 + + name = "plasmic-${var.environment}-${var.service_name}-task-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "ecs-tasks.amazonaws.com" + } + }] + }) + + tags = { + Name = "plasmic-${var.environment}-${var.service_name}-task-role" + } +} + +# Attach managed policies to task role +resource "aws_iam_role_policy_attachment" "task_policies" { + count = var.create_task_role ? length(var.task_role_policies) : 0 + + role = aws_iam_role.task[0].name + policy_arn = var.task_role_policies[count.index] +} + +# Attach inline policies to task role +resource "aws_iam_role_policy" "task_inline" { + count = var.create_task_role ? length(var.task_role_inline_policies) : 0 + + name = var.task_role_inline_policies[count.index].name + role = aws_iam_role.task[0].id + policy = var.task_role_inline_policies[count.index].policy +} diff --git a/terraform/modules/backend-service/main.tf b/terraform/modules/backend-service/main.tf new file mode 100644 index 000000000..f8b17498c --- /dev/null +++ b/terraform/modules/backend-service/main.tf @@ -0,0 +1,120 @@ +locals { + full_service_name = "plasmic-${var.environment}-${var.service_name}" +} + +# CloudWatch Log Group +resource "aws_cloudwatch_log_group" "service" { + name = "/ecs/${local.full_service_name}" + retention_in_days = var.log_retention_days + + tags = { + Name = "${local.full_service_name}-logs" + } +} + +# ECS Task Definition +resource "aws_ecs_task_definition" "service" { + family = local.full_service_name + network_mode = "awsvpc" + requires_compatibilities = ["FARGATE"] + cpu = var.cpu + memory = var.memory + execution_role_arn = var.execution_role_arn + task_role_arn = var.create_task_role ? aws_iam_role.task[0].arn : null + + # Use ARM64 (Graviton) for ~20% cost savings + runtime_platform { + operating_system_family = "LINUX" + cpu_architecture = "ARM64" + } + + container_definitions = jsonencode([{ + name = var.service_name + image = var.container_image + essential = true + command = var.container_command + + portMappings = [{ + containerPort = var.container_port + protocol = "tcp" + }] + + environment = [ + for key, value in var.environment_variables : { + name = key + value = value + } + ] + + secrets = var.secrets + + logConfiguration = { + logDriver = "awslogs" + options = { + "awslogs-group" = aws_cloudwatch_log_group.service.name + "awslogs-region" = var.aws_region + "awslogs-stream-prefix" = "ecs" + } + } + + healthCheck = { + command = ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:${var.container_port}${var.health_check_path} || exit 1"] + interval = 30 + timeout = 5 + retries = 3 + startPeriod = 60 + } + }]) + + tags = { + Name = "${local.full_service_name}-task" + } +} + +# ECS Service +resource "aws_ecs_service" "service" { + name = local.full_service_name + cluster = var.cluster_id + task_definition = aws_ecs_task_definition.service.arn + desired_count = var.desired_count + launch_type = "FARGATE" + + network_configuration { + security_groups = [var.ecs_security_group_id] + subnets = var.private_subnet_ids + assign_public_ip = var.assign_public_ip + } + + load_balancer { + target_group_arn = aws_lb_target_group.service.arn + container_name = var.service_name + container_port = var.container_port + } + + health_check_grace_period_seconds = 60 + + deployment_maximum_percent = 200 + deployment_minimum_healthy_percent = 100 + + # Circuit breaker - automatically rollback failed deployments + deployment_circuit_breaker { + enable = var.enable_circuit_breaker + rollback = var.enable_circuit_breaker + } + + enable_ecs_managed_tags = true + propagate_tags = "SERVICE" + + lifecycle { + ignore_changes = [desired_count] + } + + depends_on = [ + aws_lb_listener_rule.service + ] + + tags = { + Name = "${local.full_service_name}-service" + } +} + diff --git a/terraform/modules/backend-service/outputs.tf b/terraform/modules/backend-service/outputs.tf new file mode 100644 index 000000000..21a75a951 --- /dev/null +++ b/terraform/modules/backend-service/outputs.tf @@ -0,0 +1,34 @@ +output "service_name" { + description = "ECS service name" + value = aws_ecs_service.service.name +} + +output "service_arn" { + description = "ECS service ARN" + value = aws_ecs_service.service.id +} + +output "task_definition_arn" { + description = "Task definition ARN" + value = aws_ecs_task_definition.service.arn +} + +output "task_role_arn" { + description = "Task role ARN" + value = var.create_task_role ? aws_iam_role.task[0].arn : null +} + +output "task_role_name" { + description = "Task role name" + value = var.create_task_role ? aws_iam_role.task[0].name : null +} + +output "target_group_arn" { + description = "Target group ARN" + value = aws_lb_target_group.service.arn +} + +output "cloudwatch_log_group" { + description = "CloudWatch log group name" + value = aws_cloudwatch_log_group.service.name +} diff --git a/terraform/modules/backend-service/variables.tf b/terraform/modules/backend-service/variables.tf new file mode 100644 index 000000000..6c7f40032 --- /dev/null +++ b/terraform/modules/backend-service/variables.tf @@ -0,0 +1,173 @@ +variable "environment" { + type = string + description = "Environment name" +} + +variable "aws_region" { + type = string + description = "AWS region" +} + +variable "service_name" { + type = string + description = "Name of the backend service (e.g., 'codegen', 'data', 'copilot')" +} + +variable "container_image" { + type = string + description = "Docker image URL" +} + +variable "container_port" { + type = number + description = "Container port" +} + +variable "container_command" { + type = list(string) + description = "Command to run in container" +} + +variable "cpu" { + type = number + description = "Task CPU units" +} + +variable "memory" { + type = number + description = "Task memory in MB" +} + +variable "desired_count" { + type = number + description = "Desired number of tasks" + default = 1 +} + +variable "health_check_path" { + type = string + default = "/healthcheck" + description = "Health check endpoint" +} + +variable "enable_circuit_breaker" { + type = bool + default = false + description = "Enable deployment circuit breaker" +} + +# Environment variables +variable "environment_variables" { + type = map(string) + description = "Environment variables for the container" + default = {} +} + +# Secrets +variable "secrets" { + type = list(object({ + name = string + valueFrom = string + })) + description = "Secrets from Secrets Manager" + default = [] +} + +# Networking +variable "cluster_id" { + type = string + description = "ECS cluster ID" +} + +variable "cluster_name" { + type = string + description = "ECS cluster name" +} + +variable "vpc_id" { + type = string + description = "VPC ID" +} + +variable "private_subnet_ids" { + type = list(string) + description = "Private subnet IDs for ECS tasks" +} + +variable "alb_arn" { + type = string + description = "Application Load Balancer ARN" +} + +variable "alb_listener_arn" { + type = string + description = "ALB Listener ARN" +} + +variable "alb_security_group_id" { + type = string + description = "ALB security group ID" +} + +variable "ecs_security_group_id" { + type = string + description = "ECS tasks security group ID" +} + +# IAM +variable "execution_role_arn" { + type = string + description = "ECS task execution role ARN" +} + +variable "create_task_role" { + type = bool + default = true + description = "Create a task role for this service" +} + +variable "task_role_policies" { + type = list(string) + description = "Additional IAM policy ARNs to attach to task role" + default = [] +} + +variable "task_role_inline_policies" { + type = list(object({ + name = string + policy = string + })) + description = "Inline IAM policies for task role" + default = [] +} + +# Listener rules +variable "host_header" { + type = string + description = "Host header for ALB listener rule" + default = null +} + +variable "path_pattern" { + type = string + description = "Path pattern for ALB listener rule" + default = null +} + +variable "listener_rule_priority" { + type = number + description = "Priority for ALB listener rule" + default = null +} + +variable "assign_public_ip" { + type = bool + default = false + description = "Assign public IP to tasks" +} + +variable "log_retention_days" { + type = number + default = 7 + description = "CloudWatch Logs retention in days" +} diff --git a/terraform/projects/database/.terraform.lock.hcl b/terraform/projects/database/.terraform.lock.hcl new file mode 100644 index 000000000..cdc1668d4 --- /dev/null +++ b/terraform/projects/database/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.100.0" + constraints = "~> 5.0" + hashes = [ + "h1:Ijt7pOlB7Tr7maGQIqtsLFbl7pSMIj06TVdkoSBcYOw=", + "zh:054b8dd49f0549c9a7cc27d159e45327b7b65cf404da5e5a20da154b90b8a644", + "zh:0b97bf8d5e03d15d83cc40b0530a1f84b459354939ba6f135a0086c20ebbe6b2", + "zh:1589a2266af699cbd5d80737a0fe02e54ec9cf2ca54e7e00ac51c7359056f274", + "zh:6330766f1d85f01ae6ea90d1b214b8b74cc8c1badc4696b165b36ddd4cc15f7b", + "zh:7c8c2e30d8e55291b86fcb64bdf6c25489d538688545eb48fd74ad622e5d3862", + "zh:99b1003bd9bd32ee323544da897148f46a527f622dc3971af63ea3e251596342", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:9f8b909d3ec50ade83c8062290378b1ec553edef6a447c56dadc01a99f4eaa93", + "zh:aaef921ff9aabaf8b1869a86d692ebd24fbd4e12c21205034bb679b9caf883a2", + "zh:ac882313207aba00dd5a76dbd572a0ddc818bb9cbf5c9d61b28fe30efaec951e", + "zh:bb64e8aff37becab373a1a0cc1080990785304141af42ed6aa3dd4913b000421", + "zh:dfe495f6621df5540d9c92ad40b8067376350b005c637ea6efac5dc15028add4", + "zh:f0ddf0eaf052766cfe09dea8200a946519f653c384ab4336e2a4a64fdd6310e9", + "zh:f1b7e684f4c7ae1eed272b6de7d2049bb87a0275cb04dbb7cda6636f600699c9", + "zh:ff461571e3f233699bf690db319dfe46aec75e58726636a0d97dd9ac6e32fb70", + ] +} diff --git a/terraform/projects/database/config/integration-backend.tfvars.example b/terraform/projects/database/config/integration-backend.tfvars.example new file mode 100644 index 000000000..bb73ca13e --- /dev/null +++ b/terraform/projects/database/config/integration-backend.tfvars.example @@ -0,0 +1,4 @@ +bucket = "" +key = "integration/database/terraform.tfstate" +dynamodb_table = "" +region = "us-east-2" diff --git a/terraform/projects/database/config/integration.tfvars.example b/terraform/projects/database/config/integration.tfvars.example new file mode 100644 index 000000000..9d6524638 --- /dev/null +++ b/terraform/projects/database/config/integration.tfvars.example @@ -0,0 +1,13 @@ +environment = "integration" +aws_region = "us-east-2" + +db_instance_class = "db.t3.small" +db_allocated_storage = 20 +db_max_allocated_storage = 0 + +db_name = "" +db_username = "" + +multi_az = false +backup_retention_period = 1 +skip_final_snapshot = true # For dev, no final snapshot needed diff --git a/terraform/projects/database/main.tf b/terraform/projects/database/main.tf new file mode 100644 index 000000000..7ac10b86f --- /dev/null +++ b/terraform/projects/database/main.tf @@ -0,0 +1,67 @@ +locals { + identifier = "plasmic-${var.environment}-db" +} + +# Get database password from Secrets Manager +data "aws_secretsmanager_secret" "db_password" { + name = "plasmic/${var.environment}/db/master-password" +} + +data "aws_secretsmanager_secret_version" "db_password" { + secret_id = data.aws_secretsmanager_secret.db_password.id +} + +# Security group for RDS +resource "aws_security_group" "rds" { + name_prefix = "${local.identifier}-" + description = "Security group for RDS database" + vpc_id = local.vpc_id + + lifecycle { + create_before_destroy = true + } + + tags = { + Name = "${local.identifier}-sg" + } +} + +# Ingress rule - will be added by ECS service +# (ECS service will add ingress rule to allow its security group) + +# Egress not needed for RDS + +# RDS Instance +resource "aws_db_instance" "main" { + identifier = local.identifier + + engine = "postgres" + engine_version = "15" + instance_class = var.db_instance_class + allocated_storage = var.db_allocated_storage + max_allocated_storage = var.db_max_allocated_storage + storage_type = "gp3" + storage_encrypted = false # Match existing unencrypted database + + db_name = var.db_name != "" ? var.db_name : null # Handle empty string + username = var.db_username + password = data.aws_secretsmanager_secret_version.db_password.secret_string + + multi_az = var.multi_az + db_subnet_group_name = local.database_subnet_group_name + vpc_security_group_ids = [aws_security_group.rds.id] + publicly_accessible = false + + backup_retention_period = var.backup_retention_period + backup_window = "03:00-04:00" # UTC + maintenance_window = "mon:04:00-mon:05:00" # UTC + + skip_final_snapshot = var.skip_final_snapshot + final_snapshot_identifier = var.skip_final_snapshot ? null : "${local.identifier}-final-${formatdate("YYYY-MM-DD-hhmm", timestamp())}" + + deletion_protection = var.environment == "prod" ? true : false + + tags = { + Name = local.identifier + } +} diff --git a/terraform/projects/database/outputs.tf b/terraform/projects/database/outputs.tf new file mode 100644 index 000000000..c4095d873 --- /dev/null +++ b/terraform/projects/database/outputs.tf @@ -0,0 +1,35 @@ +output "db_endpoint" { + description = "Database endpoint" + value = aws_db_instance.main.endpoint +} + +output "db_address" { + description = "Database address (without port)" + value = aws_db_instance.main.address +} + +output "db_port" { + description = "Database port" + value = aws_db_instance.main.port +} + +output "db_name" { + description = "Database name" + value = aws_db_instance.main.db_name +} + +output "db_username" { + description = "Database master username" + value = aws_db_instance.main.username + sensitive = true +} + +output "db_security_group_id" { + description = "Database security group ID" + value = aws_security_group.rds.id +} + +output "db_instance_id" { + description = "RDS instance ID" + value = aws_db_instance.main.id +} diff --git a/terraform/projects/database/providers.tf b/terraform/projects/database/providers.tf new file mode 100644 index 000000000..c0ab085c5 --- /dev/null +++ b/terraform/projects/database/providers.tf @@ -0,0 +1,24 @@ +terraform { + backend "s3" {} + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } + required_version = ">= 1.9" +} + +provider "aws" { + region = var.aws_region + + default_tags { + tags = { + Project = "plasmic" + Environment = var.environment + ManagedBy = "terraform" + Component = "database" + } + } +} diff --git a/terraform/projects/database/remote-state.tf b/terraform/projects/database/remote-state.tf new file mode 100644 index 000000000..49b0a9400 --- /dev/null +++ b/terraform/projects/database/remote-state.tf @@ -0,0 +1,15 @@ +data "terraform_remote_state" "vpc" { + backend = "s3" + config = { + bucket = "plasmic-terraform-state-${var.environment}-${var.aws_region}" + key = "${var.environment}/vpc/terraform.tfstate" + region = var.aws_region + } +} + +locals { + vpc_id = data.terraform_remote_state.vpc.outputs.vpc_id + database_subnet_ids = data.terraform_remote_state.vpc.outputs.database_subnet_ids + database_subnet_group_name = data.terraform_remote_state.vpc.outputs.database_subnet_group_name + vpc_cidr_block = data.terraform_remote_state.vpc.outputs.vpc_cidr_block +} diff --git a/terraform/projects/database/vars.tf b/terraform/projects/database/vars.tf new file mode 100644 index 000000000..747443bfc --- /dev/null +++ b/terraform/projects/database/vars.tf @@ -0,0 +1,53 @@ +variable "environment" { + type = string + description = "Environment name" +} + +variable "aws_region" { + type = string + description = "AWS region" + default = "us-east-1" +} + +variable "db_instance_class" { + type = string + description = "RDS instance class" +} + +variable "db_allocated_storage" { + type = number + description = "Allocated storage in GB" +} + +variable "db_max_allocated_storage" { + type = number + description = "Maximum storage for autoscaling" +} + +variable "db_name" { + type = string + description = "Database name" + default = "wab" +} + +variable "db_username" { + type = string + description = "Master username" + default = "postgres" +} + +variable "multi_az" { + type = bool + description = "Enable multi-AZ" +} + +variable "backup_retention_period" { + type = number + description = "Backup retention in days" +} + +variable "skip_final_snapshot" { + type = bool + description = "Skip final snapshot on destroy" + default = false +} diff --git a/terraform/projects/dynamodb/.terraform.lock.hcl b/terraform/projects/dynamodb/.terraform.lock.hcl new file mode 100644 index 000000000..cdc1668d4 --- /dev/null +++ b/terraform/projects/dynamodb/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.100.0" + constraints = "~> 5.0" + hashes = [ + "h1:Ijt7pOlB7Tr7maGQIqtsLFbl7pSMIj06TVdkoSBcYOw=", + "zh:054b8dd49f0549c9a7cc27d159e45327b7b65cf404da5e5a20da154b90b8a644", + "zh:0b97bf8d5e03d15d83cc40b0530a1f84b459354939ba6f135a0086c20ebbe6b2", + "zh:1589a2266af699cbd5d80737a0fe02e54ec9cf2ca54e7e00ac51c7359056f274", + "zh:6330766f1d85f01ae6ea90d1b214b8b74cc8c1badc4696b165b36ddd4cc15f7b", + "zh:7c8c2e30d8e55291b86fcb64bdf6c25489d538688545eb48fd74ad622e5d3862", + "zh:99b1003bd9bd32ee323544da897148f46a527f622dc3971af63ea3e251596342", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:9f8b909d3ec50ade83c8062290378b1ec553edef6a447c56dadc01a99f4eaa93", + "zh:aaef921ff9aabaf8b1869a86d692ebd24fbd4e12c21205034bb679b9caf883a2", + "zh:ac882313207aba00dd5a76dbd572a0ddc818bb9cbf5c9d61b28fe30efaec951e", + "zh:bb64e8aff37becab373a1a0cc1080990785304141af42ed6aa3dd4913b000421", + "zh:dfe495f6621df5540d9c92ad40b8067376350b005c637ea6efac5dc15028add4", + "zh:f0ddf0eaf052766cfe09dea8200a946519f653c384ab4336e2a4a64fdd6310e9", + "zh:f1b7e684f4c7ae1eed272b6de7d2049bb87a0275cb04dbb7cda6636f600699c9", + "zh:ff461571e3f233699bf690db319dfe46aec75e58726636a0d97dd9ac6e32fb70", + ] +} diff --git a/terraform/projects/dynamodb/config/integration-backend.tfvars.example b/terraform/projects/dynamodb/config/integration-backend.tfvars.example new file mode 100644 index 000000000..13f7fd916 --- /dev/null +++ b/terraform/projects/dynamodb/config/integration-backend.tfvars.example @@ -0,0 +1,4 @@ +bucket = "" +key = "integration/dynamodb/terraform.tfstate" +dynamodb_table = "" +region = "us-east-2" diff --git a/terraform/projects/dynamodb/config/integration.tfvars.example b/terraform/projects/dynamodb/config/integration.tfvars.example new file mode 100644 index 000000000..ae60e243f --- /dev/null +++ b/terraform/projects/dynamodb/config/integration.tfvars.example @@ -0,0 +1,5 @@ +environment = "integration" +aws_region = "us-east-2" + +# Disable point-in-time recovery for dev to save costs +enable_point_in_time_recovery = false diff --git a/terraform/projects/dynamodb/main.tf b/terraform/projects/dynamodb/main.tf new file mode 100644 index 000000000..8fb8cdf0a --- /dev/null +++ b/terraform/projects/dynamodb/main.tf @@ -0,0 +1,26 @@ +# DynamoDB table for copilot data +resource "aws_dynamodb_table" "copilot_data" { + name = "plasmic-${var.environment}-copilot-data" + billing_mode = "PAY_PER_REQUEST" + hash_key = "pk" + range_key = "sk" + + attribute { + name = "pk" + type = "S" + } + + attribute { + name = "sk" + type = "S" + } + + point_in_time_recovery { + enabled = var.enable_point_in_time_recovery + } + + tags = { + Name = "plasmic-${var.environment}-copilot-data" + Environment = var.environment + } +} diff --git a/terraform/projects/dynamodb/outputs.tf b/terraform/projects/dynamodb/outputs.tf new file mode 100644 index 000000000..55577c782 --- /dev/null +++ b/terraform/projects/dynamodb/outputs.tf @@ -0,0 +1,14 @@ +output "table_name" { + description = "Name of the DynamoDB table" + value = aws_dynamodb_table.copilot_data.name +} + +output "table_arn" { + description = "ARN of the DynamoDB table" + value = aws_dynamodb_table.copilot_data.arn +} + +output "table_id" { + description = "ID of the DynamoDB table" + value = aws_dynamodb_table.copilot_data.id +} diff --git a/terraform/projects/dynamodb/terraform.tf b/terraform/projects/dynamodb/terraform.tf new file mode 100644 index 000000000..ad48e55c8 --- /dev/null +++ b/terraform/projects/dynamodb/terraform.tf @@ -0,0 +1,24 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } + + backend "s3" {} +} + +provider "aws" { + region = var.aws_region + + default_tags { + tags = { + Project = "plasmic" + Environment = var.environment + ManagedBy = "terraform" + } + } +} diff --git a/terraform/projects/dynamodb/variables.tf b/terraform/projects/dynamodb/variables.tf new file mode 100644 index 000000000..d386bf01c --- /dev/null +++ b/terraform/projects/dynamodb/variables.tf @@ -0,0 +1,16 @@ +variable "environment" { + type = string + description = "Environment name" +} + +variable "aws_region" { + type = string + description = "AWS region" + default = "us-west-2" +} + +variable "enable_point_in_time_recovery" { + type = bool + description = "Enable point-in-time recovery" + default = false +} diff --git a/terraform/projects/ecr/.terraform.lock.hcl b/terraform/projects/ecr/.terraform.lock.hcl new file mode 100644 index 000000000..cdc1668d4 --- /dev/null +++ b/terraform/projects/ecr/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.100.0" + constraints = "~> 5.0" + hashes = [ + "h1:Ijt7pOlB7Tr7maGQIqtsLFbl7pSMIj06TVdkoSBcYOw=", + "zh:054b8dd49f0549c9a7cc27d159e45327b7b65cf404da5e5a20da154b90b8a644", + "zh:0b97bf8d5e03d15d83cc40b0530a1f84b459354939ba6f135a0086c20ebbe6b2", + "zh:1589a2266af699cbd5d80737a0fe02e54ec9cf2ca54e7e00ac51c7359056f274", + "zh:6330766f1d85f01ae6ea90d1b214b8b74cc8c1badc4696b165b36ddd4cc15f7b", + "zh:7c8c2e30d8e55291b86fcb64bdf6c25489d538688545eb48fd74ad622e5d3862", + "zh:99b1003bd9bd32ee323544da897148f46a527f622dc3971af63ea3e251596342", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:9f8b909d3ec50ade83c8062290378b1ec553edef6a447c56dadc01a99f4eaa93", + "zh:aaef921ff9aabaf8b1869a86d692ebd24fbd4e12c21205034bb679b9caf883a2", + "zh:ac882313207aba00dd5a76dbd572a0ddc818bb9cbf5c9d61b28fe30efaec951e", + "zh:bb64e8aff37becab373a1a0cc1080990785304141af42ed6aa3dd4913b000421", + "zh:dfe495f6621df5540d9c92ad40b8067376350b005c637ea6efac5dc15028add4", + "zh:f0ddf0eaf052766cfe09dea8200a946519f653c384ab4336e2a4a64fdd6310e9", + "zh:f1b7e684f4c7ae1eed272b6de7d2049bb87a0275cb04dbb7cda6636f600699c9", + "zh:ff461571e3f233699bf690db319dfe46aec75e58726636a0d97dd9ac6e32fb70", + ] +} diff --git a/terraform/projects/ecr/config/shared-backend.tfvars.example b/terraform/projects/ecr/config/shared-backend.tfvars.example new file mode 100644 index 000000000..dd6145492 --- /dev/null +++ b/terraform/projects/ecr/config/shared-backend.tfvars.example @@ -0,0 +1,4 @@ +bucket = "" +key = "integration/ecr/terraform.tfstate" +dynamodb_table = "" +region = "us-east-2" diff --git a/terraform/projects/ecr/config/shared.tfvars.example b/terraform/projects/ecr/config/shared.tfvars.example new file mode 100644 index 000000000..ed868b092 --- /dev/null +++ b/terraform/projects/ecr/config/shared.tfvars.example @@ -0,0 +1,5 @@ +# ECR is shared across all environments in same account +environment = "shared" +aws_region = "us-east-2" + +image_retention_count = 30 diff --git a/terraform/projects/ecr/main.tf b/terraform/projects/ecr/main.tf new file mode 100644 index 000000000..fd4cc85ec --- /dev/null +++ b/terraform/projects/ecr/main.tf @@ -0,0 +1,41 @@ +locals { + repository_name = "plasmic/wab" +} + +resource "aws_ecr_repository" "wab" { + name = local.repository_name + image_tag_mutability = "MUTABLE" + + image_scanning_configuration { + scan_on_push = true + } + + encryption_configuration { + encryption_type = "AES256" + } + + tags = { + Name = local.repository_name + } +} + +resource "aws_ecr_lifecycle_policy" "wab" { + repository = aws_ecr_repository.wab.name + + policy = jsonencode({ + rules = [ + { + rulePriority = 1 + description = "Keep last ${var.image_retention_count} images" + selection = { + tagStatus = "any" + countType = "imageCountMoreThan" + countNumber = var.image_retention_count + } + action = { + type = "expire" + } + } + ] + }) +} diff --git a/terraform/projects/ecr/outputs.tf b/terraform/projects/ecr/outputs.tf new file mode 100644 index 000000000..e1a713b68 --- /dev/null +++ b/terraform/projects/ecr/outputs.tf @@ -0,0 +1,14 @@ +output "repository_url" { + description = "ECR repository URL" + value = aws_ecr_repository.wab.repository_url +} + +output "repository_arn" { + description = "ECR repository ARN" + value = aws_ecr_repository.wab.arn +} + +output "repository_name" { + description = "ECR repository name" + value = aws_ecr_repository.wab.name +} diff --git a/terraform/projects/ecr/providers.tf b/terraform/projects/ecr/providers.tf new file mode 100644 index 000000000..f44b0af53 --- /dev/null +++ b/terraform/projects/ecr/providers.tf @@ -0,0 +1,24 @@ +terraform { + backend "s3" {} + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } + required_version = ">= 1.9" +} + +provider "aws" { + region = var.aws_region + + default_tags { + tags = { + Project = "plasmic" + Environment = var.environment + ManagedBy = "terraform" + Component = "ecr" + } + } +} diff --git a/terraform/projects/ecr/vars.tf b/terraform/projects/ecr/vars.tf new file mode 100644 index 000000000..6fc4500fb --- /dev/null +++ b/terraform/projects/ecr/vars.tf @@ -0,0 +1,16 @@ +variable "environment" { + type = string + description = "Environment name (dev, staging, prod)" +} + +variable "aws_region" { + type = string + description = "AWS region" + default = "us-east-1" +} + +variable "image_retention_count" { + type = number + description = "Number of images to retain" + default = 30 +} diff --git a/terraform/projects/ecs-cluster/.terraform.lock.hcl b/terraform/projects/ecs-cluster/.terraform.lock.hcl new file mode 100644 index 000000000..4d33e8f86 --- /dev/null +++ b/terraform/projects/ecs-cluster/.terraform.lock.hcl @@ -0,0 +1,44 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.100.0" + constraints = "~> 5.0" + hashes = [ + "h1:Ijt7pOlB7Tr7maGQIqtsLFbl7pSMIj06TVdkoSBcYOw=", + "zh:054b8dd49f0549c9a7cc27d159e45327b7b65cf404da5e5a20da154b90b8a644", + "zh:0b97bf8d5e03d15d83cc40b0530a1f84b459354939ba6f135a0086c20ebbe6b2", + "zh:1589a2266af699cbd5d80737a0fe02e54ec9cf2ca54e7e00ac51c7359056f274", + "zh:6330766f1d85f01ae6ea90d1b214b8b74cc8c1badc4696b165b36ddd4cc15f7b", + "zh:7c8c2e30d8e55291b86fcb64bdf6c25489d538688545eb48fd74ad622e5d3862", + "zh:99b1003bd9bd32ee323544da897148f46a527f622dc3971af63ea3e251596342", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:9f8b909d3ec50ade83c8062290378b1ec553edef6a447c56dadc01a99f4eaa93", + "zh:aaef921ff9aabaf8b1869a86d692ebd24fbd4e12c21205034bb679b9caf883a2", + "zh:ac882313207aba00dd5a76dbd572a0ddc818bb9cbf5c9d61b28fe30efaec951e", + "zh:bb64e8aff37becab373a1a0cc1080990785304141af42ed6aa3dd4913b000421", + "zh:dfe495f6621df5540d9c92ad40b8067376350b005c637ea6efac5dc15028add4", + "zh:f0ddf0eaf052766cfe09dea8200a946519f653c384ab4336e2a4a64fdd6310e9", + "zh:f1b7e684f4c7ae1eed272b6de7d2049bb87a0275cb04dbb7cda6636f600699c9", + "zh:ff461571e3f233699bf690db319dfe46aec75e58726636a0d97dd9ac6e32fb70", + ] +} + +provider "registry.terraform.io/hashicorp/tls" { + version = "4.1.0" + hashes = [ + "h1:zEv9tY1KR5vaLSyp2lkrucNJ+Vq3c+sTFK9GyQGLtFs=", + "zh:14c35d89307988c835a7f8e26f1b83ce771e5f9b41e407f86a644c0152089ac2", + "zh:2fb9fe7a8b5afdbd3e903acb6776ef1be3f2e587fb236a8c60f11a9fa165faa8", + "zh:35808142ef850c0c60dd93dc06b95c747720ed2c40c89031781165f0c2baa2fc", + "zh:35b5dc95bc75f0b3b9c5ce54d4d7600c1ebc96fbb8dfca174536e8bf103c8cdc", + "zh:38aa27c6a6c98f1712aa5cc30011884dc4b128b4073a4a27883374bfa3ec9fac", + "zh:51fb247e3a2e88f0047cb97bb9df7c228254a3b3021c5534e4563b4007e6f882", + "zh:62b981ce491e38d892ba6364d1d0cdaadcee37cc218590e07b310b1dfa34be2d", + "zh:bc8e47efc611924a79f947ce072a9ad698f311d4a60d0b4dfff6758c912b7298", + "zh:c149508bd131765d1bc085c75a870abb314ff5a6d7f5ac1035a8892d686b6297", + "zh:d38d40783503d278b63858978d40e07ac48123a2925e1a6b47e62179c046f87a", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + "zh:fb07f708e3316615f6d218cec198504984c0ce7000b9f1eebff7516e384f4b54", + ] +} diff --git a/terraform/projects/ecs-cluster/alb.tf b/terraform/projects/ecs-cluster/alb.tf new file mode 100644 index 000000000..2e4ce6054 --- /dev/null +++ b/terraform/projects/ecs-cluster/alb.tf @@ -0,0 +1,89 @@ +# Application Load Balancer (shared by all services) +resource "aws_lb" "main" { + name_prefix = substr(var.environment, 0, 6) + internal = false + load_balancer_type = "application" + security_groups = [aws_security_group.alb.id] + subnets = local.public_subnet_ids + + enable_deletion_protection = var.environment == "prod" + enable_http2 = true + + tags = { + Name = "${local.cluster_name}-alb" + } +} + +# HTTP Listener with default 404 response +# Services will add their own listener rules +resource "aws_lb_listener" "http" { + load_balancer_arn = aws_lb.main.arn + port = "80" + protocol = "HTTP" + + default_action { + type = "fixed-response" + + fixed_response { + content_type = "text/plain" + message_body = "Service not found" + status_code = "404" + } + } +} + +# Self-signed certificate for HTTPS listener (integration environment) +resource "tls_private_key" "alb" { + algorithm = "RSA" + rsa_bits = 2048 +} + +resource "tls_self_signed_cert" "alb" { + private_key_pem = tls_private_key.alb.private_key_pem + + subject { + common_name = aws_lb.main.dns_name + organization = "Plasmic ${var.environment}" + } + + validity_period_hours = 87600 # 10 years + + allowed_uses = [ + "key_encipherment", + "digital_signature", + "server_auth", + ] +} + +resource "aws_acm_certificate" "alb" { + private_key = tls_private_key.alb.private_key_pem + certificate_body = tls_self_signed_cert.alb.cert_pem + + lifecycle { + create_before_destroy = true + } + + tags = { + Name = "${local.cluster_name}-alb-cert" + } +} + +# HTTPS Listener with default 404 response +# Services will add their own listener rules +resource "aws_lb_listener" "https" { + load_balancer_arn = aws_lb.main.arn + port = "443" + protocol = "HTTPS" + ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06" + certificate_arn = aws_acm_certificate.alb.arn + + default_action { + type = "fixed-response" + + fixed_response { + content_type = "text/plain" + message_body = "Service not found" + status_code = "404" + } + } +} diff --git a/terraform/projects/ecs-cluster/config/integration-backend.tfvars.example b/terraform/projects/ecs-cluster/config/integration-backend.tfvars.example new file mode 100644 index 000000000..1f5dc447e --- /dev/null +++ b/terraform/projects/ecs-cluster/config/integration-backend.tfvars.example @@ -0,0 +1,4 @@ +bucket = "" +key = "integration/ecs-cluster/terraform.tfstate" +dynamodb_table = "" +region = "us-east-2" diff --git a/terraform/projects/ecs-cluster/config/integration.tfvars.example b/terraform/projects/ecs-cluster/config/integration.tfvars.example new file mode 100644 index 000000000..b4def5bbd --- /dev/null +++ b/terraform/projects/ecs-cluster/config/integration.tfvars.example @@ -0,0 +1,4 @@ +environment = "integration" +aws_region = "us-east-2" + +enable_container_insights = true diff --git a/terraform/projects/ecs-cluster/database-uri-secret.tf b/terraform/projects/ecs-cluster/database-uri-secret.tf new file mode 100644 index 000000000..9bb7d190e --- /dev/null +++ b/terraform/projects/ecs-cluster/database-uri-secret.tf @@ -0,0 +1,15 @@ +# Get DB password from Secrets Manager +data "aws_secretsmanager_secret_version" "db_password" { + secret_id = "plasmic/${var.environment}/db/master-password" +} + +# Get the database-uri secret (created by secrets project) +data "aws_secretsmanager_secret" "database_uri" { + name = "plasmic/${var.environment}/app/database-uri" +} + +# Populate the database-uri secret with the full connection string +resource "aws_secretsmanager_secret_version" "database_uri" { + secret_id = data.aws_secretsmanager_secret.database_uri.id + secret_string = "postgresql://${local.db_username}:${data.aws_secretsmanager_secret_version.db_password.secret_string}@${local.db_address}:${local.db_port}/${local.db_name}?sslmode=no-verify" +} \ No newline at end of file diff --git a/terraform/projects/ecs-cluster/iam.tf b/terraform/projects/ecs-cluster/iam.tf new file mode 100644 index 000000000..8fb7847cf --- /dev/null +++ b/terraform/projects/ecs-cluster/iam.tf @@ -0,0 +1,45 @@ +# ECS Task Execution Role (shared by all services) +# This role is used by ECS to pull images and read secrets +resource "aws_iam_role" "ecs_task_execution" { + name_prefix = "${local.cluster_name}-exec-" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Effect = "Allow" + Principal = { + Service = "ecs-tasks.amazonaws.com" + } + Action = "sts:AssumeRole" + }] + }) + + tags = { + Name = "${local.cluster_name}-task-execution-role" + } +} + +resource "aws_iam_role_policy_attachment" "ecs_task_execution" { + role = aws_iam_role.ecs_task_execution.name + policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" +} + +# Allow reading all secrets for this environment +resource "aws_iam_role_policy" "ecs_task_execution_secrets" { + name_prefix = "secrets-" + role = aws_iam_role.ecs_task_execution.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "secretsmanager:GetSecretValue", + "secretsmanager:DescribeSecret" + ] + Resource = "arn:aws:secretsmanager:${var.aws_region}:*:secret:plasmic/${var.environment}/*" + } + ] + }) +} diff --git a/terraform/projects/ecs-cluster/main.tf b/terraform/projects/ecs-cluster/main.tf new file mode 100644 index 000000000..d9dd030eb --- /dev/null +++ b/terraform/projects/ecs-cluster/main.tf @@ -0,0 +1,38 @@ +locals { + cluster_name = "plasmic-${var.environment}" +} + +resource "aws_ecs_cluster" "main" { + name = local.cluster_name + + setting { + name = "containerInsights" + value = var.enable_container_insights ? "enabled" : "disabled" + } + + tags = { + Name = local.cluster_name + } +} + +resource "aws_ecs_cluster_capacity_providers" "main" { + cluster_name = aws_ecs_cluster.main.name + + capacity_providers = ["FARGATE", "FARGATE_SPOT"] + + default_capacity_provider_strategy { + capacity_provider = "FARGATE" + weight = 1 + base = 1 + } +} + +# CloudWatch Log Group for cluster +resource "aws_cloudwatch_log_group" "ecs" { + name = "/ecs/${local.cluster_name}" + retention_in_days = var.environment == "prod" ? 30 : 7 + + tags = { + Name = "${local.cluster_name}-logs" + } +} diff --git a/terraform/projects/ecs-cluster/outputs.tf b/terraform/projects/ecs-cluster/outputs.tf new file mode 100644 index 000000000..b5244076b --- /dev/null +++ b/terraform/projects/ecs-cluster/outputs.tf @@ -0,0 +1,55 @@ +output "cluster_id" { + description = "ECS cluster ID" + value = aws_ecs_cluster.main.id +} + +output "cluster_arn" { + description = "ECS cluster ARN" + value = aws_ecs_cluster.main.arn +} + +output "cluster_name" { + description = "ECS cluster name" + value = aws_ecs_cluster.main.name +} + +output "log_group_name" { + description = "CloudWatch log group name" + value = aws_cloudwatch_log_group.ecs.name +} + +# Shared infrastructure outputs for services +output "alb_arn" { + description = "ALB ARN" + value = aws_lb.main.arn +} + +output "alb_dns_name" { + description = "ALB DNS name" + value = aws_lb.main.dns_name +} + +output "alb_listener_arn" { + description = "ALB HTTP listener ARN (deprecated, use alb_https_listener_arn)" + value = aws_lb_listener.http.arn +} + +output "alb_https_listener_arn" { + description = "ALB HTTPS listener ARN" + value = aws_lb_listener.https.arn +} + +output "alb_security_group_id" { + description = "ALB security group ID" + value = aws_security_group.alb.id +} + +output "ecs_security_group_id" { + description = "ECS tasks security group ID" + value = aws_security_group.ecs_tasks.id +} + +output "execution_role_arn" { + description = "ECS task execution role ARN" + value = aws_iam_role.ecs_task_execution.arn +} diff --git a/terraform/projects/ecs-cluster/providers.tf b/terraform/projects/ecs-cluster/providers.tf new file mode 100644 index 000000000..72234c523 --- /dev/null +++ b/terraform/projects/ecs-cluster/providers.tf @@ -0,0 +1,24 @@ +terraform { + backend "s3" {} + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } + required_version = ">= 1.9" +} + +provider "aws" { + region = var.aws_region + + default_tags { + tags = { + Project = "plasmic" + Environment = var.environment + ManagedBy = "terraform" + Component = "ecs-cluster" + } + } +} diff --git a/terraform/projects/ecs-cluster/remote-state.tf b/terraform/projects/ecs-cluster/remote-state.tf new file mode 100644 index 000000000..c4e44d6dc --- /dev/null +++ b/terraform/projects/ecs-cluster/remote-state.tf @@ -0,0 +1,31 @@ +data "terraform_remote_state" "vpc" { + backend = "s3" + config = { + bucket = "plasmic-terraform-state-${var.environment}-${var.aws_region}" + key = "${var.environment}/vpc/terraform.tfstate" + region = var.aws_region + } +} + +data "terraform_remote_state" "database" { + backend = "s3" + config = { + bucket = "plasmic-terraform-state-${var.environment}-${var.aws_region}" + key = "${var.environment}/database/terraform.tfstate" + region = var.aws_region + } +} + +locals { + # VPC + vpc_id = data.terraform_remote_state.vpc.outputs.vpc_id + public_subnet_ids = data.terraform_remote_state.vpc.outputs.public_subnet_ids + private_subnet_ids = data.terraform_remote_state.vpc.outputs.private_subnet_ids + + # Database + db_address = data.terraform_remote_state.database.outputs.db_address + db_port = data.terraform_remote_state.database.outputs.db_port + db_name = data.terraform_remote_state.database.outputs.db_name + db_username = data.terraform_remote_state.database.outputs.db_username + db_security_group_id = data.terraform_remote_state.database.outputs.db_security_group_id +} diff --git a/terraform/projects/ecs-cluster/security-groups.tf b/terraform/projects/ecs-cluster/security-groups.tf new file mode 100644 index 000000000..a31f3dfc9 --- /dev/null +++ b/terraform/projects/ecs-cluster/security-groups.tf @@ -0,0 +1,89 @@ +# ALB Security Group +resource "aws_security_group" "alb" { + name_prefix = "${local.cluster_name}-alb-" + description = "Security group for Application Load Balancer" + vpc_id = local.vpc_id + + lifecycle { + create_before_destroy = true + } + + tags = { + Name = "${local.cluster_name}-alb-sg" + } +} + +resource "aws_vpc_security_group_ingress_rule" "alb_http" { + security_group_id = aws_security_group.alb.id + description = "HTTP from internet" + from_port = 80 + to_port = 80 + ip_protocol = "tcp" + cidr_ipv4 = "0.0.0.0/0" +} + +resource "aws_vpc_security_group_ingress_rule" "alb_https" { + security_group_id = aws_security_group.alb.id + description = "HTTPS from internet" + from_port = 443 + to_port = 443 + ip_protocol = "tcp" + cidr_ipv4 = "0.0.0.0/0" +} + +resource "aws_vpc_security_group_egress_rule" "alb_to_ecs" { + security_group_id = aws_security_group.alb.id + description = "To ECS tasks" + ip_protocol = "-1" + referenced_security_group_id = aws_security_group.ecs_tasks.id +} + +# ECS Tasks Security Group (shared by all services) +resource "aws_security_group" "ecs_tasks" { + name_prefix = "${local.cluster_name}-ecs-" + description = "Security group for ECS tasks" + vpc_id = local.vpc_id + + lifecycle { + create_before_destroy = true + } + + tags = { + Name = "${local.cluster_name}-ecs-sg" + } +} + +resource "aws_vpc_security_group_ingress_rule" "ecs_from_alb" { + security_group_id = aws_security_group.ecs_tasks.id + description = "From ALB" + ip_protocol = "-1" + referenced_security_group_id = aws_security_group.alb.id +} + +resource "aws_vpc_security_group_egress_rule" "ecs_to_rds" { + security_group_id = aws_security_group.ecs_tasks.id + description = "To RDS" + from_port = local.db_port + to_port = local.db_port + ip_protocol = "tcp" + referenced_security_group_id = local.db_security_group_id +} + +resource "aws_vpc_security_group_egress_rule" "ecs_to_internet" { + security_group_id = aws_security_group.ecs_tasks.id + description = "To internet (HTTPS)" + from_port = 443 + to_port = 443 + ip_protocol = "tcp" + cidr_ipv4 = "0.0.0.0/0" +} + +# Allow ECS to reach RDS (add rule to RDS security group) +resource "aws_vpc_security_group_ingress_rule" "rds_from_ecs" { + security_group_id = local.db_security_group_id + description = "From ECS tasks" + from_port = local.db_port + to_port = local.db_port + ip_protocol = "tcp" + referenced_security_group_id = aws_security_group.ecs_tasks.id +} diff --git a/terraform/projects/ecs-cluster/vars.tf b/terraform/projects/ecs-cluster/vars.tf new file mode 100644 index 000000000..6485d627e --- /dev/null +++ b/terraform/projects/ecs-cluster/vars.tf @@ -0,0 +1,16 @@ +variable "environment" { + type = string + description = "Environment name" +} + +variable "aws_region" { + type = string + description = "AWS region" + default = "us-east-1" +} + +variable "enable_container_insights" { + type = bool + description = "Enable CloudWatch Container Insights" + default = true +} diff --git a/terraform/projects/frontend/.terraform.lock.hcl b/terraform/projects/frontend/.terraform.lock.hcl new file mode 100644 index 000000000..cdc1668d4 --- /dev/null +++ b/terraform/projects/frontend/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.100.0" + constraints = "~> 5.0" + hashes = [ + "h1:Ijt7pOlB7Tr7maGQIqtsLFbl7pSMIj06TVdkoSBcYOw=", + "zh:054b8dd49f0549c9a7cc27d159e45327b7b65cf404da5e5a20da154b90b8a644", + "zh:0b97bf8d5e03d15d83cc40b0530a1f84b459354939ba6f135a0086c20ebbe6b2", + "zh:1589a2266af699cbd5d80737a0fe02e54ec9cf2ca54e7e00ac51c7359056f274", + "zh:6330766f1d85f01ae6ea90d1b214b8b74cc8c1badc4696b165b36ddd4cc15f7b", + "zh:7c8c2e30d8e55291b86fcb64bdf6c25489d538688545eb48fd74ad622e5d3862", + "zh:99b1003bd9bd32ee323544da897148f46a527f622dc3971af63ea3e251596342", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:9f8b909d3ec50ade83c8062290378b1ec553edef6a447c56dadc01a99f4eaa93", + "zh:aaef921ff9aabaf8b1869a86d692ebd24fbd4e12c21205034bb679b9caf883a2", + "zh:ac882313207aba00dd5a76dbd572a0ddc818bb9cbf5c9d61b28fe30efaec951e", + "zh:bb64e8aff37becab373a1a0cc1080990785304141af42ed6aa3dd4913b000421", + "zh:dfe495f6621df5540d9c92ad40b8067376350b005c637ea6efac5dc15028add4", + "zh:f0ddf0eaf052766cfe09dea8200a946519f653c384ab4336e2a4a64fdd6310e9", + "zh:f1b7e684f4c7ae1eed272b6de7d2049bb87a0275cb04dbb7cda6636f600699c9", + "zh:ff461571e3f233699bf690db319dfe46aec75e58726636a0d97dd9ac6e32fb70", + ] +} diff --git a/terraform/projects/frontend/config/integration-backend.tfvars.example b/terraform/projects/frontend/config/integration-backend.tfvars.example new file mode 100644 index 000000000..9ee9a9baf --- /dev/null +++ b/terraform/projects/frontend/config/integration-backend.tfvars.example @@ -0,0 +1,4 @@ +bucket = "" +key = "integration/frontend/terraform.tfstate" +region = "us-east-2" +dynamodb_table = "" \ No newline at end of file diff --git a/terraform/projects/frontend/config/integration.tfvars.example b/terraform/projects/frontend/config/integration.tfvars.example new file mode 100644 index 000000000..9593bd5a6 --- /dev/null +++ b/terraform/projects/frontend/config/integration.tfvars.example @@ -0,0 +1,2 @@ +environment = "integration" +aws_region = "us-east-2" \ No newline at end of file diff --git a/terraform/projects/frontend/main.tf b/terraform/projects/frontend/main.tf new file mode 100644 index 000000000..59544128e --- /dev/null +++ b/terraform/projects/frontend/main.tf @@ -0,0 +1,232 @@ +# Frontend S3 bucket for hosting React app +resource "aws_s3_bucket" "frontend" { + bucket = "plasmic-frontend-${var.environment}" + + tags = { + Name = "plasmic-frontend-${var.environment}" + Environment = var.environment + } +} + +resource "aws_s3_bucket_public_access_block" "frontend" { + bucket = aws_s3_bucket.frontend.id + + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} + +# CloudFront Origin Access Identity +resource "aws_cloudfront_origin_access_identity" "frontend" { + comment = "OAI for Plasmic frontend ${var.environment}" +} + +# S3 bucket policy to allow CloudFront access +resource "aws_s3_bucket_policy" "frontend" { + bucket = aws_s3_bucket.frontend.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "AllowCloudFrontAccess" + Effect = "Allow" + Principal = { + AWS = aws_cloudfront_origin_access_identity.frontend.iam_arn + } + Action = "s3:GetObject" + Resource = "${aws_s3_bucket.frontend.arn}/*" + } + ] + }) +} + +# Get ALB DNS from ECS cluster remote state +data "terraform_remote_state" "ecs_cluster" { + backend = "s3" + config = { + bucket = "plasmic-terraform-state-${var.environment}-${var.aws_region}" + key = "${var.environment}/ecs-cluster/terraform.tfstate" + region = var.aws_region + } +} + +locals { + alb_dns_name = data.terraform_remote_state.ecs_cluster.outputs.alb_dns_name +} + +# CloudFront distribution for frontend +resource "aws_cloudfront_distribution" "frontend" { + enabled = true + is_ipv6_enabled = true + comment = "Plasmic frontend ${var.environment}" + default_root_object = "index.html" + price_class = "PriceClass_100" + + # S3 origin for static frontend assets + origin { + domain_name = aws_s3_bucket.frontend.bucket_regional_domain_name + origin_id = "S3-plasmic-frontend" + + s3_origin_config { + origin_access_identity = aws_cloudfront_origin_access_identity.frontend.cloudfront_access_identity_path + } + } + + # ALB origin for backend API + origin { + domain_name = local.alb_dns_name + origin_id = "ALB-plasmic-backend" + + custom_origin_config { + http_port = 80 + https_port = 443 + origin_protocol_policy = "https-only" + origin_ssl_protocols = ["TLSv1.2"] + } + } + + # Default behavior - serve static files from S3 + default_cache_behavior { + allowed_methods = ["GET", "HEAD", "OPTIONS"] + cached_methods = ["GET", "HEAD"] + target_origin_id = "S3-plasmic-frontend" + + forwarded_values { + query_string = false + cookies { + forward = "none" + } + } + + viewer_protocol_policy = "redirect-to-https" + min_ttl = 0 + default_ttl = 3600 + max_ttl = 86400 + compress = true + } + + # API routes - forward to backend ALB (no caching) + ordered_cache_behavior { + path_pattern = "/api/*" + allowed_methods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"] + cached_methods = ["GET", "HEAD"] + target_origin_id = "ALB-plasmic-backend" + + cache_policy_id = "4135ea2d-6df8-44a3-9df3-4b5a84be39ad" # Managed-CachingDisabled + origin_request_policy_id = "33f36d7e-f396-46d9-90e0-52428a34d9dc" # Managed-AllViewerAndCloudFrontHeaders-2022-06 + + viewer_protocol_policy = "redirect-to-https" + compress = false + } + restrictions { + geo_restriction { + restriction_type = "none" + } + } + + viewer_certificate { + cloudfront_default_certificate = true + } + + tags = { + Name = "plasmic-frontend-${var.environment}" + Environment = var.environment + } +} + +# Host S3 bucket for host.html and static files +resource "aws_s3_bucket" "host" { + bucket = "plasmic-host-static-${var.environment}" + + tags = { + Name = "plasmic-host-static-${var.environment}" + Environment = var.environment + } +} + +resource "aws_s3_bucket_public_access_block" "host" { + bucket = aws_s3_bucket.host.id + + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} + +# CloudFront Origin Access Identity for host +resource "aws_cloudfront_origin_access_identity" "host" { + comment = "OAI for Plasmic host static ${var.environment}" +} + +# S3 bucket policy for host +resource "aws_s3_bucket_policy" "host" { + bucket = aws_s3_bucket.host.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "AllowCloudFrontAccess" + Effect = "Allow" + Principal = { + AWS = aws_cloudfront_origin_access_identity.host.iam_arn + } + Action = "s3:GetObject" + Resource = "${aws_s3_bucket.host.arn}/*" + } + ] + }) +} + +# CloudFront distribution for host files +resource "aws_cloudfront_distribution" "host" { + enabled = true + is_ipv6_enabled = true + comment = "Plasmic host static ${var.environment}" + price_class = "PriceClass_100" + + origin { + domain_name = aws_s3_bucket.host.bucket_regional_domain_name + origin_id = "S3-plasmic-host" + + s3_origin_config { + origin_access_identity = aws_cloudfront_origin_access_identity.host.cloudfront_access_identity_path + } + } + + default_cache_behavior { + allowed_methods = ["GET", "HEAD", "OPTIONS"] + cached_methods = ["GET", "HEAD"] + target_origin_id = "S3-plasmic-host" + + forwarded_values { + query_string = false + cookies { + forward = "none" + } + } + + viewer_protocol_policy = "redirect-to-https" + min_ttl = 0 + default_ttl = 300 + max_ttl = 31536000 + compress = true + } + + restrictions { + geo_restriction { + restriction_type = "none" + } + } + + viewer_certificate { + cloudfront_default_certificate = true + } + + tags = { + Name = "plasmic-host-static-${var.environment}" + Environment = var.environment + } +} \ No newline at end of file diff --git a/terraform/projects/frontend/outputs.tf b/terraform/projects/frontend/outputs.tf new file mode 100644 index 000000000..f1906d527 --- /dev/null +++ b/terraform/projects/frontend/outputs.tf @@ -0,0 +1,39 @@ +output "frontend_bucket_name" { + description = "Frontend S3 bucket name" + value = aws_s3_bucket.frontend.id +} + +output "frontend_cloudfront_distribution_id" { + description = "Frontend CloudFront distribution ID" + value = aws_cloudfront_distribution.frontend.id +} + +output "frontend_cloudfront_domain" { + description = "Frontend CloudFront domain name" + value = aws_cloudfront_distribution.frontend.domain_name +} + +output "frontend_url" { + description = "Frontend URL" + value = "https://${aws_cloudfront_distribution.frontend.domain_name}" +} + +output "host_bucket_name" { + description = "Host static files S3 bucket name" + value = aws_s3_bucket.host.id +} + +output "host_cloudfront_distribution_id" { + description = "Host CloudFront distribution ID" + value = aws_cloudfront_distribution.host.id +} + +output "host_cloudfront_domain" { + description = "Host CloudFront domain name" + value = aws_cloudfront_distribution.host.domain_name +} + +output "host_url" { + description = "Host static files URL" + value = "https://${aws_cloudfront_distribution.host.domain_name}/static/host.html" +} \ No newline at end of file diff --git a/terraform/projects/frontend/providers.tf b/terraform/projects/frontend/providers.tf new file mode 100644 index 000000000..5c1b0b4dc --- /dev/null +++ b/terraform/projects/frontend/providers.tf @@ -0,0 +1,26 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } + + backend "s3" { + # Configuration provided via backend-config file + } +} + +provider "aws" { + region = var.aws_region + + default_tags { + tags = { + Project = "plasmic" + Environment = var.environment + ManagedBy = "terraform" + } + } +} \ No newline at end of file diff --git a/terraform/projects/frontend/variables.tf b/terraform/projects/frontend/variables.tf new file mode 100644 index 000000000..a7eff7a4c --- /dev/null +++ b/terraform/projects/frontend/variables.tf @@ -0,0 +1,9 @@ +variable "environment" { + type = string + description = "Environment name" +} + +variable "aws_region" { + type = string + description = "AWS region" +} \ No newline at end of file diff --git a/terraform/projects/s3-assets/.terraform.lock.hcl b/terraform/projects/s3-assets/.terraform.lock.hcl new file mode 100644 index 000000000..cdc1668d4 --- /dev/null +++ b/terraform/projects/s3-assets/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.100.0" + constraints = "~> 5.0" + hashes = [ + "h1:Ijt7pOlB7Tr7maGQIqtsLFbl7pSMIj06TVdkoSBcYOw=", + "zh:054b8dd49f0549c9a7cc27d159e45327b7b65cf404da5e5a20da154b90b8a644", + "zh:0b97bf8d5e03d15d83cc40b0530a1f84b459354939ba6f135a0086c20ebbe6b2", + "zh:1589a2266af699cbd5d80737a0fe02e54ec9cf2ca54e7e00ac51c7359056f274", + "zh:6330766f1d85f01ae6ea90d1b214b8b74cc8c1badc4696b165b36ddd4cc15f7b", + "zh:7c8c2e30d8e55291b86fcb64bdf6c25489d538688545eb48fd74ad622e5d3862", + "zh:99b1003bd9bd32ee323544da897148f46a527f622dc3971af63ea3e251596342", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:9f8b909d3ec50ade83c8062290378b1ec553edef6a447c56dadc01a99f4eaa93", + "zh:aaef921ff9aabaf8b1869a86d692ebd24fbd4e12c21205034bb679b9caf883a2", + "zh:ac882313207aba00dd5a76dbd572a0ddc818bb9cbf5c9d61b28fe30efaec951e", + "zh:bb64e8aff37becab373a1a0cc1080990785304141af42ed6aa3dd4913b000421", + "zh:dfe495f6621df5540d9c92ad40b8067376350b005c637ea6efac5dc15028add4", + "zh:f0ddf0eaf052766cfe09dea8200a946519f653c384ab4336e2a4a64fdd6310e9", + "zh:f1b7e684f4c7ae1eed272b6de7d2049bb87a0275cb04dbb7cda6636f600699c9", + "zh:ff461571e3f233699bf690db319dfe46aec75e58726636a0d97dd9ac6e32fb70", + ] +} diff --git a/terraform/projects/s3-assets/config/integration-backend.tfvars.example b/terraform/projects/s3-assets/config/integration-backend.tfvars.example new file mode 100644 index 000000000..3200a3e5e --- /dev/null +++ b/terraform/projects/s3-assets/config/integration-backend.tfvars.example @@ -0,0 +1,4 @@ +bucket = "" +key = "integration/s3-assets/terraform.tfstate" +dynamodb_table = "" +region = "us-east-2" diff --git a/terraform/projects/s3-assets/config/integration.tfvars.example b/terraform/projects/s3-assets/config/integration.tfvars.example new file mode 100644 index 000000000..fb9cd5194 --- /dev/null +++ b/terraform/projects/s3-assets/config/integration.tfvars.example @@ -0,0 +1,9 @@ +environment = "integration" +aws_region = "us-east-2" + +# CORS configuration for loader access +allowed_origins = [ + "https://", + "https://*.", + "https://*.plasmic.app" +] diff --git a/terraform/projects/s3-assets/main.tf b/terraform/projects/s3-assets/main.tf new file mode 100644 index 000000000..8aba1f39a --- /dev/null +++ b/terraform/projects/s3-assets/main.tf @@ -0,0 +1,58 @@ +# S3 bucket for loader assets +resource "aws_s3_bucket" "loader_assets" { + bucket = "plasmic-elastic-path-loader-assets-${var.environment}" + + tags = { + Name = "plasmic-elastic-path-loader-assets-${var.environment}" + Environment = var.environment + } +} + +# Block public access +resource "aws_s3_bucket_public_access_block" "loader_assets" { + bucket = aws_s3_bucket.loader_assets.id + + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} + +# CORS configuration for web access +resource "aws_s3_bucket_cors_configuration" "loader_assets" { + bucket = aws_s3_bucket.loader_assets.id + + cors_rule { + allowed_headers = ["*"] + allowed_methods = ["GET", "HEAD"] + allowed_origins = var.allowed_origins + expose_headers = ["ETag"] + max_age_seconds = 3000 + } +} + +# Bucket policy for SSL-only +resource "aws_s3_bucket_policy" "loader_assets" { + bucket = aws_s3_bucket.loader_assets.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "AllowSSLRequestsOnly" + Effect = "Deny" + Principal = "*" + Action = "s3:*" + Resource = [ + aws_s3_bucket.loader_assets.arn, + "${aws_s3_bucket.loader_assets.arn}/*" + ] + Condition = { + Bool = { + "aws:SecureTransport" = "false" + } + } + } + ] + }) +} diff --git a/terraform/projects/s3-assets/outputs.tf b/terraform/projects/s3-assets/outputs.tf new file mode 100644 index 000000000..7e4821a68 --- /dev/null +++ b/terraform/projects/s3-assets/outputs.tf @@ -0,0 +1,9 @@ +output "bucket_name" { + description = "Name of the S3 bucket" + value = aws_s3_bucket.loader_assets.id +} + +output "bucket_arn" { + description = "ARN of the S3 bucket" + value = aws_s3_bucket.loader_assets.arn +} diff --git a/terraform/projects/s3-assets/terraform.tf b/terraform/projects/s3-assets/terraform.tf new file mode 100644 index 000000000..ad48e55c8 --- /dev/null +++ b/terraform/projects/s3-assets/terraform.tf @@ -0,0 +1,24 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } + + backend "s3" {} +} + +provider "aws" { + region = var.aws_region + + default_tags { + tags = { + Project = "plasmic" + Environment = var.environment + ManagedBy = "terraform" + } + } +} diff --git a/terraform/projects/s3-assets/variables.tf b/terraform/projects/s3-assets/variables.tf new file mode 100644 index 000000000..872288dd9 --- /dev/null +++ b/terraform/projects/s3-assets/variables.tf @@ -0,0 +1,16 @@ +variable "environment" { + type = string + description = "Environment name" +} + +variable "aws_region" { + type = string + description = "AWS region" + default = "us-east-1" +} + +variable "allowed_origins" { + type = list(string) + description = "List of allowed CORS origins" + default = ["*"] +} diff --git a/terraform/projects/s3-clips/.terraform.lock.hcl b/terraform/projects/s3-clips/.terraform.lock.hcl new file mode 100644 index 000000000..cdc1668d4 --- /dev/null +++ b/terraform/projects/s3-clips/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.100.0" + constraints = "~> 5.0" + hashes = [ + "h1:Ijt7pOlB7Tr7maGQIqtsLFbl7pSMIj06TVdkoSBcYOw=", + "zh:054b8dd49f0549c9a7cc27d159e45327b7b65cf404da5e5a20da154b90b8a644", + "zh:0b97bf8d5e03d15d83cc40b0530a1f84b459354939ba6f135a0086c20ebbe6b2", + "zh:1589a2266af699cbd5d80737a0fe02e54ec9cf2ca54e7e00ac51c7359056f274", + "zh:6330766f1d85f01ae6ea90d1b214b8b74cc8c1badc4696b165b36ddd4cc15f7b", + "zh:7c8c2e30d8e55291b86fcb64bdf6c25489d538688545eb48fd74ad622e5d3862", + "zh:99b1003bd9bd32ee323544da897148f46a527f622dc3971af63ea3e251596342", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:9f8b909d3ec50ade83c8062290378b1ec553edef6a447c56dadc01a99f4eaa93", + "zh:aaef921ff9aabaf8b1869a86d692ebd24fbd4e12c21205034bb679b9caf883a2", + "zh:ac882313207aba00dd5a76dbd572a0ddc818bb9cbf5c9d61b28fe30efaec951e", + "zh:bb64e8aff37becab373a1a0cc1080990785304141af42ed6aa3dd4913b000421", + "zh:dfe495f6621df5540d9c92ad40b8067376350b005c637ea6efac5dc15028add4", + "zh:f0ddf0eaf052766cfe09dea8200a946519f653c384ab4336e2a4a64fdd6310e9", + "zh:f1b7e684f4c7ae1eed272b6de7d2049bb87a0275cb04dbb7cda6636f600699c9", + "zh:ff461571e3f233699bf690db319dfe46aec75e58726636a0d97dd9ac6e32fb70", + ] +} diff --git a/terraform/projects/s3-clips/config/integration-backend.tfvars.example b/terraform/projects/s3-clips/config/integration-backend.tfvars.example new file mode 100644 index 000000000..85a154d3b --- /dev/null +++ b/terraform/projects/s3-clips/config/integration-backend.tfvars.example @@ -0,0 +1,4 @@ +bucket = "" +key = "integration/s3-clips/terraform.tfstate" +dynamodb_table = "" +region = "us-east-2" diff --git a/terraform/projects/s3-clips/config/integration.tfvars.example b/terraform/projects/s3-clips/config/integration.tfvars.example new file mode 100644 index 000000000..f26997ac2 --- /dev/null +++ b/terraform/projects/s3-clips/config/integration.tfvars.example @@ -0,0 +1,2 @@ +environment = "integration" +aws_region = "us-east-2" diff --git a/terraform/projects/s3-clips/main.tf b/terraform/projects/s3-clips/main.tf new file mode 100644 index 000000000..3f68b3254 --- /dev/null +++ b/terraform/projects/s3-clips/main.tf @@ -0,0 +1,45 @@ +# S3 bucket for clipboard data +resource "aws_s3_bucket" "clips" { + bucket = "plasmic-elastic-path-clips-${var.environment}" + + tags = { + Name = "plasmic-elastic-path-clips-${var.environment}" + Environment = var.environment + } +} + +# Block all public access +resource "aws_s3_bucket_public_access_block" "clips" { + bucket = aws_s3_bucket.clips.id + + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} + +# Bucket policy for SSL-only +resource "aws_s3_bucket_policy" "clips" { + bucket = aws_s3_bucket.clips.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "AllowSSLRequestsOnly" + Effect = "Deny" + Principal = "*" + Action = "s3:*" + Resource = [ + aws_s3_bucket.clips.arn, + "${aws_s3_bucket.clips.arn}/*" + ] + Condition = { + Bool = { + "aws:SecureTransport" = "false" + } + } + } + ] + }) +} diff --git a/terraform/projects/s3-clips/outputs.tf b/terraform/projects/s3-clips/outputs.tf new file mode 100644 index 000000000..1e3749275 --- /dev/null +++ b/terraform/projects/s3-clips/outputs.tf @@ -0,0 +1,9 @@ +output "bucket_name" { + description = "Name of the S3 bucket" + value = aws_s3_bucket.clips.id +} + +output "bucket_arn" { + description = "ARN of the S3 bucket" + value = aws_s3_bucket.clips.arn +} diff --git a/terraform/projects/s3-clips/terraform.tf b/terraform/projects/s3-clips/terraform.tf new file mode 100644 index 000000000..ad48e55c8 --- /dev/null +++ b/terraform/projects/s3-clips/terraform.tf @@ -0,0 +1,24 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } + + backend "s3" {} +} + +provider "aws" { + region = var.aws_region + + default_tags { + tags = { + Project = "plasmic" + Environment = var.environment + ManagedBy = "terraform" + } + } +} diff --git a/terraform/projects/s3-clips/variables.tf b/terraform/projects/s3-clips/variables.tf new file mode 100644 index 000000000..c593305f0 --- /dev/null +++ b/terraform/projects/s3-clips/variables.tf @@ -0,0 +1,10 @@ +variable "environment" { + type = string + description = "Environment name" +} + +variable "aws_region" { + type = string + description = "AWS region" + default = "us-east-1" +} diff --git a/terraform/projects/s3-site-assets/.terraform.lock.hcl b/terraform/projects/s3-site-assets/.terraform.lock.hcl new file mode 100644 index 000000000..cdc1668d4 --- /dev/null +++ b/terraform/projects/s3-site-assets/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.100.0" + constraints = "~> 5.0" + hashes = [ + "h1:Ijt7pOlB7Tr7maGQIqtsLFbl7pSMIj06TVdkoSBcYOw=", + "zh:054b8dd49f0549c9a7cc27d159e45327b7b65cf404da5e5a20da154b90b8a644", + "zh:0b97bf8d5e03d15d83cc40b0530a1f84b459354939ba6f135a0086c20ebbe6b2", + "zh:1589a2266af699cbd5d80737a0fe02e54ec9cf2ca54e7e00ac51c7359056f274", + "zh:6330766f1d85f01ae6ea90d1b214b8b74cc8c1badc4696b165b36ddd4cc15f7b", + "zh:7c8c2e30d8e55291b86fcb64bdf6c25489d538688545eb48fd74ad622e5d3862", + "zh:99b1003bd9bd32ee323544da897148f46a527f622dc3971af63ea3e251596342", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:9f8b909d3ec50ade83c8062290378b1ec553edef6a447c56dadc01a99f4eaa93", + "zh:aaef921ff9aabaf8b1869a86d692ebd24fbd4e12c21205034bb679b9caf883a2", + "zh:ac882313207aba00dd5a76dbd572a0ddc818bb9cbf5c9d61b28fe30efaec951e", + "zh:bb64e8aff37becab373a1a0cc1080990785304141af42ed6aa3dd4913b000421", + "zh:dfe495f6621df5540d9c92ad40b8067376350b005c637ea6efac5dc15028add4", + "zh:f0ddf0eaf052766cfe09dea8200a946519f653c384ab4336e2a4a64fdd6310e9", + "zh:f1b7e684f4c7ae1eed272b6de7d2049bb87a0275cb04dbb7cda6636f600699c9", + "zh:ff461571e3f233699bf690db319dfe46aec75e58726636a0d97dd9ac6e32fb70", + ] +} diff --git a/terraform/projects/s3-site-assets/config/integration-backend.tfvars.example b/terraform/projects/s3-site-assets/config/integration-backend.tfvars.example new file mode 100644 index 000000000..581352f70 --- /dev/null +++ b/terraform/projects/s3-site-assets/config/integration-backend.tfvars.example @@ -0,0 +1,4 @@ +bucket = "" +key = "integration/s3-site-assets/terraform.tfstate" +dynamodb_table = "" +region = "us-east-2" diff --git a/terraform/projects/s3-site-assets/config/integration.tfvars.example b/terraform/projects/s3-site-assets/config/integration.tfvars.example new file mode 100644 index 000000000..5cc4f8656 --- /dev/null +++ b/terraform/projects/s3-site-assets/config/integration.tfvars.example @@ -0,0 +1,10 @@ +environment = "integration" +aws_region = "us-east-2" + +# CORS configuration for uploads from Plasmic Studio +allowed_origins = [ + "https://", + "https://*.", + "https://*.plasmic.app", + "http://localhost:3003" +] diff --git a/terraform/projects/s3-site-assets/main.tf b/terraform/projects/s3-site-assets/main.tf new file mode 100644 index 000000000..d7742861e --- /dev/null +++ b/terraform/projects/s3-site-assets/main.tf @@ -0,0 +1,65 @@ +# S3 bucket for site assets (images, files, etc.) +resource "aws_s3_bucket" "site_assets" { + bucket = "plasmic-elastic-path-site-assets-${var.environment}" + + tags = { + Name = "plasmic-elastic-path-site-assets-${var.environment}" + Environment = var.environment + } +} + +# Public access configuration - allow public read for CDN/CloudFront +resource "aws_s3_bucket_public_access_block" "site_assets" { + bucket = aws_s3_bucket.site_assets.id + + block_public_acls = false + block_public_policy = false + ignore_public_acls = false + restrict_public_buckets = false +} + +# CORS configuration for web uploads +resource "aws_s3_bucket_cors_configuration" "site_assets" { + bucket = aws_s3_bucket.site_assets.id + + cors_rule { + allowed_headers = ["*"] + allowed_methods = ["GET", "HEAD", "PUT", "POST"] + allowed_origins = var.allowed_origins + expose_headers = ["ETag"] + max_age_seconds = 3000 + } +} + +# Bucket policy for public read and SSL-only +resource "aws_s3_bucket_policy" "site_assets" { + bucket = aws_s3_bucket.site_assets.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "PublicReadGetObject" + Effect = "Allow" + Principal = "*" + Action = "s3:GetObject" + Resource = "${aws_s3_bucket.site_assets.arn}/*" + }, + { + Sid = "AllowSSLRequestsOnly" + Effect = "Deny" + Principal = "*" + Action = "s3:*" + Resource = [ + aws_s3_bucket.site_assets.arn, + "${aws_s3_bucket.site_assets.arn}/*" + ] + Condition = { + Bool = { + "aws:SecureTransport" = "false" + } + } + } + ] + }) +} diff --git a/terraform/projects/s3-site-assets/outputs.tf b/terraform/projects/s3-site-assets/outputs.tf new file mode 100644 index 000000000..70efa86b7 --- /dev/null +++ b/terraform/projects/s3-site-assets/outputs.tf @@ -0,0 +1,14 @@ +output "bucket_name" { + description = "Name of the S3 bucket" + value = aws_s3_bucket.site_assets.id +} + +output "bucket_arn" { + description = "ARN of the S3 bucket" + value = aws_s3_bucket.site_assets.arn +} + +output "bucket_regional_domain_name" { + description = "Regional domain name of the S3 bucket" + value = aws_s3_bucket.site_assets.bucket_regional_domain_name +} diff --git a/terraform/projects/s3-site-assets/terraform.tf b/terraform/projects/s3-site-assets/terraform.tf new file mode 100644 index 000000000..ad48e55c8 --- /dev/null +++ b/terraform/projects/s3-site-assets/terraform.tf @@ -0,0 +1,24 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } + + backend "s3" {} +} + +provider "aws" { + region = var.aws_region + + default_tags { + tags = { + Project = "plasmic" + Environment = var.environment + ManagedBy = "terraform" + } + } +} diff --git a/terraform/projects/s3-site-assets/variables.tf b/terraform/projects/s3-site-assets/variables.tf new file mode 100644 index 000000000..872288dd9 --- /dev/null +++ b/terraform/projects/s3-site-assets/variables.tf @@ -0,0 +1,16 @@ +variable "environment" { + type = string + description = "Environment name" +} + +variable "aws_region" { + type = string + description = "AWS region" + default = "us-east-1" +} + +variable "allowed_origins" { + type = list(string) + description = "List of allowed CORS origins" + default = ["*"] +} diff --git a/terraform/projects/secrets/.terraform.lock.hcl b/terraform/projects/secrets/.terraform.lock.hcl new file mode 100644 index 000000000..cdc1668d4 --- /dev/null +++ b/terraform/projects/secrets/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.100.0" + constraints = "~> 5.0" + hashes = [ + "h1:Ijt7pOlB7Tr7maGQIqtsLFbl7pSMIj06TVdkoSBcYOw=", + "zh:054b8dd49f0549c9a7cc27d159e45327b7b65cf404da5e5a20da154b90b8a644", + "zh:0b97bf8d5e03d15d83cc40b0530a1f84b459354939ba6f135a0086c20ebbe6b2", + "zh:1589a2266af699cbd5d80737a0fe02e54ec9cf2ca54e7e00ac51c7359056f274", + "zh:6330766f1d85f01ae6ea90d1b214b8b74cc8c1badc4696b165b36ddd4cc15f7b", + "zh:7c8c2e30d8e55291b86fcb64bdf6c25489d538688545eb48fd74ad622e5d3862", + "zh:99b1003bd9bd32ee323544da897148f46a527f622dc3971af63ea3e251596342", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:9f8b909d3ec50ade83c8062290378b1ec553edef6a447c56dadc01a99f4eaa93", + "zh:aaef921ff9aabaf8b1869a86d692ebd24fbd4e12c21205034bb679b9caf883a2", + "zh:ac882313207aba00dd5a76dbd572a0ddc818bb9cbf5c9d61b28fe30efaec951e", + "zh:bb64e8aff37becab373a1a0cc1080990785304141af42ed6aa3dd4913b000421", + "zh:dfe495f6621df5540d9c92ad40b8067376350b005c637ea6efac5dc15028add4", + "zh:f0ddf0eaf052766cfe09dea8200a946519f653c384ab4336e2a4a64fdd6310e9", + "zh:f1b7e684f4c7ae1eed272b6de7d2049bb87a0275cb04dbb7cda6636f600699c9", + "zh:ff461571e3f233699bf690db319dfe46aec75e58726636a0d97dd9ac6e32fb70", + ] +} diff --git a/terraform/projects/secrets/config/integration-backend.tfvars.example b/terraform/projects/secrets/config/integration-backend.tfvars.example new file mode 100644 index 000000000..e4cb2796e --- /dev/null +++ b/terraform/projects/secrets/config/integration-backend.tfvars.example @@ -0,0 +1,4 @@ +bucket = "" +key = "integration/secrets/terraform.tfstate" +dynamodb_table = "" +region = "us-east-2" diff --git a/terraform/projects/secrets/config/integration.tfvars.example b/terraform/projects/secrets/config/integration.tfvars.example new file mode 100644 index 000000000..a032cc1e2 --- /dev/null +++ b/terraform/projects/secrets/config/integration.tfvars.example @@ -0,0 +1,15 @@ +environment = "integration" +aws_region = "us-east-2" + +# Secret recovery window - 7 days for dev +recovery_window_in_days = 7 + +# NOTE: Secret values should be provided via environment variables or secure parameter store +# DO NOT commit actual secret values to version control +# Example usage: +# export TF_VAR_database_uri="postgresql://..." +# export TF_VAR_session_secret="..." +# export TF_VAR_openai_api_key="sk-..." +# export TF_VAR_anthropic_api_key="..." +# export TF_VAR_dynamodb_access_key="..." +# export TF_VAR_dynamodb_secret_key="..." diff --git a/terraform/projects/secrets/main.tf b/terraform/projects/secrets/main.tf new file mode 100644 index 000000000..a528ebabb --- /dev/null +++ b/terraform/projects/secrets/main.tf @@ -0,0 +1,113 @@ +# Database URI secret - shared across all services +# The actual connection string is constructed and populated by the database project +resource "aws_secretsmanager_secret" "database_uri" { + name = "plasmic/${var.environment}/app/database-uri" + description = "PostgreSQL database connection URI for all services" + + recovery_window_in_days = var.recovery_window_in_days + + tags = { + Name = "plasmic-${var.environment}-database-uri" + Environment = var.environment + } +} + +# Session secret +resource "aws_secretsmanager_secret" "session_secret" { + name = "plasmic/session-secret" + description = "Session secret for authentication" + + recovery_window_in_days = var.recovery_window_in_days + + tags = { + Name = "plasmic-session-secret" + Environment = var.environment + } +} + +resource "aws_secretsmanager_secret_version" "session_secret" { + count = var.session_secret != "" ? 1 : 0 + + secret_id = aws_secretsmanager_secret.session_secret.id + secret_string = var.session_secret +} + +# OpenAI API key +resource "aws_secretsmanager_secret" "openai_api_key" { + name = "plasmic/openai-api-key" + description = "OpenAI API key for copilot service" + + recovery_window_in_days = var.recovery_window_in_days + + tags = { + Name = "plasmic-openai-api-key" + Environment = var.environment + } +} + +resource "aws_secretsmanager_secret_version" "openai_api_key" { + count = var.openai_api_key != "" ? 1 : 0 + + secret_id = aws_secretsmanager_secret.openai_api_key.id + secret_string = var.openai_api_key +} + +# Anthropic API key +resource "aws_secretsmanager_secret" "anthropic_api_key" { + name = "plasmic/anthropic-api-key" + description = "Anthropic API key for copilot service" + + recovery_window_in_days = var.recovery_window_in_days + + tags = { + Name = "plasmic-anthropic-api-key" + Environment = var.environment + } +} + +resource "aws_secretsmanager_secret_version" "anthropic_api_key" { + count = var.anthropic_api_key != "" ? 1 : 0 + + secret_id = aws_secretsmanager_secret.anthropic_api_key.id + secret_string = var.anthropic_api_key +} + +# DynamoDB access key +resource "aws_secretsmanager_secret" "dynamodb_access_key" { + name = "plasmic/dynamodb-access-key" + description = "DynamoDB access key for copilot service" + + recovery_window_in_days = var.recovery_window_in_days + + tags = { + Name = "plasmic-dynamodb-access-key" + Environment = var.environment + } +} + +resource "aws_secretsmanager_secret_version" "dynamodb_access_key" { + count = var.dynamodb_access_key != "" ? 1 : 0 + + secret_id = aws_secretsmanager_secret.dynamodb_access_key.id + secret_string = var.dynamodb_access_key +} + +# DynamoDB secret key +resource "aws_secretsmanager_secret" "dynamodb_secret_key" { + name = "plasmic/dynamodb-secret-key" + description = "DynamoDB secret key for copilot service" + + recovery_window_in_days = var.recovery_window_in_days + + tags = { + Name = "plasmic-dynamodb-secret-key" + Environment = var.environment + } +} + +resource "aws_secretsmanager_secret_version" "dynamodb_secret_key" { + count = var.dynamodb_secret_key != "" ? 1 : 0 + + secret_id = aws_secretsmanager_secret.dynamodb_secret_key.id + secret_string = var.dynamodb_secret_key +} diff --git a/terraform/projects/secrets/outputs.tf b/terraform/projects/secrets/outputs.tf new file mode 100644 index 000000000..608f5ae99 --- /dev/null +++ b/terraform/projects/secrets/outputs.tf @@ -0,0 +1,29 @@ +output "database_uri_arn" { + description = "ARN of the database URI secret" + value = aws_secretsmanager_secret.database_uri.arn +} + +output "session_secret_arn" { + description = "ARN of the session secret" + value = aws_secretsmanager_secret.session_secret.arn +} + +output "openai_api_key_arn" { + description = "ARN of the OpenAI API key secret" + value = aws_secretsmanager_secret.openai_api_key.arn +} + +output "anthropic_api_key_arn" { + description = "ARN of the Anthropic API key secret" + value = aws_secretsmanager_secret.anthropic_api_key.arn +} + +output "dynamodb_access_key_arn" { + description = "ARN of the DynamoDB access key secret" + value = aws_secretsmanager_secret.dynamodb_access_key.arn +} + +output "dynamodb_secret_key_arn" { + description = "ARN of the DynamoDB secret key secret" + value = aws_secretsmanager_secret.dynamodb_secret_key.arn +} diff --git a/terraform/projects/secrets/terraform.tf b/terraform/projects/secrets/terraform.tf new file mode 100644 index 000000000..ad48e55c8 --- /dev/null +++ b/terraform/projects/secrets/terraform.tf @@ -0,0 +1,24 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } + + backend "s3" {} +} + +provider "aws" { + region = var.aws_region + + default_tags { + tags = { + Project = "plasmic" + Environment = var.environment + ManagedBy = "terraform" + } + } +} diff --git a/terraform/projects/secrets/variables.tf b/terraform/projects/secrets/variables.tf new file mode 100644 index 000000000..a5f1a37b5 --- /dev/null +++ b/terraform/projects/secrets/variables.tf @@ -0,0 +1,64 @@ +variable "environment" { + type = string + description = "Environment name" +} + +variable "aws_region" { + type = string + description = "AWS region" + default = "us-east-1" +} + +variable "recovery_window_in_days" { + type = number + description = "Number of days to recover deleted secrets" + default = 7 + validation { + condition = var.recovery_window_in_days >= 7 && var.recovery_window_in_days <= 30 + error_message = "recovery_window_in_days must be between 7 and 30 days" + } +} + +# Secret values - should be provided via tfvars or environment variables +# Leave empty to create secret placeholders without values +variable "database_uri" { + type = string + description = "PostgreSQL database connection URI" + default = "" + sensitive = true +} + +variable "session_secret" { + type = string + description = "Session secret for authentication" + default = "" + sensitive = true +} + +variable "openai_api_key" { + type = string + description = "OpenAI API key" + default = "" + sensitive = true +} + +variable "anthropic_api_key" { + type = string + description = "Anthropic API key" + default = "" + sensitive = true +} + +variable "dynamodb_access_key" { + type = string + description = "DynamoDB access key" + default = "" + sensitive = true +} + +variable "dynamodb_secret_key" { + type = string + description = "DynamoDB secret key" + default = "" + sensitive = true +} diff --git a/terraform/projects/services/codegen/.terraform.lock.hcl b/terraform/projects/services/codegen/.terraform.lock.hcl new file mode 100644 index 000000000..cdc1668d4 --- /dev/null +++ b/terraform/projects/services/codegen/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.100.0" + constraints = "~> 5.0" + hashes = [ + "h1:Ijt7pOlB7Tr7maGQIqtsLFbl7pSMIj06TVdkoSBcYOw=", + "zh:054b8dd49f0549c9a7cc27d159e45327b7b65cf404da5e5a20da154b90b8a644", + "zh:0b97bf8d5e03d15d83cc40b0530a1f84b459354939ba6f135a0086c20ebbe6b2", + "zh:1589a2266af699cbd5d80737a0fe02e54ec9cf2ca54e7e00ac51c7359056f274", + "zh:6330766f1d85f01ae6ea90d1b214b8b74cc8c1badc4696b165b36ddd4cc15f7b", + "zh:7c8c2e30d8e55291b86fcb64bdf6c25489d538688545eb48fd74ad622e5d3862", + "zh:99b1003bd9bd32ee323544da897148f46a527f622dc3971af63ea3e251596342", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:9f8b909d3ec50ade83c8062290378b1ec553edef6a447c56dadc01a99f4eaa93", + "zh:aaef921ff9aabaf8b1869a86d692ebd24fbd4e12c21205034bb679b9caf883a2", + "zh:ac882313207aba00dd5a76dbd572a0ddc818bb9cbf5c9d61b28fe30efaec951e", + "zh:bb64e8aff37becab373a1a0cc1080990785304141af42ed6aa3dd4913b000421", + "zh:dfe495f6621df5540d9c92ad40b8067376350b005c637ea6efac5dc15028add4", + "zh:f0ddf0eaf052766cfe09dea8200a946519f653c384ab4336e2a4a64fdd6310e9", + "zh:f1b7e684f4c7ae1eed272b6de7d2049bb87a0275cb04dbb7cda6636f600699c9", + "zh:ff461571e3f233699bf690db319dfe46aec75e58726636a0d97dd9ac6e32fb70", + ] +} diff --git a/terraform/projects/services/codegen/config/integration-backend.tfvars.example b/terraform/projects/services/codegen/config/integration-backend.tfvars.example new file mode 100644 index 000000000..305a5fab4 --- /dev/null +++ b/terraform/projects/services/codegen/config/integration-backend.tfvars.example @@ -0,0 +1,4 @@ +bucket = "" +key = "integration/services/codegen/terraform.tfstate" +dynamodb_table = "" +region = "us-east-2" diff --git a/terraform/projects/services/codegen/config/integration.tfvars.example b/terraform/projects/services/codegen/config/integration.tfvars.example new file mode 100644 index 000000000..e616505af --- /dev/null +++ b/terraform/projects/services/codegen/config/integration.tfvars.example @@ -0,0 +1,26 @@ +environment = "integration" +aws_region = "us-east-2" + +# Container Configuration +# This will be updated by deploy-all.sh with the actual ECR URL +codegen_container_image = ".dkr.ecr.us-east-2.amazonaws.com/plasmic/wab:integration-latest" + +# ECS Configuration - Fargate minimum for 512 CPU is 1024 memory +codegen_cpu = 512 +codegen_memory = 1024 + +# Scaling +codegen_desired_count = 1 + +# Worker Pool Sizes +loader_worker_pool_size = 4 +generic_worker_pool_size = 2 + +# Application Configuration +log_level = "debug" +host_url = "https://" +codegen_host_url = "https://codegen." +loader_assets_bucket = "" + +# Networking +assign_public_ip = false diff --git a/terraform/projects/services/codegen/main.tf b/terraform/projects/services/codegen/main.tf new file mode 100644 index 000000000..db2712cc2 --- /dev/null +++ b/terraform/projects/services/codegen/main.tf @@ -0,0 +1,99 @@ +# Get shared DATABASE_URI secret (created and populated by ecs-cluster project) +data "aws_secretsmanager_secret" "database_uri" { + name = "plasmic/${var.environment}/app/database-uri" +} + +module "codegen_service" { + source = "../../../modules/backend-service" + + environment = var.environment + aws_region = var.aws_region + service_name = "codegen" + + # Container configuration + container_image = var.codegen_container_image + container_port = 3008 + container_command = [ + "node", + "-r", + "esbuild-register", + "src/wab/server/codegen-backend.ts" + ] + + # Resources + cpu = var.codegen_cpu + memory = var.codegen_memory + + # Scaling + desired_count = var.codegen_desired_count + + # Health check + health_check_path = "/healthcheck" + + # Deployment + enable_circuit_breaker = true + + # Environment variables - using production mode to ensure DATABASE_URI is used + environment_variables = { + NODE_ENV = "production" # Always use production mode for deployed environments + AWS_REGION = var.aws_region + PINO_LOGGER_LEVEL = var.log_level + LOADER_WORKER_POOL_SIZE = tostring(var.loader_worker_pool_size) + BACKEND_PORT = "3008" + CODEGEN_HOST = var.codegen_host_url + HOST = var.host_url + GENERIC_WORKER_POOL_SIZE = tostring(var.generic_worker_pool_size) + LOADER_ASSETS_BUCKET = var.loader_assets_bucket + DEBUG = "connect:typeorm" + } + + # Secrets - DATABASE_URI contains the full PostgreSQL connection string + secrets = [ + { + name = "DATABASE_URI" + valueFrom = data.aws_secretsmanager_secret.database_uri.arn + }, + { + name = "SESSION_SECRET" + valueFrom = data.aws_secretsmanager_secret.session_secret.arn + } + ] + + # Networking + cluster_id = local.cluster_id + cluster_name = local.cluster_name + vpc_id = local.vpc_id + private_subnet_ids = local.private_subnet_ids + alb_arn = local.alb_arn + alb_listener_arn = local.alb_listener_arn + alb_security_group_id = local.alb_security_group_id + ecs_security_group_id = local.ecs_security_group_id + assign_public_ip = var.assign_public_ip + + # IAM + execution_role_arn = local.execution_role_arn + create_task_role = true + + task_role_inline_policies = [ + { + name = "S3Access" + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "s3:GetObject", + "s3:PutObject" + ] + Resource = "arn:aws:s3:::${var.loader_assets_bucket}/*" + } + ] + }) + } + ] + + # ALB routing + path_pattern = "/api/v1/code/*" + listener_rule_priority = 100 +} diff --git a/terraform/projects/services/codegen/outputs.tf b/terraform/projects/services/codegen/outputs.tf new file mode 100644 index 000000000..a6428de83 --- /dev/null +++ b/terraform/projects/services/codegen/outputs.tf @@ -0,0 +1,29 @@ +output "service_name" { + description = "ECS service name" + value = module.codegen_service.service_name +} + +output "service_arn" { + description = "ECS service ARN" + value = module.codegen_service.service_arn +} + +output "task_definition_arn" { + description = "Task definition ARN" + value = module.codegen_service.task_definition_arn +} + +output "task_role_arn" { + description = "Task role ARN" + value = module.codegen_service.task_role_arn +} + +output "target_group_arn" { + description = "Target group ARN" + value = module.codegen_service.target_group_arn +} + +output "cloudwatch_log_group" { + description = "CloudWatch log group name" + value = module.codegen_service.cloudwatch_log_group +} diff --git a/terraform/projects/services/codegen/providers.tf b/terraform/projects/services/codegen/providers.tf new file mode 100644 index 000000000..309613655 --- /dev/null +++ b/terraform/projects/services/codegen/providers.tf @@ -0,0 +1,27 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } + + backend "s3" { + # Backend configuration provided via backend config file + } +} + +provider "aws" { + region = var.aws_region + + default_tags { + tags = { + Environment = var.environment + Project = "plasmic" + Service = "codegen" + ManagedBy = "terraform" + } + } +} diff --git a/terraform/projects/services/codegen/remote-state.tf b/terraform/projects/services/codegen/remote-state.tf new file mode 100644 index 000000000..34e9098c5 --- /dev/null +++ b/terraform/projects/services/codegen/remote-state.tf @@ -0,0 +1,52 @@ +data "terraform_remote_state" "vpc" { + backend = "s3" + config = { + bucket = "plasmic-terraform-state-${var.environment}-${var.aws_region}" + key = "${var.environment}/vpc/terraform.tfstate" + region = var.aws_region + } +} + +data "terraform_remote_state" "ecs_cluster" { + backend = "s3" + config = { + bucket = "plasmic-terraform-state-${var.environment}-${var.aws_region}" + key = "${var.environment}/ecs-cluster/terraform.tfstate" + region = var.aws_region + } +} + +data "terraform_remote_state" "database" { + backend = "s3" + config = { + bucket = "plasmic-terraform-state-${var.environment}-${var.aws_region}" + key = "${var.environment}/database/terraform.tfstate" + region = var.aws_region + } +} + +# Secrets +data "aws_secretsmanager_secret" "session_secret" { + name = "plasmic/${var.environment}/app/session-secret" +} + +locals { + # VPC + vpc_id = data.terraform_remote_state.vpc.outputs.vpc_id + private_subnet_ids = data.terraform_remote_state.vpc.outputs.private_subnet_ids + + # ECS Cluster (includes ALB, security groups, execution role) + cluster_id = data.terraform_remote_state.ecs_cluster.outputs.cluster_id + cluster_name = data.terraform_remote_state.ecs_cluster.outputs.cluster_name + alb_arn = data.terraform_remote_state.ecs_cluster.outputs.alb_arn + alb_listener_arn = data.terraform_remote_state.ecs_cluster.outputs.alb_listener_arn + alb_security_group_id = data.terraform_remote_state.ecs_cluster.outputs.alb_security_group_id + ecs_security_group_id = data.terraform_remote_state.ecs_cluster.outputs.ecs_security_group_id + execution_role_arn = data.terraform_remote_state.ecs_cluster.outputs.execution_role_arn + + # Database + db_address = data.terraform_remote_state.database.outputs.db_address + db_port = data.terraform_remote_state.database.outputs.db_port + db_name = data.terraform_remote_state.database.outputs.db_name + db_username = data.terraform_remote_state.database.outputs.db_username +} diff --git a/terraform/projects/services/codegen/vars.tf b/terraform/projects/services/codegen/vars.tf new file mode 100644 index 000000000..1a9183ee2 --- /dev/null +++ b/terraform/projects/services/codegen/vars.tf @@ -0,0 +1,72 @@ +variable "environment" { + type = string + description = "Environment name" +} + +variable "aws_region" { + type = string + description = "AWS region" + default = "us-east-1" +} + +variable "codegen_container_image" { + type = string + description = "Docker image URL for codegen container" +} + +variable "codegen_cpu" { + type = number + description = "Task CPU units" + default = 1024 +} + +variable "codegen_memory" { + type = number + description = "Task memory in MB" + default = 2048 +} + +variable "codegen_desired_count" { + type = number + description = "Desired number of tasks" + default = 1 +} + +variable "loader_worker_pool_size" { + type = number + description = "Loader worker pool size" + default = 4 +} + +variable "generic_worker_pool_size" { + type = number + description = "Generic worker pool size" + default = 2 +} + +variable "assign_public_ip" { + type = bool + description = "Assign public IP to tasks" + default = false +} + +variable "log_level" { + type = string + description = "Application log level" + default = "info" +} + +variable "host_url" { + type = string + description = "Main application host URL" +} + +variable "codegen_host_url" { + type = string + description = "Codegen service host URL" +} + +variable "loader_assets_bucket" { + type = string + description = "S3 bucket for loader assets" +} diff --git a/terraform/projects/services/copilot/.terraform.lock.hcl b/terraform/projects/services/copilot/.terraform.lock.hcl new file mode 100644 index 000000000..cdc1668d4 --- /dev/null +++ b/terraform/projects/services/copilot/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.100.0" + constraints = "~> 5.0" + hashes = [ + "h1:Ijt7pOlB7Tr7maGQIqtsLFbl7pSMIj06TVdkoSBcYOw=", + "zh:054b8dd49f0549c9a7cc27d159e45327b7b65cf404da5e5a20da154b90b8a644", + "zh:0b97bf8d5e03d15d83cc40b0530a1f84b459354939ba6f135a0086c20ebbe6b2", + "zh:1589a2266af699cbd5d80737a0fe02e54ec9cf2ca54e7e00ac51c7359056f274", + "zh:6330766f1d85f01ae6ea90d1b214b8b74cc8c1badc4696b165b36ddd4cc15f7b", + "zh:7c8c2e30d8e55291b86fcb64bdf6c25489d538688545eb48fd74ad622e5d3862", + "zh:99b1003bd9bd32ee323544da897148f46a527f622dc3971af63ea3e251596342", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:9f8b909d3ec50ade83c8062290378b1ec553edef6a447c56dadc01a99f4eaa93", + "zh:aaef921ff9aabaf8b1869a86d692ebd24fbd4e12c21205034bb679b9caf883a2", + "zh:ac882313207aba00dd5a76dbd572a0ddc818bb9cbf5c9d61b28fe30efaec951e", + "zh:bb64e8aff37becab373a1a0cc1080990785304141af42ed6aa3dd4913b000421", + "zh:dfe495f6621df5540d9c92ad40b8067376350b005c637ea6efac5dc15028add4", + "zh:f0ddf0eaf052766cfe09dea8200a946519f653c384ab4336e2a4a64fdd6310e9", + "zh:f1b7e684f4c7ae1eed272b6de7d2049bb87a0275cb04dbb7cda6636f600699c9", + "zh:ff461571e3f233699bf690db319dfe46aec75e58726636a0d97dd9ac6e32fb70", + ] +} diff --git a/terraform/projects/services/copilot/config/integration-backend.tfvars.example b/terraform/projects/services/copilot/config/integration-backend.tfvars.example new file mode 100644 index 000000000..5cf639702 --- /dev/null +++ b/terraform/projects/services/copilot/config/integration-backend.tfvars.example @@ -0,0 +1,4 @@ +bucket = "" +key = "integration/services/copilot/terraform.tfstate" +dynamodb_table = "" +region = "us-east-2" diff --git a/terraform/projects/services/copilot/config/integration.tfvars.example b/terraform/projects/services/copilot/config/integration.tfvars.example new file mode 100644 index 000000000..d5061bc7c --- /dev/null +++ b/terraform/projects/services/copilot/config/integration.tfvars.example @@ -0,0 +1,26 @@ +environment = "integration" +aws_region = "us-east-2" + +# Container Configuration +copilot_container_image = ".dkr.ecr.us-east-2.amazonaws.com/plasmic/wab:integration-latest" + +# ECS Configuration - Copilot needs more resources +copilot_cpu = 2048 +copilot_memory = 4096 + +# Scaling +copilot_desired_count = 0 # Disabled until copilot-backend.ts is merged from feat/copilot-placeholder branch + +# Worker Pool Sizes +generic_worker_pool_size = 2 + +# DynamoDB +dynamodb_region = "us-west-2" + +# Networking +assign_public_ip = false + +# Application Configuration +log_level = "debug" +host_url = "https://" +codegen_host_url = "https://codegen." diff --git a/terraform/projects/services/copilot/main.tf b/terraform/projects/services/copilot/main.tf new file mode 100644 index 000000000..34d16905f --- /dev/null +++ b/terraform/projects/services/copilot/main.tf @@ -0,0 +1,123 @@ +# Get shared DATABASE_URI secret (created and populated by ecs-cluster project) +data "aws_secretsmanager_secret" "database_uri" { + name = "plasmic/${var.environment}/app/database-uri" +} + +module "copilot_service" { + source = "../../../modules/backend-service" + + environment = var.environment + aws_region = var.aws_region + service_name = "copilot" + + # Container configuration + container_image = var.copilot_container_image + container_port = 3009 + container_command = [ + "node", + "-r", + "esbuild-register", + "src/wab/server/copilot-backend.ts" + ] + + # Resources - Copilot needs more resources + cpu = var.copilot_cpu + memory = var.copilot_memory + + # Scaling + desired_count = var.copilot_desired_count + + # Health check + health_check_path = "/healthcheck" + + # Deployment + enable_circuit_breaker = true + + # Environment variables - using production mode to ensure DATABASE_URI is used + environment_variables = { + NODE_ENV = "production" # Always use production mode for deployed environments + AWS_REGION = var.aws_region + PINO_LOGGER_LEVEL = var.log_level + BACKEND_PORT = "3009" + CODEGEN_HOST = var.codegen_host_url + HOST = var.host_url + GENERIC_WORKER_POOL_SIZE = tostring(var.generic_worker_pool_size) + DEBUG = "connect:typeorm" + DYNAMODB_REGION = var.dynamodb_region + } + + # Secrets - DATABASE_URI contains the full PostgreSQL connection string + secrets = concat( + [ + { + name = "DATABASE_URI" + valueFrom = data.aws_secretsmanager_secret.database_uri.arn + }, + { + name = "SESSION_SECRET" + valueFrom = data.aws_secretsmanager_secret.session_secret.arn + } + ], + var.enable_ai_features ? [ + { + name = "OPENAI_API_KEY" + valueFrom = data.aws_secretsmanager_secret.openai_api_key[0].arn + }, + { + name = "ANTHROPIC_API_KEY" + valueFrom = data.aws_secretsmanager_secret.anthropic_api_key[0].arn + } + ] : [], + var.enable_dynamodb_secrets ? [ + { + name = "DYNAMODB_ACCESS_KEY" + valueFrom = data.aws_secretsmanager_secret.dynamodb_access_key[0].arn + }, + { + name = "DYNAMODB_SECRET_KEY" + valueFrom = data.aws_secretsmanager_secret.dynamodb_secret_key[0].arn + } + ] : [] + ) + + # Networking + cluster_id = local.cluster_id + cluster_name = local.cluster_name + vpc_id = local.vpc_id + private_subnet_ids = local.private_subnet_ids + alb_arn = local.alb_arn + alb_listener_arn = local.alb_listener_arn + alb_security_group_id = local.alb_security_group_id + ecs_security_group_id = local.ecs_security_group_id + assign_public_ip = var.assign_public_ip + + # IAM + execution_role_arn = local.execution_role_arn + create_task_role = true + + task_role_inline_policies = [ + { + name = "DynamoDBAccess" + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "dynamodb:PutItem", + "dynamodb:GetItem", + "dynamodb:UpdateItem", + "dynamodb:Query", + "dynamodb:Scan" + ] + Resource = "arn:aws:dynamodb:${var.dynamodb_region}:*:table/plasmic-*" + } + ] + }) + } + ] + + # ALB routing + path_pattern = "/api/v1/copilot/*" + listener_rule_priority = 102 +} diff --git a/terraform/projects/services/copilot/outputs.tf b/terraform/projects/services/copilot/outputs.tf new file mode 100644 index 000000000..83c8bec71 --- /dev/null +++ b/terraform/projects/services/copilot/outputs.tf @@ -0,0 +1,29 @@ +output "service_name" { + description = "ECS service name" + value = module.copilot_service.service_name +} + +output "service_arn" { + description = "ECS service ARN" + value = module.copilot_service.service_arn +} + +output "task_definition_arn" { + description = "Task definition ARN" + value = module.copilot_service.task_definition_arn +} + +output "task_role_arn" { + description = "Task role ARN" + value = module.copilot_service.task_role_arn +} + +output "target_group_arn" { + description = "Target group ARN" + value = module.copilot_service.target_group_arn +} + +output "cloudwatch_log_group" { + description = "CloudWatch log group name" + value = module.copilot_service.cloudwatch_log_group +} diff --git a/terraform/projects/services/copilot/providers.tf b/terraform/projects/services/copilot/providers.tf new file mode 100644 index 000000000..b0ad4ad9a --- /dev/null +++ b/terraform/projects/services/copilot/providers.tf @@ -0,0 +1,27 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } + + backend "s3" { + # Backend configuration provided via backend config file + } +} + +provider "aws" { + region = var.aws_region + + default_tags { + tags = { + Environment = var.environment + Project = "plasmic" + Service = "copilot" + ManagedBy = "terraform" + } + } +} diff --git a/terraform/projects/services/copilot/remote-state.tf b/terraform/projects/services/copilot/remote-state.tf new file mode 100644 index 000000000..f8c3b5028 --- /dev/null +++ b/terraform/projects/services/copilot/remote-state.tf @@ -0,0 +1,76 @@ +data "terraform_remote_state" "vpc" { + backend = "s3" + config = { + bucket = "plasmic-terraform-state-${var.environment}-${var.aws_region}" + key = "${var.environment}/vpc/terraform.tfstate" + region = var.aws_region + } +} + +data "terraform_remote_state" "ecs_cluster" { + backend = "s3" + config = { + bucket = "plasmic-terraform-state-${var.environment}-${var.aws_region}" + key = "${var.environment}/ecs-cluster/terraform.tfstate" + region = var.aws_region + } +} + +data "terraform_remote_state" "database" { + backend = "s3" + config = { + bucket = "plasmic-terraform-state-${var.environment}-${var.aws_region}" + key = "${var.environment}/database/terraform.tfstate" + region = var.aws_region + } +} + +# Secrets +data "aws_secretsmanager_secret" "session_secret" { + name = "plasmic/${var.environment}/app/session-secret" +} + +# Optional AI secrets - only load if they exist +data "aws_secretsmanager_secret" "openai_api_key" { + count = var.enable_ai_features ? 1 : 0 + name = "plasmic/${var.environment}/openai-api-key" +} + +data "aws_secretsmanager_secret" "anthropic_api_key" { + count = var.enable_ai_features ? 1 : 0 + name = "plasmic/${var.environment}/anthropic-api-key" +} + +# Optional DynamoDB secrets - only load if they exist +data "aws_secretsmanager_secret" "dynamodb_access_key" { + count = var.enable_dynamodb_secrets ? 1 : 0 + name = "plasmic/${var.environment}/dynamodb-access-key" +} + +data "aws_secretsmanager_secret" "dynamodb_secret_key" { + count = var.enable_dynamodb_secrets ? 1 : 0 + name = "plasmic/${var.environment}/dynamodb-secret-key" +} + +locals { + # VPC + vpc_id = data.terraform_remote_state.vpc.outputs.vpc_id + private_subnet_ids = data.terraform_remote_state.vpc.outputs.private_subnet_ids + + # ECS Cluster + cluster_id = data.terraform_remote_state.ecs_cluster.outputs.cluster_id + cluster_name = data.terraform_remote_state.ecs_cluster.outputs.cluster_name + + # Database + db_address = data.terraform_remote_state.database.outputs.db_address + db_port = data.terraform_remote_state.database.outputs.db_port + db_name = data.terraform_remote_state.database.outputs.db_name + db_username = data.terraform_remote_state.database.outputs.db_username + + # ECS Cluster (includes ALB, security groups, execution role) + alb_arn = data.terraform_remote_state.ecs_cluster.outputs.alb_arn + alb_listener_arn = data.terraform_remote_state.ecs_cluster.outputs.alb_listener_arn + alb_security_group_id = data.terraform_remote_state.ecs_cluster.outputs.alb_security_group_id + ecs_security_group_id = data.terraform_remote_state.ecs_cluster.outputs.ecs_security_group_id + execution_role_arn = data.terraform_remote_state.ecs_cluster.outputs.execution_role_arn +} diff --git a/terraform/projects/services/copilot/vars.tf b/terraform/projects/services/copilot/vars.tf new file mode 100644 index 000000000..3efbad764 --- /dev/null +++ b/terraform/projects/services/copilot/vars.tf @@ -0,0 +1,79 @@ +variable "environment" { + type = string + description = "Environment name" +} + +variable "aws_region" { + type = string + description = "AWS region" + default = "us-east-1" +} + +variable "copilot_container_image" { + type = string + description = "Docker image URL for copilot container" +} + +variable "copilot_cpu" { + type = number + description = "Task CPU units" + default = 2048 +} + +variable "copilot_memory" { + type = number + description = "Task memory in MB" + default = 4096 +} + +variable "copilot_desired_count" { + type = number + description = "Desired number of tasks" + default = 1 +} + +variable "generic_worker_pool_size" { + type = number + description = "Generic worker pool size" + default = 2 +} + +variable "dynamodb_region" { + type = string + description = "DynamoDB region" + default = "us-west-2" +} + +variable "assign_public_ip" { + type = bool + description = "Assign public IP to tasks" + default = false +} + +variable "log_level" { + type = string + description = "Application log level" + default = "info" +} + +variable "host_url" { + type = string + description = "Main application host URL" +} + +variable "codegen_host_url" { + type = string + description = "Codegen service host URL" +} + +variable "enable_ai_features" { + type = bool + description = "Enable AI features (requires OpenAI and Anthropic API keys)" + default = false +} + +variable "enable_dynamodb_secrets" { + type = bool + description = "Enable DynamoDB access using secret credentials" + default = false +} diff --git a/terraform/projects/services/data/.terraform.lock.hcl b/terraform/projects/services/data/.terraform.lock.hcl new file mode 100644 index 000000000..cdc1668d4 --- /dev/null +++ b/terraform/projects/services/data/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.100.0" + constraints = "~> 5.0" + hashes = [ + "h1:Ijt7pOlB7Tr7maGQIqtsLFbl7pSMIj06TVdkoSBcYOw=", + "zh:054b8dd49f0549c9a7cc27d159e45327b7b65cf404da5e5a20da154b90b8a644", + "zh:0b97bf8d5e03d15d83cc40b0530a1f84b459354939ba6f135a0086c20ebbe6b2", + "zh:1589a2266af699cbd5d80737a0fe02e54ec9cf2ca54e7e00ac51c7359056f274", + "zh:6330766f1d85f01ae6ea90d1b214b8b74cc8c1badc4696b165b36ddd4cc15f7b", + "zh:7c8c2e30d8e55291b86fcb64bdf6c25489d538688545eb48fd74ad622e5d3862", + "zh:99b1003bd9bd32ee323544da897148f46a527f622dc3971af63ea3e251596342", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:9f8b909d3ec50ade83c8062290378b1ec553edef6a447c56dadc01a99f4eaa93", + "zh:aaef921ff9aabaf8b1869a86d692ebd24fbd4e12c21205034bb679b9caf883a2", + "zh:ac882313207aba00dd5a76dbd572a0ddc818bb9cbf5c9d61b28fe30efaec951e", + "zh:bb64e8aff37becab373a1a0cc1080990785304141af42ed6aa3dd4913b000421", + "zh:dfe495f6621df5540d9c92ad40b8067376350b005c637ea6efac5dc15028add4", + "zh:f0ddf0eaf052766cfe09dea8200a946519f653c384ab4336e2a4a64fdd6310e9", + "zh:f1b7e684f4c7ae1eed272b6de7d2049bb87a0275cb04dbb7cda6636f600699c9", + "zh:ff461571e3f233699bf690db319dfe46aec75e58726636a0d97dd9ac6e32fb70", + ] +} diff --git a/terraform/projects/services/data/config/integration-backend.tfvars.example b/terraform/projects/services/data/config/integration-backend.tfvars.example new file mode 100644 index 000000000..52e2361be --- /dev/null +++ b/terraform/projects/services/data/config/integration-backend.tfvars.example @@ -0,0 +1,4 @@ +bucket = "" +key = "integration/services/data/terraform.tfstate" +dynamodb_table = "" +region = "us-east-2" diff --git a/terraform/projects/services/data/config/integration.tfvars.example b/terraform/projects/services/data/config/integration.tfvars.example new file mode 100644 index 000000000..061a716db --- /dev/null +++ b/terraform/projects/services/data/config/integration.tfvars.example @@ -0,0 +1,23 @@ +environment = "integration" +aws_region = "us-east-2" + +# Container Configuration +data_container_image = ".dkr.ecr.us-east-2.amazonaws.com/plasmic/wab:integration-latest" + +# ECS Configuration - Fargate minimum for 512 CPU is 1024 memory +data_cpu = 512 +data_memory = 1024 + +# Scaling +data_desired_count = 1 + +# Worker Pool Sizes +loader_worker_pool_size = 4 +generic_worker_pool_size = 2 + +# Networking +assign_public_ip = false + +# Application Configuration +log_level = "info" +host_url = "https://" diff --git a/terraform/projects/services/data/main.tf b/terraform/projects/services/data/main.tf new file mode 100644 index 000000000..60c946f85 --- /dev/null +++ b/terraform/projects/services/data/main.tf @@ -0,0 +1,78 @@ +# Get shared DATABASE_URI secret (created and populated by ecs-cluster project) +data "aws_secretsmanager_secret" "database_uri" { + name = "plasmic/${var.environment}/app/database-uri" +} + +module "data_service" { + source = "../../../modules/backend-service" + + environment = var.environment + aws_region = var.aws_region + service_name = "data" + + # Container configuration + container_image = var.data_container_image + container_port = 3004 + container_command = [ + "node", + "-r", + "esbuild-register", + "src/wab/server/integrations-backend.ts" + ] + + # Resources + cpu = var.data_cpu + memory = var.data_memory + + # Scaling + desired_count = var.data_desired_count + + # Health check + health_check_path = "/healthcheck" + + # Deployment - data service has circuit breaker enabled + enable_circuit_breaker = true + + # Environment variables - using production mode to ensure DATABASE_URI is used + environment_variables = { + NODE_ENV = "production" # Always use production mode for deployed environments + AWS_REGION = var.aws_region + PINO_LOGGER_LEVEL = var.log_level + LOADER_WORKER_POOL_SIZE = tostring(var.loader_worker_pool_size) + BACKEND_PORT = "3004" + NODE_OPTIONS = "--max-old-space-size=1536" + HOST = var.host_url + GENERIC_WORKER_POOL_SIZE = tostring(var.generic_worker_pool_size) + } + + # Secrets - DATABASE_URI contains the full PostgreSQL connection string + secrets = [ + { + name = "DATABASE_URI" + valueFrom = data.aws_secretsmanager_secret.database_uri.arn + }, + { + name = "SESSION_SECRET" + valueFrom = data.aws_secretsmanager_secret.session_secret.arn + } + ] + + # Networking + cluster_id = local.cluster_id + cluster_name = local.cluster_name + vpc_id = local.vpc_id + private_subnet_ids = local.private_subnet_ids + alb_arn = local.alb_arn + alb_listener_arn = local.alb_listener_arn + alb_security_group_id = local.alb_security_group_id + ecs_security_group_id = local.ecs_security_group_id + assign_public_ip = var.assign_public_ip + + # IAM + execution_role_arn = local.execution_role_arn + create_task_role = true + + # ALB routing + path_pattern = "/api/v1/data/*" + listener_rule_priority = 101 +} diff --git a/terraform/projects/services/data/outputs.tf b/terraform/projects/services/data/outputs.tf new file mode 100644 index 000000000..616290bac --- /dev/null +++ b/terraform/projects/services/data/outputs.tf @@ -0,0 +1,29 @@ +output "service_name" { + description = "ECS service name" + value = module.data_service.service_name +} + +output "service_arn" { + description = "ECS service ARN" + value = module.data_service.service_arn +} + +output "task_definition_arn" { + description = "Task definition ARN" + value = module.data_service.task_definition_arn +} + +output "task_role_arn" { + description = "Task role ARN" + value = module.data_service.task_role_arn +} + +output "target_group_arn" { + description = "Target group ARN" + value = module.data_service.target_group_arn +} + +output "cloudwatch_log_group" { + description = "CloudWatch log group name" + value = module.data_service.cloudwatch_log_group +} diff --git a/terraform/projects/services/data/providers.tf b/terraform/projects/services/data/providers.tf new file mode 100644 index 000000000..7b57e2bf1 --- /dev/null +++ b/terraform/projects/services/data/providers.tf @@ -0,0 +1,27 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } + + backend "s3" { + # Backend configuration provided via backend config file + } +} + +provider "aws" { + region = var.aws_region + + default_tags { + tags = { + Environment = var.environment + Project = "plasmic" + Service = "data" + ManagedBy = "terraform" + } + } +} diff --git a/terraform/projects/services/data/remote-state.tf b/terraform/projects/services/data/remote-state.tf new file mode 100644 index 000000000..bdaf099d7 --- /dev/null +++ b/terraform/projects/services/data/remote-state.tf @@ -0,0 +1,54 @@ +data "terraform_remote_state" "vpc" { + backend = "s3" + config = { + bucket = "plasmic-terraform-state-${var.environment}-${var.aws_region}" + key = "${var.environment}/vpc/terraform.tfstate" + region = var.aws_region + } +} + +data "terraform_remote_state" "ecs_cluster" { + backend = "s3" + config = { + bucket = "plasmic-terraform-state-${var.environment}-${var.aws_region}" + key = "${var.environment}/ecs-cluster/terraform.tfstate" + region = var.aws_region + } +} + +data "terraform_remote_state" "database" { + backend = "s3" + config = { + bucket = "plasmic-terraform-state-${var.environment}-${var.aws_region}" + key = "${var.environment}/database/terraform.tfstate" + region = var.aws_region + } +} + +# Secrets +data "aws_secretsmanager_secret" "session_secret" { + name = "plasmic/${var.environment}/app/session-secret" +} + +locals { + # VPC + vpc_id = data.terraform_remote_state.vpc.outputs.vpc_id + private_subnet_ids = data.terraform_remote_state.vpc.outputs.private_subnet_ids + + # ECS Cluster + cluster_id = data.terraform_remote_state.ecs_cluster.outputs.cluster_id + cluster_name = data.terraform_remote_state.ecs_cluster.outputs.cluster_name + + # Database + db_address = data.terraform_remote_state.database.outputs.db_address + db_port = data.terraform_remote_state.database.outputs.db_port + db_name = data.terraform_remote_state.database.outputs.db_name + db_username = data.terraform_remote_state.database.outputs.db_username + + # ECS Cluster (includes ALB, security groups, execution role) + alb_arn = data.terraform_remote_state.ecs_cluster.outputs.alb_arn + alb_listener_arn = data.terraform_remote_state.ecs_cluster.outputs.alb_listener_arn + alb_security_group_id = data.terraform_remote_state.ecs_cluster.outputs.alb_security_group_id + ecs_security_group_id = data.terraform_remote_state.ecs_cluster.outputs.ecs_security_group_id + execution_role_arn = data.terraform_remote_state.ecs_cluster.outputs.execution_role_arn +} diff --git a/terraform/projects/services/data/vars.tf b/terraform/projects/services/data/vars.tf new file mode 100644 index 000000000..a865ee200 --- /dev/null +++ b/terraform/projects/services/data/vars.tf @@ -0,0 +1,62 @@ +variable "environment" { + type = string + description = "Environment name" +} + +variable "aws_region" { + type = string + description = "AWS region" + default = "us-east-1" +} + +variable "data_container_image" { + type = string + description = "Docker image URL for data container" +} + +variable "data_cpu" { + type = number + description = "Task CPU units" + default = 1024 +} + +variable "data_memory" { + type = number + description = "Task memory in MB" + default = 2048 +} + +variable "data_desired_count" { + type = number + description = "Desired number of tasks" + default = 1 +} + +variable "loader_worker_pool_size" { + type = number + description = "Loader worker pool size" + default = 4 +} + +variable "generic_worker_pool_size" { + type = number + description = "Generic worker pool size" + default = 2 +} + +variable "assign_public_ip" { + type = bool + description = "Assign public IP to tasks" + default = false +} + +variable "log_level" { + type = string + description = "Application log level" + default = "info" +} + +variable "host_url" { + type = string + description = "Main application host URL" +} diff --git a/terraform/projects/services/wab/.terraform.lock.hcl b/terraform/projects/services/wab/.terraform.lock.hcl new file mode 100644 index 000000000..cdc1668d4 --- /dev/null +++ b/terraform/projects/services/wab/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.100.0" + constraints = "~> 5.0" + hashes = [ + "h1:Ijt7pOlB7Tr7maGQIqtsLFbl7pSMIj06TVdkoSBcYOw=", + "zh:054b8dd49f0549c9a7cc27d159e45327b7b65cf404da5e5a20da154b90b8a644", + "zh:0b97bf8d5e03d15d83cc40b0530a1f84b459354939ba6f135a0086c20ebbe6b2", + "zh:1589a2266af699cbd5d80737a0fe02e54ec9cf2ca54e7e00ac51c7359056f274", + "zh:6330766f1d85f01ae6ea90d1b214b8b74cc8c1badc4696b165b36ddd4cc15f7b", + "zh:7c8c2e30d8e55291b86fcb64bdf6c25489d538688545eb48fd74ad622e5d3862", + "zh:99b1003bd9bd32ee323544da897148f46a527f622dc3971af63ea3e251596342", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:9f8b909d3ec50ade83c8062290378b1ec553edef6a447c56dadc01a99f4eaa93", + "zh:aaef921ff9aabaf8b1869a86d692ebd24fbd4e12c21205034bb679b9caf883a2", + "zh:ac882313207aba00dd5a76dbd572a0ddc818bb9cbf5c9d61b28fe30efaec951e", + "zh:bb64e8aff37becab373a1a0cc1080990785304141af42ed6aa3dd4913b000421", + "zh:dfe495f6621df5540d9c92ad40b8067376350b005c637ea6efac5dc15028add4", + "zh:f0ddf0eaf052766cfe09dea8200a946519f653c384ab4336e2a4a64fdd6310e9", + "zh:f1b7e684f4c7ae1eed272b6de7d2049bb87a0275cb04dbb7cda6636f600699c9", + "zh:ff461571e3f233699bf690db319dfe46aec75e58726636a0d97dd9ac6e32fb70", + ] +} diff --git a/terraform/projects/services/wab/build-and-push.sh b/terraform/projects/services/wab/build-and-push.sh new file mode 100755 index 000000000..421b76bc7 --- /dev/null +++ b/terraform/projects/services/wab/build-and-push.sh @@ -0,0 +1,74 @@ +#!/bin/bash +# Build and push ARM64 Docker image to ECR for Fargate +# Usage: ./build-and-push.sh [environment] [aws-region] + +set -e + +ENVIRONMENT="${1:-integration}" +AWS_REGION="${2:-us-east-2}" + +echo "๐Ÿณ Building and pushing ARM64 Docker image" +echo "Environment: $ENVIRONMENT" +echo "Region: $AWS_REGION" +echo "" + +# Colors +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +step() { + echo -e "${GREEN}โ–ถ $1${NC}" +} + +# Get AWS account ID +ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) +ECR_REPO="${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/plasmic/wab" + +step "1. Logging into ECR" +aws ecr get-login-password --region ${AWS_REGION} | \ + docker login --username AWS --password-stdin ${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com +echo "โœ… Logged in to ECR" + +step "2. Building ARM64 image" + +# Check if buildx builder exists, create if not +if ! docker buildx inspect arm64-builder >/dev/null 2>&1; then + echo "Creating buildx builder..." + docker buildx create --name arm64-builder --use +else + docker buildx use arm64-builder +fi + +# Build for ARM64 platform +echo "Building for linux/arm64..." +docker buildx build \ + --platform linux/arm64 \ + --load \ + -t plasmic-wab:latest \ + -t ${ECR_REPO}:${ENVIRONMENT}-latest \ + -f platform/wab/Dockerfile \ + platform/ + +echo "โœ… Image built successfully" + +step "3. Pushing to ECR" +docker push ${ECR_REPO}:${ENVIRONMENT}-latest +echo "โœ… Image pushed: ${ECR_REPO}:${ENVIRONMENT}-latest" + +step "4. Verifying image in ECR" +IMAGE_DIGEST=$(aws ecr describe-images \ + --repository-name plasmic/wab \ + --image-ids imageTag=${ENVIRONMENT}-latest \ + --region ${AWS_REGION} \ + --query 'imageDetails[0].imageDigest' \ + --output text) + +echo "โœ… Image verified in ECR" +echo " Digest: ${IMAGE_DIGEST}" +echo "" +echo "๐ŸŽ‰ Done! Image ready for deployment." +echo "" +echo "Next steps:" +echo " cd terraform/projects/services/wab" +echo " terraform apply -var-file=config/${ENVIRONMENT}.tfvars" diff --git a/terraform/projects/services/wab/config/integration-backend.tfvars.example b/terraform/projects/services/wab/config/integration-backend.tfvars.example new file mode 100644 index 000000000..a26df559f --- /dev/null +++ b/terraform/projects/services/wab/config/integration-backend.tfvars.example @@ -0,0 +1,4 @@ +bucket = "" +key = "integration/services/wab/terraform.tfstate" +dynamodb_table = "" +region = "us-east-2" diff --git a/terraform/projects/services/wab/config/integration.tfvars.example b/terraform/projects/services/wab/config/integration.tfvars.example new file mode 100644 index 000000000..7de0530e4 --- /dev/null +++ b/terraform/projects/services/wab/config/integration.tfvars.example @@ -0,0 +1,28 @@ +environment = "integration" +aws_region = "us-east-2" + +# Container Configuration +wab_container_image = ".dkr.ecr.us-east-2.amazonaws.com/plasmic/wab:integration-latest" +wab_container_port = 3004 + +# ECS Configuration - Fargate minimum for 512 CPU is 1024 memory +wab_cpu = 512 +wab_memory = 1024 + +# Scaling +wab_desired_count = 1 + +# Health Check +health_check_path = "/healthcheck" + +# S3 Configuration +# Set to CloudFront distribution URL for site assets, or leave empty to use S3 direct URL +site_assets_base_url = "" + +# Application Configuration +# Get the ALB DNS after deployment: terraform output -raw alb_dns_name +host_url = "http://.us-east-2.elb.amazonaws.com" + +# Worker Pool Sizes +generic_worker_pool_size = 2 +loader_worker_pool_size = 2 diff --git a/terraform/projects/services/wab/main.tf b/terraform/projects/services/wab/main.tf new file mode 100644 index 000000000..542f81c18 --- /dev/null +++ b/terraform/projects/services/wab/main.tf @@ -0,0 +1,119 @@ +data "aws_caller_identity" "current" {} +data "aws_region" "current" {} + +locals { + service_name = "wab" +} + +# Get shared DATABASE_URI secret (created and populated by ecs-cluster project) +data "aws_secretsmanager_secret" "database_uri" { + name = "plasmic/${var.environment}/app/database-uri" +} + +# Get session secret +data "aws_secretsmanager_secret" "session_secret" { + name = "plasmic/${var.environment}/app/session-secret" +} + +module "wab_service" { + source = "../../../modules/backend-service" + + environment = var.environment + aws_region = var.aws_region + service_name = local.service_name + + # Container configuration + container_image = var.wab_container_image + container_port = var.wab_container_port + container_command = [] + + # Resources + cpu = var.wab_cpu + memory = var.wab_memory + + # Scaling + desired_count = var.wab_desired_count + + # Health check + health_check_path = var.health_check_path + + # Deployment - enable circuit breaker to stop retrying failed deployments + enable_circuit_breaker = true + + # Environment variables - using WAB's expected variable names + environment_variables = { + NODE_ENV = "production" # Always use production mode for deployed environments + PORT = tostring(var.wab_container_port) + HOST = var.host_url + AWS_REGION = var.aws_region + SITE_ASSETS_BUCKET = local.site_assets_bucket_name + SITE_ASSETS_BASE_URL = var.site_assets_base_url + CLIP_BUCKET = local.clips_bucket_name + GENERIC_WORKER_POOL_SIZE = tostring(var.generic_worker_pool_size) + LOADER_WORKER_POOL_SIZE = tostring(var.loader_worker_pool_size) + } + + # Secrets - DATABASE_URI contains the full PostgreSQL connection string + secrets = [ + { + name = "DATABASE_URI" + valueFrom = data.aws_secretsmanager_secret.database_uri.arn + }, + { + name = "SESSION_SECRET" + valueFrom = data.aws_secretsmanager_secret.session_secret.arn + } + ] + + # Networking + cluster_id = local.cluster_id + cluster_name = local.cluster_name + vpc_id = local.vpc_id + private_subnet_ids = local.private_subnet_ids + alb_arn = local.alb_arn + alb_listener_arn = local.alb_listener_arn + alb_security_group_id = local.alb_security_group_id + ecs_security_group_id = local.ecs_security_group_id + assign_public_ip = false + + # IAM + execution_role_arn = local.execution_role_arn + create_task_role = true + + task_role_inline_policies = [ + { + name = "S3Access" + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "s3:PutObject", + "s3:GetObject", + "s3:DeleteObject" + ] + Resource = [ + "${local.site_assets_bucket_arn}/*", + "${local.clips_bucket_arn}/*" + ] + }, + { + Effect = "Allow" + Action = [ + "s3:ListBucket" + ] + Resource = [ + local.site_assets_bucket_arn, + local.clips_bucket_arn + ] + } + ] + }) + } + ] + + # ALB routing - WAB gets the default route (lowest priority / catch-all) + path_pattern = "/*" + listener_rule_priority = 1000 # Lowest priority = catch-all +} diff --git a/terraform/projects/services/wab/outputs.tf b/terraform/projects/services/wab/outputs.tf new file mode 100644 index 000000000..942475841 --- /dev/null +++ b/terraform/projects/services/wab/outputs.tf @@ -0,0 +1,30 @@ +output "service_name" { + description = "ECS service name" + value = module.wab_service.service_name +} + +output "service_arn" { + description = "ECS service ARN" + value = module.wab_service.service_arn +} + +output "task_definition_arn" { + description = "Task definition ARN" + value = module.wab_service.task_definition_arn +} + +output "log_group_name" { + description = "CloudWatch log group name" + value = module.wab_service.cloudwatch_log_group +} + +output "application_url" { + description = "Application URL" + value = "http://${data.terraform_remote_state.ecs_cluster.outputs.alb_dns_name}" +} + +# Expose ALB DNS for convenience +output "alb_dns_name" { + description = "ALB DNS name" + value = data.terraform_remote_state.ecs_cluster.outputs.alb_dns_name +} diff --git a/terraform/projects/services/wab/providers.tf b/terraform/projects/services/wab/providers.tf new file mode 100644 index 000000000..678fd8415 --- /dev/null +++ b/terraform/projects/services/wab/providers.tf @@ -0,0 +1,24 @@ +terraform { + backend "s3" {} + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } + required_version = ">= 1.9" +} + +provider "aws" { + region = var.aws_region + + default_tags { + tags = { + Project = "plasmic" + Environment = var.environment + ManagedBy = "terraform" + Component = "wab" + } + } +} diff --git a/terraform/projects/services/wab/remote-state.tf b/terraform/projects/services/wab/remote-state.tf new file mode 100644 index 000000000..85a17e3be --- /dev/null +++ b/terraform/projects/services/wab/remote-state.tf @@ -0,0 +1,66 @@ +data "terraform_remote_state" "vpc" { + backend = "s3" + config = { + bucket = "plasmic-terraform-state-${var.environment}-${var.aws_region}" + key = "${var.environment}/vpc/terraform.tfstate" + region = var.aws_region + } +} + +data "terraform_remote_state" "database" { + backend = "s3" + config = { + bucket = "plasmic-terraform-state-${var.environment}-${var.aws_region}" + key = "${var.environment}/database/terraform.tfstate" + region = var.aws_region + } +} + +data "terraform_remote_state" "ecs_cluster" { + backend = "s3" + config = { + bucket = "plasmic-terraform-state-${var.environment}-${var.aws_region}" + key = "${var.environment}/ecs-cluster/terraform.tfstate" + region = var.aws_region + } +} + +data "terraform_remote_state" "s3_site_assets" { + backend = "s3" + config = { + bucket = "plasmic-terraform-state-${var.environment}-${var.aws_region}" + key = "${var.environment}/s3-site-assets/terraform.tfstate" + region = var.aws_region + } +} + +data "terraform_remote_state" "s3_clips" { + backend = "s3" + config = { + bucket = "plasmic-terraform-state-${var.environment}-${var.aws_region}" + key = "${var.environment}/s3-clips/terraform.tfstate" + region = var.aws_region + } +} + +locals { + # VPC + vpc_id = data.terraform_remote_state.vpc.outputs.vpc_id + private_subnet_ids = data.terraform_remote_state.vpc.outputs.private_subnet_ids + + + # ECS Cluster (includes ALB, security groups, execution role) + cluster_id = data.terraform_remote_state.ecs_cluster.outputs.cluster_id + cluster_name = data.terraform_remote_state.ecs_cluster.outputs.cluster_name + alb_arn = data.terraform_remote_state.ecs_cluster.outputs.alb_arn + alb_listener_arn = data.terraform_remote_state.ecs_cluster.outputs.alb_https_listener_arn + alb_security_group_id = data.terraform_remote_state.ecs_cluster.outputs.alb_security_group_id + ecs_security_group_id = data.terraform_remote_state.ecs_cluster.outputs.ecs_security_group_id + execution_role_arn = data.terraform_remote_state.ecs_cluster.outputs.execution_role_arn + + # S3 Buckets + site_assets_bucket_name = data.terraform_remote_state.s3_site_assets.outputs.bucket_name + site_assets_bucket_arn = data.terraform_remote_state.s3_site_assets.outputs.bucket_arn + clips_bucket_name = data.terraform_remote_state.s3_clips.outputs.bucket_name + clips_bucket_arn = data.terraform_remote_state.s3_clips.outputs.bucket_arn +} diff --git a/terraform/projects/services/wab/vars.tf b/terraform/projects/services/wab/vars.tf new file mode 100644 index 000000000..6b6db0aa3 --- /dev/null +++ b/terraform/projects/services/wab/vars.tf @@ -0,0 +1,70 @@ +variable "environment" { + type = string + description = "Environment name" +} + +variable "aws_region" { + type = string + description = "AWS region" + default = "us-east-1" +} + +# Container Configuration +variable "wab_container_image" { + type = string + description = "Docker image URL for WAB container" +} + +variable "wab_container_port" { + type = number + default = 3004 + description = "Container port" +} + +# ECS Configuration +variable "wab_cpu" { + type = number + description = "Task CPU units" +} + +variable "wab_memory" { + type = number + description = "Task memory in MB" +} + +variable "wab_desired_count" { + type = number + description = "Desired number of tasks" +} + +# Health Check +variable "health_check_path" { + type = string + default = "/api/v1/health" + description = "Health check endpoint" +} + +# S3 Configuration +variable "site_assets_base_url" { + type = string + description = "Base URL for site assets (CloudFront CDN URL)" + default = "" +} + +# Application Configuration +variable "host_url" { + type = string + description = "Main application host URL" +} + +variable "generic_worker_pool_size" { + type = number + description = "Generic worker pool size" + default = 2 +} + +variable "loader_worker_pool_size" { + type = number + description = "Loader worker pool size" + default = 2 +} diff --git a/terraform/projects/vpc/.terraform.lock.hcl b/terraform/projects/vpc/.terraform.lock.hcl new file mode 100644 index 000000000..eaf7861b8 --- /dev/null +++ b/terraform/projects/vpc/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.100.0" + constraints = "~> 5.0, >= 5.79.0" + hashes = [ + "h1:Ijt7pOlB7Tr7maGQIqtsLFbl7pSMIj06TVdkoSBcYOw=", + "zh:054b8dd49f0549c9a7cc27d159e45327b7b65cf404da5e5a20da154b90b8a644", + "zh:0b97bf8d5e03d15d83cc40b0530a1f84b459354939ba6f135a0086c20ebbe6b2", + "zh:1589a2266af699cbd5d80737a0fe02e54ec9cf2ca54e7e00ac51c7359056f274", + "zh:6330766f1d85f01ae6ea90d1b214b8b74cc8c1badc4696b165b36ddd4cc15f7b", + "zh:7c8c2e30d8e55291b86fcb64bdf6c25489d538688545eb48fd74ad622e5d3862", + "zh:99b1003bd9bd32ee323544da897148f46a527f622dc3971af63ea3e251596342", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:9f8b909d3ec50ade83c8062290378b1ec553edef6a447c56dadc01a99f4eaa93", + "zh:aaef921ff9aabaf8b1869a86d692ebd24fbd4e12c21205034bb679b9caf883a2", + "zh:ac882313207aba00dd5a76dbd572a0ddc818bb9cbf5c9d61b28fe30efaec951e", + "zh:bb64e8aff37becab373a1a0cc1080990785304141af42ed6aa3dd4913b000421", + "zh:dfe495f6621df5540d9c92ad40b8067376350b005c637ea6efac5dc15028add4", + "zh:f0ddf0eaf052766cfe09dea8200a946519f653c384ab4336e2a4a64fdd6310e9", + "zh:f1b7e684f4c7ae1eed272b6de7d2049bb87a0275cb04dbb7cda6636f600699c9", + "zh:ff461571e3f233699bf690db319dfe46aec75e58726636a0d97dd9ac6e32fb70", + ] +} diff --git a/terraform/projects/vpc/config/integration-backend.tfvars.example b/terraform/projects/vpc/config/integration-backend.tfvars.example new file mode 100644 index 000000000..f0382a823 --- /dev/null +++ b/terraform/projects/vpc/config/integration-backend.tfvars.example @@ -0,0 +1,4 @@ +bucket = "" +key = "integration/vpc/terraform.tfstate" +dynamodb_table = "" +region = "us-east-2" diff --git a/terraform/projects/vpc/config/integration.tfvars.example b/terraform/projects/vpc/config/integration.tfvars.example new file mode 100644 index 000000000..75905700e --- /dev/null +++ b/terraform/projects/vpc/config/integration.tfvars.example @@ -0,0 +1,7 @@ +environment = "integration" +aws_region = "us-east-2" + +vpc_cidr = "10.0.0.0/16" + +enable_nat_gateway = false + diff --git a/terraform/projects/vpc/main.tf b/terraform/projects/vpc/main.tf new file mode 100644 index 000000000..299515181 --- /dev/null +++ b/terraform/projects/vpc/main.tf @@ -0,0 +1,149 @@ +data "aws_availability_zones" "available" { + state = "available" +} + +locals { + name = "plasmic-${var.environment}" + azs = slice(data.aws_availability_zones.available.names, 0, 3) +} + +module "vpc" { + source = "terraform-aws-modules/vpc/aws" + version = "~> 5.0" + + name = local.name + cidr = var.vpc_cidr + + azs = local.azs + private_subnets = [for k, v in local.azs : cidrsubnet(var.vpc_cidr, 4, k)] + public_subnets = [for k, v in local.azs : cidrsubnet(var.vpc_cidr, 8, k + 48)] + database_subnets = [for k, v in local.azs : cidrsubnet(var.vpc_cidr, 8, k + 64)] + + enable_nat_gateway = var.enable_nat_gateway + single_nat_gateway = var.single_nat_gateway + + # Allow database subnets to be publicly accessible + create_database_subnet_route_table = true + create_database_internet_gateway_route = true + + enable_dns_hostnames = true + enable_dns_support = true + + # VPC Flow Logs + enable_flow_log = true + create_flow_log_cloudwatch_iam_role = true + create_flow_log_cloudwatch_log_group = true + + tags = { + Name = local.name + } +} + +# Security group for VPC endpoints +resource "aws_security_group" "vpc_endpoints" { + name_prefix = "${local.name}-vpce-" + description = "Security group for VPC endpoints" + vpc_id = module.vpc.vpc_id + + ingress { + description = "HTTPS from VPC" + from_port = 443 + to_port = 443 + protocol = "tcp" + cidr_blocks = [var.vpc_cidr] + } + + egress { + description = "Allow all outbound" + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + lifecycle { + create_before_destroy = true + } + + tags = { + Name = "${local.name}-vpce-sg" + } +} + +# VPC Endpoints for private connectivity +resource "aws_vpc_endpoint" "ecr_api" { + vpc_id = module.vpc.vpc_id + service_name = "com.amazonaws.${var.aws_region}.ecr.api" + vpc_endpoint_type = "Interface" + subnet_ids = module.vpc.private_subnets + security_group_ids = [aws_security_group.vpc_endpoints.id] + private_dns_enabled = true + + tags = { + Name = "${local.name}-ecr-api" + } +} + +resource "aws_vpc_endpoint" "ecr_dkr" { + vpc_id = module.vpc.vpc_id + service_name = "com.amazonaws.${var.aws_region}.ecr.dkr" + vpc_endpoint_type = "Interface" + subnet_ids = module.vpc.private_subnets + security_group_ids = [aws_security_group.vpc_endpoints.id] + private_dns_enabled = true + + tags = { + Name = "${local.name}-ecr-dkr" + } +} + +resource "aws_vpc_endpoint" "logs" { + vpc_id = module.vpc.vpc_id + service_name = "com.amazonaws.${var.aws_region}.logs" + vpc_endpoint_type = "Interface" + subnet_ids = module.vpc.private_subnets + security_group_ids = [aws_security_group.vpc_endpoints.id] + private_dns_enabled = true + + tags = { + Name = "${local.name}-logs" + } +} + +# Secrets Manager endpoint - required for ECS tasks to pull secrets +resource "aws_vpc_endpoint" "secretsmanager" { + vpc_id = module.vpc.vpc_id + service_name = "com.amazonaws.${var.aws_region}.secretsmanager" + vpc_endpoint_type = "Interface" + subnet_ids = module.vpc.private_subnets + security_group_ids = [aws_security_group.vpc_endpoints.id] + private_dns_enabled = true + + tags = { + Name = "${local.name}-secretsmanager" + } +} + +# S3 Gateway Endpoint (no cost) +resource "aws_vpc_endpoint" "s3" { + vpc_id = module.vpc.vpc_id + service_name = "com.amazonaws.${var.aws_region}.s3" + vpc_endpoint_type = "Gateway" + route_table_ids = concat(module.vpc.private_route_table_ids, module.vpc.public_route_table_ids) + + tags = { + Name = "${local.name}-s3" + } +} + +# DynamoDB Gateway Endpoint (no cost) - required for copilot service +resource "aws_vpc_endpoint" "dynamodb" { + vpc_id = module.vpc.vpc_id + service_name = "com.amazonaws.${var.aws_region}.dynamodb" + vpc_endpoint_type = "Gateway" + route_table_ids = module.vpc.private_route_table_ids + + tags = { + Name = "${local.name}-dynamodb" + } +} diff --git a/terraform/projects/vpc/outputs.tf b/terraform/projects/vpc/outputs.tf new file mode 100644 index 000000000..4bb8485fe --- /dev/null +++ b/terraform/projects/vpc/outputs.tf @@ -0,0 +1,34 @@ +output "vpc_id" { + description = "VPC ID" + value = module.vpc.vpc_id +} + +output "vpc_cidr_block" { + description = "VPC CIDR block" + value = module.vpc.vpc_cidr_block +} + +output "private_subnet_ids" { + description = "Private subnet IDs" + value = module.vpc.private_subnets +} + +output "public_subnet_ids" { + description = "Public subnet IDs" + value = module.vpc.public_subnets +} + +output "database_subnet_ids" { + description = "Database subnet IDs" + value = module.vpc.database_subnets +} + +output "database_subnet_group_name" { + description = "Database subnet group name" + value = module.vpc.database_subnet_group_name +} + +output "nat_gateway_ips" { + description = "NAT Gateway public IPs" + value = module.vpc.nat_public_ips +} diff --git a/terraform/projects/vpc/providers.tf b/terraform/projects/vpc/providers.tf new file mode 100644 index 000000000..a9e961b1e --- /dev/null +++ b/terraform/projects/vpc/providers.tf @@ -0,0 +1,24 @@ +terraform { + backend "s3" {} + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } + required_version = ">= 1.9" +} + +provider "aws" { + region = var.aws_region + + default_tags { + tags = { + Project = "plasmic" + Environment = var.environment + ManagedBy = "terraform" + Component = "vpc" + } + } +} diff --git a/terraform/projects/vpc/vars.tf b/terraform/projects/vpc/vars.tf new file mode 100644 index 000000000..fe707bf52 --- /dev/null +++ b/terraform/projects/vpc/vars.tf @@ -0,0 +1,27 @@ +variable "environment" { + type = string + description = "Environment name (dev, staging, prod)" +} + +variable "aws_region" { + type = string + description = "AWS region" + default = "us-east-1" +} + +variable "vpc_cidr" { + type = string + description = "VPC CIDR block" +} + +variable "enable_nat_gateway" { + type = bool + description = "Enable NAT Gateway for private subnets" + default = false +} + +variable "single_nat_gateway" { + type = bool + description = "Use a single NAT Gateway (cost savings for non-prod)" + default = false +} diff --git a/terraform/scripts/bootstrap.sh b/terraform/scripts/bootstrap.sh new file mode 100755 index 000000000..eff32c7b8 --- /dev/null +++ b/terraform/scripts/bootstrap.sh @@ -0,0 +1,92 @@ +#!/bin/bash +# bootstrap-env.sh +# Usage: ./bootstrap-env.sh [region] +# Example: ./bootstrap-env.sh dev us-east-1 + +set -euo pipefail + +ENVIRONMENT="${1:?Environment required (integration|staging|prod-us|prod-eu)}" +AWS_REGION="${2:-us-east-2}" + +# Validate environment +if [[ ! "$ENVIRONMENT" =~ ^(integration|staging|prod-us|prod-eu)$ ]]; then + echo "โŒ Invalid environment. Use: integration, dev, staging, prod-us, or prod-eu" + exit 1 +fi + +echo "๐Ÿš€ Bootstrapping ${ENVIRONMENT} in ${AWS_REGION}" + +# Check if resource exists +check_bucket_exists() { + aws s3 ls "s3://$1" &>/dev/null 2>&1 +} + +check_table_exists() { + aws dynamodb describe-table --table-name "$1" --region "${AWS_REGION}" &>/dev/null 2>&1 +} + +# Create S3 bucket +BUCKET_NAME="plasmic-terraform-state-${ENVIRONMENT}-${AWS_REGION}" +echo "๐Ÿ“ฆ Creating S3 bucket: ${BUCKET_NAME}..." + +if check_bucket_exists "${BUCKET_NAME}"; then + echo "โญ๏ธ Bucket already exists" +else + aws s3 mb "s3://${BUCKET_NAME}" --region ${AWS_REGION} + echo "โœ… Created bucket" +fi + +# Configure bucket (in parallel for speed) +echo "โš™๏ธ Configuring bucket..." + +aws s3api put-bucket-versioning \ + --bucket "${BUCKET_NAME}" \ + --versioning-configuration Status=Enabled & +PID_VERSIONING=$! + +aws s3api put-bucket-encryption \ + --bucket "${BUCKET_NAME}" \ + --server-side-encryption-configuration '{ + "Rules": [{ + "ApplyServerSideEncryptionByDefault": { + "SSEAlgorithm": "AES256" + }, + "BucketKeyEnabled": true + }] + }' & +PID_ENCRYPTION=$! + +aws s3api put-public-access-block \ + --bucket "${BUCKET_NAME}" \ + --public-access-block-configuration \ + BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true & +PID_PUBLIC_BLOCK=$! + +wait $PID_VERSIONING $PID_ENCRYPTION $PID_PUBLIC_BLOCK +echo "โœ… Bucket configured" + +# Create DynamoDB table +TABLE_NAME="plasmic-terraform-locks-${ENVIRONMENT}" +echo "๐Ÿ“ฆ Creating DynamoDB table: ${TABLE_NAME}..." + +if check_table_exists "${TABLE_NAME}"; then + echo "โญ๏ธ Table already exists" +else + aws dynamodb create-table \ + --table-name "${TABLE_NAME}" \ + --attribute-definitions AttributeName=LockID,AttributeType=S \ + --key-schema AttributeName=LockID,KeyType=HASH \ + --billing-mode PAY_PER_REQUEST \ + --region ${AWS_REGION} \ + --no-cli-pager > /dev/null + echo "โœ… Created table" +fi + +echo "" +echo "๐ŸŽ‰ Bootstrap complete for ${ENVIRONMENT}" +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo " Environment: ${ENVIRONMENT}" +echo " Region: ${AWS_REGION}" +echo " State bucket: ${BUCKET_NAME}" +echo " Lock table: ${TABLE_NAME}" +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" \ No newline at end of file diff --git a/terraform/scripts/create-secret.sh b/terraform/scripts/create-secret.sh new file mode 100755 index 000000000..7a8db9887 --- /dev/null +++ b/terraform/scripts/create-secret.sh @@ -0,0 +1,64 @@ +#!/bin/bash +# Create a secret in AWS Secrets Manager +# Usage: ./create-secret.sh + +set -e + +if [ $# -ne 3 ]; then + echo "Usage: $0 " + echo "" + echo "Example:" + echo " $0 integration openai-api-key sk-proj-xxxxx" + exit 1 +fi + +ENVIRONMENT="$1" +KEY="$2" +SECRET_VALUE="$3" +AWS_REGION="${AWS_REGION:-us-east-2}" + +# Validate environment +VALID_ENVS="integration staging prod-us prod-eu" +if [[ ! " $VALID_ENVS " =~ " $ENVIRONMENT " ]]; then + echo "โŒ Invalid environment: $ENVIRONMENT" + echo "Valid environments: integration, staging, prod-us, prod-eu" + exit 1 +fi + +# Validate key +VALID_KEYS="openai-api-key anthropic-api-key dynamodb-access-key dynamodb-secret-key app/session-secret app/jwt-secret db/master-password" +if [[ ! " $VALID_KEYS " =~ " $KEY " ]]; then + echo "โŒ Invalid key: $KEY" + echo "Valid keys:" + echo " openai-api-key" + echo " anthropic-api-key" + echo " dynamodb-access-key" + echo " dynamodb-secret-key" + echo " app/session-secret" + echo " app/jwt-secret" + echo " db/master-password" + exit 1 +fi + +SECRET_NAME="plasmic/${ENVIRONMENT}/${KEY}" + +echo "Creating secret: ${SECRET_NAME}" + +# Check if secret exists +if aws secretsmanager describe-secret --secret-id "$SECRET_NAME" --region "$AWS_REGION" &>/dev/null; then + # Update existing + aws secretsmanager put-secret-value \ + --secret-id "$SECRET_NAME" \ + --secret-string "$SECRET_VALUE" \ + --region "$AWS_REGION" >/dev/null + echo "โœ… Secret updated" +else + # Create new + ARN=$(aws secretsmanager create-secret \ + --name "$SECRET_NAME" \ + --secret-string "$SECRET_VALUE" \ + --region "$AWS_REGION" \ + --output text \ + --query 'ARN') + echo "โœ… Secret created: ${ARN}" +fi \ No newline at end of file diff --git a/terraform/scripts/create-secrets.sh b/terraform/scripts/create-secrets.sh new file mode 100755 index 000000000..f4939a3e1 --- /dev/null +++ b/terraform/scripts/create-secrets.sh @@ -0,0 +1,82 @@ +#!/bin/bash +# create-secrets.sh +# Usage: ./create-secrets.sh [region] +# Example: ./create-secrets.sh dev us-east-1 + +set -euo pipefail + +ENVIRONMENT="${1:?Environment required (integration|staging|prod-us|prod-eu)}" +AWS_REGION="${2:-us-east-1}" + +# Validate environment +if [[ ! "$ENVIRONMENT" =~ ^(integration|staging|prod-us|prod-eu)$ ]]; then + echo "โŒ Invalid environment. Use: integration, staging, prod-us, or prod-eu" + exit 1 +fi + + +echo "๐Ÿ” Creating secrets for ${ENVIRONMENT} in ${AWS_REGION}" + +# Create secret only if it doesn't exist (never updates existing secrets) +create_secret_if_not_exists() { + local name=$1 + local description=$2 + local value=$3 + + if aws secretsmanager describe-secret --secret-id "${name}" --region ${AWS_REGION} &>/dev/null; then + echo "โญ๏ธ ${name} already exists, skipping" + aws secretsmanager get-secret-value --secret-id "${name}" --region ${AWS_REGION} --query 'ARN' --output text + else + echo "๐Ÿ“ Creating ${name}" + aws secretsmanager create-secret \ + --name "${name}" \ + --description "${description}" \ + --secret-string "${value}" \ + --region ${AWS_REGION} \ + --query 'ARN' \ + --output text + fi +} + +# Generate secrets +echo "๐ŸŽฒ Generating secrets..." +DB_MASTER_PASSWORD=$(openssl rand -base64 64 | tr -dc 'a-zA-Z0-9' | head -c 64) +DB_APP_PASSWORD=$(openssl rand -base64 64 | tr -dc 'a-zA-Z0-9' | head -c 64) +SESSION_SECRET=$(openssl rand -base64 64 | tr -dc 'a-zA-Z0-9' | head -c 64) +JWT_SECRET=$(openssl rand -base64 64 | tr -dc 'a-zA-Z0-9' | head -c 64) + +# Create secrets in parallel (only creates, never updates) +echo "๐Ÿ“ฆ Storing in AWS Secrets Manager..." + +create_secret_if_not_exists \ + "plasmic/${ENVIRONMENT}/db/master-password" \ + "RDS master password for Plasmic ${ENVIRONMENT}" \ + "${DB_MASTER_PASSWORD}" & +PID_DB_MASTER=$! + +create_secret_if_not_exists \ + "plasmic/${ENVIRONMENT}/app/session-secret" \ + "Session encryption key for Plasmic ${ENVIRONMENT}" \ + "${SESSION_SECRET}" & +PID_SESSION=$! + +create_secret_if_not_exists \ + "plasmic/${ENVIRONMENT}/app/jwt-secret" \ + "JWT signing key for Plasmic ${ENVIRONMENT}" \ + "${JWT_SECRET}" & +PID_JWT=$! + +wait $PID_DB_MASTER $PID_DB_APP $PID_SESSION $PID_JWT + +echo "" +echo "๐ŸŽ‰ Secrets created for ${ENVIRONMENT}" +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo "Secret ARNs:" + +aws secretsmanager list-secrets \ + --region ${AWS_REGION} \ + --query "SecretList[?starts_with(Name, 'plasmic/${ENVIRONMENT}/')].{Name: Name, ARN: ARN}" \ + --output table + +echo "" +echo "๐Ÿ’ก Use these ARN values in your Terraform tfvars files" \ No newline at end of file diff --git a/terraform/scripts/deploy-all.sh b/terraform/scripts/deploy-all.sh new file mode 100755 index 000000000..34b799174 --- /dev/null +++ b/terraform/scripts/deploy-all.sh @@ -0,0 +1,259 @@ +#!/bin/bash +# Master deployment script for Plasmic infrastructure +# Usage: ./deploy-all.sh [environment] + +set -e + +ENVIRONMENT="${1:-integration}" +AWS_REGION="${2:-us-east-2}" + +echo "๐Ÿš€ Deploying Plasmic infrastructure to: $ENVIRONMENT" +echo "Region: $AWS_REGION" +echo "" + +# Colors for output +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +step() { + echo "" + echo -e "${GREEN}โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”${NC}" + echo -e "${GREEN}โ–ถ $1${NC}" + echo -e "${GREEN}โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”${NC}" +} + +warn() { + echo -e "${YELLOW}โš ๏ธ $1${NC}" +} + +# Check prerequisites +step "Step 0: Checking prerequisites" +command -v aws >/dev/null 2>&1 || { echo "AWS CLI not found. Install it first."; exit 1; } +command -v terraform >/dev/null 2>&1 || { echo "Terraform not found. Install it first."; exit 1; } +command -v docker >/dev/null 2>&1 || { echo "Docker not found. Install it first."; exit 1; } + +aws sts get-caller-identity >/dev/null 2>&1 || { echo "AWS credentials not configured."; exit 1; } + +ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) +echo "โœ… AWS Account: $ACCOUNT_ID" +echo "โœ… All prerequisites met" + +# 1. VPC +step "Step 1: Deploying VPC" +cd ../projects/vpc +terraform init -backend-config=config/${ENVIRONMENT}-backend.tfvars -reconfigure +terraform apply -var-file=config/${ENVIRONMENT}.tfvars -auto-approve +echo "โœ… VPC deployed" + +# 2. ECR (shared) +step "Step 2: Deploying ECR" +cd ../ecr +terraform init -backend-config=config/shared-backend.tfvars -reconfigure +terraform apply -var-file=config/shared.tfvars -auto-approve +echo "โœ… ECR deployed" + +# Save ECR repo URL +ECR_REPO=$(terraform output -raw repository_url) +echo "ECR Repository: $ECR_REPO" + +# 3. Secrets +step "Step 3: Deploying Secrets" +cd ../secrets +terraform init -backend-config=config/${ENVIRONMENT}-backend.tfvars -reconfigure +terraform apply -var-file=config/${ENVIRONMENT}.tfvars -auto-approve +echo "โœ… Secrets deployed" + +# 4. Database +step "Step 4: Deploying Database" +cd ../database +terraform init -backend-config=config/${ENVIRONMENT}-backend.tfvars -reconfigure +terraform apply -var-file=config/${ENVIRONMENT}.tfvars -auto-approve +echo "โœ… Database deployed" + +DB_ENDPOINT=$(terraform output -raw db_endpoint) +echo "Database endpoint: $DB_ENDPOINT" + +# 5. S3 Buckets +step "Step 5: Deploying S3 Buckets" + +# 5a. Site Assets +echo " โ†’ Deploying site-assets bucket..." +cd ../s3-site-assets +terraform init -backend-config=config/${ENVIRONMENT}-backend.tfvars -reconfigure +terraform apply -var-file=config/${ENVIRONMENT}.tfvars -auto-approve + +# 5b. Clips +echo " โ†’ Deploying clips bucket..." +cd ../s3-clips +terraform init -backend-config=config/${ENVIRONMENT}-backend.tfvars -reconfigure +terraform apply -var-file=config/${ENVIRONMENT}.tfvars -auto-approve + +# 5c. Assets +echo " โ†’ Deploying assets bucket..." +cd ../s3-assets +terraform init -backend-config=config/${ENVIRONMENT}-backend.tfvars -reconfigure +terraform apply -var-file=config/${ENVIRONMENT}.tfvars -auto-approve + +echo "โœ… S3 Buckets deployed" + +# 6. DynamoDB +step "Step 6: Deploying DynamoDB" +cd ../dynamodb +terraform init -backend-config=config/${ENVIRONMENT}-backend.tfvars -reconfigure +terraform apply -var-file=config/${ENVIRONMENT}.tfvars -auto-approve +echo "โœ… DynamoDB deployed" + +# 7. Frontend (S3 + CloudFront) +step "Step 7: Deploying Frontend Infrastructure" +cd ../frontend +terraform init -backend-config=config/${ENVIRONMENT}-backend.tfvars -reconfigure +terraform apply -var-file=config/${ENVIRONMENT}.tfvars -auto-approve +echo "โœ… Frontend Infrastructure deployed" + +FRONTEND_URL=$(terraform output -raw frontend_url) +HOST_URL=$(terraform output -raw host_url) +FRONTEND_CF_ID=$(terraform output -raw frontend_cloudfront_distribution_id) +HOST_CF_ID=$(terraform output -raw host_cloudfront_distribution_id) +echo "Frontend URL: $FRONTEND_URL" +echo "Host URL: $HOST_URL" + +# 8. ECS Cluster +step "Step 8: Deploying ECS Cluster" +cd ../ecs-cluster +terraform init -backend-config=config/${ENVIRONMENT}-backend.tfvars -reconfigure +terraform apply -var-file=config/${ENVIRONMENT}.tfvars -auto-approve +echo "โœ… ECS Cluster deployed" + +# 9. Check if Docker image exists +step "Step 9: Checking Docker image" +IMAGE_EXISTS=$(aws ecr describe-images \ + --repository-name plasmic/wab \ + --image-ids imageTag=${ENVIRONMENT}-latest \ + --region ${AWS_REGION} 2>/dev/null || echo "not_found") + +if [[ "$IMAGE_EXISTS" == "not_found" ]]; then + warn "Docker image not found in ECR" + warn "You need to build and push the image first:" + echo "" + echo " # Login to ECR" + echo " aws ecr get-login-password --region ${AWS_REGION} | \\" + echo " docker login --username AWS --password-stdin ${ECR_REPO}" + echo "" + echo " # Build and push" + echo " cd ../../.." + echo " docker build -t plasmic-wab -f platform/wab/Dockerfile platform/" + echo " docker tag plasmic-wab:latest ${ECR_REPO}:${ENVIRONMENT}-latest" + echo " docker push ${ECR_REPO}:${ENVIRONMENT}-latest" + echo "" + read -p "Press Enter after you've pushed the image, or Ctrl+C to cancel..." +else + echo "โœ… Docker image found: ${ENVIRONMENT}-latest" +fi + +# 10. Update config with ECR URL +step "Step 10: Updating config with ECR URL" +cd ../services/wab + +# Backup original config +cp config/${ENVIRONMENT}.tfvars config/${ENVIRONMENT}.tfvars.bak 2>/dev/null || true + +# Update ECR URL +sed -i.tmp "s|wab_container_image = \".*\"|wab_container_image = \"${ECR_REPO}:${ENVIRONMENT}-latest\"|g" config/${ENVIRONMENT}.tfvars +rm -f config/${ENVIRONMENT}.tfvars.tmp + +echo "โœ… Config updated" + +# 11. Deploy WAB Service +step "Step 11: Deploying WAB Service" +terraform init -backend-config=config/${ENVIRONMENT}-backend.tfvars -reconfigure +terraform apply -var-file=config/${ENVIRONMENT}.tfvars -auto-approve -lock=false +echo "โœ… WAB Service deployed" + +# 12. Deploy Codegen Service +#step "Step 12: Deploying Codegen Service" +#cd ../codegen +#terraform init -backend-config=config/${ENVIRONMENT}-backend.tfvars -reconfigure +#terraform apply -var-file=config/${ENVIRONMENT}.tfvars -auto-approve +#echo "โœ… Codegen Service deployed" + +# 13. Deploy Copilot Service - DISABLED for cost savings +# step "Step 13: Deploying Copilot Service" +# cd ../copilot +# terraform init -backend-config=config/${ENVIRONMENT}-backend.tfvars -reconfigure +# terraform apply -var-file=config/${ENVIRONMENT}.tfvars -auto-approve +# echo "โœ… Copilot Service deployed" + +# 14. Deploy Data Service +#step "Step 14: Deploying Data Service" +#cd ../data +#terraform init -backend-config=config/${ENVIRONMENT}-backend.tfvars -reconfigure +#terraform apply -var-file=config/${ENVIRONMENT}.tfvars -auto-approve +#echo "โœ… Data Service deployed" + +# 15. Get outputs +step "Step 15: Deployment Summary" +cd ../wab +APP_URL=$(terraform output -raw application_url) +LOG_GROUP=$(terraform output -raw log_group_name) + +echo "" +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo "๐ŸŽ‰ Deployment Complete!" +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo "" +echo "Environment: $ENVIRONMENT" +echo "Region: $AWS_REGION" +echo "" +echo "๐Ÿ“ URLs:" +echo " Backend API: $APP_URL" +echo " Frontend: $FRONTEND_URL" +echo " Host URL: $HOST_URL" +echo "" +echo "๐Ÿ“Š Resources:" +echo " VPC: plasmic-${ENVIRONMENT}" +echo " Database: plasmic-${ENVIRONMENT}-db" +echo " ECS Cluster: plasmic-${ENVIRONMENT}" +echo " ECS Services:" +echo " - plasmic-${ENVIRONMENT}-wab" +echo " - plasmic-${ENVIRONMENT}-codegen" +echo " - plasmic-${ENVIRONMENT}-data" +echo " - plasmic-${ENVIRONMENT}-copilot (disabled for cost savings)" +echo "" +echo "๐Ÿ“ CloudWatch Logs:" +echo " aws logs tail $LOG_GROUP --follow" +echo "" +echo "๐Ÿงช Test health endpoint:" +echo " curl ${APP_URL}/api/v1/health" +echo "" +echo "๐Ÿ” Check service status:" +echo " aws ecs describe-services \\" +echo " --cluster plasmic-${ENVIRONMENT} \\" +echo " --services plasmic-${ENVIRONMENT}-wab" +echo "" +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo "" + +# Wait for services to stabilize +echo "Waiting for all services to stabilize (this may take 2-3 minutes)..." +aws ecs wait services-stable \ + --cluster plasmic-${ENVIRONMENT} \ + --services plasmic-${ENVIRONMENT}-wab plasmic-${ENVIRONMENT}-codegen plasmic-${ENVIRONMENT}-data \ + --region ${AWS_REGION} + +echo "" +echo "โœ… All services are stable!" +echo "" + +# Test health endpoint +echo "Testing health endpoint..." +sleep 10 # Give ALB a moment +if curl -f -s -o /dev/null ${APP_URL}/api/v1/health; then + echo "โœ… Health check passed!" +else + warn "Health check failed. Check logs:" + echo " aws logs tail $LOG_GROUP --follow" +fi + +echo "" +echo "๐Ÿš€ Deployment successful!" diff --git a/terraform/scripts/deploy-frontend.sh b/terraform/scripts/deploy-frontend.sh new file mode 100755 index 000000000..bff0547f0 --- /dev/null +++ b/terraform/scripts/deploy-frontend.sh @@ -0,0 +1,275 @@ +#!/bin/bash +# Frontend build and deployment script for Plasmic +# Usage: ./deploy-frontend.sh [environment] [aws-region] + +set -e + +ENVIRONMENT="${1:-integration}" +AWS_REGION="${2:-us-east-2}" + +echo "๐ŸŽจ Building and deploying Plasmic frontend to: $ENVIRONMENT" +echo "Region: $AWS_REGION" +echo "" + +# Colors for output +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +step() { + echo "" + echo -e "${GREEN}โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”${NC}" + echo -e "${GREEN}โ–ถ $1${NC}" + echo -e "${GREEN}โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”${NC}" +} + +warn() { + echo -e "${YELLOW}โš ๏ธ $1${NC}" +} + +error() { + echo -e "${RED}โŒ $1${NC}" +} + +# Get CloudFront distribution IDs and URLs from Terraform +step "Step 1: Getting infrastructure details" +cd ../projects/frontend +terraform init -backend-config=config/${ENVIRONMENT}-backend.tfvars -reconfigure >/dev/null 2>&1 + +FRONTEND_URL=$(terraform output -raw frontend_url) +HOST_URL=$(terraform output -raw host_url) +FRONTEND_CF_ID=$(terraform output -raw frontend_cloudfront_distribution_id) +HOST_CF_ID=$(terraform output -raw host_cloudfront_distribution_id) +FRONTEND_BUCKET=$(terraform output -raw frontend_bucket_name) +HOST_BUCKET=$(terraform output -raw host_bucket_name) + +echo "โœ… Frontend URL: $FRONTEND_URL" +echo "โœ… Host URL: $HOST_URL" +echo "โœ… Frontend S3 Bucket: $FRONTEND_BUCKET" +echo "โœ… Host S3 Bucket: $HOST_BUCKET" + +# Get backend API URL +cd ../services/wab +APP_URL=$(terraform output -raw application_url) +echo "โœ… Backend API: $APP_URL" + +# Navigate to platform directory (from terraform/projects/services/wab to platform/wab) +cd ../../../../platform/wab + +# Install dependencies +step "Step 2: Installing dependencies" +echo "Installing root dependencies..." +cd ../.. +yarn install --frozen-lockfile + +echo "Setting up monorepo packages..." +yarn setup +yarn setup:canvas-packages + +echo "Installing wab dependencies..." +cd platform/wab +yarn install --frozen-lockfile + +# Add missing dependencies if needed +yarn add --dev raw-loader 2>/dev/null || true + +echo "โœ… Dependencies installed" + +# Generate required files +step "Step 3: Generating required files" +echo "Generating model classes and parsers..." +make +echo "โœ… Generated required files" + +# Build CSS files +step "Step 4: Building CSS files" +yarn build-css +echo "โœ… CSS files built" + +# Create .env file +step "Step 5: Creating environment configuration" +cat > .env << EOF +REACT_APP_DEFAULT_HOST_URL=${HOST_URL} +AMPLITUDE_API_KEY=placeholder +INTERCOM_APP_ID=placeholder +POSTHOG_API_KEY=placeholder +POSTHOG_HOST=placeholder +POSTHOG_REVERSE_PROXY_HOST=placeholder +SENTRY_DSN=placeholder +SENTRY_ORG_ID=placeholder +SENTRY_PROJECT_ID=placeholder +STRIPE_PUBLISHABLE_KEY=placeholder +EOF + +echo "โœ… Environment configuration created" + +# Patch rsbuild.config.ts +step "Step 6: Patching build configuration" +cat > patch-rsbuild.js << 'PATCH_EOF' +const fs = require('fs'); +const content = fs.readFileSync('rsbuild.config.ts', 'utf8'); + +// Add REACT_APP_DEFAULT_HOST_URL after STRIPE_PUBLISHABLE_KEY +const patched = content.replace( + /(\s*)(STRIPE_PUBLISHABLE_KEY: OPTIONAL_VAR,)/, + '$1$2\n$1REACT_APP_DEFAULT_HOST_URL: REQUIRED_VAR,' +); + +if (patched === content) { + console.error('Failed to patch rsbuild.config.ts - pattern not found'); + process.exit(1); +} + +fs.writeFileSync('rsbuild.config.ts', patched); +console.log('โœ… Patched rsbuild.config.ts to include REACT_APP_DEFAULT_HOST_URL'); +PATCH_EOF + +node patch-rsbuild.js +rm patch-rsbuild.js +echo "โœ… Build configuration patched" + +# Build frontend +step "Step 7: Building frontend" +NODE_ENV=production PUBLIC_URL=${FRONTEND_URL} yarn build +echo "โœ… Frontend built successfully" +ls -lh build/ + +# Deploy to S3 +step "Step 8: Deploying to S3" + +# Sync all assets with cache headers (excluding HTML files) +echo "Uploading static assets..." +aws s3 sync build/ s3://${FRONTEND_BUCKET}/ \ + --delete \ + --cache-control "public, max-age=31536000, immutable" \ + --exclude "index.html" \ + --exclude "*.map" \ + --exclude "static/js/*.js" \ + --exclude "static/css/*.css" \ + --exclude "static/host.html" \ + --exclude "static/popup.html" \ + --exclude "static/sub/*" \ + --region ${AWS_REGION} + +# Upload JS and CSS with specific cache headers +echo "Uploading JavaScript files..." +aws s3 sync build/static/js s3://${FRONTEND_BUCKET}/static/js \ + --cache-control "public, max-age=31536000, immutable" \ + --content-type "application/javascript" \ + --region ${AWS_REGION} + +echo "Uploading CSS files..." +aws s3 sync build/static/css s3://${FRONTEND_BUCKET}/static/css \ + --cache-control "public, max-age=31536000, immutable" \ + --content-type "text/css" \ + --region ${AWS_REGION} + +# Upload index.html with no-cache +echo "Uploading index.html..." +aws s3 cp build/index.html s3://${FRONTEND_BUCKET}/ \ + --cache-control "no-cache, no-store, must-revalidate" \ + --content-type "text/html" \ + --region ${AWS_REGION} + +echo "โœ… Frontend deployed to S3" + +# Deploy Host Files +step "Step 9: Deploying host files" + +# Create deployment directory +mkdir -p ../../host-deploy/static + +# Copy host files - maintaining the /static/ structure +if [ -f build/static/host.html ]; then + cp build/static/host.html ../../host-deploy/static/ + echo "โœ… Copied host.html" +fi + +if [ -d build/static/sub ]; then + cp -r build/static/sub ../../host-deploy/static/ + echo "โœ… Copied sub directory" +fi + +if [ -f build/static/popup.html ]; then + cp build/static/popup.html ../../host-deploy/static/ + echo "โœ… Copied popup.html" +fi + +if [ -d build/static/styles ]; then + mkdir -p ../../host-deploy/static/styles + cp -r build/static/styles/* ../../host-deploy/static/styles/ 2>/dev/null || true + echo "โœ… Copied styles" +fi + +# Deploy to Host S3 bucket +echo "Uploading to host S3 bucket..." +aws s3 sync ../../host-deploy/ s3://${HOST_BUCKET}/ \ + --delete \ + --cache-control "public, max-age=31536000, immutable" \ + --exclude "*.html" \ + --region ${AWS_REGION} + +# Upload HTML files with shorter cache +if [ -f ../../host-deploy/static/host.html ]; then + aws s3 cp ../../host-deploy/static/host.html s3://${HOST_BUCKET}/static/host.html \ + --cache-control "public, max-age=300" \ + --content-type "text/html" \ + --region ${AWS_REGION} +fi + +if [ -f ../../host-deploy/static/popup.html ]; then + aws s3 cp ../../host-deploy/static/popup.html s3://${HOST_BUCKET}/static/popup.html \ + --cache-control "public, max-age=300" \ + --content-type "text/html" \ + --region ${AWS_REGION} +fi + +# Cleanup +rm -rf ../../host-deploy + +echo "โœ… Host files deployed" + +# Invalidate CloudFront +step "Step 10: Invalidating CloudFront caches" + +echo "Invalidating frontend CloudFront..." +aws cloudfront create-invalidation \ + --distribution-id ${FRONTEND_CF_ID} \ + --paths "/index.html" "/*" \ + --region ${AWS_REGION} >/dev/null + +echo "Invalidating host CloudFront..." +aws cloudfront create-invalidation \ + --distribution-id ${HOST_CF_ID} \ + --paths "/*" \ + --region ${AWS_REGION} >/dev/null + +echo "โœ… CloudFront invalidations created" + +# Summary +echo "" +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo "๐ŸŽ‰ Frontend Deployment Complete!" +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo "" +echo "Environment: $ENVIRONMENT" +echo "Region: $AWS_REGION" +echo "" +echo "๐Ÿ“ URLs:" +echo " Frontend: $FRONTEND_URL" +echo " Host: $HOST_URL" +echo " Backend API: $APP_URL" +echo "" +echo "๐Ÿ“ฆ S3 Buckets:" +echo " Frontend: $FRONTEND_BUCKET" +echo " Host: $HOST_BUCKET" +echo "" +echo "๐Ÿ”„ CloudFront Distributions:" +echo " Frontend: $FRONTEND_CF_ID" +echo " Host: $HOST_CF_ID" +echo "" +echo "โฑ๏ธ Note: CloudFront invalidations may take 5-10 minutes to complete" +echo "" +echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" +echo "" \ No newline at end of file From d9bd7ca5a18cb35d5599ad4020f2366781fcfaa9 Mon Sep 17 00:00:00 2001 From: James Willis Date: Thu, 23 Oct 2025 14:38:34 +0100 Subject: [PATCH 2/4] removed http listener --- terraform/projects/ecs-cluster/alb.tf | 18 ----------------- terraform/projects/ecs-cluster/outputs.tf | 5 ----- .../projects/ecs-cluster/security-groups.tf | 9 --------- .../projects/services/codegen/remote-state.tf | 2 +- .../projects/services/copilot/remote-state.tf | 2 +- .../projects/services/data/remote-state.tf | 2 +- terraform/scripts/deploy-all.sh | 20 +++++++++---------- 7 files changed, 13 insertions(+), 45 deletions(-) diff --git a/terraform/projects/ecs-cluster/alb.tf b/terraform/projects/ecs-cluster/alb.tf index 2e4ce6054..4ab4c6861 100644 --- a/terraform/projects/ecs-cluster/alb.tf +++ b/terraform/projects/ecs-cluster/alb.tf @@ -14,24 +14,6 @@ resource "aws_lb" "main" { } } -# HTTP Listener with default 404 response -# Services will add their own listener rules -resource "aws_lb_listener" "http" { - load_balancer_arn = aws_lb.main.arn - port = "80" - protocol = "HTTP" - - default_action { - type = "fixed-response" - - fixed_response { - content_type = "text/plain" - message_body = "Service not found" - status_code = "404" - } - } -} - # Self-signed certificate for HTTPS listener (integration environment) resource "tls_private_key" "alb" { algorithm = "RSA" diff --git a/terraform/projects/ecs-cluster/outputs.tf b/terraform/projects/ecs-cluster/outputs.tf index b5244076b..af3ea5fcc 100644 --- a/terraform/projects/ecs-cluster/outputs.tf +++ b/terraform/projects/ecs-cluster/outputs.tf @@ -29,11 +29,6 @@ output "alb_dns_name" { value = aws_lb.main.dns_name } -output "alb_listener_arn" { - description = "ALB HTTP listener ARN (deprecated, use alb_https_listener_arn)" - value = aws_lb_listener.http.arn -} - output "alb_https_listener_arn" { description = "ALB HTTPS listener ARN" value = aws_lb_listener.https.arn diff --git a/terraform/projects/ecs-cluster/security-groups.tf b/terraform/projects/ecs-cluster/security-groups.tf index a31f3dfc9..2b51b637b 100644 --- a/terraform/projects/ecs-cluster/security-groups.tf +++ b/terraform/projects/ecs-cluster/security-groups.tf @@ -13,15 +13,6 @@ resource "aws_security_group" "alb" { } } -resource "aws_vpc_security_group_ingress_rule" "alb_http" { - security_group_id = aws_security_group.alb.id - description = "HTTP from internet" - from_port = 80 - to_port = 80 - ip_protocol = "tcp" - cidr_ipv4 = "0.0.0.0/0" -} - resource "aws_vpc_security_group_ingress_rule" "alb_https" { security_group_id = aws_security_group.alb.id description = "HTTPS from internet" diff --git a/terraform/projects/services/codegen/remote-state.tf b/terraform/projects/services/codegen/remote-state.tf index 34e9098c5..3a341ea73 100644 --- a/terraform/projects/services/codegen/remote-state.tf +++ b/terraform/projects/services/codegen/remote-state.tf @@ -39,7 +39,7 @@ locals { cluster_id = data.terraform_remote_state.ecs_cluster.outputs.cluster_id cluster_name = data.terraform_remote_state.ecs_cluster.outputs.cluster_name alb_arn = data.terraform_remote_state.ecs_cluster.outputs.alb_arn - alb_listener_arn = data.terraform_remote_state.ecs_cluster.outputs.alb_listener_arn + alb_listener_arn = data.terraform_remote_state.ecs_cluster.outputs.alb_https_listener_arn alb_security_group_id = data.terraform_remote_state.ecs_cluster.outputs.alb_security_group_id ecs_security_group_id = data.terraform_remote_state.ecs_cluster.outputs.ecs_security_group_id execution_role_arn = data.terraform_remote_state.ecs_cluster.outputs.execution_role_arn diff --git a/terraform/projects/services/copilot/remote-state.tf b/terraform/projects/services/copilot/remote-state.tf index f8c3b5028..50201c72d 100644 --- a/terraform/projects/services/copilot/remote-state.tf +++ b/terraform/projects/services/copilot/remote-state.tf @@ -69,7 +69,7 @@ locals { # ECS Cluster (includes ALB, security groups, execution role) alb_arn = data.terraform_remote_state.ecs_cluster.outputs.alb_arn - alb_listener_arn = data.terraform_remote_state.ecs_cluster.outputs.alb_listener_arn + alb_listener_arn = data.terraform_remote_state.ecs_cluster.outputs.alb_https_listener_arn alb_security_group_id = data.terraform_remote_state.ecs_cluster.outputs.alb_security_group_id ecs_security_group_id = data.terraform_remote_state.ecs_cluster.outputs.ecs_security_group_id execution_role_arn = data.terraform_remote_state.ecs_cluster.outputs.execution_role_arn diff --git a/terraform/projects/services/data/remote-state.tf b/terraform/projects/services/data/remote-state.tf index bdaf099d7..9d7179290 100644 --- a/terraform/projects/services/data/remote-state.tf +++ b/terraform/projects/services/data/remote-state.tf @@ -47,7 +47,7 @@ locals { # ECS Cluster (includes ALB, security groups, execution role) alb_arn = data.terraform_remote_state.ecs_cluster.outputs.alb_arn - alb_listener_arn = data.terraform_remote_state.ecs_cluster.outputs.alb_listener_arn + alb_listener_arn = data.terraform_remote_state.ecs_cluster.outputs.alb_https_listener_arn alb_security_group_id = data.terraform_remote_state.ecs_cluster.outputs.alb_security_group_id ecs_security_group_id = data.terraform_remote_state.ecs_cluster.outputs.ecs_security_group_id execution_role_arn = data.terraform_remote_state.ecs_cluster.outputs.execution_role_arn diff --git a/terraform/scripts/deploy-all.sh b/terraform/scripts/deploy-all.sh index 34b799174..55282f0eb 100755 --- a/terraform/scripts/deploy-all.sh +++ b/terraform/scripts/deploy-all.sh @@ -171,11 +171,11 @@ terraform apply -var-file=config/${ENVIRONMENT}.tfvars -auto-approve -lock=false echo "โœ… WAB Service deployed" # 12. Deploy Codegen Service -#step "Step 12: Deploying Codegen Service" -#cd ../codegen -#terraform init -backend-config=config/${ENVIRONMENT}-backend.tfvars -reconfigure -#terraform apply -var-file=config/${ENVIRONMENT}.tfvars -auto-approve -#echo "โœ… Codegen Service deployed" +step "Step 12: Deploying Codegen Service" +cd ../codegen +terraform init -backend-config=config/${ENVIRONMENT}-backend.tfvars -reconfigure +terraform apply -var-file=config/${ENVIRONMENT}.tfvars -auto-approve +echo "โœ… Codegen Service deployed" # 13. Deploy Copilot Service - DISABLED for cost savings # step "Step 13: Deploying Copilot Service" @@ -185,11 +185,11 @@ echo "โœ… WAB Service deployed" # echo "โœ… Copilot Service deployed" # 14. Deploy Data Service -#step "Step 14: Deploying Data Service" -#cd ../data -#terraform init -backend-config=config/${ENVIRONMENT}-backend.tfvars -reconfigure -#terraform apply -var-file=config/${ENVIRONMENT}.tfvars -auto-approve -#echo "โœ… Data Service deployed" +step "Step 14: Deploying Data Service" +cd ../data +terraform init -backend-config=config/${ENVIRONMENT}-backend.tfvars -reconfigure +terraform apply -var-file=config/${ENVIRONMENT}.tfvars -auto-approve +echo "โœ… Data Service deployed" # 15. Get outputs step "Step 15: Deployment Summary" From 39d9163f663fe8d36309a26b28220c97b6999832 Mon Sep 17 00:00:00 2001 From: James Willis Date: Thu, 23 Oct 2025 15:03:32 +0100 Subject: [PATCH 3/4] set ALB access only from cloudfront. --- terraform/projects/ecs-cluster/security-groups.tf | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/terraform/projects/ecs-cluster/security-groups.tf b/terraform/projects/ecs-cluster/security-groups.tf index 2b51b637b..07938e41e 100644 --- a/terraform/projects/ecs-cluster/security-groups.tf +++ b/terraform/projects/ecs-cluster/security-groups.tf @@ -1,3 +1,8 @@ +# Get CloudFront managed prefix list for origin-facing IPs +data "aws_ec2_managed_prefix_list" "cloudfront" { + name = "com.amazonaws.global.cloudfront.origin-facing" +} + # ALB Security Group resource "aws_security_group" "alb" { name_prefix = "${local.cluster_name}-alb-" @@ -15,11 +20,11 @@ resource "aws_security_group" "alb" { resource "aws_vpc_security_group_ingress_rule" "alb_https" { security_group_id = aws_security_group.alb.id - description = "HTTPS from internet" + description = "HTTPS from CloudFront" from_port = 443 to_port = 443 ip_protocol = "tcp" - cidr_ipv4 = "0.0.0.0/0" + prefix_list_id = data.aws_ec2_managed_prefix_list.cloudfront.id } resource "aws_vpc_security_group_egress_rule" "alb_to_ecs" { From 02465978c40904970b80f8e81dac595ee82a9329 Mon Sep 17 00:00:00 2001 From: James Willis Date: Fri, 24 Oct 2025 11:36:50 +0100 Subject: [PATCH 4/4] revert platform dir --- platform/live-frame/yarn.lock | 38 +++++++++++++++---------------- platform/wab/package.json | 1 - platform/wab/rsbuild.config.ts | 2 -- platform/wab/yarn.lock | 41 ++++++++++++++++++++++++---------- 4 files changed, 48 insertions(+), 34 deletions(-) diff --git a/platform/live-frame/yarn.lock b/platform/live-frame/yarn.lock index 5c613c8b9..15354044e 100644 --- a/platform/live-frame/yarn.lock +++ b/platform/live-frame/yarn.lock @@ -112,21 +112,21 @@ resolved "https://registry.yarnpkg.com/@plasmicapp/data-sources-context/-/data-sources-context-0.1.22.tgz#03ee66b603df6bea52ff0f4708a6cd019e57c73a" integrity sha512-FxXHCZj/pVysamgBhbeVKP14xTfilQI+2peZixrY09gKCz+C2iVqKZCKuwhuXodFdMfCveiMSzWg8Hqz9xJRqQ== -"@plasmicapp/data-sources@0.1.190": - version "0.1.190" - resolved "https://registry.yarnpkg.com/@plasmicapp/data-sources/-/data-sources-0.1.190.tgz#44e6b669d00367c003eb9ab0abb3025cfb2e5ce3" - integrity sha512-oivcCp+F4GS8DTDieng8qTlnUcRa7lni+GINCn6egRURwA9LV2CtMv/z7m80dNC8myuNpEi7U3x7/XIs/f9FTg== +"@plasmicapp/data-sources@0.1.188": + version "0.1.188" + resolved "https://registry.yarnpkg.com/@plasmicapp/data-sources/-/data-sources-0.1.188.tgz#5ac1613f8781deeb5c05f875b2f0118925180fab" + integrity sha512-MFynj9ZWGi99wQldotfgx5afgEcQ7MXBU7bblF2b9B5WapZtiXWR62l9JBs/VMU1yRfVkm7mZVXBBCHRAayp8A== dependencies: "@plasmicapp/data-sources-context" "0.1.22" - "@plasmicapp/host" "1.0.226" + "@plasmicapp/host" "1.0.224" "@plasmicapp/isomorphic-unfetch" "1.0.3" "@plasmicapp/query" "0.1.80" fast-stringify "^2.0.0" -"@plasmicapp/host@1.0.226": - version "1.0.226" - resolved "https://registry.yarnpkg.com/@plasmicapp/host/-/host-1.0.226.tgz#3807c401d2ac3c126d18973684b42db23abeba75" - integrity sha512-8tCtx2FqRaPkKREkFbwfuoVmhCmUl/BS7/7sE/xO1IlZXrKWh4qQV4t2iXQLaz4QmectkNaFm38BUjlnnpKYvA== +"@plasmicapp/host@1.0.224": + version "1.0.224" + resolved "https://registry.yarnpkg.com/@plasmicapp/host/-/host-1.0.224.tgz#e37ac84107ca786a266dcbb495127c59e8448c75" + integrity sha512-vvyTVIPUjjfG148RyKuRYylpzpJPb0YNdlYNlaQGoxIdO3xjTGSS1v0ouI9zG80CKI3MqtYRgfoCFQ1RycbSUg== dependencies: "@plasmicapp/query" "0.1.80" csstype "^3.1.2" @@ -139,17 +139,17 @@ dependencies: unfetch "^4.2.0" -"@plasmicapp/loader-splits@1.0.65": - version "1.0.65" - resolved "https://registry.yarnpkg.com/@plasmicapp/loader-splits/-/loader-splits-1.0.65.tgz#c167efe15b46c2505abdd4d13f45926c99d11e99" - integrity sha512-kU+5Ky157i9JUu5cSCp+yIohQKF2lGNr4mQcaV7SHpE7aexQ4WDIr36kyZehwh4EcFTIHHc3H2g+DzAt64AIDA== +"@plasmicapp/loader-splits@1.0.64": + version "1.0.64" + resolved "https://registry.yarnpkg.com/@plasmicapp/loader-splits/-/loader-splits-1.0.64.tgz#07a68e66927eb97aa6814a794754c0abe7110641" + integrity sha512-PrZNSokH7aedwXtFD0tWn/P7yL+h1oDpEqKDm7zD0d0tjq6spL90I61lBU8MJlc5/dngLBrovLPyzO51EmPLqg== dependencies: json-logic-js "^2.0.2" -"@plasmicapp/nextjs-app-router@1.0.18": - version "1.0.18" - resolved "https://registry.yarnpkg.com/@plasmicapp/nextjs-app-router/-/nextjs-app-router-1.0.18.tgz#86ed48fabbfc1790b4aa4b41ce95cd00500b1c1b" - integrity sha512-Rai/CrOfOzr6nmuDvNS18Rfl4Qsn2fk+vYYgNWUlcp3S76XW0RFJPBt0iDpBo+f1ysHfkI+L0P8hz+aSPUVL6w== +"@plasmicapp/nextjs-app-router@1.0.17": + version "1.0.17" + resolved "https://registry.yarnpkg.com/@plasmicapp/nextjs-app-router/-/nextjs-app-router-1.0.17.tgz#bee6242f48e83f516a44901e70a63351154d3150" + integrity sha512-aIYkQZoFunwDGo9Xf+zeJagHasVyORa41RtD17RbDvk6CSQM3rtTEwvz/xua0KZsF1DTfE8bMUP1pCRdJGQrbA== dependencies: "@plasmicapp/prepass" "1.0.20" "@plasmicapp/query" "0.1.80" @@ -1684,7 +1684,7 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= -loader-utils@^1.1.0: +loader-utils@1.4.2, loader-utils@^1.1.0: version "1.4.2" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.2.tgz#29a957f3a63973883eb684f10ffd3d151fec01a3" integrity sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg== @@ -1737,7 +1737,7 @@ minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" -minimist@^1.2.0, minimist@^1.2.5: +minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== diff --git a/platform/wab/package.json b/platform/wab/package.json index 038424df0..c270cfb1d 100644 --- a/platform/wab/package.json +++ b/platform/wab/package.json @@ -168,7 +168,6 @@ "pegjs": "~0.10.0", "pegjs-coffee-plugin": "~0.3.0", "prando": "^6.0.1", - "raw-loader": "^4.0.2", "storybook": "^7.6.20", "sucrase": "^3.35.0", "ts-node": "^10.9.2", diff --git a/platform/wab/rsbuild.config.ts b/platform/wab/rsbuild.config.ts index 23bd974f4..8980a457a 100644 --- a/platform/wab/rsbuild.config.ts +++ b/platform/wab/rsbuild.config.ts @@ -233,8 +233,6 @@ export default defineConfig({ SENTRY_ORG_ID: OPTIONAL_VAR, SENTRY_PROJECT_ID: OPTIONAL_VAR, STRIPE_PUBLISHABLE_KEY: OPTIONAL_VAR, - - REACT_APP_DEFAULT_HOST_URL: REQUIRED_VAR, }) ), new MonacoWebpackPlugin(), diff --git a/platform/wab/yarn.lock b/platform/wab/yarn.lock index 1ebde0931..7edc10e0c 100644 --- a/platform/wab/yarn.lock +++ b/platform/wab/yarn.lock @@ -15580,7 +15580,7 @@ loader-utils@^1.1.0: emojis-list "^3.0.0" json5 "^1.0.1" -loader-utils@^2.0.0, loader-utils@^2.0.2, loader-utils@^2.0.4: +loader-utils@^2.0.2, loader-utils@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c" integrity sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw== @@ -18256,14 +18256,6 @@ raw-body@2.5.2: iconv-lite "0.4.24" unpipe "1.0.0" -raw-loader@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-4.0.2.tgz#1aac6b7d1ad1501e66efdac1522c73e59a584eb6" - integrity sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA== - dependencies: - loader-utils "^2.0.0" - schema-utils "^3.0.0" - rc-align@^4.0.0: version "4.0.9" resolved "https://registry.yarnpkg.com/rc-align/-/rc-align-4.0.9.tgz#46d8801c4a139ff6a65ad1674e8efceac98f85f2" @@ -20969,7 +20961,16 @@ string-length@^5.0.1: char-regex "^2.0.0" strip-ansi "^7.0.1" -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -21019,7 +21020,7 @@ stringify-object@^3.2.1: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -21033,6 +21034,13 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2" @@ -22701,7 +22709,7 @@ workerpool@^6.1.4: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.1.4.tgz#6a972b6df82e38d50248ee2820aa98e2d0ad3090" integrity sha512-jGWPzsUqzkow8HoAvqaPWTUPCrlPJaJ5tY8Iz7n1uCz3tTp6s3CDG0FF1NsX42WNlkRSW6Mr+CDZGnNoSsKa7g== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -22728,6 +22736,15 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"