mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-08 23:15:24 +02:00
Merge pull request #40 from larswmh/remove-aws-dependencies
feat: remove AWS dependencies
This commit is contained in:
commit
b26e3921f3
39 changed files with 7 additions and 7226 deletions
|
@ -1,7 +1,5 @@
|
||||||
# Ignores everything except dist/ prisma/ apps/ and yarn.lock
|
# Ignores everything except dist/ prisma/ apps/ and yarn.lock
|
||||||
# apps/ is needed so that CDK (in /aws/maybe-app) has access to the per-app Dockerfile
|
|
||||||
*
|
*
|
||||||
!apps/
|
|
||||||
!dist/
|
!dist/
|
||||||
!prisma/
|
!prisma/
|
||||||
!yarn.lock
|
!yarn.lock
|
|
@ -17,11 +17,6 @@ AUTH0_DEPLOY_CLIENT_SECRET=
|
||||||
POSTMARK_SMTP_PASS=
|
POSTMARK_SMTP_PASS=
|
||||||
NX_SESSION_SECRET=
|
NX_SESSION_SECRET=
|
||||||
|
|
||||||
# If you want to test any code that utilizes the AWS SDK locally, add temporary STAGING credentials (can be retrieved from SSO dashboard)
|
|
||||||
AWS_ACCESS_KEY_ID=
|
|
||||||
AWS_SECRET_ACCESS_KEY=
|
|
||||||
AWS_SESSION_TOKEN=
|
|
||||||
|
|
||||||
NX_PLAID_SECRET=
|
NX_PLAID_SECRET=
|
||||||
NX_FINICITY_APP_KEY=
|
NX_FINICITY_APP_KEY=
|
||||||
NX_FINICITY_PARTNER_SECRET=
|
NX_FINICITY_PARTNER_SECRET=
|
||||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -46,8 +46,6 @@ Thumbs.db
|
||||||
**/.env
|
**/.env
|
||||||
**/.env.local
|
**/.env.local
|
||||||
|
|
||||||
aws/maybe-app/cdk.out
|
|
||||||
|
|
||||||
# Next.js
|
# Next.js
|
||||||
.next
|
.next
|
||||||
|
|
||||||
|
|
|
@ -57,8 +57,6 @@ import finicity, { getFinicityTxPushUrl, getFinicityWebhookUrl } from './finicit
|
||||||
import stripe from './stripe'
|
import stripe from './stripe'
|
||||||
import postmark from './postmark'
|
import postmark from './postmark'
|
||||||
import { managementClient } from './auth0'
|
import { managementClient } from './auth0'
|
||||||
import s3 from './s3'
|
|
||||||
import secretsClient from './secretsClient'
|
|
||||||
import defineAbilityFor from './ability'
|
import defineAbilityFor from './ability'
|
||||||
import env from '../../env'
|
import env from '../../env'
|
||||||
import logger from '../lib/logger'
|
import logger from '../lib/logger'
|
||||||
|
@ -314,8 +312,6 @@ export async function createContext(req: Request) {
|
||||||
prisma,
|
prisma,
|
||||||
plaid,
|
plaid,
|
||||||
stripe,
|
stripe,
|
||||||
s3,
|
|
||||||
secretsClient,
|
|
||||||
managementClient,
|
managementClient,
|
||||||
logger,
|
logger,
|
||||||
user,
|
user,
|
||||||
|
|
|
@ -1,77 +0,0 @@
|
||||||
/**
|
|
||||||
* Custom storage engine for S3
|
|
||||||
* @see https://github.com/expressjs/multer/blob/master/StorageEngine.md
|
|
||||||
*
|
|
||||||
* A simplified version of the `multer-s3` engine that properly supports
|
|
||||||
* MD5 digests to satisfy S3 Object Lock PutObject requirements
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { StorageEngine } from 'multer'
|
|
||||||
import type { Logger } from 'winston'
|
|
||||||
import { v4 as uuid } from 'uuid'
|
|
||||||
import mime from 'mime-types'
|
|
||||||
import crypto from 'crypto'
|
|
||||||
import type { Request } from 'express'
|
|
||||||
import type { S3Service } from '@maybe-finance/server/shared'
|
|
||||||
|
|
||||||
type S3StorageOpts = {
|
|
||||||
s3: S3Service
|
|
||||||
getFilename?: (req: Request) => string
|
|
||||||
}
|
|
||||||
|
|
||||||
class S3Storage implements StorageEngine {
|
|
||||||
constructor(private readonly opts: S3StorageOpts, private readonly logger: Logger) {
|
|
||||||
if (!opts.s3) throw new Error('Must provide S3Client instance')
|
|
||||||
}
|
|
||||||
|
|
||||||
_handleFile(...params: Parameters<StorageEngine['_handleFile']>) {
|
|
||||||
const [req, file, callback] = params
|
|
||||||
|
|
||||||
const chunks: any[] = []
|
|
||||||
|
|
||||||
file.stream.on('data', (chunk) => chunks.push(chunk))
|
|
||||||
file.stream.on('end', () => {
|
|
||||||
const Body = Buffer.concat(chunks)
|
|
||||||
const md5Hash = crypto.createHash('md5').update(Body).digest('base64')
|
|
||||||
|
|
||||||
const filename = this.opts.getFilename ? this.opts.getFilename(req) : null
|
|
||||||
const Key = `${filename ?? uuid()}.${mime.extension(file.mimetype)}`
|
|
||||||
|
|
||||||
this.logger.info(`Multer uploading file key=${Key} size=${Body.length}`)
|
|
||||||
|
|
||||||
this.opts.s3
|
|
||||||
.upload({
|
|
||||||
bucketKey: 'private',
|
|
||||||
Key,
|
|
||||||
Body,
|
|
||||||
ContentMD5: md5Hash,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
callback(null, {
|
|
||||||
size: Body.length,
|
|
||||||
path: Key,
|
|
||||||
mimetype: file.mimetype,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
_removeFile(...params: Parameters<StorageEngine['_removeFile']>) {
|
|
||||||
const [req, file, callback] = params
|
|
||||||
|
|
||||||
this.logger.warn(`Multer removing file ${file.path}`)
|
|
||||||
|
|
||||||
// Since object lock is turned on, this will create a new version, but never delete/overwrite
|
|
||||||
this.opts.s3
|
|
||||||
.delete({
|
|
||||||
bucketKey: 'private',
|
|
||||||
Key: file.path,
|
|
||||||
})
|
|
||||||
.then(() => callback(null))
|
|
||||||
.catch((err) => callback(err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function (opts: S3StorageOpts, logger: Logger) {
|
|
||||||
return new S3Storage(opts, logger) as StorageEngine
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
import { S3Client } from '@aws-sdk/client-s3'
|
|
||||||
import { S3Service } from '@maybe-finance/server/shared'
|
|
||||||
import env from '../../env'
|
|
||||||
|
|
||||||
// https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/loading-node-credentials-environment.html
|
|
||||||
const s3Client = new S3Client({
|
|
||||||
region: 'us-west-2',
|
|
||||||
})
|
|
||||||
|
|
||||||
const s3Service = new S3Service(s3Client, {
|
|
||||||
public: env.NX_CDN_PUBLIC_BUCKET,
|
|
||||||
private: env.NX_CDN_PRIVATE_BUCKET,
|
|
||||||
})
|
|
||||||
|
|
||||||
export default s3Service
|
|
|
@ -1,7 +0,0 @@
|
||||||
import { SecretsManagerClient } from '@aws-sdk/client-secrets-manager'
|
|
||||||
|
|
||||||
const secretsClient = new SecretsManagerClient({
|
|
||||||
region: 'us-west-2',
|
|
||||||
})
|
|
||||||
|
|
||||||
export default secretsClient
|
|
|
@ -1,15 +0,0 @@
|
||||||
import { S3Client } from '@aws-sdk/client-s3'
|
|
||||||
import { S3Service } from '@maybe-finance/server/shared'
|
|
||||||
import env from '../../env'
|
|
||||||
|
|
||||||
// https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/loading-node-credentials-environment.html
|
|
||||||
const s3Client = new S3Client({
|
|
||||||
region: 'us-west-2',
|
|
||||||
})
|
|
||||||
|
|
||||||
const s3Service = new S3Service(s3Client, {
|
|
||||||
public: env.NX_CDN_PUBLIC_BUCKET,
|
|
||||||
private: env.NX_CDN_PRIVATE_BUCKET,
|
|
||||||
})
|
|
||||||
|
|
||||||
export default s3Service
|
|
8
aws/maybe-app/.gitignore
vendored
8
aws/maybe-app/.gitignore
vendored
|
@ -1,8 +0,0 @@
|
||||||
*.js
|
|
||||||
!jest.config.js
|
|
||||||
*.d.ts
|
|
||||||
node_modules
|
|
||||||
|
|
||||||
# CDK asset staging directory
|
|
||||||
.cdk.staging
|
|
||||||
cdk.out
|
|
|
@ -1,6 +0,0 @@
|
||||||
*.ts
|
|
||||||
!*.d.ts
|
|
||||||
|
|
||||||
# CDK asset staging directory
|
|
||||||
.cdk.staging
|
|
||||||
cdk.out
|
|
|
@ -1,19 +0,0 @@
|
||||||
## Local deployment
|
|
||||||
|
|
||||||
**PROD should not be deployed locally**
|
|
||||||
|
|
||||||
To test a local deployment:
|
|
||||||
|
|
||||||
1. Set `~/.aws/credentials` file to have a profile called `[maybe_tools]` which should have credentials of IAM user in the tools account (which can cross-deploy to staging/prod accounts). Additional security creds can be generated in AWS dashboard.
|
|
||||||
|
|
||||||
```
|
|
||||||
[maybe_tools]
|
|
||||||
aws_access_key_id=
|
|
||||||
aws_secret_access_key=
|
|
||||||
region=us-east-1
|
|
||||||
output=json
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Run `yarn cdk:ls` to verify everything is working correctly
|
|
||||||
|
|
||||||
3. Run `yarn cdk deploy [SomeStack] --profile=maybe_tools` to deploy a certain stack in the **staging** environment. Production should not be deployed locally ever.
|
|
|
@ -1,93 +0,0 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
import 'source-map-support/register'
|
|
||||||
import * as cdk from 'aws-cdk-lib'
|
|
||||||
import { getContext } from '../lib/utils/get-context'
|
|
||||||
import { SharedStack } from '../lib/stacks/shared-stack'
|
|
||||||
import { ServerStack } from '../lib/stacks/server-stack'
|
|
||||||
import { WorkersStack } from '../lib/stacks/workers-stack'
|
|
||||||
import { StackProps } from 'aws-cdk-lib'
|
|
||||||
import { ToolsStack } from '../lib/stacks/tools-stack'
|
|
||||||
|
|
||||||
const app = new cdk.App()
|
|
||||||
|
|
||||||
const ctx = getContext(app)
|
|
||||||
|
|
||||||
const { AWS_ACCOUNT, DEFAULT_AWS_REGION, ENV_NAME, sharedEnv, stackContexts } = ctx
|
|
||||||
|
|
||||||
const commonStackProps: StackProps = {
|
|
||||||
env: {
|
|
||||||
region: DEFAULT_AWS_REGION,
|
|
||||||
account: AWS_ACCOUNT,
|
|
||||||
},
|
|
||||||
tags: {
|
|
||||||
environment: ENV_NAME,
|
|
||||||
'deploy-type': 'cdk',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ENV_NAME === 'tools') {
|
|
||||||
new ToolsStack(
|
|
||||||
app,
|
|
||||||
'ToolsStack',
|
|
||||||
{
|
|
||||||
...commonStackProps,
|
|
||||||
stackName: `ci-cd-deployments-tools-stack`,
|
|
||||||
description: 'Resources used for CI/CD, such as self-hosted runners and pipelines',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
DEFAULT_AWS_REGION,
|
|
||||||
sharedEnv,
|
|
||||||
...stackContexts.Tools,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ENV_NAME === 'staging' || ENV_NAME === 'production') {
|
|
||||||
const sharedStack = new SharedStack(
|
|
||||||
app,
|
|
||||||
'SharedStack',
|
|
||||||
{
|
|
||||||
...commonStackProps,
|
|
||||||
stackName: `maybe-app-${ENV_NAME}-shared-stack`,
|
|
||||||
description:
|
|
||||||
'Common infrastructure used by all services including VPC, Redis, and ECS Cluster',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
DEFAULT_AWS_REGION,
|
|
||||||
sharedEnv,
|
|
||||||
...stackContexts.Shared,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
new ServerStack(
|
|
||||||
app,
|
|
||||||
'ServerStack',
|
|
||||||
{
|
|
||||||
...commonStackProps,
|
|
||||||
stackName: `maybe-app-${ENV_NAME}-server-stack`,
|
|
||||||
description: 'ECS Fargate RESTful API with ALB, Cloudfront, and WAF',
|
|
||||||
sharedStack,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
DEFAULT_AWS_REGION,
|
|
||||||
sharedEnv,
|
|
||||||
...stackContexts.Server,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
new WorkersStack(
|
|
||||||
app,
|
|
||||||
'WorkersStack',
|
|
||||||
{
|
|
||||||
...commonStackProps,
|
|
||||||
stackName: `maybe-app-${ENV_NAME}-workers-stack`,
|
|
||||||
description: 'Bull.js workers on ECS Fargate',
|
|
||||||
sharedStack,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
DEFAULT_AWS_REGION,
|
|
||||||
sharedEnv,
|
|
||||||
...stackContexts.Workers,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
{
|
|
||||||
"availability-zones:account=180027004049:region=us-west-2": [
|
|
||||||
"us-west-2a",
|
|
||||||
"us-west-2b",
|
|
||||||
"us-west-2c",
|
|
||||||
"us-west-2d"
|
|
||||||
],
|
|
||||||
"availability-zones:account=541001830411:region=us-west-2": [
|
|
||||||
"us-west-2a",
|
|
||||||
"us-west-2b",
|
|
||||||
"us-west-2c",
|
|
||||||
"us-west-2d"
|
|
||||||
],
|
|
||||||
"availability-zones:account=604977304163:region=us-west-2": [
|
|
||||||
"us-west-2a",
|
|
||||||
"us-west-2b",
|
|
||||||
"us-west-2c",
|
|
||||||
"us-west-2d"
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,206 +0,0 @@
|
||||||
{
|
|
||||||
"app": "npx ts-node --prefer-ts-exts bin/maybe-app.ts",
|
|
||||||
"watch": {
|
|
||||||
"include": ["**"],
|
|
||||||
"exclude": [
|
|
||||||
"README.md",
|
|
||||||
"cdk*.json",
|
|
||||||
"**/*.d.ts",
|
|
||||||
"**/*.js",
|
|
||||||
"tsconfig.json",
|
|
||||||
"package*.json",
|
|
||||||
"yarn.lock",
|
|
||||||
"node_modules",
|
|
||||||
"test"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"context": {
|
|
||||||
"@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true,
|
|
||||||
"@aws-cdk/core:stackRelativeExports": true,
|
|
||||||
"@aws-cdk/aws-rds:lowercaseDbIdentifier": true,
|
|
||||||
"@aws-cdk/aws-lambda:recognizeVersionProps": true,
|
|
||||||
"@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true,
|
|
||||||
"@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
|
|
||||||
"@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
|
|
||||||
"@aws-cdk/core:checkSecretUsage": true,
|
|
||||||
"@aws-cdk/aws-iam:minimizePolicies": true,
|
|
||||||
"@aws-cdk/core:target-partitions": ["aws", "aws-cn"],
|
|
||||||
"environments": {
|
|
||||||
"tools": {
|
|
||||||
"ENV_NAME": "tools",
|
|
||||||
"AWS_ACCOUNT": "REPLACE_THIS",
|
|
||||||
"DEFAULT_AWS_REGION": "us-west-2",
|
|
||||||
"sharedEnv": {},
|
|
||||||
"stackContexts": {
|
|
||||||
"Tools": {
|
|
||||||
"VPC": {
|
|
||||||
"IPAllowList": [""]
|
|
||||||
},
|
|
||||||
"GithubRunner": {
|
|
||||||
"SSMKeyArn": "REPLACE_THIS",
|
|
||||||
"GithubTokenArn": "REPLACE_THIS"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"staging": {
|
|
||||||
"ENV_NAME": "staging",
|
|
||||||
"AWS_ACCOUNT": "REPLACE_THIS",
|
|
||||||
"DEFAULT_AWS_REGION": "us-west-2",
|
|
||||||
"sharedEnv": {
|
|
||||||
"NX_API_URL": "https://staging-api.maybe.co",
|
|
||||||
"NX_CDN_URL": "https://staging-cdn.maybe.co",
|
|
||||||
"NODE_ENV": "production",
|
|
||||||
"NX_AUTH0_AUDIENCE": "https://maybe-finance-api/v1",
|
|
||||||
"NX_PLAID_CLIENT_ID": "REPLACE_THIS",
|
|
||||||
"NX_PLAID_ENV": "sandbox",
|
|
||||||
"NX_FINICITY_PARTNER_ID": "REPLACE_THIS",
|
|
||||||
"NX_FINICITY_ENV": "sandbox",
|
|
||||||
"NX_CLIENT_URL": "https://staging-app.maybe.co",
|
|
||||||
"NX_CLIENT_URL_CUSTOM": "https://staging-app.maybe.co",
|
|
||||||
"NX_AUTH0_DOMAIN": "REPLACE_THIS",
|
|
||||||
"NX_AUTH0_CUSTOM_DOMAIN": "REPLACE_THIS",
|
|
||||||
"NX_AUTH0_CLIENT_ID": "REPLACE_THIS",
|
|
||||||
"NX_AUTH0_MGMT_CLIENT_ID": "REPLACE_THIS",
|
|
||||||
"NX_SENTRY_ENV": "staging",
|
|
||||||
"NX_POSTMARK_FROM_ADDRESS": "account@maybe.co"
|
|
||||||
},
|
|
||||||
"stackContexts": {
|
|
||||||
"Shared": {
|
|
||||||
"PeeringCnxId": "REPLACE_THIS",
|
|
||||||
"PeeringCnxCidr": "10.1.0.0/16",
|
|
||||||
"VPC": {
|
|
||||||
"NATCount": 1,
|
|
||||||
"IPAllowList": [""]
|
|
||||||
},
|
|
||||||
"Redis": {
|
|
||||||
"size": "cache.t4g.small",
|
|
||||||
"count": 1
|
|
||||||
},
|
|
||||||
"Cloudfront": {
|
|
||||||
"CertificateArn": "REPLACE_THIS",
|
|
||||||
"CNAMES": ["staging-cdn.maybe.co"]
|
|
||||||
},
|
|
||||||
"UploadOrigins": [
|
|
||||||
"http://localhost:4200",
|
|
||||||
"http://localhost:4201",
|
|
||||||
"https://staging-app.maybe.co"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"Server": {
|
|
||||||
"Container": {
|
|
||||||
"Env": {
|
|
||||||
"NX_PORT": 3333,
|
|
||||||
"NX_CORS_ORIGINS": "maybe.co,vercel.app",
|
|
||||||
"NX_MORGAN_LOG_LEVEL": "combined",
|
|
||||||
"NX_SENTRY_DSN": "REPLACE_THIS"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"CertificateArn": "REPLACE_THIS",
|
|
||||||
"DesiredTaskCount": 1,
|
|
||||||
"WAFArn": "REPLACE_THIS",
|
|
||||||
"ComputeCapacity": {
|
|
||||||
"cpu": 512,
|
|
||||||
"memory": "1024"
|
|
||||||
},
|
|
||||||
"Cloudfront": {
|
|
||||||
"CNAMES": ["staging-api.maybe.co"],
|
|
||||||
"CertificateArn": "REPLACE_THIS"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Workers": {
|
|
||||||
"Container": {
|
|
||||||
"Env": {
|
|
||||||
"NX_PORT": 3334,
|
|
||||||
"NX_SENTRY_DSN": "REPLACE_THIS"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"DesiredTaskCount": 1,
|
|
||||||
"ComputeCapacity": {
|
|
||||||
"cpu": 512,
|
|
||||||
"memory": "1024"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"production": {
|
|
||||||
"ENV_NAME": "production",
|
|
||||||
"AWS_ACCOUNT": "REPLACE_THIS",
|
|
||||||
"DEFAULT_AWS_REGION": "us-west-2",
|
|
||||||
"sharedEnv": {
|
|
||||||
"NODE_ENV": "production",
|
|
||||||
"NX_AUTH0_AUDIENCE": "https://maybe-finance-api/v1",
|
|
||||||
"NX_PLAID_CLIENT_ID": "REPLACE_THIS",
|
|
||||||
"NX_PLAID_ENV": "production",
|
|
||||||
"NX_FINICITY_PARTNER_ID": "REPLACE_THIS",
|
|
||||||
"NX_FINICITY_ENV": "production",
|
|
||||||
"NX_API_URL": "https://api.maybe.co",
|
|
||||||
"NX_CDN_URL": "https://cdn.maybe.co",
|
|
||||||
"NX_CLIENT_URL": "https://app.maybe.co",
|
|
||||||
"NX_CLIENT_URL_CUSTOM": "https://app.maybe.co",
|
|
||||||
"NX_AUTH0_DOMAIN": "REPLACE_THIS",
|
|
||||||
"NX_AUTH0_CUSTOM_DOMAIN": "login.maybe.co",
|
|
||||||
"NX_AUTH0_CLIENT_ID": "REPLACE_THIS",
|
|
||||||
"NX_AUTH0_MGMT_CLIENT_ID": "REPLACE_THIS",
|
|
||||||
"NX_SENTRY_ENV": "production",
|
|
||||||
"NX_POSTMARK_FROM_ADDRESS": "account@maybe.co",
|
|
||||||
"NX_STRIPE_PREMIUM_MONTHLY_PRICE_ID": "REPLACE_THIS",
|
|
||||||
"NX_STRIPE_PREMIUM_YEARLY_PRICE_ID": "REPLACE_THIS"
|
|
||||||
},
|
|
||||||
"stackContexts": {
|
|
||||||
"Shared": {
|
|
||||||
"PeeringCnxId": "REPLACE_THIS",
|
|
||||||
"PeeringCnxCidr": "10.2.0.0/16",
|
|
||||||
"VPC": {
|
|
||||||
"NATCount": 2,
|
|
||||||
"IPAllowList": [""]
|
|
||||||
},
|
|
||||||
"Redis": {
|
|
||||||
"size": "cache.m6g.2xlarge",
|
|
||||||
"count": 1
|
|
||||||
},
|
|
||||||
"Cloudfront": {
|
|
||||||
"CertificateArn": "REPLACE_THIS",
|
|
||||||
"CNAMES": ["cdn.maybe.co"]
|
|
||||||
},
|
|
||||||
"UploadOrigins": "https://app.maybe.co"
|
|
||||||
},
|
|
||||||
"Server": {
|
|
||||||
"Container": {
|
|
||||||
"Env": {
|
|
||||||
"NX_PORT": 3333,
|
|
||||||
"NX_CORS_ORIGINS": "maybe.co",
|
|
||||||
"NX_MORGAN_LOG_LEVEL": "combined",
|
|
||||||
"NX_SENTRY_DSN": "REPLACE_THIS"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"CertificateArn": "REPLACE_THIS",
|
|
||||||
"DesiredTaskCount": 1,
|
|
||||||
"WAFArn": "REPLACE_THIS",
|
|
||||||
"ComputeCapacity": {
|
|
||||||
"cpu": 4096,
|
|
||||||
"memory": "8192"
|
|
||||||
},
|
|
||||||
"Cloudfront": {
|
|
||||||
"CNAMES": ["api.maybe.co"],
|
|
||||||
"CertificateArn": "REPLACE_THIS"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Workers": {
|
|
||||||
"Container": {
|
|
||||||
"Env": {
|
|
||||||
"NX_PORT": 3334,
|
|
||||||
"NX_SENTRY_DSN": "REPLACE_THIS"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"DesiredTaskCount": 1,
|
|
||||||
"ComputeCapacity": {
|
|
||||||
"cpu": 4096,
|
|
||||||
"memory": "8192"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
testEnvironment: 'node',
|
|
||||||
roots: ['<rootDir>/test'],
|
|
||||||
testMatch: ['**/*.test.ts'],
|
|
||||||
transform: {
|
|
||||||
'^.+\\.tsx?$': 'ts-jest',
|
|
||||||
},
|
|
||||||
}
|
|
|
@ -1,73 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Go to home directory
|
|
||||||
cd /home/ubuntu
|
|
||||||
|
|
||||||
# Install necessary packages
|
|
||||||
apt-get update
|
|
||||||
apt-get install unzip jq -y
|
|
||||||
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
|
|
||||||
unzip awscliv2.zip
|
|
||||||
./aws/install
|
|
||||||
export PATH="$(which aws):$PATH"
|
|
||||||
source ~/.bashrc
|
|
||||||
aws --version
|
|
||||||
|
|
||||||
# Initializes the Github runner
|
|
||||||
export RUNNER_ALLOW_RUNASROOT="1" # This allows the root user to set everything up
|
|
||||||
mkdir actions-runner && cd actions-runner
|
|
||||||
curl -o actions-runner-linux-x64-2.291.1.tar.gz -L https://github.com/actions/runner/releases/download/v2.291.1/actions-runner-linux-x64-2.291.1.tar.gz
|
|
||||||
echo "1bde3f2baf514adda5f8cf2ce531edd2f6be52ed84b9b6733bf43006d36dcd4c actions-runner-linux-x64-2.291.1.tar.gz" | shasum -a 256 -c
|
|
||||||
tar xzf ./actions-runner-linux-x64-2.291.1.tar.gz
|
|
||||||
|
|
||||||
# Grabs Github API key from Secure SSM param
|
|
||||||
# Make sure there is a secure parameter called '/github/api-token' in format githubusername:githubpersonalaccesstoken (with "repo" scopes enabled)
|
|
||||||
GITHUB_API_TOKEN=$(aws ssm get-parameter --name /github/api-token --region us-west-2 --with-decryption | jq -r '.Parameter.Value')
|
|
||||||
|
|
||||||
# Uses token to get a runner registration token, which is used to configure the runner
|
|
||||||
ADD_RUNNER_TOKEN=$(curl -u $GITHUB_API_TOKEN \
|
|
||||||
-X POST \
|
|
||||||
-H "Accept: application/vnd.github.v3+json" \
|
|
||||||
https://api.github.com/repos/maybe-finance/maybe-app/actions/runners/registration-token | jq -r '.token')
|
|
||||||
|
|
||||||
./config.sh --url https://github.com/maybe-finance/maybe-app --labels aws --unattended --token $ADD_RUNNER_TOKEN
|
|
||||||
|
|
||||||
./svc.sh install
|
|
||||||
|
|
||||||
# Install Node v.16.15 and yarn
|
|
||||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash
|
|
||||||
export NVM_DIR="$HOME/.nvm"
|
|
||||||
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
|
||||||
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
|
|
||||||
|
|
||||||
nvm install v16.15.0
|
|
||||||
nvm use v16.15.0
|
|
||||||
nvm alias default v16.15.0
|
|
||||||
npm install -g yarn
|
|
||||||
|
|
||||||
# Need to add these binaries to the runner's `.path` file, which is what the runner uses to locate packages
|
|
||||||
# https://github.com/actions/setup-node/issues/182#issuecomment-718233039
|
|
||||||
echo $PATH > .path
|
|
||||||
echo -n ":/home/ubuntu/.nvm/versions/node/v16.15.0/bin" >> .path
|
|
||||||
|
|
||||||
# Install Docker
|
|
||||||
apt-get update
|
|
||||||
apt-get install \
|
|
||||||
ca-certificates \
|
|
||||||
curl \
|
|
||||||
gnupg \
|
|
||||||
lsb-release -y
|
|
||||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
|
|
||||||
echo \
|
|
||||||
"deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
|
|
||||||
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
|
||||||
apt-get update
|
|
||||||
apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin -y
|
|
||||||
|
|
||||||
# https://docs.docker.com/engine/install/linux-postinstall/#manage-docker-as-a-non-root-user
|
|
||||||
# Give non-root user ability to run docker commands
|
|
||||||
usermod -aG docker ubuntu
|
|
||||||
newgrp docker
|
|
||||||
|
|
||||||
# Start the Github runner as a service
|
|
||||||
sudo ./svc.sh start
|
|
|
@ -1,104 +0,0 @@
|
||||||
import {
|
|
||||||
BlockDeviceVolume,
|
|
||||||
Instance,
|
|
||||||
InstanceClass,
|
|
||||||
InstanceSize,
|
|
||||||
InstanceType,
|
|
||||||
IVpc,
|
|
||||||
MachineImage,
|
|
||||||
Peer,
|
|
||||||
Port,
|
|
||||||
SecurityGroup,
|
|
||||||
SubnetType,
|
|
||||||
} from 'aws-cdk-lib/aws-ec2'
|
|
||||||
import {
|
|
||||||
ManagedPolicy,
|
|
||||||
PolicyDocument,
|
|
||||||
PolicyStatement,
|
|
||||||
Role,
|
|
||||||
ServicePrincipal,
|
|
||||||
} from 'aws-cdk-lib/aws-iam'
|
|
||||||
import { Construct } from 'constructs'
|
|
||||||
import { ToolsStackContext } from '../../utils/get-context'
|
|
||||||
import * as path from 'path'
|
|
||||||
import { readFileSync } from 'fs'
|
|
||||||
|
|
||||||
export interface GithubActionsRunnerProps {
|
|
||||||
vpc: IVpc
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers an on-demand EC2 instance for running GH Actions workflows
|
|
||||||
*/
|
|
||||||
export class GithubActionsRunner extends Construct {
|
|
||||||
constructor(
|
|
||||||
scope: Construct,
|
|
||||||
id: string,
|
|
||||||
props: GithubActionsRunnerProps,
|
|
||||||
ctx: ToolsStackContext
|
|
||||||
) {
|
|
||||||
super(scope, id)
|
|
||||||
|
|
||||||
const runnerSG = new SecurityGroup(this, 'RunnerSecurityGroup', {
|
|
||||||
vpc: props.vpc,
|
|
||||||
})
|
|
||||||
|
|
||||||
ctx.VPC.IPAllowList.forEach((ip, index) => {
|
|
||||||
runnerSG.addIngressRule(Peer.ipv4(`${ip}/32`), Port.tcp(22))
|
|
||||||
})
|
|
||||||
|
|
||||||
// Details retrieved from console, see command below:
|
|
||||||
// aws ec2 describe-images --region us-west-2 --image-ids ami-0cfa91bdbc3be780c
|
|
||||||
const ami = {
|
|
||||||
id: 'ami-0cfa91bdbc3be780c',
|
|
||||||
deviceName: '/dev/sda1',
|
|
||||||
}
|
|
||||||
|
|
||||||
const runner = new Instance(this, 'GithubActionsRunner', {
|
|
||||||
vpc: props.vpc,
|
|
||||||
vpcSubnets: { subnetType: SubnetType.PUBLIC },
|
|
||||||
machineImage: MachineImage.genericLinux({
|
|
||||||
[ctx.DEFAULT_AWS_REGION]: ami.id,
|
|
||||||
}),
|
|
||||||
instanceType: InstanceType.of(InstanceClass.T3A, InstanceSize.MEDIUM),
|
|
||||||
securityGroup: runnerSG,
|
|
||||||
// This role allows for Session Manager connections - https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager.html
|
|
||||||
role: new Role(this, 'SessionManagerRole', {
|
|
||||||
assumedBy: new ServicePrincipal('ec2.amazonaws.com'),
|
|
||||||
description: 'Allows Session Manager to connect to EC2 instance',
|
|
||||||
managedPolicies: [
|
|
||||||
ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore'),
|
|
||||||
],
|
|
||||||
inlinePolicies: {
|
|
||||||
// Allows EC2 instance to grab the Github PAT and make a call to get a "create runner" registration token from Github API
|
|
||||||
ReadGithubKey: new PolicyDocument({
|
|
||||||
statements: [
|
|
||||||
new PolicyStatement({
|
|
||||||
actions: ['kms:Decrypt'],
|
|
||||||
resources: [ctx.GithubRunner.SSMKeyArn],
|
|
||||||
}),
|
|
||||||
new PolicyStatement({
|
|
||||||
actions: ['ssm:GetParameters'],
|
|
||||||
resources: [ctx.GithubRunner.GithubTokenArn],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
blockDevices: [
|
|
||||||
{
|
|
||||||
deviceName: ami.deviceName,
|
|
||||||
volume: BlockDeviceVolume.ebs(100),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
userDataCausesReplacement: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
runner.addUserData(
|
|
||||||
readFileSync(path.join(__dirname, './configure-github-runner.sh'), 'utf8')
|
|
||||||
)
|
|
||||||
|
|
||||||
// Associate the keypair for SSH connections - https://stackoverflow.com/a/60713522/7437737
|
|
||||||
runner.instance.addPropertyOverride('KeyName', 'gh-actions-runner-key') // key was manually created in Console, available in 1Password
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,60 +0,0 @@
|
||||||
import {
|
|
||||||
Instance,
|
|
||||||
InstanceClass,
|
|
||||||
InstanceSize,
|
|
||||||
InstanceType,
|
|
||||||
IVpc,
|
|
||||||
MachineImage,
|
|
||||||
Peer,
|
|
||||||
Port,
|
|
||||||
SecurityGroup,
|
|
||||||
SubnetType,
|
|
||||||
} from 'aws-cdk-lib/aws-ec2'
|
|
||||||
import { ManagedPolicy, Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam'
|
|
||||||
import { Construct } from 'constructs'
|
|
||||||
import { SharedStackContext } from '../utils/get-context'
|
|
||||||
|
|
||||||
export interface JumpBoxProps {
|
|
||||||
vpc: IVpc
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a "Jump Box", or "Bastion" EC2 instance in public subnet for connections to Postgres
|
|
||||||
*/
|
|
||||||
export class JumpBox extends Construct {
|
|
||||||
public readonly redisUrl: string
|
|
||||||
|
|
||||||
constructor(scope: Construct, id: string, props: JumpBoxProps, ctx: SharedStackContext) {
|
|
||||||
super(scope, id)
|
|
||||||
|
|
||||||
const jumpBoxSecurityGroup = new SecurityGroup(this, 'JumpBoxSecurityGroup', {
|
|
||||||
vpc: props.vpc,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Allow SSH connections to specified IP addresses
|
|
||||||
ctx.VPC.IPAllowList.forEach((ip, index) => {
|
|
||||||
jumpBoxSecurityGroup.addIngressRule(Peer.ipv4(`${ip}/32`), Port.tcp(22))
|
|
||||||
})
|
|
||||||
|
|
||||||
const jumpBox = new Instance(this, 'JumpBox', {
|
|
||||||
vpc: props.vpc,
|
|
||||||
vpcSubnets: { subnetType: SubnetType.PUBLIC },
|
|
||||||
machineImage: MachineImage.genericLinux({
|
|
||||||
[ctx.DEFAULT_AWS_REGION]: 'ami-0cfa91bdbc3be780c',
|
|
||||||
}),
|
|
||||||
instanceType: InstanceType.of(InstanceClass.T2, InstanceSize.MICRO),
|
|
||||||
securityGroup: jumpBoxSecurityGroup,
|
|
||||||
// This role allows for Session Manager connections - https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager.html
|
|
||||||
role: new Role(this, 'SessionManagerRole', {
|
|
||||||
assumedBy: new ServicePrincipal('ec2.amazonaws.com'),
|
|
||||||
description: 'Allows Session Manager to connect to EC2 instance',
|
|
||||||
managedPolicies: [
|
|
||||||
ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore'),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
// Associate the keypair for SSH connections - https://stackoverflow.com/a/60713522/7437737
|
|
||||||
jumpBox.instance.addPropertyOverride('KeyName', 'jumpbox-key') // key was manually created in Console, available in 1Password
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,130 +0,0 @@
|
||||||
import {
|
|
||||||
AclCidr,
|
|
||||||
AclTraffic,
|
|
||||||
Action,
|
|
||||||
IVpc,
|
|
||||||
NetworkAcl,
|
|
||||||
SubnetType,
|
|
||||||
TrafficDirection,
|
|
||||||
} from 'aws-cdk-lib/aws-ec2'
|
|
||||||
import { Construct } from 'constructs'
|
|
||||||
|
|
||||||
export interface NetworkACLConfigProps {
|
|
||||||
vpc: IVpc
|
|
||||||
authorizedIPs?: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates the "best practice" network ACL config for a public/private subnet VPC (some rules are more lenient than the docs suggest)
|
|
||||||
*
|
|
||||||
* @see https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Scenario2.html#nacl-rules-scenario-2
|
|
||||||
*/
|
|
||||||
export class NetworkACLConfig extends Construct {
|
|
||||||
public readonly redisUrl: string
|
|
||||||
|
|
||||||
constructor(scope: Construct, id: string, props: NetworkACLConfigProps) {
|
|
||||||
super(scope, id)
|
|
||||||
|
|
||||||
const publicNetworkACL = new NetworkAcl(this, 'PublicNetworkACL', {
|
|
||||||
vpc: props.vpc,
|
|
||||||
subnetSelection: { subnetType: SubnetType.PUBLIC },
|
|
||||||
})
|
|
||||||
|
|
||||||
// --------------------------------
|
|
||||||
// Inbound Public Entries
|
|
||||||
// --------------------------------
|
|
||||||
publicNetworkACL.addEntry('InboundHTTPPublic', {
|
|
||||||
cidr: AclCidr.anyIpv4(),
|
|
||||||
ruleNumber: 101,
|
|
||||||
traffic: AclTraffic.tcpPort(80),
|
|
||||||
direction: TrafficDirection.INGRESS,
|
|
||||||
ruleAction: Action.ALLOW,
|
|
||||||
})
|
|
||||||
|
|
||||||
publicNetworkACL.addEntry('InboundHTTPSPublic', {
|
|
||||||
cidr: AclCidr.anyIpv4(),
|
|
||||||
ruleNumber: 110,
|
|
||||||
traffic: AclTraffic.tcpPort(443),
|
|
||||||
direction: TrafficDirection.INGRESS,
|
|
||||||
ruleAction: Action.ALLOW,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Individual IP address inbound access (20 reserved slots)
|
|
||||||
// http://checkip.amazonaws.com/
|
|
||||||
if (props.authorizedIPs?.length) {
|
|
||||||
props.authorizedIPs.forEach((ip, index) => {
|
|
||||||
publicNetworkACL.addEntry(`IPAllowPublic${index}`, {
|
|
||||||
cidr: AclCidr.ipv4(`${ip}/32`),
|
|
||||||
ruleNumber: 111 + index,
|
|
||||||
traffic: AclTraffic.tcpPort(22),
|
|
||||||
direction: TrafficDirection.INGRESS,
|
|
||||||
ruleAction: Action.ALLOW,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
publicNetworkACL.addEntry('EphemeralInboundPublic', {
|
|
||||||
cidr: AclCidr.anyIpv4(),
|
|
||||||
ruleNumber: 140,
|
|
||||||
traffic: AclTraffic.tcpPortRange(1024, 65535),
|
|
||||||
direction: TrafficDirection.INGRESS,
|
|
||||||
ruleAction: Action.ALLOW,
|
|
||||||
})
|
|
||||||
|
|
||||||
// --------------------------------
|
|
||||||
// Outbound Public Entries
|
|
||||||
// --------------------------------
|
|
||||||
|
|
||||||
publicNetworkACL.addEntry('AllowAllOutbound', {
|
|
||||||
cidr: AclCidr.anyIpv4(),
|
|
||||||
ruleNumber: 100,
|
|
||||||
traffic: AclTraffic.allTraffic(),
|
|
||||||
direction: TrafficDirection.EGRESS,
|
|
||||||
ruleAction: Action.ALLOW,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (props.vpc.privateSubnets.length) {
|
|
||||||
const privateNetworkACL = new NetworkAcl(this, 'PrivateNetworkACL', {
|
|
||||||
vpc: props.vpc,
|
|
||||||
subnetSelection: { subnetType: SubnetType.PRIVATE_WITH_EGRESS },
|
|
||||||
})
|
|
||||||
|
|
||||||
// --------------------------------
|
|
||||||
// Inbound Private Entries
|
|
||||||
// --------------------------------
|
|
||||||
|
|
||||||
// Allow all inbound traffic from public subnets
|
|
||||||
props.vpc
|
|
||||||
.selectSubnets({ subnetType: SubnetType.PUBLIC })
|
|
||||||
.subnets.forEach((sn, index) => {
|
|
||||||
privateNetworkACL.addEntry(`InboundPrivate${index}`, {
|
|
||||||
cidr: AclCidr.ipv4(sn.ipv4CidrBlock),
|
|
||||||
ruleNumber: 101 + index,
|
|
||||||
traffic: AclTraffic.allTraffic(),
|
|
||||||
direction: TrafficDirection.INGRESS,
|
|
||||||
ruleAction: Action.ALLOW,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
privateNetworkACL.addEntry('InboundEphemeralPrivate', {
|
|
||||||
cidr: AclCidr.anyIpv4(),
|
|
||||||
ruleNumber: 110,
|
|
||||||
traffic: AclTraffic.tcpPortRange(1024, 65535),
|
|
||||||
direction: TrafficDirection.INGRESS,
|
|
||||||
ruleAction: Action.ALLOW,
|
|
||||||
})
|
|
||||||
|
|
||||||
// --------------------------------
|
|
||||||
// Outbound Private Entries
|
|
||||||
// --------------------------------
|
|
||||||
|
|
||||||
privateNetworkACL.addEntry('AllowAllEgressPrivate', {
|
|
||||||
cidr: AclCidr.anyIpv4(),
|
|
||||||
ruleNumber: 100,
|
|
||||||
traffic: AclTraffic.allTraffic(),
|
|
||||||
direction: TrafficDirection.EGRESS,
|
|
||||||
ruleAction: Action.ALLOW,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,73 +0,0 @@
|
||||||
import { IVpc, Peer, Port, SecurityGroup, SubnetType } from 'aws-cdk-lib/aws-ec2'
|
|
||||||
import { CfnCacheCluster, CfnCacheClusterProps, CfnSubnetGroup } from 'aws-cdk-lib/aws-elasticache'
|
|
||||||
import { LogGroup, RetentionDays } from 'aws-cdk-lib/aws-logs'
|
|
||||||
import { Construct } from 'constructs'
|
|
||||||
|
|
||||||
export interface RedisProps {
|
|
||||||
vpc: IVpc
|
|
||||||
redisConfig: Pick<CfnCacheClusterProps, 'numCacheNodes' | 'cacheNodeType'>
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Redis extends Construct {
|
|
||||||
public readonly redisUrl: string
|
|
||||||
|
|
||||||
constructor(scope: Construct, id: string, props: RedisProps) {
|
|
||||||
super(scope, id)
|
|
||||||
|
|
||||||
const redisSecurityGroup = new SecurityGroup(this, 'RedisSecurityGroup', {
|
|
||||||
vpc: props.vpc,
|
|
||||||
description: 'Redis instance security group',
|
|
||||||
allowAllOutbound: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
const privateSubnets = props.vpc.selectSubnets({
|
|
||||||
subnetType: SubnetType.PRIVATE_WITH_EGRESS,
|
|
||||||
})
|
|
||||||
|
|
||||||
const redisSubnetGroup = new CfnSubnetGroup(this, 'RedisSubnetGroup', {
|
|
||||||
description: 'Subnet group for Redis cluster',
|
|
||||||
subnetIds: privateSubnets.subnetIds,
|
|
||||||
cacheSubnetGroupName: 'redis-subnet',
|
|
||||||
})
|
|
||||||
|
|
||||||
const REDIS_PORT = 6379
|
|
||||||
|
|
||||||
const { logGroupName } = new LogGroup(this, 'RedisLogGroup', {
|
|
||||||
retention: RetentionDays.TWO_WEEKS,
|
|
||||||
})
|
|
||||||
|
|
||||||
const redisCluster = new CfnCacheCluster(this, 'RedisCluster', {
|
|
||||||
engine: 'redis',
|
|
||||||
cacheNodeType: props.redisConfig.cacheNodeType,
|
|
||||||
numCacheNodes: props.redisConfig.numCacheNodes,
|
|
||||||
autoMinorVersionUpgrade: true,
|
|
||||||
port: REDIS_PORT,
|
|
||||||
vpcSecurityGroupIds: [redisSecurityGroup.securityGroupId],
|
|
||||||
cacheSubnetGroupName: 'redis-subnet',
|
|
||||||
logDeliveryConfigurations: [
|
|
||||||
{
|
|
||||||
destinationDetails: { cloudWatchLogsDetails: { logGroup: logGroupName } },
|
|
||||||
destinationType: 'cloudwatch-logs',
|
|
||||||
logFormat: 'json',
|
|
||||||
logType: 'slow-log',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
destinationDetails: {
|
|
||||||
cloudWatchLogsDetails: { logGroup: logGroupName },
|
|
||||||
},
|
|
||||||
destinationType: 'cloudwatch-logs',
|
|
||||||
logFormat: 'json',
|
|
||||||
logType: 'engine-log',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
// Require the subnet group to be created before the cluster
|
|
||||||
redisCluster.node.addDependency(redisSubnetGroup)
|
|
||||||
|
|
||||||
// Open incoming traffic to the Redis port (e.g. 6379)
|
|
||||||
redisSecurityGroup.addIngressRule(Peer.anyIpv4(), Port.tcp(REDIS_PORT))
|
|
||||||
|
|
||||||
this.redisUrl = `redis://${redisCluster.attrRedisEndpointAddress}:${redisCluster.attrRedisEndpointPort}`
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,295 +0,0 @@
|
||||||
import { Duration, Stack, StackProps, Tags } from 'aws-cdk-lib'
|
|
||||||
|
|
||||||
import { NetworkMode, Platform } from 'aws-cdk-lib/aws-ecr-assets'
|
|
||||||
import {
|
|
||||||
ContainerImage,
|
|
||||||
FargateService,
|
|
||||||
FargateTaskDefinition,
|
|
||||||
LogDriver,
|
|
||||||
PropagatedTagSource,
|
|
||||||
Secret as ECSSecret,
|
|
||||||
} from 'aws-cdk-lib/aws-ecs'
|
|
||||||
import {
|
|
||||||
ApplicationLoadBalancer,
|
|
||||||
ApplicationProtocol,
|
|
||||||
ApplicationProtocolVersion,
|
|
||||||
IApplicationLoadBalancer,
|
|
||||||
ListenerAction,
|
|
||||||
SslPolicy,
|
|
||||||
} from 'aws-cdk-lib/aws-elasticloadbalancingv2'
|
|
||||||
import { Certificate } from 'aws-cdk-lib/aws-certificatemanager'
|
|
||||||
import { StringParameter } from 'aws-cdk-lib/aws-ssm'
|
|
||||||
import { Construct } from 'constructs'
|
|
||||||
import { ServerStackContext } from '../utils/get-context'
|
|
||||||
import {
|
|
||||||
AllowedMethods,
|
|
||||||
CachePolicy,
|
|
||||||
Distribution,
|
|
||||||
OriginProtocolPolicy,
|
|
||||||
OriginRequestPolicy,
|
|
||||||
OriginSslPolicy,
|
|
||||||
PriceClass,
|
|
||||||
ViewerProtocolPolicy,
|
|
||||||
} from 'aws-cdk-lib/aws-cloudfront'
|
|
||||||
import { LoadBalancerV2Origin } from 'aws-cdk-lib/aws-cloudfront-origins'
|
|
||||||
import { Secret } from 'aws-cdk-lib/aws-secretsmanager'
|
|
||||||
import { SharedStack } from './shared-stack'
|
|
||||||
import { Effect, PolicyStatement } from 'aws-cdk-lib/aws-iam'
|
|
||||||
|
|
||||||
interface ServerStackProps extends StackProps {
|
|
||||||
sharedStack: SharedStack
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ServerStack extends Stack {
|
|
||||||
public readonly loadBalancer: IApplicationLoadBalancer
|
|
||||||
|
|
||||||
constructor(scope: Construct, id: string, props: ServerStackProps, ctx: ServerStackContext) {
|
|
||||||
super(scope, id, props)
|
|
||||||
|
|
||||||
const {
|
|
||||||
redisUrl,
|
|
||||||
userAccessKeyId,
|
|
||||||
userAccessSecretArn,
|
|
||||||
publicBucketName,
|
|
||||||
privateBucketName,
|
|
||||||
vpc,
|
|
||||||
cluster,
|
|
||||||
signerPubKeyId,
|
|
||||||
signerSecretId,
|
|
||||||
} = props.sharedStack
|
|
||||||
|
|
||||||
const taskDefinition = new FargateTaskDefinition(this, 'ServerTaskDefinition', {
|
|
||||||
memoryLimitMiB: +ctx.ComputeCapacity.memory,
|
|
||||||
cpu: ctx.ComputeCapacity.cpu,
|
|
||||||
})
|
|
||||||
|
|
||||||
// CDK does not add these permissions by default - https://github.com/aws/aws-cdk/issues/17156
|
|
||||||
// https://docs.aws.amazon.com/AmazonECS/latest/userguide/specifying-sensitive-data-secrets.html#secrets-iam
|
|
||||||
taskDefinition.addToExecutionRolePolicy(
|
|
||||||
new PolicyStatement({
|
|
||||||
effect: Effect.ALLOW,
|
|
||||||
actions: ['secretsmanager:GetSecretValue'],
|
|
||||||
resources: [userAccessSecretArn],
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
taskDefinition.addContainer('ServerContainer', {
|
|
||||||
logging: LogDriver.awsLogs({ streamPrefix: 'server-container' }),
|
|
||||||
image: ContainerImage.fromAsset('../../', {
|
|
||||||
file: 'apps/server/Dockerfile',
|
|
||||||
networkMode: NetworkMode.HOST,
|
|
||||||
target: 'prod',
|
|
||||||
platform: Platform.LINUX_AMD64, // explicitly define so can build on Macbook M1 (https://stackoverflow.com/a/71102144)
|
|
||||||
}),
|
|
||||||
environment: {
|
|
||||||
AWS_ACCESS_KEY_ID: userAccessKeyId,
|
|
||||||
NX_REDIS_URL: redisUrl,
|
|
||||||
NX_CDN_PUBLIC_BUCKET: publicBucketName,
|
|
||||||
NX_CDN_PRIVATE_BUCKET: privateBucketName,
|
|
||||||
NX_CDN_SIGNER_SECRET_ID: signerSecretId,
|
|
||||||
NX_CDN_SIGNER_PUBKEY_ID: signerPubKeyId,
|
|
||||||
...ctx.sharedEnv,
|
|
||||||
...ctx.Container.Env,
|
|
||||||
},
|
|
||||||
portMappings: [{ containerPort: +ctx.Container.Env.NX_PORT }],
|
|
||||||
entryPoint: ['sh', '-c'],
|
|
||||||
command: ['/bin/sh -c "node ./main.js"'],
|
|
||||||
healthCheck: {
|
|
||||||
command: [
|
|
||||||
'CMD-SHELL',
|
|
||||||
`curl --fail -s http://localhost:${ctx.Container.Env.NX_PORT}/health || exit 1`,
|
|
||||||
],
|
|
||||||
interval: Duration.seconds(10),
|
|
||||||
retries: 3,
|
|
||||||
startPeriod: Duration.seconds(10),
|
|
||||||
timeout: Duration.seconds(5),
|
|
||||||
},
|
|
||||||
stopTimeout: Duration.seconds(10), // waits 10 seconds after SIGTERM to fire SIGKILL
|
|
||||||
secrets: {
|
|
||||||
NX_DATABASE_URL: ECSSecret.fromSsmParameter(
|
|
||||||
StringParameter.fromSecureStringParameterAttributes(this, 'DatabaseUrlParam', {
|
|
||||||
parameterName: '/apps/maybe-app/NX_DATABASE_URL',
|
|
||||||
})
|
|
||||||
),
|
|
||||||
NX_DATABASE_SECRET: ECSSecret.fromSsmParameter(
|
|
||||||
StringParameter.fromSecureStringParameterAttributes(
|
|
||||||
this,
|
|
||||||
'DatabaseSecretParam',
|
|
||||||
{
|
|
||||||
parameterName: '/apps/maybe-app/NX_DATABASE_SECRET',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
),
|
|
||||||
NX_SESSION_SECRET: ECSSecret.fromSsmParameter(
|
|
||||||
StringParameter.fromSecureStringParameterAttributes(
|
|
||||||
this,
|
|
||||||
'SessionSecretParam',
|
|
||||||
{
|
|
||||||
parameterName: '/apps/maybe-app/NX_SESSION_SECRET',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
),
|
|
||||||
NX_AUTH0_CLIENT_SECRET: ECSSecret.fromSsmParameter(
|
|
||||||
StringParameter.fromSecureStringParameterAttributes(
|
|
||||||
this,
|
|
||||||
'Auth0ClientSecretParam',
|
|
||||||
{
|
|
||||||
parameterName: '/apps/maybe-app/NX_AUTH0_CLIENT_SECRET',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
),
|
|
||||||
NX_AUTH0_MGMT_CLIENT_SECRET: ECSSecret.fromSsmParameter(
|
|
||||||
StringParameter.fromSecureStringParameterAttributes(
|
|
||||||
this,
|
|
||||||
'Auth0MgmtClientSecretParam',
|
|
||||||
{
|
|
||||||
parameterName: '/apps/maybe-app/NX_AUTH0_MGMT_CLIENT_SECRET',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
),
|
|
||||||
NX_PLAID_SECRET: ECSSecret.fromSsmParameter(
|
|
||||||
StringParameter.fromSecureStringParameterAttributes(this, 'PlaidSecretParam', {
|
|
||||||
parameterName: '/providers/NX_PLAID_SECRET',
|
|
||||||
})
|
|
||||||
),
|
|
||||||
NX_FINICITY_APP_KEY: ECSSecret.fromSsmParameter(
|
|
||||||
StringParameter.fromSecureStringParameterAttributes(
|
|
||||||
this,
|
|
||||||
'FinicityAppKeyParam',
|
|
||||||
{
|
|
||||||
parameterName: '/providers/NX_FINICITY_APP_KEY',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
),
|
|
||||||
NX_FINICITY_PARTNER_SECRET: ECSSecret.fromSsmParameter(
|
|
||||||
StringParameter.fromSecureStringParameterAttributes(
|
|
||||||
this,
|
|
||||||
'FinicityPartnerSecretParam',
|
|
||||||
{
|
|
||||||
parameterName: '/providers/NX_FINICITY_PARTNER_SECRET',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
),
|
|
||||||
NX_POLYGON_API_KEY: ECSSecret.fromSsmParameter(
|
|
||||||
StringParameter.fromSecureStringParameterAttributes(
|
|
||||||
this,
|
|
||||||
'PolygonApiKeyParam',
|
|
||||||
{
|
|
||||||
parameterName: '/providers/NX_POLYGON_API_KEY',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
),
|
|
||||||
NX_STRIPE_SECRET_KEY: ECSSecret.fromSsmParameter(
|
|
||||||
StringParameter.fromSecureStringParameterAttributes(
|
|
||||||
this,
|
|
||||||
'StripeSecretKeyParam',
|
|
||||||
{
|
|
||||||
parameterName: '/providers/NX_STRIPE_SECRET_KEY',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
),
|
|
||||||
NX_STRIPE_WEBHOOK_SECRET: ECSSecret.fromSsmParameter(
|
|
||||||
StringParameter.fromSecureStringParameterAttributes(
|
|
||||||
this,
|
|
||||||
'StripeWebhookSecretParam',
|
|
||||||
{
|
|
||||||
parameterName: '/providers/NX_STRIPE_WEBHOOK_SECRET',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
),
|
|
||||||
NX_POSTMARK_API_TOKEN: ECSSecret.fromSsmParameter(
|
|
||||||
StringParameter.fromSecureStringParameterAttributes(
|
|
||||||
this,
|
|
||||||
'PostmarkApiTokenParam',
|
|
||||||
{
|
|
||||||
parameterName: '/providers/NX_POSTMARK_API_TOKEN',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
),
|
|
||||||
// references the User access key created in SharedStack
|
|
||||||
AWS_SECRET_ACCESS_KEY: ECSSecret.fromSecretsManager(
|
|
||||||
Secret.fromSecretPartialArn(this, 'AwsSdkUserSecret', userAccessSecretArn)
|
|
||||||
),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const server = new FargateService(this, 'ServerService', {
|
|
||||||
cluster, // reference to SharedStack
|
|
||||||
taskDefinition,
|
|
||||||
enableECSManagedTags: true,
|
|
||||||
propagateTags: PropagatedTagSource.SERVICE,
|
|
||||||
desiredCount: ctx.DesiredTaskCount,
|
|
||||||
circuitBreaker: {
|
|
||||||
rollback: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
this.loadBalancer = new ApplicationLoadBalancer(this, 'ServerLoadBalancer', {
|
|
||||||
vpc, // reference to SharedStack
|
|
||||||
internetFacing: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
const httpsListener = this.loadBalancer.addListener('PublicListener', {
|
|
||||||
protocol: ApplicationProtocol.HTTPS,
|
|
||||||
port: 443,
|
|
||||||
open: true,
|
|
||||||
sslPolicy: SslPolicy.RECOMMENDED,
|
|
||||||
})
|
|
||||||
|
|
||||||
httpsListener.addCertificates('ServerLBCertificates', [
|
|
||||||
Certificate.fromCertificateArn(this, 'ServerCertificate', ctx.CertificateArn),
|
|
||||||
])
|
|
||||||
|
|
||||||
// Point the load balancer at the ECS task
|
|
||||||
httpsListener.addTargets('ECS', {
|
|
||||||
protocol: ApplicationProtocol.HTTP,
|
|
||||||
protocolVersion: ApplicationProtocolVersion.HTTP1,
|
|
||||||
targets: [server],
|
|
||||||
deregistrationDelay: Duration.seconds(10),
|
|
||||||
healthCheck: {
|
|
||||||
path: '/health',
|
|
||||||
healthyThresholdCount: 2,
|
|
||||||
timeout: Duration.seconds(3),
|
|
||||||
interval: Duration.seconds(5),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Redirect all HTTP traffic to HTTPs
|
|
||||||
this.loadBalancer.addListener('RedirectToHTTPSListener', {
|
|
||||||
protocol: ApplicationProtocol.HTTP,
|
|
||||||
port: 80,
|
|
||||||
open: true,
|
|
||||||
defaultAction: ListenerAction.redirect({
|
|
||||||
port: '443',
|
|
||||||
protocol: ApplicationProtocol.HTTPS,
|
|
||||||
permanent: true,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
// WAF => Cloudfront => ALB
|
|
||||||
new Distribution(this, 'ServerCloudfrontDistribution', {
|
|
||||||
comment: 'ALB for ECS Fargate server',
|
|
||||||
defaultBehavior: {
|
|
||||||
origin: new LoadBalancerV2Origin(this.loadBalancer, {
|
|
||||||
protocolPolicy: OriginProtocolPolicy.HTTPS_ONLY,
|
|
||||||
originSslProtocols: [OriginSslPolicy.TLS_V1_1],
|
|
||||||
}),
|
|
||||||
allowedMethods: AllowedMethods.ALLOW_ALL,
|
|
||||||
cachePolicy: CachePolicy.CACHING_DISABLED,
|
|
||||||
originRequestPolicy: OriginRequestPolicy.ALL_VIEWER,
|
|
||||||
viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
|
|
||||||
},
|
|
||||||
priceClass: PriceClass.PRICE_CLASS_100,
|
|
||||||
webAclId: ctx.WAFArn,
|
|
||||||
certificate: Certificate.fromCertificateArn(
|
|
||||||
this,
|
|
||||||
'CloudfrontCertificate',
|
|
||||||
ctx.Cloudfront.CertificateArn
|
|
||||||
),
|
|
||||||
domainNames: ctx.Cloudfront.CNAMES,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Put a tag on this service so we can track it easier in Cost Explorer
|
|
||||||
Tags.of(server).add('ecs-service-name', 'server')
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,229 +0,0 @@
|
||||||
import { RemovalPolicy, Stack, StackProps } from 'aws-cdk-lib'
|
|
||||||
import { Certificate } from 'aws-cdk-lib/aws-certificatemanager'
|
|
||||||
import { Distribution, KeyGroup, PriceClass, PublicKey } from 'aws-cdk-lib/aws-cloudfront'
|
|
||||||
import { S3Origin } from 'aws-cdk-lib/aws-cloudfront-origins'
|
|
||||||
import {
|
|
||||||
CfnRoute,
|
|
||||||
GatewayVpcEndpointAwsService,
|
|
||||||
InterfaceVpcEndpointAwsService,
|
|
||||||
IVpc,
|
|
||||||
Vpc,
|
|
||||||
} from 'aws-cdk-lib/aws-ec2'
|
|
||||||
import { Cluster, ICluster } from 'aws-cdk-lib/aws-ecs'
|
|
||||||
import { AccessKey, Policy, PolicyStatement, User } from 'aws-cdk-lib/aws-iam'
|
|
||||||
import { Bucket, CfnBucket, HttpMethods } from 'aws-cdk-lib/aws-s3'
|
|
||||||
import { Secret } from 'aws-cdk-lib/aws-secretsmanager'
|
|
||||||
import { Construct } from 'constructs'
|
|
||||||
import { JumpBox } from '../constructs/jump-box'
|
|
||||||
import { NetworkACLConfig } from '../constructs/network-acl'
|
|
||||||
import { Redis } from '../constructs/redis'
|
|
||||||
import { SharedStackContext } from '../utils/get-context'
|
|
||||||
import { StringParameter } from 'aws-cdk-lib/aws-ssm'
|
|
||||||
|
|
||||||
interface SharedStackProps extends StackProps {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shared resources that workloads are deployed to
|
|
||||||
*
|
|
||||||
* @see - https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.Vpc.html
|
|
||||||
*/
|
|
||||||
export class SharedStack extends Stack {
|
|
||||||
public readonly vpc: IVpc
|
|
||||||
public readonly cluster: ICluster
|
|
||||||
public readonly redisUrl: string
|
|
||||||
public readonly userAccessSecretArn: string
|
|
||||||
public readonly userAccessKeyId: string
|
|
||||||
public readonly privateBucketName: string
|
|
||||||
public readonly publicBucketName: string
|
|
||||||
public readonly signerSecretId: string
|
|
||||||
public readonly signerPubKeyId: string
|
|
||||||
|
|
||||||
constructor(scope: Construct, id: string, props: SharedStackProps, ctx: SharedStackContext) {
|
|
||||||
super(scope, id, props)
|
|
||||||
|
|
||||||
this.vpc = new Vpc(this, 'MaybeAppVPC', {
|
|
||||||
maxAzs: 2,
|
|
||||||
natGateways: ctx.VPC.NATCount,
|
|
||||||
})
|
|
||||||
|
|
||||||
new NetworkACLConfig(this, 'MaybeAppVPCNetworkACLConfig', {
|
|
||||||
vpc: this.vpc,
|
|
||||||
authorizedIPs: ctx.VPC.IPAllowList,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Fargate >= 1.4.0 requires S3 Gateway, ECR, ECR Docker, and Cloudwatch Logs
|
|
||||||
// https://docs.aws.amazon.com/AmazonECR/latest/userguide/vpc-endpoints.html#ecr-vpc-endpoint-considerations
|
|
||||||
this.vpc.addGatewayEndpoint('S3GatewayEndpoint', {
|
|
||||||
service: GatewayVpcEndpointAwsService.S3,
|
|
||||||
})
|
|
||||||
|
|
||||||
this.vpc.addInterfaceEndpoint('ECRInterfaceEndpoint', {
|
|
||||||
service: InterfaceVpcEndpointAwsService.ECR,
|
|
||||||
})
|
|
||||||
|
|
||||||
this.vpc.addInterfaceEndpoint('DockerInterfaceEndpoint', {
|
|
||||||
service: InterfaceVpcEndpointAwsService.ECR_DOCKER,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Containers write logs directly to Cloudwatch
|
|
||||||
this.vpc.addInterfaceEndpoint('CloudwatchLogsInterfaceEndpoint', {
|
|
||||||
service: InterfaceVpcEndpointAwsService.CLOUDWATCH_LOGS,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Open a communication channel with TimescaleDB
|
|
||||||
if (ctx.PeeringCnxId) {
|
|
||||||
const addPeeringRoute = (routeTableId: string, index: number, subnetType: string) => {
|
|
||||||
new CfnRoute(this, `PeeringRoute-${subnetType}-${index}`, {
|
|
||||||
destinationCidrBlock: ctx.PeeringCnxCidr,
|
|
||||||
routeTableId,
|
|
||||||
vpcPeeringConnectionId: ctx.PeeringCnxId,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allows our private services to connect to DB
|
|
||||||
this.vpc.privateSubnets.forEach((subnet, index) =>
|
|
||||||
addPeeringRoute(subnet.routeTable.routeTableId, index, 'private')
|
|
||||||
)
|
|
||||||
|
|
||||||
// This allows our JumpBox SSH tunnel to work
|
|
||||||
this.vpc.publicSubnets.forEach((subnet, index) =>
|
|
||||||
addPeeringRoute(subnet.routeTable.routeTableId, index, 'public')
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The ECS Cluster that all ECS services and tasks are deployed to
|
|
||||||
this.cluster = new Cluster(this, 'ECSCluster', { vpc: this.vpc })
|
|
||||||
|
|
||||||
const redis = new Redis(this, 'Redis', {
|
|
||||||
vpc: this.vpc,
|
|
||||||
redisConfig: {
|
|
||||||
numCacheNodes: ctx.Redis.count,
|
|
||||||
cacheNodeType: ctx.Redis.size,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
this.redisUrl = redis.redisUrl
|
|
||||||
|
|
||||||
// For connecting to DB through EC2 instance as SSH tunnel
|
|
||||||
new JumpBox(this, 'JumpBox', { vpc: this.vpc }, ctx)
|
|
||||||
|
|
||||||
// Where we put public assets like fonts, logos, etc.
|
|
||||||
// Everything stored here is publicly available over our CDN
|
|
||||||
const publicBucket = new Bucket(this, 'Public', {
|
|
||||||
removalPolicy: RemovalPolicy.RETAIN,
|
|
||||||
})
|
|
||||||
|
|
||||||
// WORM compliant bucket to store CDN assets such as AMA uploads
|
|
||||||
const privateBucket = new Bucket(this, 'Assets', {
|
|
||||||
versioned: true,
|
|
||||||
removalPolicy: RemovalPolicy.RETAIN,
|
|
||||||
cors: [
|
|
||||||
{
|
|
||||||
allowedHeaders: [
|
|
||||||
'Authorization',
|
|
||||||
'x-amz-date',
|
|
||||||
'x-amz-content-sha256',
|
|
||||||
'content-type',
|
|
||||||
],
|
|
||||||
allowedMethods: [HttpMethods.GET, HttpMethods.POST],
|
|
||||||
allowedOrigins: ctx.UploadOrigins,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
const privateBucketRef = privateBucket.node.defaultChild as CfnBucket
|
|
||||||
privateBucketRef.addPropertyOverride('ObjectLockEnabled', true)
|
|
||||||
privateBucketRef.addPropertyOverride('ObjectLockConfiguration.ObjectLockEnabled', 'Enabled')
|
|
||||||
privateBucketRef.addPropertyOverride(
|
|
||||||
'ObjectLockConfiguration.Rule.DefaultRetention.Years',
|
|
||||||
5
|
|
||||||
)
|
|
||||||
privateBucketRef.addPropertyOverride(
|
|
||||||
'ObjectLockConfiguration.Rule.DefaultRetention.Mode',
|
|
||||||
'GOVERNANCE'
|
|
||||||
)
|
|
||||||
|
|
||||||
const signerPublicKey = StringParameter.fromStringParameterName(
|
|
||||||
this,
|
|
||||||
'SignerPKParam',
|
|
||||||
'/apps/maybe-app/CLOUDFRONT_SIGNER1_PUB'
|
|
||||||
)
|
|
||||||
|
|
||||||
const signerPubKey = new PublicKey(this, 'SignerPub', {
|
|
||||||
encodedKey: signerPublicKey.stringValue,
|
|
||||||
})
|
|
||||||
|
|
||||||
// server app will grab this ID as a reference
|
|
||||||
this.signerPubKeyId = signerPubKey.publicKeyId
|
|
||||||
|
|
||||||
const signerPrivateKey = Secret.fromSecretNameV2(
|
|
||||||
this,
|
|
||||||
'SignerPriv',
|
|
||||||
'/apps/maybe-app/CLOUDFRONT_SIGNER1_PRIV'
|
|
||||||
)
|
|
||||||
|
|
||||||
// server app will dynamically grab this (via SDK) by its ID to sign urls
|
|
||||||
this.signerSecretId = signerPrivateKey.secretName
|
|
||||||
|
|
||||||
// This group holds public keys that can sign URLs and cookies to serve restricted content from the Distribution
|
|
||||||
const keyGroup = new KeyGroup(this, 'KeyGroup', {
|
|
||||||
items: [signerPubKey],
|
|
||||||
})
|
|
||||||
|
|
||||||
const distribution = new Distribution(this, 'Distribution', {
|
|
||||||
comment: 'Maybe App Cloudfront CDN',
|
|
||||||
domainNames: ctx.Cloudfront.CNAMES,
|
|
||||||
certificate: Certificate.fromCertificateArn(
|
|
||||||
this,
|
|
||||||
'CdnCert',
|
|
||||||
ctx.Cloudfront.CertificateArn
|
|
||||||
),
|
|
||||||
defaultBehavior: {
|
|
||||||
origin: new S3Origin(publicBucket),
|
|
||||||
},
|
|
||||||
additionalBehaviors: {
|
|
||||||
// All content served here requires signed urls to view
|
|
||||||
'/private/*': {
|
|
||||||
origin: new S3Origin(privateBucket),
|
|
||||||
trustedKeyGroups: [keyGroup],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
priceClass: PriceClass.PRICE_CLASS_100,
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* User that AWS SDK will use within server and workers apps
|
|
||||||
*
|
|
||||||
* Permissions added as-needed here.
|
|
||||||
*/
|
|
||||||
const user = new User(this, 'SdkUser')
|
|
||||||
user.attachInlinePolicy(
|
|
||||||
new Policy(this, 'SdkUserPolicy', {
|
|
||||||
statements: [
|
|
||||||
new PolicyStatement({
|
|
||||||
actions: ['s3:PutObject', 's3:GetObject', 's3:GetObjectAttributes'],
|
|
||||||
resources: [`${privateBucket.bucketArn}/*`, `${publicBucket.bucketArn}/*`],
|
|
||||||
}),
|
|
||||||
new PolicyStatement({
|
|
||||||
actions: ['secretsmanager:GetSecretValue'],
|
|
||||||
// Can access any key stored under the /apps/maybe-app path
|
|
||||||
resources: [
|
|
||||||
`arn:aws:secretsmanager:${this.region}:${this.account}:secret:/apps/maybe-app/*`,
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
// Access key secret safely stored in secrets manager (and retrieved in server/workers stacks)
|
|
||||||
const accessKey = new AccessKey(this, 'SdkUserAcessKey', { user })
|
|
||||||
const accessKeySecret = new Secret(this, 'SdkUserAccessKeySecret', {
|
|
||||||
secretStringValue: accessKey.secretAccessKey,
|
|
||||||
})
|
|
||||||
|
|
||||||
this.userAccessKeyId = accessKey.accessKeyId
|
|
||||||
this.userAccessSecretArn = accessKeySecret.secretArn
|
|
||||||
this.privateBucketName = privateBucket.bucketName
|
|
||||||
this.publicBucketName = publicBucket.bucketName
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
import { Stack, StackProps } from 'aws-cdk-lib'
|
|
||||||
import { SubnetType, Vpc } from 'aws-cdk-lib/aws-ec2'
|
|
||||||
import { Construct } from 'constructs'
|
|
||||||
import { GithubActionsRunner } from '../constructs/github-actions-runner/github-actions-runner'
|
|
||||||
import { NetworkACLConfig } from '../constructs/network-acl'
|
|
||||||
import { ToolsStackContext } from '../utils/get-context'
|
|
||||||
|
|
||||||
interface ToolsStackProps extends StackProps {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resources deployed to the Deployments account for CI/CD purposes
|
|
||||||
*/
|
|
||||||
export class ToolsStack extends Stack {
|
|
||||||
constructor(scope: Construct, id: string, props: ToolsStackProps, ctx: ToolsStackContext) {
|
|
||||||
super(scope, id, props)
|
|
||||||
|
|
||||||
const vpc = new Vpc(this, 'ToolsVPC', {
|
|
||||||
maxAzs: 1,
|
|
||||||
natGateways: 0,
|
|
||||||
subnetConfiguration: [{ subnetType: SubnetType.PUBLIC, name: 'public' }],
|
|
||||||
})
|
|
||||||
|
|
||||||
new NetworkACLConfig(this, 'ToolsNetworkACL', {
|
|
||||||
vpc,
|
|
||||||
authorizedIPs: ctx.VPC.IPAllowList,
|
|
||||||
})
|
|
||||||
|
|
||||||
new GithubActionsRunner(this, 'GithubActionsRunnerInstance', { vpc }, ctx)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,179 +0,0 @@
|
||||||
import { Duration, Stack, StackProps, Tags } from 'aws-cdk-lib'
|
|
||||||
import { NetworkMode, Platform } from 'aws-cdk-lib/aws-ecr-assets'
|
|
||||||
import {
|
|
||||||
ContainerImage,
|
|
||||||
FargateService,
|
|
||||||
FargateTaskDefinition,
|
|
||||||
LogDrivers,
|
|
||||||
PropagatedTagSource,
|
|
||||||
Secret as ECSSecret,
|
|
||||||
} from 'aws-cdk-lib/aws-ecs'
|
|
||||||
import { Effect, PolicyStatement } from 'aws-cdk-lib/aws-iam'
|
|
||||||
import { Secret } from 'aws-cdk-lib/aws-secretsmanager'
|
|
||||||
import { StringParameter } from 'aws-cdk-lib/aws-ssm'
|
|
||||||
import { Construct } from 'constructs'
|
|
||||||
import { WorkersStackContext } from '../utils/get-context'
|
|
||||||
import { SharedStack } from './shared-stack'
|
|
||||||
|
|
||||||
interface WorkersStackProps extends StackProps {
|
|
||||||
sharedStack: SharedStack
|
|
||||||
}
|
|
||||||
|
|
||||||
export class WorkersStack extends Stack {
|
|
||||||
constructor(scope: Construct, id: string, props: WorkersStackProps, ctx: WorkersStackContext) {
|
|
||||||
super(scope, id, props)
|
|
||||||
|
|
||||||
const {
|
|
||||||
redisUrl,
|
|
||||||
userAccessKeyId,
|
|
||||||
userAccessSecretArn,
|
|
||||||
publicBucketName,
|
|
||||||
privateBucketName,
|
|
||||||
cluster,
|
|
||||||
} = props.sharedStack
|
|
||||||
|
|
||||||
const workerTask = new FargateTaskDefinition(this, 'WorkerTaskDefinition', {
|
|
||||||
memoryLimitMiB: +ctx.ComputeCapacity.memory,
|
|
||||||
cpu: ctx.ComputeCapacity.cpu,
|
|
||||||
})
|
|
||||||
|
|
||||||
// CDK does not add these permissions by default - https://github.com/aws/aws-cdk/issues/17156
|
|
||||||
// https://docs.aws.amazon.com/AmazonECS/latest/userguide/specifying-sensitive-data-secrets.html#secrets-iam
|
|
||||||
workerTask.addToExecutionRolePolicy(
|
|
||||||
new PolicyStatement({
|
|
||||||
effect: Effect.ALLOW,
|
|
||||||
actions: ['secretsmanager:GetSecretValue'],
|
|
||||||
resources: [userAccessSecretArn],
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
workerTask.addContainer('worker-container', {
|
|
||||||
logging: LogDrivers.awsLogs({ streamPrefix: 'WorkerLog' }),
|
|
||||||
image: ContainerImage.fromAsset('../../', {
|
|
||||||
file: 'apps/workers/Dockerfile',
|
|
||||||
networkMode: NetworkMode.HOST,
|
|
||||||
target: 'prod',
|
|
||||||
platform: Platform.LINUX_AMD64, // explicitly define so can build on Macbook M1 (https://stackoverflow.com/a/71102144)
|
|
||||||
}),
|
|
||||||
environment: {
|
|
||||||
AWS_ACCESS_KEY_ID: userAccessKeyId,
|
|
||||||
NX_REDIS_URL: redisUrl,
|
|
||||||
NX_CDN_PUBLIC_BUCKET: publicBucketName,
|
|
||||||
NX_CDN_PRIVATE_BUCKET: privateBucketName,
|
|
||||||
...ctx.sharedEnv,
|
|
||||||
...ctx.Container.Env,
|
|
||||||
},
|
|
||||||
portMappings: [{ containerPort: +ctx.Container.Env.NX_PORT }],
|
|
||||||
entryPoint: ['sh', '-c'],
|
|
||||||
command: ['/bin/sh -c "node ./main.js"'],
|
|
||||||
healthCheck: {
|
|
||||||
command: [
|
|
||||||
'CMD-SHELL',
|
|
||||||
`curl --fail -s http://localhost:${ctx.Container.Env.NX_PORT}/health || exit 1`,
|
|
||||||
],
|
|
||||||
interval: Duration.seconds(10),
|
|
||||||
retries: 3,
|
|
||||||
startPeriod: Duration.seconds(10),
|
|
||||||
timeout: Duration.seconds(5),
|
|
||||||
},
|
|
||||||
stopTimeout: Duration.seconds(10),
|
|
||||||
secrets: {
|
|
||||||
NX_DATABASE_URL: ECSSecret.fromSsmParameter(
|
|
||||||
StringParameter.fromSecureStringParameterAttributes(this, 'DatabaseUrlParam', {
|
|
||||||
parameterName: '/apps/maybe-app/NX_DATABASE_URL',
|
|
||||||
})
|
|
||||||
),
|
|
||||||
NX_DATABASE_SECRET: ECSSecret.fromSsmParameter(
|
|
||||||
StringParameter.fromSecureStringParameterAttributes(
|
|
||||||
this,
|
|
||||||
'DatabaseSecretParam',
|
|
||||||
{
|
|
||||||
parameterName: '/apps/maybe-app/NX_DATABASE_SECRET',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
),
|
|
||||||
NX_AUTH0_MGMT_CLIENT_SECRET: ECSSecret.fromSsmParameter(
|
|
||||||
StringParameter.fromSecureStringParameterAttributes(
|
|
||||||
this,
|
|
||||||
'Auth0MgmtClientSecretParam',
|
|
||||||
{
|
|
||||||
parameterName: '/apps/maybe-app/NX_AUTH0_MGMT_CLIENT_SECRET',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
),
|
|
||||||
NX_PLAID_SECRET: ECSSecret.fromSsmParameter(
|
|
||||||
StringParameter.fromSecureStringParameterAttributes(this, 'PlaidSecretParam', {
|
|
||||||
parameterName: '/providers/NX_PLAID_SECRET',
|
|
||||||
})
|
|
||||||
),
|
|
||||||
NX_FINICITY_APP_KEY: ECSSecret.fromSsmParameter(
|
|
||||||
StringParameter.fromSecureStringParameterAttributes(
|
|
||||||
this,
|
|
||||||
'FinicityAppKeyParam',
|
|
||||||
{
|
|
||||||
parameterName: '/providers/NX_FINICITY_APP_KEY',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
),
|
|
||||||
NX_FINICITY_PARTNER_SECRET: ECSSecret.fromSsmParameter(
|
|
||||||
StringParameter.fromSecureStringParameterAttributes(
|
|
||||||
this,
|
|
||||||
'FinicityPartnerSecretParam',
|
|
||||||
{
|
|
||||||
parameterName: '/providers/NX_FINICITY_PARTNER_SECRET',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
),
|
|
||||||
NX_POLYGON_API_KEY: ECSSecret.fromSsmParameter(
|
|
||||||
StringParameter.fromSecureStringParameterAttributes(
|
|
||||||
this,
|
|
||||||
'PolygonApiKeyParam',
|
|
||||||
{
|
|
||||||
parameterName: '/providers/NX_POLYGON_API_KEY',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
),
|
|
||||||
NX_POSTMARK_API_TOKEN: ECSSecret.fromSsmParameter(
|
|
||||||
StringParameter.fromSecureStringParameterAttributes(
|
|
||||||
this,
|
|
||||||
'PostmarkApiTokenParam',
|
|
||||||
{
|
|
||||||
parameterName: '/providers/NX_POSTMARK_API_TOKEN',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
),
|
|
||||||
NX_STRIPE_SECRET_KEY: ECSSecret.fromSsmParameter(
|
|
||||||
StringParameter.fromSecureStringParameterAttributes(
|
|
||||||
this,
|
|
||||||
'StripeSecretKeyParam',
|
|
||||||
{
|
|
||||||
parameterName: '/providers/NX_STRIPE_SECRET_KEY',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
),
|
|
||||||
// references the User access key created in SharedStack
|
|
||||||
AWS_SECRET_ACCESS_KEY: ECSSecret.fromSecretsManager(
|
|
||||||
Secret.fromSecretPartialArn(this, 'AwsSdkUserSecret', userAccessSecretArn)
|
|
||||||
),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const workerService = new FargateService(this, 'WorkerService', {
|
|
||||||
cluster, // reference from SharedStack
|
|
||||||
taskDefinition: workerTask,
|
|
||||||
|
|
||||||
enableECSManagedTags: true,
|
|
||||||
propagateTags: PropagatedTagSource.SERVICE,
|
|
||||||
|
|
||||||
// ECS deployment config
|
|
||||||
// https://docs.aws.amazon.com/AmazonECS/latest/developerguide/deployment-type-ecs.html#deployment-circuit-breaker
|
|
||||||
desiredCount: ctx.DesiredTaskCount,
|
|
||||||
circuitBreaker: {
|
|
||||||
rollback: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Tag service for easier tracking in cost explorer
|
|
||||||
Tags.of(workerService).add('ecs-service-name', 'workers')
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,231 +0,0 @@
|
||||||
import { App } from 'aws-cdk-lib'
|
|
||||||
import { z } from 'zod'
|
|
||||||
|
|
||||||
const toStr = (v: number) => v.toString()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fargate compute capacity schema
|
|
||||||
*
|
|
||||||
* @see https://docs.aws.amazon.com/AmazonECS/latest/userguide/fargate-task-defs.html#fargate-tasks-size
|
|
||||||
*/
|
|
||||||
const fargateComputeSchema = z.union([
|
|
||||||
z
|
|
||||||
.object({
|
|
||||||
cpu: z.literal(512),
|
|
||||||
memory: z.enum(['1024', '2048', '3072', '4096']),
|
|
||||||
})
|
|
||||||
.strict(),
|
|
||||||
z
|
|
||||||
.object({
|
|
||||||
cpu: z.literal(1024),
|
|
||||||
memory: z.enum(['2048', '3072', '4096', '5120', '6144', '7168', '8192']),
|
|
||||||
})
|
|
||||||
.strict(),
|
|
||||||
z
|
|
||||||
.object({
|
|
||||||
cpu: z.literal(2048),
|
|
||||||
memory: z.enum(['4096', '5120', '6144', '7168', '8192', '9216', '10240']),
|
|
||||||
})
|
|
||||||
.strict(),
|
|
||||||
z
|
|
||||||
.object({
|
|
||||||
cpu: z.literal(4096),
|
|
||||||
memory: z.enum(['4096', '5120', '6144', '7168', '8192', '9216', '10240']),
|
|
||||||
})
|
|
||||||
.strict(),
|
|
||||||
])
|
|
||||||
|
|
||||||
const maybeAppSharedEnv = z
|
|
||||||
.object({
|
|
||||||
NX_API_URL: z.string(),
|
|
||||||
NX_CDN_URL: z.string(),
|
|
||||||
NODE_ENV: z.enum(['production', 'development']),
|
|
||||||
NX_AUTH0_AUDIENCE: z.literal('https://maybe-finance-api/v1'),
|
|
||||||
NX_PLAID_CLIENT_ID: z.literal('REPLACE_THIS'),
|
|
||||||
NX_PLAID_ENV: z.string(),
|
|
||||||
NX_FINICITY_PARTNER_ID: z.literal('REPLACE_THIS'),
|
|
||||||
NX_FINICITY_ENV: z.enum(['sandbox', 'production']),
|
|
||||||
NX_CLIENT_URL: z.string(),
|
|
||||||
NX_CLIENT_URL_CUSTOM: z.string(),
|
|
||||||
NX_AUTH0_DOMAIN: z.string(),
|
|
||||||
NX_AUTH0_CUSTOM_DOMAIN: z.string(),
|
|
||||||
NX_AUTH0_CLIENT_ID: z.string(),
|
|
||||||
NX_AUTH0_MGMT_CLIENT_ID: z.string(),
|
|
||||||
NX_SENTRY_ENV: z.string(),
|
|
||||||
NX_POSTMARK_FROM_ADDRESS: z.string(),
|
|
||||||
NX_STRIPE_PREMIUM_YEARLY_PRICE_ID: z.string().optional(),
|
|
||||||
NX_STRIPE_PREMIUM_MONTHLY_PRICE_ID: z.string().optional(),
|
|
||||||
})
|
|
||||||
.strict()
|
|
||||||
|
|
||||||
const maybeAppSharedStackContext = z
|
|
||||||
.object({
|
|
||||||
PeeringCnxId: z.string().optional(),
|
|
||||||
PeeringCnxCidr: z.string(),
|
|
||||||
VPC: z
|
|
||||||
.object({
|
|
||||||
NATCount: z.number().lte(2),
|
|
||||||
IPAllowList: z.string().array().max(20), // Limit to 20 addresses to avoid rule priority conflicts
|
|
||||||
})
|
|
||||||
.strict(),
|
|
||||||
Redis: z
|
|
||||||
.object({
|
|
||||||
size: z.enum([
|
|
||||||
'cache.t4g.micro',
|
|
||||||
'cache.t4g.small',
|
|
||||||
'cache.t4g.medium',
|
|
||||||
'cache.m5.large',
|
|
||||||
'cache.m6g.large',
|
|
||||||
'cache.m6g.xlarge',
|
|
||||||
'cache.m6g.2xlarge',
|
|
||||||
]),
|
|
||||||
count: z.number().lte(5),
|
|
||||||
})
|
|
||||||
.strict(),
|
|
||||||
Cloudfront: z
|
|
||||||
.object({
|
|
||||||
CertificateArn: z.string(), // located in us-east-1 region
|
|
||||||
CNAMES: z.string().array(),
|
|
||||||
})
|
|
||||||
.strict(),
|
|
||||||
UploadOrigins: z.array(z.string()).min(1),
|
|
||||||
})
|
|
||||||
.strict()
|
|
||||||
|
|
||||||
const maybeAppServerStackContext = z.object({
|
|
||||||
CertificateArn: z.string(),
|
|
||||||
DesiredTaskCount: z.number(),
|
|
||||||
WAFArn: z.string(),
|
|
||||||
ComputeCapacity: fargateComputeSchema,
|
|
||||||
Cloudfront: z
|
|
||||||
.object({
|
|
||||||
CNAMES: z.string().array(),
|
|
||||||
CertificateArn: z.string(), // located in us-east-1 region
|
|
||||||
})
|
|
||||||
.strict(),
|
|
||||||
Container: z
|
|
||||||
.object({
|
|
||||||
Env: z
|
|
||||||
.object({
|
|
||||||
NX_CORS_ORIGINS: z.string(),
|
|
||||||
NX_MORGAN_LOG_LEVEL: z.string(),
|
|
||||||
NX_PORT: z.number().transform(toStr),
|
|
||||||
NX_SENTRY_DSN: z.string(),
|
|
||||||
})
|
|
||||||
.strict(),
|
|
||||||
})
|
|
||||||
.strict(),
|
|
||||||
})
|
|
||||||
|
|
||||||
const maybeAppWorkersStackContext = z
|
|
||||||
.object({
|
|
||||||
DesiredTaskCount: z.number(),
|
|
||||||
ComputeCapacity: fargateComputeSchema,
|
|
||||||
Container: z
|
|
||||||
.object({
|
|
||||||
Env: z
|
|
||||||
.object({
|
|
||||||
NX_PORT: z.number().transform(toStr),
|
|
||||||
NX_SENTRY_DSN: z.string(),
|
|
||||||
})
|
|
||||||
.strict(),
|
|
||||||
})
|
|
||||||
.strict(),
|
|
||||||
})
|
|
||||||
.strict()
|
|
||||||
|
|
||||||
const maybeAppStackContexts = z
|
|
||||||
.object({
|
|
||||||
Shared: maybeAppSharedStackContext,
|
|
||||||
Server: maybeAppServerStackContext,
|
|
||||||
Workers: maybeAppWorkersStackContext,
|
|
||||||
})
|
|
||||||
.strict()
|
|
||||||
|
|
||||||
const stagingEnvSchema = z
|
|
||||||
.object({
|
|
||||||
ENV_NAME: z.literal('staging'),
|
|
||||||
AWS_ACCOUNT: z.literal('REPLACE_THIS'),
|
|
||||||
DEFAULT_AWS_REGION: z.literal('us-west-2'),
|
|
||||||
sharedEnv: maybeAppSharedEnv,
|
|
||||||
stackContexts: maybeAppStackContexts,
|
|
||||||
})
|
|
||||||
.strict()
|
|
||||||
|
|
||||||
const prodEnvSchema = z
|
|
||||||
.object({
|
|
||||||
ENV_NAME: z.literal('production'),
|
|
||||||
AWS_ACCOUNT: z.literal('541001830411'),
|
|
||||||
DEFAULT_AWS_REGION: z.literal('us-west-2'),
|
|
||||||
sharedEnv: maybeAppSharedEnv,
|
|
||||||
stackContexts: maybeAppStackContexts,
|
|
||||||
})
|
|
||||||
.strict()
|
|
||||||
|
|
||||||
const toolsStackContexts = z
|
|
||||||
.object({
|
|
||||||
Tools: z.object({
|
|
||||||
VPC: z
|
|
||||||
.object({
|
|
||||||
IPAllowList: z.string().array().max(20),
|
|
||||||
})
|
|
||||||
.strict(),
|
|
||||||
GithubRunner: z
|
|
||||||
.object({
|
|
||||||
SSMKeyArn: z.string(),
|
|
||||||
GithubTokenArn: z.string(),
|
|
||||||
})
|
|
||||||
.strict(),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.strict()
|
|
||||||
|
|
||||||
const toolsSharedEnv = z.object({}).strict()
|
|
||||||
|
|
||||||
const toolsEnvSchema = z
|
|
||||||
.object({
|
|
||||||
ENV_NAME: z.literal('tools'),
|
|
||||||
AWS_ACCOUNT: z.literal('REPLACE_THIS'),
|
|
||||||
DEFAULT_AWS_REGION: z.literal('us-west-2'),
|
|
||||||
sharedEnv: toolsSharedEnv,
|
|
||||||
stackContexts: toolsStackContexts,
|
|
||||||
})
|
|
||||||
.strict()
|
|
||||||
|
|
||||||
// Determines what account the configuration is deployed to
|
|
||||||
const envSchema = z
|
|
||||||
.object({
|
|
||||||
tools: toolsEnvSchema,
|
|
||||||
staging: stagingEnvSchema,
|
|
||||||
production: prodEnvSchema,
|
|
||||||
})
|
|
||||||
.strict()
|
|
||||||
|
|
||||||
export const getContext = (app: App) => {
|
|
||||||
const envKeySchema = z.enum(['tools', 'staging', 'production'])
|
|
||||||
const envKey = envKeySchema.parse(process.env.CDK_ENV)
|
|
||||||
|
|
||||||
const envRaw = app.node.tryGetContext('environments')
|
|
||||||
if (!envRaw) throw new Error(`cdk.json is in an invalid config state`)
|
|
||||||
|
|
||||||
const env = envSchema.parse(envRaw)
|
|
||||||
|
|
||||||
console.log(`Environment: ${envKey}`)
|
|
||||||
console.log(JSON.stringify(env[envKey], null, 2))
|
|
||||||
|
|
||||||
return env[envKey]
|
|
||||||
}
|
|
||||||
|
|
||||||
type MaybeAppContext<TContext> = { sharedEnv: z.infer<typeof maybeAppSharedEnv> } & {
|
|
||||||
DEFAULT_AWS_REGION: string
|
|
||||||
} & TContext
|
|
||||||
|
|
||||||
export type SharedStackContext = MaybeAppContext<z.infer<typeof maybeAppStackContexts>['Shared']>
|
|
||||||
export type ServerStackContext = MaybeAppContext<z.infer<typeof maybeAppStackContexts>['Server']>
|
|
||||||
export type WorkersStackContext = MaybeAppContext<z.infer<typeof maybeAppStackContexts>['Workers']>
|
|
||||||
|
|
||||||
type ToolsContext<TContext> = { sharedEnv: z.infer<typeof toolsSharedEnv> } & {
|
|
||||||
DEFAULT_AWS_REGION: string
|
|
||||||
} & TContext
|
|
||||||
|
|
||||||
export type ToolsStackContext = ToolsContext<z.infer<typeof toolsStackContexts>['Tools']>
|
|
|
@ -1,30 +0,0 @@
|
||||||
{
|
|
||||||
"name": "maybe-app",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"bin": {
|
|
||||||
"maybe-app": "bin/maybe-app.js"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"build": "tsc",
|
|
||||||
"watch": "tsc -w",
|
|
||||||
"test": "jest",
|
|
||||||
"cdk": "CDK_ENV=staging cdk",
|
|
||||||
"cdk:ls": "yarn cdk ls --profile=maybe_tools",
|
|
||||||
"cdk:deploy:shared": "yarn cdk deploy SharedStack --profile=maybe_tools"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/jest": "^26.0.10",
|
|
||||||
"@types/node": "10.17.27",
|
|
||||||
"aws-cdk": "^2.52.0",
|
|
||||||
"jest": "^26.4.2",
|
|
||||||
"ts-jest": "^26.2.0",
|
|
||||||
"ts-node": "^10.9.1",
|
|
||||||
"typescript": "^4.6.4"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"aws-cdk-lib": "^2.52.0",
|
|
||||||
"constructs": "^10.0.0",
|
|
||||||
"source-map-support": "^0.5.16",
|
|
||||||
"zod": "^3.14.4"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,202 +0,0 @@
|
||||||
import * as cdk from 'aws-cdk-lib'
|
|
||||||
import { Template } from 'aws-cdk-lib/assertions'
|
|
||||||
import { SharedStack } from '../lib/stacks/shared-stack'
|
|
||||||
import { ServerStack } from '../lib/stacks/server-stack'
|
|
||||||
import { WorkersStack } from '../lib/stacks/workers-stack'
|
|
||||||
import { getContext } from '../lib/utils/get-context'
|
|
||||||
|
|
||||||
// Workaround - by default, context is not automatically passed to tests
|
|
||||||
// https://github.com/aws/aws-cdk/issues/5149#issuecomment-1084788745
|
|
||||||
import cdkJsonRaw from '../cdk.json'
|
|
||||||
|
|
||||||
// ======================================================================
|
|
||||||
// ===================== STAGING TESTS ==================================
|
|
||||||
// ======================================================================
|
|
||||||
|
|
||||||
test('Shared staging stack created', () => {
|
|
||||||
process.env['CDK_ENV'] = 'staging'
|
|
||||||
|
|
||||||
const app = new cdk.App({ context: cdkJsonRaw.context })
|
|
||||||
|
|
||||||
const { DEFAULT_AWS_REGION, sharedEnv, stackContexts } = getContext(app)
|
|
||||||
|
|
||||||
const sharedStack = new SharedStack(
|
|
||||||
app,
|
|
||||||
'SharedStagingStack',
|
|
||||||
{},
|
|
||||||
{ DEFAULT_AWS_REGION, sharedEnv, ...(stackContexts as any).Shared }
|
|
||||||
)
|
|
||||||
|
|
||||||
const template = Template.fromStack(sharedStack)
|
|
||||||
|
|
||||||
// VPC created correctly
|
|
||||||
template.resourceCountIs('AWS::EC2::VPC', 1)
|
|
||||||
template.resourceCountIs('AWS::EC2::Subnet', 4)
|
|
||||||
template.resourceCountIs('AWS::EC2::NatGateway', 1)
|
|
||||||
template.resourceCountIs('AWS::EC2::RouteTable', 4)
|
|
||||||
template.resourceCountIs('AWS::EC2::EIP', 1)
|
|
||||||
template.resourceCountIs('AWS::EC2::InternetGateway', 1)
|
|
||||||
|
|
||||||
// Redis cluster created
|
|
||||||
template.resourceCountIs('AWS::ElastiCache::CacheCluster', 1)
|
|
||||||
|
|
||||||
// ECS cluster created
|
|
||||||
template.resourceCountIs('AWS::ECS::Cluster', 1)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('Server staging stack created', () => {
|
|
||||||
process.env['CDK_ENV'] = 'staging'
|
|
||||||
|
|
||||||
const app = new cdk.App({ context: cdkJsonRaw.context })
|
|
||||||
|
|
||||||
const { DEFAULT_AWS_REGION, sharedEnv, stackContexts } = getContext(app)
|
|
||||||
|
|
||||||
const sharedStack = new SharedStack(
|
|
||||||
app,
|
|
||||||
'SharedStagingStack',
|
|
||||||
{},
|
|
||||||
{ DEFAULT_AWS_REGION, sharedEnv, ...(stackContexts as any).Shared }
|
|
||||||
)
|
|
||||||
|
|
||||||
const serverStack = new ServerStack(
|
|
||||||
app,
|
|
||||||
'ServerStagingStack',
|
|
||||||
{
|
|
||||||
sharedStack,
|
|
||||||
},
|
|
||||||
{ DEFAULT_AWS_REGION, sharedEnv, ...(stackContexts as any).Server }
|
|
||||||
)
|
|
||||||
|
|
||||||
const template = Template.fromStack(serverStack)
|
|
||||||
|
|
||||||
template.resourceCountIs('AWS::ElasticLoadBalancingV2::LoadBalancer', 1)
|
|
||||||
template.resourceCountIs('AWS::ElasticLoadBalancingV2::Listener', 2)
|
|
||||||
template.resourceCountIs('AWS::ElasticLoadBalancingV2::TargetGroup', 1)
|
|
||||||
template.resourceCountIs('AWS::ECS::TaskDefinition', 1)
|
|
||||||
template.resourceCountIs('AWS::ECS::Service', 1)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('Workers staging stack created', () => {
|
|
||||||
process.env['CDK_ENV'] = 'staging'
|
|
||||||
|
|
||||||
const app = new cdk.App({ context: cdkJsonRaw.context })
|
|
||||||
|
|
||||||
const { DEFAULT_AWS_REGION, sharedEnv, stackContexts } = getContext(app)
|
|
||||||
|
|
||||||
const sharedStack = new SharedStack(
|
|
||||||
app,
|
|
||||||
'SharedStagingStack',
|
|
||||||
{},
|
|
||||||
{ DEFAULT_AWS_REGION, sharedEnv, ...(stackContexts as any).Shared }
|
|
||||||
)
|
|
||||||
|
|
||||||
const workersStack = new WorkersStack(
|
|
||||||
app,
|
|
||||||
'WorkersStagingStack',
|
|
||||||
{
|
|
||||||
sharedStack,
|
|
||||||
},
|
|
||||||
{ DEFAULT_AWS_REGION, sharedEnv, ...(stackContexts as any).Workers }
|
|
||||||
)
|
|
||||||
|
|
||||||
const template = Template.fromStack(workersStack)
|
|
||||||
|
|
||||||
template.resourceCountIs('AWS::ECS::TaskDefinition', 1)
|
|
||||||
template.resourceCountIs('AWS::ECS::Service', 1)
|
|
||||||
})
|
|
||||||
|
|
||||||
// ======================================================================
|
|
||||||
// ================== PRODUCTION TESTS ==================================
|
|
||||||
// ======================================================================
|
|
||||||
|
|
||||||
test('Shared production stack created', () => {
|
|
||||||
process.env['CDK_ENV'] = 'production'
|
|
||||||
|
|
||||||
const app = new cdk.App({ context: cdkJsonRaw.context })
|
|
||||||
|
|
||||||
const { DEFAULT_AWS_REGION, sharedEnv, stackContexts } = getContext(app)
|
|
||||||
|
|
||||||
const sharedStack = new SharedStack(
|
|
||||||
app,
|
|
||||||
'SharedStack',
|
|
||||||
{},
|
|
||||||
{ DEFAULT_AWS_REGION, sharedEnv, ...(stackContexts as any).Shared }
|
|
||||||
)
|
|
||||||
|
|
||||||
const template = Template.fromStack(sharedStack)
|
|
||||||
|
|
||||||
// VPC created correctly
|
|
||||||
template.resourceCountIs('AWS::EC2::VPC', 1)
|
|
||||||
template.resourceCountIs('AWS::EC2::Subnet', 4)
|
|
||||||
template.resourceCountIs('AWS::EC2::NatGateway', 2)
|
|
||||||
template.resourceCountIs('AWS::EC2::RouteTable', 4)
|
|
||||||
template.resourceCountIs('AWS::EC2::EIP', 2)
|
|
||||||
template.resourceCountIs('AWS::EC2::InternetGateway', 1)
|
|
||||||
|
|
||||||
// Redis cluster created
|
|
||||||
template.resourceCountIs('AWS::ElastiCache::CacheCluster', 1)
|
|
||||||
|
|
||||||
// ECS cluster created
|
|
||||||
template.resourceCountIs('AWS::ECS::Cluster', 1)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('Server production stack created', () => {
|
|
||||||
process.env['CDK_ENV'] = 'production'
|
|
||||||
|
|
||||||
const app = new cdk.App({ context: cdkJsonRaw.context })
|
|
||||||
|
|
||||||
const { DEFAULT_AWS_REGION, sharedEnv, stackContexts } = getContext(app)
|
|
||||||
|
|
||||||
const sharedStack = new SharedStack(
|
|
||||||
app,
|
|
||||||
'SharedProductionStack',
|
|
||||||
{},
|
|
||||||
{ DEFAULT_AWS_REGION, sharedEnv, ...(stackContexts as any).Shared }
|
|
||||||
)
|
|
||||||
|
|
||||||
const serverStack = new ServerStack(
|
|
||||||
app,
|
|
||||||
'ServerProductionStack',
|
|
||||||
{
|
|
||||||
sharedStack,
|
|
||||||
},
|
|
||||||
{ DEFAULT_AWS_REGION, sharedEnv, ...(stackContexts as any).Server }
|
|
||||||
)
|
|
||||||
|
|
||||||
const template = Template.fromStack(serverStack)
|
|
||||||
|
|
||||||
template.resourceCountIs('AWS::ElasticLoadBalancingV2::LoadBalancer', 1)
|
|
||||||
template.resourceCountIs('AWS::ElasticLoadBalancingV2::Listener', 2)
|
|
||||||
template.resourceCountIs('AWS::ElasticLoadBalancingV2::TargetGroup', 1)
|
|
||||||
template.resourceCountIs('AWS::ECS::TaskDefinition', 1)
|
|
||||||
template.resourceCountIs('AWS::ECS::Service', 1)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('Workers production stack created', () => {
|
|
||||||
process.env['CDK_ENV'] = 'production'
|
|
||||||
|
|
||||||
const app = new cdk.App({ context: cdkJsonRaw.context })
|
|
||||||
|
|
||||||
const { DEFAULT_AWS_REGION, sharedEnv, stackContexts } = getContext(app)
|
|
||||||
|
|
||||||
const sharedStack = new SharedStack(
|
|
||||||
app,
|
|
||||||
'SharedProductionStack',
|
|
||||||
{},
|
|
||||||
{ DEFAULT_AWS_REGION, sharedEnv, ...(stackContexts as any).Shared }
|
|
||||||
)
|
|
||||||
|
|
||||||
const workersStack = new WorkersStack(
|
|
||||||
app,
|
|
||||||
'WorkersProductionStack',
|
|
||||||
{
|
|
||||||
sharedStack,
|
|
||||||
},
|
|
||||||
{ DEFAULT_AWS_REGION, sharedEnv, ...(stackContexts as any).Workers }
|
|
||||||
)
|
|
||||||
|
|
||||||
const template = Template.fromStack(workersStack)
|
|
||||||
|
|
||||||
template.resourceCountIs('AWS::ECS::TaskDefinition', 1)
|
|
||||||
template.resourceCountIs('AWS::ECS::Service', 1)
|
|
||||||
})
|
|
|
@ -1,25 +0,0 @@
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ES2018",
|
|
||||||
"module": "commonjs",
|
|
||||||
"lib": ["es2018"],
|
|
||||||
"declaration": true,
|
|
||||||
"strict": true,
|
|
||||||
"noImplicitAny": true,
|
|
||||||
"strictNullChecks": true,
|
|
||||||
"noImplicitThis": true,
|
|
||||||
"alwaysStrict": true,
|
|
||||||
"noUnusedLocals": false,
|
|
||||||
"noUnusedParameters": false,
|
|
||||||
"noImplicitReturns": true,
|
|
||||||
"noFallthroughCasesInSwitch": false,
|
|
||||||
"inlineSourceMap": true,
|
|
||||||
"inlineSources": true,
|
|
||||||
"experimentalDecorators": true,
|
|
||||||
"strictPropertyInitialization": false,
|
|
||||||
"typeRoots": ["./node_modules/@types"],
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"esModuleInterop": true
|
|
||||||
},
|
|
||||||
"exclude": ["node_modules", "cdk.out"]
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load diff
|
@ -147,7 +147,7 @@ export default function InstitutionGrid({
|
||||||
<Image
|
<Image
|
||||||
className="cursor-pointer hover:opacity-90"
|
className="cursor-pointer hover:opacity-90"
|
||||||
key={img.alt}
|
key={img.alt}
|
||||||
loader={BrowserUtil.s3Loader}
|
loader={BrowserUtil.enhancerizerLoader}
|
||||||
src={`financial-institutions/${img.src}`}
|
src={`financial-institutions/${img.src}`}
|
||||||
alt={img.alt}
|
alt={img.alt}
|
||||||
layout="responsive"
|
layout="responsive"
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { Button } from '@maybe-finance/design-system'
|
||||||
import { BrowserUtil, useAccountContext, useUserAccountContext } from '@maybe-finance/client/shared'
|
import { BrowserUtil, useAccountContext, useUserAccountContext } from '@maybe-finance/client/shared'
|
||||||
import { ExampleApp } from '../../ExampleApp'
|
import { ExampleApp } from '../../ExampleApp'
|
||||||
import { AiOutlineLoading3Quarters as LoadingIcon } from 'react-icons/ai'
|
import { AiOutlineLoading3Quarters as LoadingIcon } from 'react-icons/ai'
|
||||||
|
import Image from 'next/legacy/image'
|
||||||
import type { StepProps } from '../StepProps'
|
import type { StepProps } from '../StepProps'
|
||||||
|
|
||||||
export function AddFirstAccount({ title, onNext }: StepProps) {
|
export function AddFirstAccount({ title, onNext }: StepProps) {
|
||||||
|
@ -54,10 +55,9 @@ export function AddFirstAccount({ title, onNext }: StepProps) {
|
||||||
['Citibank', 'citi'],
|
['Citibank', 'citi'],
|
||||||
].map(([name, src]) => (
|
].map(([name, src]) => (
|
||||||
<div key={name} className="h-6">
|
<div key={name} className="h-6">
|
||||||
<img
|
<Image
|
||||||
src={BrowserUtil.s3Loader({
|
loader={BrowserUtil.enhancerizerLoader}
|
||||||
src: `financial-institutions/white/${src}.svg`,
|
src={`financial-institutions/white/${src}.svg`}
|
||||||
})}
|
|
||||||
alt={name}
|
alt={name}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -11,7 +11,3 @@ export function enhancerizerLoader({ src, width }: ImageLoaderProps): string {
|
||||||
|
|
||||||
return `https://enhancerizer.maybe.co/images?${queryString}`
|
return `https://enhancerizer.maybe.co/images?${queryString}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function s3Loader({ src }: Pick<ImageLoaderProps, 'src'>): string {
|
|
||||||
return `https://assets.maybe.co/images/${src}`
|
|
||||||
}
|
|
||||||
|
|
|
@ -4,4 +4,3 @@ export * from './queue'
|
||||||
export * from './cache.service'
|
export * from './cache.service'
|
||||||
export * from './market-data.service'
|
export * from './market-data.service'
|
||||||
export * from './pg.service'
|
export * from './pg.service'
|
||||||
export * from './s3.service'
|
|
||||||
|
|
|
@ -1,62 +0,0 @@
|
||||||
import {
|
|
||||||
type S3Client,
|
|
||||||
type PutObjectCommandOutput,
|
|
||||||
type PutObjectCommandInput,
|
|
||||||
type DeleteObjectCommandInput,
|
|
||||||
type DeleteObjectCommandOutput,
|
|
||||||
PutObjectCommand,
|
|
||||||
DeleteObjectCommand,
|
|
||||||
} from '@aws-sdk/client-s3'
|
|
||||||
|
|
||||||
import { v4 as uuid } from 'uuid'
|
|
||||||
|
|
||||||
type UploadOptions = {
|
|
||||||
bucketKey: BucketKey
|
|
||||||
Key?: string
|
|
||||||
} & Omit<PutObjectCommandInput, 'Bucket' | 'Key'>
|
|
||||||
|
|
||||||
type DeleteOptions = {
|
|
||||||
bucketKey: BucketKey
|
|
||||||
Key: string
|
|
||||||
} & Omit<DeleteObjectCommandInput, 'Bucket' | 'Key'>
|
|
||||||
|
|
||||||
export interface IS3Service {
|
|
||||||
upload(opts?: UploadOptions): Promise<PutObjectCommandOutput & { Key: string }>
|
|
||||||
delete(opts: DeleteOptions): Promise<DeleteObjectCommandOutput & { Key: string }>
|
|
||||||
}
|
|
||||||
|
|
||||||
export type BucketKey = 'public' | 'private'
|
|
||||||
|
|
||||||
export class S3Service implements IS3Service {
|
|
||||||
constructor(readonly cli: S3Client, readonly buckets: Record<BucketKey, string>) {}
|
|
||||||
|
|
||||||
async upload({ bucketKey, Key = uuid(), ...rest }: UploadOptions) {
|
|
||||||
const uploadRes = await this.cli.send(
|
|
||||||
new PutObjectCommand({
|
|
||||||
Bucket: this.buckets[bucketKey],
|
|
||||||
Key,
|
|
||||||
...rest,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
...uploadRes,
|
|
||||||
Key,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete({ bucketKey, Key, ...rest }: DeleteOptions) {
|
|
||||||
const deleteRes = await this.cli.send(
|
|
||||||
new DeleteObjectCommand({
|
|
||||||
Bucket: this.buckets[bucketKey],
|
|
||||||
Key,
|
|
||||||
...rest,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
...deleteRes,
|
|
||||||
Key,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -40,10 +40,6 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth0/auth0-react": "^2.0.0",
|
"@auth0/auth0-react": "^2.0.0",
|
||||||
"@auth0/nextjs-auth0": "^2.0.0",
|
"@auth0/nextjs-auth0": "^2.0.0",
|
||||||
"@aws-sdk/client-s3": "^3.231.0",
|
|
||||||
"@aws-sdk/client-secrets-manager": "^3.235.0",
|
|
||||||
"@aws-sdk/cloudfront-signer": "^3.229.0",
|
|
||||||
"@aws-sdk/s3-presigned-post": "^3.234.0",
|
|
||||||
"@bull-board/express": "^4.6.4",
|
"@bull-board/express": "^4.6.4",
|
||||||
"@casl/ability": "^6.3.2",
|
"@casl/ability": "^6.3.2",
|
||||||
"@casl/prisma": "^1.4.1",
|
"@casl/prisma": "^1.4.1",
|
||||||
|
@ -71,7 +67,6 @@
|
||||||
"@trpc/react-query": "^10.4.3",
|
"@trpc/react-query": "^10.4.3",
|
||||||
"@trpc/server": "^10.4.3",
|
"@trpc/server": "^10.4.3",
|
||||||
"@types/sanitize-html": "^2.8.0",
|
"@types/sanitize-html": "^2.8.0",
|
||||||
"@uppy/aws-s3": "^3.0.4",
|
|
||||||
"@uppy/core": "^3.0.4",
|
"@uppy/core": "^3.0.4",
|
||||||
"@uppy/dashboard": "^3.2.0",
|
"@uppy/dashboard": "^3.2.0",
|
||||||
"@uppy/drag-drop": "^3.0.1",
|
"@uppy/drag-drop": "^3.0.1",
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
if test -f ~/.ssh/jumpbox_key_production; then
|
|
||||||
echo "Key exists, starting session..."
|
|
||||||
else
|
|
||||||
echo -e "Prereqs: \n\n1. Create a file called ~/.ssh/jumpbox_key_production\n2. Get key from 1Password, paste into file\n3. Run chmod 400 ~/.ssh/jumpbox_key_production\n\n"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Enter Postgres string in host:port format"
|
|
||||||
read PG_HOST_PORT
|
|
||||||
|
|
||||||
ssh -i ~/.ssh/jumpbox_key_production -L 5555:$PG_HOST_PORT ubuntu@ec2-34-222-246-3.us-west-2.compute.amazonaws.com
|
|
|
@ -1,12 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
if test -f ~/.ssh/jumpbox_key_staging; then
|
|
||||||
echo "Key exists, starting session..."
|
|
||||||
else
|
|
||||||
echo -e "Prereqs: \n\n1. Create a file called ~/.ssh/jumpbox_key_staging\n2. Get key from 1Password, paste into file\n3. Run chmod 400 ~/.ssh/jumpbox_key_staging\n\n"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Enter Postgres string in host:port format"
|
|
||||||
read PG_HOST_PORT
|
|
||||||
|
|
||||||
ssh -i ~/.ssh/jumpbox_key_staging -L 5555:$PG_HOST_PORT ubuntu@ec2-54-185-10-3.us-west-2.compute.amazonaws.com
|
|
|
@ -15,10 +15,6 @@ console.log(`CI_PROJECT_NAME: ${CI_PROJECT_NAME}`)
|
||||||
* from a deploy hook, and therefore, all manual builds would be cancelled without this logic here.
|
* from a deploy hook, and therefore, all manual builds would be cancelled without this logic here.
|
||||||
* @see https://github.com/vercel/community/discussions/285#discussioncomment-1696833
|
* @see https://github.com/vercel/community/discussions/285#discussioncomment-1696833
|
||||||
*
|
*
|
||||||
* We skip automatic deploys to main because our AWS resources (server, workers)
|
|
||||||
* take longer to deploy. Instead, we programmatically deploy after these AWS
|
|
||||||
* services have successfully deployed to minimize any mismatches in app versions.
|
|
||||||
*
|
|
||||||
* We deploy the staging-client project on merge to main to avoid conflicting deploys
|
* We deploy the staging-client project on merge to main to avoid conflicting deploys
|
||||||
* with our main app's PR preview deploys.
|
* with our main app's PR preview deploys.
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue