From 9a693bbd5b346861b88b345f3bf3137a77bbdcfb Mon Sep 17 00:00:00 2001 From: Max Isom Date: Tue, 25 Jul 2023 20:25:16 -0700 Subject: [PATCH] Deploy to Google Cloud (#159) Co-authored-by: Anup Mantri --- .prettierignore | 1 + Dockerfile | 4 +- package-lock.json | 223 +++++++++++++------ package.json | 6 +- src/backend/scripts/run-with-gcp-metadata.ts | 63 ++++++ terraform/.gitignore | 1 + terraform/README.md | 24 +- terraform/backend.hcl | 2 + terraform/gcs-backend/main.tf | 44 ++++ terraform/gcs-backend/variables.tf | 4 + terraform/gcs/cloud_run.tf | 71 ++++++ terraform/gcs/cloud_sql.tf | 73 ++++++ terraform/gcs/main.tf | 15 ++ terraform/gcs/project_services.tf | 14 ++ terraform/gcs/secrets.tf | 28 +++ terraform/gcs/variables.tf | 15 ++ 16 files changed, 507 insertions(+), 81 deletions(-) create mode 100644 src/backend/scripts/run-with-gcp-metadata.ts create mode 100644 terraform/.gitignore create mode 100644 terraform/backend.hcl create mode 100644 terraform/gcs-backend/main.tf create mode 100644 terraform/gcs-backend/variables.tf create mode 100644 terraform/gcs/cloud_run.tf create mode 100644 terraform/gcs/cloud_sql.tf create mode 100644 terraform/gcs/main.tf create mode 100644 terraform/gcs/project_services.tf create mode 100644 terraform/gcs/secrets.tf create mode 100644 terraform/gcs/variables.tf diff --git a/.prettierignore b/.prettierignore index eaebc651..411bfa3b 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,4 @@ src/backend/db/zapatos .next .nsm +.terraform diff --git a/Dockerfile b/Dockerfile index 104a1bcc..a0a4198a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,11 +26,11 @@ ENV APP_HOME=/opt/node/app USER node:node WORKDIR $APP_HOME -COPY --chown=node:node package*.json . +COPY --chown=node:node package*.json ./ # We install Husky via a "prepare" lifecycle script - disable it in prod # See: https://typicode.github.io/husky/guide.html#disable-husky-in-ci-docker-prod -RUN npm pkg delete scripts.prepare +RUN npm pkg delete scripts.prepare && npm pkg delete scripts.postinstall RUN npm ci # Copy the project files into the app directory diff --git a/package-lock.json b/package-lock.json index 0d958b91..98d19460 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,8 @@ "@trpc/server": "^10.16.0", "axios": "^1.3.2", "dotenv": "16.0.3", + "execa": "^7.1.1", + "gcp-metadata": "^6.0.0", "kysely": "^0.23.4", "ms": "^2.1.3", "next": "13.1.6", @@ -34,6 +36,7 @@ "react-hot-toast": "^2.4.1", "react-timer-hook": "^3.0.6", "superjson": "^1.12.3", + "tsx": "^3.12.7", "winston": "^3.8.2", "zapatos": "^6.1.4", "zod": "^3.21.4" @@ -1615,7 +1618,6 @@ }, "node_modules/@esbuild-kit/cjs-loader": { "version": "2.4.2", - "dev": true, "license": "MIT", "dependencies": { "@esbuild-kit/core-utils": "^3.0.0", @@ -1624,13 +1626,21 @@ }, "node_modules/@esbuild-kit/core-utils": { "version": "3.1.0", - "dev": true, "license": "MIT", "dependencies": { "esbuild": "~0.17.6", "source-map-support": "^0.5.21" } }, + "node_modules/@esbuild-kit/esm-loader": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.5.5.tgz", + "integrity": "sha512-Qwfvj/qoPbClxCRNuac1Du01r9gvNOT+pMYtJDapfB1eoGN1YlJ1BixLyL9WVENRx5RXgNLdfYdx/CuswlGhMw==", + "dependencies": { + "@esbuild-kit/core-utils": "^3.0.0", + "get-tsconfig": "^4.4.0" + } + }, "node_modules/@eslint/eslintrc": { "version": "1.4.1", "dev": true, @@ -3105,6 +3115,17 @@ "node": ">=0.4.0" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/aggregate-error": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-4.0.1.tgz", @@ -3772,6 +3793,14 @@ "node": ">=0.6" } }, + "node_modules/bignumber.js": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.1.tgz", + "integrity": "sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig==", + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -3880,8 +3909,7 @@ "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, "node_modules/buffer-writer": { "version": "2.0.0", @@ -4474,7 +4502,6 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -4526,7 +4553,6 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "license": "MIT", "dependencies": { "ms": "2.1.2" @@ -4544,7 +4570,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true, "license": "MIT" }, "node_modules/deep-is": { @@ -4588,38 +4613,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/default-browser/node_modules/execa": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-7.1.1.tgz", - "integrity": "sha512-wH0eMf/UXckdUYnO21+HDztteVv05rq2GXksxT4fCGeHkBhw1DROXh40wcjMcRqDOWE7iPJ4n3M7e2+YFP+76Q==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.1", - "human-signals": "^4.3.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^3.0.7", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": "^14.18.0 || ^16.14.0 || >=18.0.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/default-browser/node_modules/human-signals": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", - "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", - "dev": true, - "engines": { - "node": ">=14.18.0" - } - }, "node_modules/define-lazy-prop": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", @@ -4989,7 +4982,6 @@ }, "node_modules/esbuild": { "version": "0.17.8", - "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -5633,13 +5625,13 @@ "license": "MIT" }, "node_modules/execa": { - "version": "6.1.0", - "dev": true, - "license": "MIT", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-7.1.1.tgz", + "integrity": "sha512-wH0eMf/UXckdUYnO21+HDztteVv05rq2GXksxT4fCGeHkBhw1DROXh40wcjMcRqDOWE7iPJ4n3M7e2+YFP+76Q==", "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.1", - "human-signals": "^3.0.1", + "human-signals": "^4.3.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", @@ -5648,12 +5640,17 @@ "strip-final-newline": "^3.0.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": "^14.18.0 || ^16.14.0 || >=18.0.0" }, "funding": { "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -5955,7 +5952,6 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, "hasInstallScript": true, "optional": true, "os": [ @@ -5997,6 +5993,43 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gaxios": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.0.3.tgz", + "integrity": "sha512-ns+NiTWT9daIerko/qAj8HSPNuNNlzGGrEVB7y8MQ8CP0bymuR8fCql6Ec+rlh7b9BW18JDXQnJNXWMiWO3jlg==", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gaxios/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gcp-metadata": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.0.0.tgz", + "integrity": "sha512-Ozxyi23/1Ar51wjUT2RDklK+3HxqDr8TLBNK8rBBFQ7T85iIGnXnVusauj06QyqCXRFZig8LZC+TUddWbndlpQ==", + "dependencies": { + "gaxios": "^6.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -6039,7 +6072,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -6068,7 +6100,6 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.6.2.tgz", "integrity": "sha512-E5XrT4CbbXcXWy+1jChlZmrmCwd5KGx502kDCXJJ7y898TtWW9FwoG5HfOLVRKmlmDGkWN2HM9Ho+/Y8F0sJDg==", - "dev": true, "dependencies": { "resolve-pkg-maps": "^1.0.0" }, @@ -6374,12 +6405,24 @@ "node": ">=8.0.0" } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/human-signals": { - "version": "3.0.1", - "dev": true, - "license": "Apache-2.0", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", + "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", "engines": { - "node": ">=12.20.0" + "node": ">=14.18.0" } }, "node_modules/husky": { @@ -6842,7 +6885,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true, "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" @@ -6966,7 +7008,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/jackspeak": { @@ -7034,6 +7075,14 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -7233,6 +7282,38 @@ "url": "https://opencollective.com/lint-staged" } }, + "node_modules/lint-staged/node_modules/execa": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-6.1.0.tgz", + "integrity": "sha512-QVWlX2e50heYJcCPG0iWtf8r0xjEYfz/OYLGDYH+IyjWezzPNxz63qNFOu0l4YftGWuizFVZHHs8PrLU5p2IDA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^3.0.1", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/lint-staged/node_modules/human-signals": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-3.0.1.tgz", + "integrity": "sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ==", + "dev": true, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/listr2": { "version": "5.0.7", "dev": true, @@ -7802,7 +7883,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, "license": "MIT" }, "node_modules/merge2": { @@ -7854,7 +7934,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -8123,7 +8202,6 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", "integrity": "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==", - "dev": true, "license": "MIT", "dependencies": { "whatwg-url": "^5.0.0" @@ -8184,7 +8262,6 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^4.0.0" @@ -8200,7 +8277,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -8366,7 +8442,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dev": true, "license": "MIT", "dependencies": { "mimic-fn": "^4.0.0" @@ -8602,7 +8677,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -9487,7 +9561,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, "funding": { "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } @@ -9882,7 +9955,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -9895,7 +9967,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -9929,7 +10000,6 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, "license": "ISC" }, "node_modules/simple-swizzle": { @@ -9971,7 +10041,6 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -9989,7 +10058,6 @@ "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -10315,7 +10383,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -10610,7 +10677,6 @@ "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true, "license": "MIT" }, "node_modules/triple-beam": { @@ -10659,6 +10725,22 @@ "dev": true, "license": "0BSD" }, + "node_modules/tsx": { + "version": "3.12.7", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-3.12.7.tgz", + "integrity": "sha512-C2Ip+jPmqKd1GWVQDvz/Eyc6QJbGfE7NrR3fx5BpEHMZsEHoIxHL1j+lKdGobr8ovEyqeNkPLSKp6SCSOt7gmw==", + "dependencies": { + "@esbuild-kit/cjs-loader": "^2.4.2", + "@esbuild-kit/core-utils": "^3.0.0", + "@esbuild-kit/esm-loader": "^2.5.5" + }, + "bin": { + "tsx": "dist/cli.js" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, "node_modules/tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", @@ -10800,7 +10882,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true, "license": "BSD-2-Clause" }, "node_modules/well-known-symbols": { @@ -10817,7 +10898,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dev": true, "license": "MIT", "dependencies": { "tr46": "~0.0.3", @@ -10828,7 +10908,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" diff --git a/package.json b/package.json index ce3a07d8..20b087af 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,8 @@ "scripts": { "dev": "next dev", "build": "next build", - "start": "next start", + "start": "npm run with-gcp-metadata next start", + "with-gcp-metadata": "npx tsx src/backend/scripts/run-with-gcp-metadata.ts", "type-check": "tsc --noEmit", "test": "ava", "test:watch": "ava --watch", @@ -31,6 +32,8 @@ "@trpc/server": "^10.16.0", "axios": "^1.3.2", "dotenv": "16.0.3", + "execa": "^7.1.1", + "gcp-metadata": "^6.0.0", "kysely": "^0.23.4", "ms": "^2.1.3", "next": "13.1.6", @@ -44,6 +47,7 @@ "react-hot-toast": "^2.4.1", "react-timer-hook": "^3.0.6", "superjson": "^1.12.3", + "tsx": "^3.12.7", "winston": "^3.8.2", "zapatos": "^6.1.4", "zod": "^3.21.4" diff --git a/src/backend/scripts/run-with-gcp-metadata.ts b/src/backend/scripts/run-with-gcp-metadata.ts new file mode 100644 index 00000000..fd9df86e --- /dev/null +++ b/src/backend/scripts/run-with-gcp-metadata.ts @@ -0,0 +1,63 @@ +import { execa } from "execa"; +import * as gcpMetadata from "gcp-metadata"; +import axios from "axios"; + +/** + * This script injects Google Cloud metadata (when available) into the environment. + * Specifically, it injects the NEXTAUTH_URL and BASE_HTTP_ENDPOINT environment variables. + * Run with `npm run with-gcp-metadata `. + */ +const runWithGcpMetadata = async () => { + const env: NodeJS.ProcessEnv = { + NODE_ENV: process.env.NODE_ENV, + }; + + const isGcp = await gcpMetadata.isAvailable(); + if (isGcp) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const [projectId, regionPath, { access_token }]: [ + string, + string, + { access_token: string } + ] = await Promise.all([ + gcpMetadata.project("project-id"), + gcpMetadata.instance("region"), + gcpMetadata.instance("service-accounts/default/token"), + ]); + + const region = regionPath.split("regions/")[1]; + + const getServiceResponse = await axios<{ status: { url: string } }>( + `https://${region}-run.googleapis.com/apis/serving.knative.dev/v1/namespaces/${projectId}/services/compass`, + { + headers: { + Authorization: `Bearer ${access_token}`, + }, + } + ); + + env.NEXTAUTH_URL = getServiceResponse.data.status.url; + env.BASE_HTTP_ENDPOINT = getServiceResponse.data.status.url; + + console.log( + "🕵️ running on GCP, environment will be modified with NEXTAUTH_URL and BASE_HTTP_ENDPOINT." + ); + } else { + console.log("🕵️ not running on GCP, environment will not be modified."); + } + + const splitArgsAt = process.argv.findIndex((arg) => + arg.endsWith("run-with-gcp-metadata.ts") + ); + const args = process.argv.slice(splitArgsAt + 1); + + await execa(args[0], args.slice(1), { + preferLocal: true, + stderr: process.stderr, + stdout: process.stdout, + stdin: process.stdin, + env, + }); +}; + +void runWithGcpMetadata(); diff --git a/terraform/.gitignore b/terraform/.gitignore new file mode 100644 index 00000000..68affd30 --- /dev/null +++ b/terraform/.gitignore @@ -0,0 +1 @@ +backend.dev.hcl diff --git a/terraform/README.md b/terraform/README.md index 47fa59ef..26734e8a 100644 --- a/terraform/README.md +++ b/terraform/README.md @@ -1,7 +1,19 @@ -## Terraform workflow +# Terraform -- Select environment: `cd terraform/staging` -- One time setup: `terraform init` -- Update tf files -- Review changes: `terraform plan` -- Apply changes: `terraform apply` +## Initializing a new Google Cloud project + +To initialize the Terraform backend (must be done once manually per Google Cloud project): + +1. `cd terraform/gcs-backend` +2. `terraform init` +3. `terraform apply` + +If this is for a local development environment, copy `backend.hcl` to `backend.dev.hcl` and update `bucket` with what is output from the `terraform apply` command. + +Otherwise, update `backend.hcl` directly. + +## Deploying + +1. `cd terraform/gcs` +2. `terraform init -backend-config=../backend.dev.hcl` (you only need to run this once) +3. `terraform apply` diff --git a/terraform/backend.hcl b/terraform/backend.hcl new file mode 100644 index 00000000..9abc8f7f --- /dev/null +++ b/terraform/backend.hcl @@ -0,0 +1,2 @@ +bucket = "compass-staging-tf-state" +prefix = "compass/staging" diff --git a/terraform/gcs-backend/main.tf b/terraform/gcs-backend/main.tf new file mode 100644 index 00000000..0ebddda7 --- /dev/null +++ b/terraform/gcs-backend/main.tf @@ -0,0 +1,44 @@ +terraform { + required_version = ">= 1.3" + + required_providers { + google = ">= 3.3" + } +} + +provider "google" { + project = var.project +} + +# Enable Cloud Storage API +resource "google_project_service" "storage_service" { + service = "storage.googleapis.com" +} + +# Enable Artifact Registry API +resource "google_project_service" "artifact_registry" { + service = "artifactregistry.googleapis.com" +} + +# Bucket name must be globally unique +resource "random_id" "bucket_prefix" { + byte_length = 8 +} + +# Create state bucket +resource "google_storage_bucket" "default" { + name = "${random_id.bucket_prefix.hex}-bucket-tfstate" + location = "US" + storage_class = "STANDARD" + + public_access_prevention = "enforced" + uniform_bucket_level_access = true + + versioning { + enabled = true + } +} + +output "state_bucket_name" { + value = google_storage_bucket.default.name +} diff --git a/terraform/gcs-backend/variables.tf b/terraform/gcs-backend/variables.tf new file mode 100644 index 00000000..ed103318 --- /dev/null +++ b/terraform/gcs-backend/variables.tf @@ -0,0 +1,4 @@ +# Google Cloud project slug +variable "project" { + type = string +} diff --git a/terraform/gcs/cloud_run.tf b/terraform/gcs/cloud_run.tf new file mode 100644 index 00000000..2f4e05f5 --- /dev/null +++ b/terraform/gcs/cloud_run.tf @@ -0,0 +1,71 @@ +# Our container +resource "google_cloud_run_v2_service" "run_service" { + name = "compass" + location = var.region + ingress = "INGRESS_TRAFFIC_ALL" + + template { + containers { + image = var.image + + ports { + container_port = 3000 + } + + env { + name = "DATABASE_URL" + value_source { + secret_key_ref { + secret = google_secret_manager_secret.database_url.secret_id + version = "latest" + } + } + } + + env { + name = "NEXTAUTH_SECRET" + value_source { + secret_key_ref { + secret = google_secret_manager_secret.nextauth_secret.secret_id + version = "latest" + } + } + } + + startup_probe { + tcp_socket { + port = 3000 + } + } + + liveness_probe { + http_get { + path = "/" + } + } + } + + # Cloud SQL instance is connected via Unix socket + volumes { + name = "cloudsql" + cloud_sql_instance { + instances = [google_sql_database_instance.postgres_instance.connection_name] + } + } + } + + depends_on = [ + google_project_service.cloud_run_service, + google_sql_database_instance.postgres_instance + ] +} + +# Allow public access +resource "google_cloud_run_service_iam_binding" "default" { + location = google_cloud_run_v2_service.run_service.location + service = google_cloud_run_v2_service.run_service.name + role = "roles/run.invoker" + members = [ + "allUsers" + ] +} diff --git a/terraform/gcs/cloud_sql.tf b/terraform/gcs/cloud_sql.tf new file mode 100644 index 00000000..6edc2ae2 --- /dev/null +++ b/terraform/gcs/cloud_sql.tf @@ -0,0 +1,73 @@ +# Create SQL instance +resource "google_sql_database_instance" "postgres_instance" { + name = "postgres-instance" + region = "us-west1" + database_version = "POSTGRES_15" + + settings { + tier = "db-f1-micro" + } + + # TODO: set `deletion_protection` depending on the environment + + depends_on = [google_project_service.sqladmin_api] +} + +# Generate database password +resource "random_password" "database_password" { + length = 32 + special = true + override_special = "_%@" +} + +# Store database password as a secret +resource "google_secret_manager_secret" "postgres_password" { + secret_id = "postgres-password" + + replication { + automatic = true + } + + depends_on = [google_project_service.secretmanager_api] +} + +resource "google_secret_manager_secret_version" "postgres_password_data" { + secret = google_secret_manager_secret.postgres_password.id + secret_data = random_password.database_password.result +} + +# Create database on Postgres instance +resource "google_sql_database" "database" { + name = "compass" + instance = google_sql_database_instance.postgres_instance.name +} + +# Create database user on Postgres instance +resource "google_sql_user" "database_user" { + name = "application" + instance = google_sql_database_instance.postgres_instance.name + password = random_password.database_password.result +} + +# Store database URL as secret (assumes that Cloud SQL is mounted as a Unix socket) +resource "google_secret_manager_secret" "database_url" { + secret_id = "database_url" + + replication { + automatic = true + } + + depends_on = [google_project_service.secretmanager_api] +} + +resource "google_secret_manager_secret_version" "database_url_data" { + secret = google_secret_manager_secret.database_url.name + secret_data = "postgres://${google_sql_user.database_user.name}:${random_password.database_password.result}@/${google_sql_database.database.name}?host=/cloudsql/${google_sql_database_instance.postgres_instance.connection_name}" +} + +resource "google_secret_manager_secret_iam_member" "database_url_access" { + secret_id = google_secret_manager_secret.database_url.id + role = "roles/secretmanager.secretAccessor" + member = "serviceAccount:${data.google_project.project.number}-compute@developer.gserviceaccount.com" + depends_on = [google_secret_manager_secret.database_url] +} diff --git a/terraform/gcs/main.tf b/terraform/gcs/main.tf new file mode 100644 index 00000000..273132d8 --- /dev/null +++ b/terraform/gcs/main.tf @@ -0,0 +1,15 @@ +terraform { + required_version = ">= 1.3" + + required_providers { + google = ">= 3.3" + } + + backend "gcs" {} +} + +provider "google" { + project = var.project +} + +data "google_project" "project" {} diff --git a/terraform/gcs/project_services.tf b/terraform/gcs/project_services.tf new file mode 100644 index 00000000..cb62acaa --- /dev/null +++ b/terraform/gcs/project_services.tf @@ -0,0 +1,14 @@ +# Enable Secret Manager API +resource "google_project_service" "secretmanager_api" { + service = "secretmanager.googleapis.com" +} + +# Enable SQL Admin API +resource "google_project_service" "sqladmin_api" { + service = "sqladmin.googleapis.com" +} + +# Enable Cloud Run API +resource "google_project_service" "cloud_run_service" { + service = "run.googleapis.com" +} diff --git a/terraform/gcs/secrets.tf b/terraform/gcs/secrets.tf new file mode 100644 index 00000000..d9b9d6cc --- /dev/null +++ b/terraform/gcs/secrets.tf @@ -0,0 +1,28 @@ +# Random value used for NextAuth secret +resource "random_password" "nextauth_secret" { + length = 32 + special = true + override_special = "_%@" +} + +resource "google_secret_manager_secret" "nextauth_secret" { + secret_id = "nextauth_secret" + + replication { + automatic = true + } + + depends_on = [google_project_service.secretmanager_api] +} + +resource "google_secret_manager_secret_version" "nextauth_secret_data" { + secret = google_secret_manager_secret.nextauth_secret.name + secret_data = random_password.nextauth_secret.result +} + +resource "google_secret_manager_secret_iam_member" "nextauth_secret_access" { + secret_id = google_secret_manager_secret.nextauth_secret.id + role = "roles/secretmanager.secretAccessor" + member = "serviceAccount:${data.google_project.project.number}-compute@developer.gserviceaccount.com" + depends_on = [google_secret_manager_secret.nextauth_secret] +} diff --git a/terraform/gcs/variables.tf b/terraform/gcs/variables.tf new file mode 100644 index 00000000..6744d784 --- /dev/null +++ b/terraform/gcs/variables.tf @@ -0,0 +1,15 @@ +# Google Cloud project slug +variable "project" { + type = string +} + +# Region to deploy to +variable "region" { + type = string + default = "us-west1" +} + +# Image to deploy +variable "image" { + type = string +}