import camelCase from 'camelcase'; import { App, RemoteBackend, TerraformOutput, TerraformStack } from 'cdktf'; import { Construct } from 'constructs'; import { APP_NAME, COMPANY_NAME } from '../../src/constants'; import Bucket from './constructs/Bucket'; import ContainerCluster from './constructs/ContainerCluster'; import Database from './constructs/Database'; import DatabaseCredentials from './constructs/DatabaseCredentials'; import DockerFileBuilder from './constructs/DockerFileBuilder'; import EnvVarsBucket from './constructs/EnvVarsBucket'; import Network from './constructs/Network'; import RandomSecret from './constructs/RandomSecret'; import { ArchiveProvider } from './generated/providers/archive'; import { AwsProvider } from './generated/providers/aws'; import { IamAccessKey, IamUser } from './generated/providers/aws/iam'; import { NullProvider } from './generated/providers/null/'; import { RandomProvider } from './generated/providers/random'; import { getId } from './utils'; /** * This configuration was based on [this tutorial](https://medium.com/avmconsulting-blog/how-to-deploy-a-dockerised-node-js-application-on-aws-ecs-with-terraform-3e6bceb48785) */ const AWS_REGION = process.env.AWS_REGION ?? 'us-east-2'; class APIStack extends TerraformStack { constructor(scope: Construct, id: string) { if (!process.env.NODE_ENV) { throw new Error('Must sepcify NODE_ENV to run infra commands'); } super(scope, id); // register providers const providerId = getId('provider'); new AwsProvider(this, getId(providerId, 'aws'), { region: AWS_REGION, }); new NullProvider(this, getId(providerId, 'null'), {}); new ArchiveProvider(this, getId(providerId, 'archive'), {}); new RandomProvider(this, getId(providerId, 'random'), {}); const apiId = getId('api'); const outputId = getId('output'); // NOTE: this is hard coded in the Dockerfile, so changing this would // require changing it from there const apiInternalPort = 8000; // create a basic user for making aws requests to from the api const apiUserId = getId('iam', 'user'); const apiAWSUser = new IamUser(this, apiUserId, { name: camelCase(apiId, { pascalCase: true }), }); // setup a basic network const network = new Network(this, apiId, { region: AWS_REGION, }); // create a container cluster const apiContainerCluster = new ContainerCluster(this, 'cluster', { network, region: AWS_REGION, port: apiInternalPort, }); // build a local docker file const apiDockerFileBuilder = new DockerFileBuilder(this, 'docker_build'); const jobWorkerDockerFileBuilder = new DockerFileBuilder( this, 'worker_image', { repoSuffix: 'worker', localFileHashLocation: 'jobqueue/worker/src' } ); // grab credentials for db const dbCredentials = new DatabaseCredentials(this, 'db_creds'); // create the db with those creds const db = new Database(this, getId('db'), { securityGroupIds: [apiContainerCluster.securityGroup.id], credentials: dbCredentials, }); // create a bucket for accepting uploads const uploadBucket = new Bucket(this, 'file-upload'); uploadBucket.allowUserToReadOrAddObjects({ user: apiAWSUser }); // create an access key for the user const apiAWSUserAccessKey = new IamAccessKey( this, getId(apiUserId, 'key'), { user: apiAWSUser.name, } ); const adminClientSecret = new RandomSecret(this, 'admin_client_secret', { length: 60, special: false, }); // setup a new bucket for env vars const envVarsBucket = new EnvVarsBucket(this, 'env_vars'); // create files with relevant keys const apiEnvVarsArns = [ envVarsBucket.ensureEnvFileExists('overrides'), envVarsBucket.addEnvFile('db', [ ['TYPEORM_HOST', db.address], ['TYPEORM_PORT', `${db.port}`], ['TYPEORM_DATABASE', db.name], ['TYPEORM_USERNAME', dbCredentials.username], ['TYPEORM_PASSWORD', dbCredentials.password], ]), envVarsBucket.addEnvFile('server', [ ['PORT', `${apiInternalPort}`], [ 'CORS_ORIGIN', `${apiContainerCluster.loadBalancer.domainName},${apiContainerCluster.loadBalancer.legacyDomainName},spoken.io,www.spoken.io,staging.spoken.io,spoken-staging.com,api.spoken-staging.com,www.spoken-staging.com`, ], ['ROOT_API_KEY_CLIENT_ID', 'admin'], ['ROOT_API_KEY_CLIENT_SECRET', adminClientSecret.value], ]), envVarsBucket.addEnvFile('aws', [ ['AWS_S3_UPLOAD_ACCESS_KEY_ID', apiAWSUserAccessKey.id], ['AWS_S3_UPLOAD_SECRET_ACCESS_KEY', apiAWSUserAccessKey.secret], ['AWS_S3_FILE_UPLOAD_BUCKET_NAME', uploadBucket.bucketName], ]), envVarsBucket.addEnvFile('algolia', [['ALGOLIA_SYNCING_ENABLED', '1']]), envVarsBucket.addEnvFile('jwt', [ ['JWT_SECRET', new RandomSecret(this, 'jwt_secret').value], [ 'REFRESH_TOKEN_SECRET', new RandomSecret(this, 'refresh_token_jwt_secret').value, ], ]), ]; const jobQueueWorkerEnvVarsArns = [ envVarsBucket.addEnvFile('worker', [ [ 'API_URL', `https://${apiContainerCluster.loadBalancer.dnsRecord.fqdn}`, ], ['API_CLIENT_ID', 'admin'], ['API_CLIENT_SECRET', adminClientSecret.value], ]), envVarsBucket.ensureEnvFileExists('overrides-worker'), ]; apiContainerCluster.setupAPIService({ envVarsArns: apiEnvVarsArns, envVarsBucketArn: envVarsBucket.arn, imageUrl: apiDockerFileBuilder.imageUrl, }); apiContainerCluster.setupJobQueueWorkers({ envVarsArns: jobQueueWorkerEnvVarsArns, envVarsBucketArn: envVarsBucket.arn, imageUrl: jobWorkerDockerFileBuilder.imageUrl, }); new TerraformOutput(this, getId(outputId, 'server', 'url'), { value: `The api was deployed to https://${apiContainerCluster.loadBalancer.dnsRecord.fqdn} 🚀`, }); } } const app = new App(); const stack = new APIStack(app, 'api'); new RemoteBackend(stack, { hostname: 'app.terraform.io', organization: 'Create-Inc', workspaces: { name: `${COMPANY_NAME}-${APP_NAME}-${process.env.NODE_ENV}`, }, token: process.env.TF_API_TOKEN, }); app.synth();