mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-09 07:25:19 +02:00
feat: remove AWS dependencies
This commit is contained in:
parent
879e5d94bc
commit
19e66aa38a
39 changed files with 7 additions and 7226 deletions
|
@ -1,7 +1,5 @@
|
|||
# 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/
|
||||
!prisma/
|
||||
!yarn.lock
|
|
@ -17,11 +17,6 @@ AUTH0_DEPLOY_CLIENT_SECRET=
|
|||
POSTMARK_SMTP_PASS=
|
||||
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_FINICITY_APP_KEY=
|
||||
NX_FINICITY_PARTNER_SECRET=
|
||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -46,8 +46,6 @@ Thumbs.db
|
|||
**/.env
|
||||
**/.env.local
|
||||
|
||||
aws/maybe-app/cdk.out
|
||||
|
||||
# Next.js
|
||||
.next
|
||||
|
||||
|
|
|
@ -57,8 +57,6 @@ import finicity, { getFinicityTxPushUrl, getFinicityWebhookUrl } from './finicit
|
|||
import stripe from './stripe'
|
||||
import postmark from './postmark'
|
||||
import { managementClient } from './auth0'
|
||||
import s3 from './s3'
|
||||
import secretsClient from './secretsClient'
|
||||
import defineAbilityFor from './ability'
|
||||
import env from '../../env'
|
||||
import logger from '../lib/logger'
|
||||
|
@ -314,8 +312,6 @@ export async function createContext(req: Request) {
|
|||
prisma,
|
||||
plaid,
|
||||
stripe,
|
||||
s3,
|
||||
secretsClient,
|
||||
managementClient,
|
||||
logger,
|
||||
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
|
||||
className="cursor-pointer hover:opacity-90"
|
||||
key={img.alt}
|
||||
loader={BrowserUtil.s3Loader}
|
||||
loader={BrowserUtil.enhancerizerLoader}
|
||||
src={`financial-institutions/${img.src}`}
|
||||
alt={img.alt}
|
||||
layout="responsive"
|
||||
|
|
|
@ -6,6 +6,7 @@ import { Button } from '@maybe-finance/design-system'
|
|||
import { BrowserUtil, useAccountContext, useUserAccountContext } from '@maybe-finance/client/shared'
|
||||
import { ExampleApp } from '../../ExampleApp'
|
||||
import { AiOutlineLoading3Quarters as LoadingIcon } from 'react-icons/ai'
|
||||
import Image from 'next/legacy/image'
|
||||
import type { StepProps } from '../StepProps'
|
||||
|
||||
export function AddFirstAccount({ title, onNext }: StepProps) {
|
||||
|
@ -54,10 +55,9 @@ export function AddFirstAccount({ title, onNext }: StepProps) {
|
|||
['Citibank', 'citi'],
|
||||
].map(([name, src]) => (
|
||||
<div key={name} className="h-6">
|
||||
<img
|
||||
src={BrowserUtil.s3Loader({
|
||||
src: `financial-institutions/white/${src}.svg`,
|
||||
})}
|
||||
<Image
|
||||
loader={BrowserUtil.enhancerizerLoader}
|
||||
src={`financial-institutions/white/${src}.svg`}
|
||||
alt={name}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -11,7 +11,3 @@ export function enhancerizerLoader({ src, width }: ImageLoaderProps): string {
|
|||
|
||||
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 './market-data.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": {
|
||||
"@auth0/auth0-react": "^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",
|
||||
"@casl/ability": "^6.3.2",
|
||||
"@casl/prisma": "^1.4.1",
|
||||
|
@ -71,7 +67,6 @@
|
|||
"@trpc/react-query": "^10.4.3",
|
||||
"@trpc/server": "^10.4.3",
|
||||
"@types/sanitize-html": "^2.8.0",
|
||||
"@uppy/aws-s3": "^3.0.4",
|
||||
"@uppy/core": "^3.0.4",
|
||||
"@uppy/dashboard": "^3.2.0",
|
||||
"@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.
|
||||
* @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
|
||||
* with our main app's PR preview deploys.
|
||||
*/
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue