diff --git a/.circleci/config.yml b/.circleci/config.yml index 3c2021bd..1801de8d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -125,9 +125,6 @@ jobs: aws-access-key-id: AWS_ACCESS_KEY_ID aws-secret-access-key: AWS_SECRET_ACCESS_KEY aws-region: AWS_DEFAULT_REGION - - run: - name: Build infra - command: npm run build:infra - run: name: Diff command: npx cdk diff --all > diff.txt && cat diff.txt @@ -148,9 +145,6 @@ jobs: aws-access-key-id: AWS_ACCESS_KEY_ID aws-secret-access-key: AWS_SECRET_ACCESS_KEY aws-region: AWS_DEFAULT_REGION - - run: - name: Build infra - command: npm run build:infra - run: name: Deploy command: npx cdk deploy --all --require-approval never diff --git a/.env.example b/.env.example index 8debbef4..cb910587 100644 --- a/.env.example +++ b/.env.example @@ -1,9 +1,10 @@ NODE_ENV=development APP_PORT=3000 -APP_NAME="NestJS Boilerplate" +APP_NAME="Atlas Boilerplate" API_PREFIX=api APP_FALLBACK_LANGUAGE=en APP_HEADER_LANGUAGE=x-custom-lang +FRONTEND_DOMAIN=https://localhost:4200 BACKEND_DOMAIN=http://localhost:3000 SWAGGER_ENABLED=true I18N_DIRECTORY=src/i18n @@ -18,9 +19,16 @@ DATABASE_SYNCHRONIZE=false DATABASE_MAX_CONNECTIONS=100 DATABASE_SSL_ENABLED=false DATABASE_REJECT_UNAUTHORIZED=false -DATABASE_CA= -DATABASE_KEY= -DATABASE_CERT= AUTH_JWT_SECRET=secret AUTH_JWT_TOKEN_EXPIRES_IN=1d + +MAIL_TEMPLATES_PATH="templates" +MAIL_FROM="info@yourdomain.com.br" + +AWS_STORAGE_CREDENTIAL="profile" +AWS_STORAGE_REGION="us-east-1" + +# Extra environment variables to run infraestructure with CDK + +ORGANIZATION_GITHUB_ORGANIZATION_ID="dad87ad4-e90b-4f05-83d7-071757479c11" \ No newline at end of file diff --git a/.env.production b/.env.production new file mode 100644 index 00000000..fab23171 --- /dev/null +++ b/.env.production @@ -0,0 +1,40 @@ +NODE_ENV=production +APP_PORT=3000 +APP_NAME="Atlas Boilerplate" +API_PREFIX=api +APP_FALLBACK_LANGUAGE=en +APP_HEADER_LANGUAGE=x-custom-lang +FRONTEND_DOMAIN=https://localhost:4200 +BACKEND_DOMAIN=http://localhost:3000 +SWAGGER_ENABLED=true +I18N_DIRECTORY=i18n + +DATABASE_TYPE=postgres +DATABASE_HOST=localhost +DATABASE_PORT=5432 +DATABASE_USERNAME=postgres +DATABASE_PASSWORD=postgres +DATABASE_NAME=postgres +DATABASE_SYNCHRONIZE=false +DATABASE_MAX_CONNECTIONS=100 +DATABASE_SSL_ENABLED=false +DATABASE_REJECT_UNAUTHORIZED=false + +AUTH_JWT_SECRET=secret +AUTH_JWT_TOKEN_EXPIRES_IN=1d + +MAIL_TEMPLATES_PATH="templates" +MAIL_FROM="info@yourdomain.com.br" + +AWS_STORAGE_CREDENTIAL="profile" +AWS_STORAGE_REGION="us-east-1" + +# Extra environment variables to run infraestructure with CDK + +INFRA_APPLICATION_NAME="Atlas" +INFRA_STAGE_NAME="production" + +INFRA_AWS_ACCOUNT="025066284119" +INFRA_AWS_REGION="us-east-1" + +ORGANIZATION_GITHUB_ORGANIZATION_ID="dad87ad4-e90b-4f05-83d7-071757479c11" \ No newline at end of file diff --git a/README.md b/README.md index b128def1..ad363af8 100644 --- a/README.md +++ b/README.md @@ -1,113 +1,48 @@ -# NestJS and PostgreSQL on AWS using CDK +# Modular Monolith with AWS Lambda and CDK [![Quality gate](https://sonarcloud.io/api/project_badges/quality_gate?project=atlas-cli_nestjs-boilerplate)](https://sonarcloud.io/summary/new_code?id=atlas-cli_nestjs-boilerplate) [![CircleCI](https://circleci.com/gh/circleci/circleci-docs.svg?style=svg)](https://circleci.com/gh/circleci/circleci-docs) ## Description -Provide a robust backend solution leveraging NestJS with PostgreSQL on AWS infrastructure orchestrated with CDK (Cloud Development Kit). Employing a combination of AWS Lambda, Relational Database Service (RDS), RDS Proxy, CloudFormation, and CDK, this project ensures continuous deployment and management of your backend architecture using Infrastructure as Code (IAC) principles. Leverage the scalability, reliability, and flexibility of AWS services while maintaining efficient development workflows with CDK's programmable infrastructure approach. +This project provides a robust backend solution by leveraging a modular monolith architecture with AWS Lambda, NestJS, and PostgreSQL, all orchestrated using AWS CDK (Cloud Development Kit). Utilizing a combination of AWS Lambda, Relational Database Service (RDS), RDS Data API, and CDK, this setup ensures continuous deployment and management of your backend architecture through Infrastructure as Code (IAC) principles. Explore the benefits of a lambdalith approach to achieve scalability and flexibility while maintaining efficient development workflows with CDK's programmable infrastructure. ## Table of Contents - [Installing and Running](docs/installing-and-running.md) -- [Working with database](docs/database.md) +- [Working with Database](docs/database.md) +- [Deploy on AWS](docs/deploy.md) - [Auth](docs/auth.md) -## Features +## Quick Run -- :file_folder: Database: This feature uses TypeORM, an ORM (Object-Relational Mapping) library that simplifies the process of working with databases in your application. - -- :seedling: Seeding: This feature allows you to seed your database with initial data, which can be useful for testing and development purposes. - -- :wrench: Config Service: This feature uses @nestjs/config, a module that helps you manage configuration in your NestJS application. - -- :e-mail: Sign in and sign up via email: This feature allows users to sign in and sign up to your application using their email address. - -- :key: Use Admin and User roles: This feature allows you to define different roles for users of your application, such as Admin and User, and control access to certain features based on these roles. - -- :earth_americas: Internacionalization with I18N: This feature uses nestjs-i18n to support internationalization (I18N) in your application, allowing you to translate your application into different languages. - -- :bar_chart: Swagger: This feature uses Swagger, a tool for generating API documentation, to document your application's API. - -- :pill: E2E Tests: This feature includes end-to-end (E2E) tests, which test the full stack of your application to ensure it is working as expected. - -- :whale: Docker and Docker Composer Files: This feature includes Docker and Docker Compose files, which allow you to containerize your application and run it in a Docker environment. - -- :stopwatch: CircleCI: This feature uses CircleCI, a continuous integration and delivery platform, to set up pipelines that build, test, lint, run integration tests, diff, and deploy your code. The pipelines can also be put on hold if needed. - -- :construction: Infraestructure as a code with CDK: This feature allows you to define your infrastructure as code using the AWS Cloud Development Kit (CDK), which makes it easier to automate the process of creating and managing your infrastructure. - -- :computer: Typescript config in tsconfing.infra.json for CDK: This feature includes a Typescript config file (tsconfing.infra.json) that is used to configure the CDK for your application. - -- :earth_americas: Default environments development and production: This feature includes default environments for development and production, which can be used to separate your code and infrastructure for different stages of the development process. - -- :file_folder: TypeORM lambda environment: This feature allows you to use TypeORM in a lambda environment, allowing you to run your database operations in a serverless environment. - -- :file_folder: Postgres environment: This feature allows you to set up a Postgres database environment, which can be used to store and manage data. - -- :key: AWS IAM authentication for database and environment setup: This feature allows you to use AWS IAM (Identity and Access Management) to authenticate and authorize access to the database and environment. - -- :loop: Multiple lambda services in the same repository: This feature allows you to store multiple lambda services, or small, independent units of code that are triggered by certain events, in the same repository. This can be useful for organizing your code and making it easier to maintain. - -- :file_folder: Common folder for share code between different services: This feature allows you to create a common folder that can be used to share code between different services. This can be useful for reducing duplication and improving code reuse. - -- :construction: Multiple infrastructure as a code layers: This feature allows you to define your infrastructure as code, meaning that you can use code to automate the process of creating and managing your infrastructure. The core layer is for database and storage, while the application layer is for lambda and clients. - -- :package: ESBuild for compilate small lambda zip: This feature uses ESBuild, a super-fast JavaScript bundler and minifier, to compile small lambda zip files. This can help improve the performance and efficiency of your lambdas. - -- :computer: Aurora Serverless V2 and security group: This feature allows you to use Aurora Serverless V2, a fully managed, autoscaling MySQL-compatible database, and a security group, which is a virtual firewall that controls inbound and outbound traffic to your database. - -- :pill: Create lambda connection pool with RDS proxy: This feature allows you to create a connection pool, a group of reusable connections that can be used to connect to a database, and use an RDS (Relational Database Service) proxy to manage connections to the database. This can help improve the performance and reliability of your database. - -- :rocket: Create lambda for run migrations in staging: This feature allows you to create a lambda function that can be used to run migrations, or changes to the database schema, in the staging environment. This can be useful for testing and debugging your database. - -- :train2: API Gateway: This feature allows you to use API Gateway, a fully managed service that makes it easy to create, publish, maintain, monitor, and secure APIs, to connect your backend services to your applications. - -- :twisted_rightwards_arrows: Lambda API Gateway proxy to NestJS: This feature allows you to use a lambda function as an API Gateway proxy to connect to NestJS, a modular, fast, and powerful server-side application framework built with TypeScript. - -- :notebook: Base repository documentation: This feature provides documentation for the base repository, which can be used as a reference for understanding and using the code in the repository. - -- :key: OIDC Service for secure tokens: This feature allows you to use an OIDC (OpenID Connect) service to generate secure tokens, which can be used to authenticate and authorize access to your application. - -- :paperclip: You can use Compodoc or TSDoc to generate technical documentation for your methods, or you can manually write documentation in the docs/ folder and update the docs/summary.json file. Compodoc and TSDoc are tools that allow you to easily generate documentation for your codebase. - -- :rocket: A contribution template guide has been created for sharing with your team or for contributing to this project. - -- :shield: Set up SonarCloud project settings to improve your code security. - -## Quick run - -```bash +`bash` git clone --depth 1 https://github.com/atlas-cli/nestjs-boilerplate.git my-app cd my-app/ cp .env.example .env docker compose up -d -``` -For check status run +To check status, run: -```bash +`bash` docker compose logs -``` -## Comfortable development +## Comfortable Development -```bash +`bash` git clone --depth 1 https://github.com/atlas-cli/nestjs-boilerplate.git my-app cd my-app/ cp env.example .env -``` -Run all using in docker compose: +Run all using Docker Compose: -```bash +`bash` docker compose up -``` -Local use: localhost in host -Inside a docker use: postgres host +For local use: localhost in host +Inside a Docker container: postgres host -```bash +`bash` npm install npm run migration:run @@ -115,55 +50,43 @@ npm run migration:run npm run seed:run npm run start:dev -``` ## Links - Swagger: http://localhost:3000/swagger/docs -## Database utils +## Database Utils -Generate migration +Generate migration: -```bash -npm run migration:generate -- src/database/migrations/CreateNameTable -``` +`bash` +npm run migration:generate -- src/database/migrations/CreateNameTable -Run migration +Run migration: -```bash +`bash` npm run migration:run -``` -Revert migration +Revert migration: -```bash +`bash` npm run migration:revert -``` -Drop all tables in database +Drop all tables in the database: -```bash +`bash` npm run schema:drop -``` -Run seed +Run seed: -```bash +`bash` npm run seed:run -``` ## Tests -```bash -# unit tests +`bash` +# Unit tests npm run test -# e2e tests -npm run test:e2e -``` - -## Inspirations: - -https://github.com/brocoders/nestjs-boilerplate -https://github.com/NeoSOFT-Technologies/rest-node-nestjs \ No newline at end of file +# E2E tests +npm run test:e2e \ No newline at end of file diff --git a/cdk.context.json b/cdk.context.json index 4b4e748e..9e26dfee 100644 --- a/cdk.context.json +++ b/cdk.context.json @@ -1,52 +1 @@ -{ - "vpc-provider:account=767397837500:filter.isDefault=true:region=us-east-1:returnAsymmetricSubnets=true": { - "vpcId": "vpc-0a49111bfc7e90ad0", - "vpcCidrBlock": "172.31.0.0/16", - "ownerAccountId": "767397837500", - "availabilityZones": [], - "subnetGroups": [ - { - "name": "Public", - "type": "Public", - "subnets": [ - { - "subnetId": "subnet-05c72d8557ff1be9d", - "cidr": "172.31.0.0/20", - "availabilityZone": "us-east-1a", - "routeTableId": "rtb-07d8e51a395a7eea8" - }, - { - "subnetId": "subnet-0d8d024d5a7e57ab7", - "cidr": "172.31.80.0/20", - "availabilityZone": "us-east-1b", - "routeTableId": "rtb-07d8e51a395a7eea8" - }, - { - "subnetId": "subnet-0f7ff3dccf94de46d", - "cidr": "172.31.16.0/20", - "availabilityZone": "us-east-1c", - "routeTableId": "rtb-07d8e51a395a7eea8" - }, - { - "subnetId": "subnet-08b89a94b49962951", - "cidr": "172.31.32.0/20", - "availabilityZone": "us-east-1d", - "routeTableId": "rtb-07d8e51a395a7eea8" - }, - { - "subnetId": "subnet-03c7ec87f8ba43176", - "cidr": "172.31.48.0/20", - "availabilityZone": "us-east-1e", - "routeTableId": "rtb-07d8e51a395a7eea8" - }, - { - "subnetId": "subnet-06f1d5990cb41ffa5", - "cidr": "172.31.64.0/20", - "availabilityZone": "us-east-1f", - "routeTableId": "rtb-07d8e51a395a7eea8" - } - ] - } - ] - } -} +{} \ No newline at end of file diff --git a/cdk.json b/cdk.json index e9317e82..6a436e2e 100644 --- a/cdk.json +++ b/cdk.json @@ -1,3 +1,3 @@ { - "app": "node dist/infra/index" + "app": "npx ts-node --prefer-ts-exts infra/index.ts" } \ No newline at end of file diff --git a/docs/deploy.md b/docs/deploy.md index 32e8ef77..439b8e0d 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -46,50 +46,39 @@ You can configure your AWS credentials in two ways: ## Step 3: Define Your Infrastructure -1. Open the `infra/index.ts` file and update it with the following content: +1. Open the `infra/configs/production.ts` file and update it with the following content: ```typescript - import { AtlasInfraestructure, createStacks } from 'path-to-your-AtlasInfraestructure-module'; - - const infrastructure = new AtlasInfraestructure({ - production: { - // Name of your application - applicationName: 'atlas', - // Stage name for the environment - stageName: 'production', - // Domain name for the environment - domainName: 'sandbox.slingui.com', - // API domain name for the environment - apiDomainName: 'api.sandbox.slingui.com', - // Public host zone ID for the environment (AWS Route 53 Hosted Zone ID) - idPublicHostZone: 'Z01545163ANT5OQYS99UY', - env: { - // AWS account ID for the environment - account: '767397837500', - // AWS region for the environment (e.g., 'us-east-1') - region: 'us-east-1', - }, - layersStack: createStacks(), - }, - development: { - // Name of your application - applicationName: 'atlas', - // Stage name for the environment - stageName: 'development', - // Domain name for the environment - domainName: 'sandbox.slingui.com', - // API domain name for the environment - apiDomainName: 'api.sandbox.slingui.com', - // Public host zone ID for the environment (AWS Route 53 Hosted Zone ID) - idPublicHostZone: 'Z01545163ANT5OQYS99UY', - env: { - // AWS account ID for the environment - account: '767397837500', - // AWS region for the environment (e.g., 'us-east-1') - region: 'us-east-1', - }, - layersStack: createStacks(), - }, - }); +export const production: ApplicationProps = { + applicationName: 'FinanceBaas', + stageName: 'production', + env: { + account: '025066284119', + region: 'us-east-1', + }, + githubOrganizationId: environment.parsed.ORGANIZATION_GITHUB_ORGANIZATION_ID, + layersStack: stacks, + applications: { + core: { + domainName: 'boilerplate.atlascli.io', + apiDomainName: 'api.boilerplate.atlascli.io', + idPublicHostZone: 'Z03396972KP6M49QCZJPD', + applicationEnvironment: { + NODE_ENV: environment.parsed.NODE_ENV || 'development', + APP_PORT: environment.parsed.APP_PORT || '3000', + APP_NAME: environment.parsed.APP_NAME || 'NestJS Boilerplate', + API_PREFIX: environment.parsed.API_PREFIX || 'api', + APP_FALLBACK_LANGUAGE: environment.parsed.APP_FALLBACK_LANGUAGE || 'en', + APP_HEADER_LANGUAGE: environment.parsed.APP_HEADER_LANGUAGE || 'x-custom-lang', + BACKEND_DOMAIN: environment.parsed.BACKEND_DOMAIN || 'http://localhost:3000', + FRONTEND_DOMAIN: environment.parsed.BACKEND_DOMAIN || 'http://localhost:4200', + SWAGGER_ENABLED: environment.parsed.SWAGGER_ENABLED || 'true', + I18N_DIRECTORY: environment.parsed.I18N_DIRECTORY || 'src/i18n', + AUTH_JWT_SECRET: environment.parsed.AUTH_JWT_SECRET || 'secret', + AUTH_JWT_TOKEN_EXPIRES_IN: environment.parsed.AUTH_JWT_TOKEN_EXPIRES_IN || '1d', + }, + }, + }, +}; // To start this repository on AWS, you need to have a Hosted Zone on Route53 on AWS, // this is important because we generate all the necessary certificates and publish @@ -104,8 +93,7 @@ If you change the applicationName in infra/index.ts, you should update the SESSI 1. Specify the profile and deploy your stack: ```bash npm run build -npm run build:infra -npx cdk deploy --all --profile AdministratorAccess-767397837500 +npx cdk deploy --all --profile AdministratorAccess-XXXXXXXXXXXX ``` ## Step 5: Run Your Lambda Function to run migrations diff --git a/infra/configs/production.ts b/infra/configs/production.ts new file mode 100644 index 00000000..827281b8 --- /dev/null +++ b/infra/configs/production.ts @@ -0,0 +1,41 @@ +import { ApplicationProps } from '.././props/application-props'; +import { stacks } from '../stacks'; +import { config } from 'dotenv'; + +const environment = config({ path: '.env.production' }); + +export const production: ApplicationProps = { + applicationName: environment.parsed.INFRA_APPLICATION_NAME ?? 'Atlas', + stageName: environment.parsed.INFRA_STAGE_NAME ?? 'production', + env: { + account: environment.parsed.INFRA_AWS_ACCOUNT, + region: environment.parsed.INFRA_AWS_REGION, + }, + githubOrganizationId: environment.parsed.ORGANIZATION_GITHUB_ORGANIZATION_ID, + layersStack: stacks, + applications: { + core: { + domainName: 'boilerplate.atlascli.io', + apiDomainName: 'api.boilerplate.atlascli.io', + idPublicHostZone: 'Z03396972KP6M49QCZJPD', + applicationEnvironment: { + NODE_ENV: environment.parsed.NODE_ENV || 'development', + APP_PORT: environment.parsed.APP_PORT || '3000', + APP_NAME: environment.parsed.APP_NAME || 'NestJS Boilerplate', + API_PREFIX: environment.parsed.API_PREFIX || 'api', + APP_FALLBACK_LANGUAGE: environment.parsed.APP_FALLBACK_LANGUAGE || 'en', + APP_HEADER_LANGUAGE: + environment.parsed.APP_HEADER_LANGUAGE || 'x-custom-lang', + BACKEND_DOMAIN: + environment.parsed.BACKEND_DOMAIN || 'http://localhost:3000', + FRONTEND_DOMAIN: + environment.parsed.BACKEND_DOMAIN || 'http://localhost:4200', + SWAGGER_ENABLED: environment.parsed.SWAGGER_ENABLED || 'true', + I18N_DIRECTORY: environment.parsed.I18N_DIRECTORY || 'src/i18n', + AUTH_JWT_SECRET: environment.parsed.AUTH_JWT_SECRET || 'secret', + AUTH_JWT_TOKEN_EXPIRES_IN: + environment.parsed.AUTH_JWT_TOKEN_EXPIRES_IN || '1d', + }, + }, + }, +}; diff --git a/infra/constants.ts b/infra/constants.ts index 91790e63..2576669a 100644 --- a/infra/constants.ts +++ b/infra/constants.ts @@ -1,2 +1,3 @@ -export const DEFAULT_STAGE_NAME = 'development'; +// TODO CHANGE ME +export const DEFAULT_STAGE_NAME = 'production'; export const vpcCDIR = '10.0.0.0/16'; diff --git a/infra/constructs/api-gateway/api-gateway.construct.ts b/infra/constructs/api-gateway/api-gateway.construct.ts deleted file mode 100644 index 3c234bc3..00000000 --- a/infra/constructs/api-gateway/api-gateway.construct.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Cors, RestApi } from 'aws-cdk-lib/aws-apigateway'; -import { Construct } from 'constructs'; -import { createName } from '../../utils/create-name'; -import { ApiGatewayProps } from './props/api-gateway.props'; - -export class ApiGateway extends Construct { - api: RestApi; - constructor(scope: Construct, id: string, props: ApiGatewayProps) { - super(scope, id); - - // get resetApiName - const { restApiName } = props; - - // create a restApi name - const REST_API_NAME = createName(restApiName, props); - - this.api = new RestApi(this, REST_API_NAME, { - restApiName: REST_API_NAME, - domainName: { - domainName: props.domainName, - certificate: props.certificate, - }, - defaultCorsPreflightOptions: { - allowHeaders: [ - 'Content-Type', - 'X-Amz-Date', - 'Authorization', - 'X-Api-Key', - ], - allowMethods: ['OPTIONS', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE'], - allowCredentials: true, - allowOrigins: Cors.ALL_ORIGINS, - }, - }); - } -} diff --git a/infra/constructs/api-gateway/props/api-gateway.props.ts b/infra/constructs/api-gateway/props/api-gateway.props.ts deleted file mode 100644 index d3bb2499..00000000 --- a/infra/constructs/api-gateway/props/api-gateway.props.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Certificate } from 'aws-cdk-lib/aws-certificatemanager'; -import { ApplicationProps } from '../../../props/application.props'; - -export interface ApiGatewayProps extends ApplicationProps { - restApiName: string; - domainName: string; - certificate: Certificate; -} diff --git a/infra/constructs/aurora-database-proxy/aurora-database-proxy.construct.ts b/infra/constructs/aurora-database-proxy/aurora-database-proxy.construct.ts deleted file mode 100644 index efc6586f..00000000 --- a/infra/constructs/aurora-database-proxy/aurora-database-proxy.construct.ts +++ /dev/null @@ -1,72 +0,0 @@ -import * as cdk from 'aws-cdk-lib'; -import * as rds from 'aws-cdk-lib/aws-rds'; -import { Construct } from 'constructs'; -import { ApplicationProps } from '../../props/application.props'; -import { createName } from '../../utils/create-name'; -import { createOutput } from '../../utils/create-output'; -import { AuroraDatabaseProxyProps } from './props/aurora-database-proxy.props'; - -export class AuroraDatabaseProxy extends Construct { - proxy: rds.DatabaseProxy; - constructor(scope: Construct, id: string, props: AuroraDatabaseProxyProps) { - super(scope, id); - - // get vpc, security group and auroraDatabaseCluster - const { securityGroup, vpc, auroraDatabaseCluster } = props; - - // create a aurora db cluster serverless v2 postgres - const DATABASE_PROXY_NAME = createName('proxy', props); - this.proxy = new rds.DatabaseProxy(this, DATABASE_PROXY_NAME, { - dbProxyName: DATABASE_PROXY_NAME, - proxyTarget: rds.ProxyTarget.fromCluster(auroraDatabaseCluster), - secrets: [auroraDatabaseCluster.secret], - vpc, - securityGroups: [securityGroup], - iamAuth: true, - }); - this.exports('aurora-database-proxy', props); - } - - // export resources - exports(scopedName: string, props: AuroraDatabaseProxyProps) { - // create name scoped - const createNameScoped = (name, config) => - createName(`${scopedName}-${name}`, config); - - // outputs - createOutput(this, createNameScoped('arn', props), this.proxy.dbProxyArn); - createOutput(this, createNameScoped('name', props), this.proxy.dbProxyName); - createOutput( - this, - createNameScoped('endpoint', props), - this.proxy.endpoint, - ); - createOutput(this, createNameScoped('host', props), this.proxy.endpoint); - } - - // import resrouces - - static fromNameAndSecurityGroup( - scope, - scopedName: string, - securityGroup, - props: ApplicationProps, - ) { - // create name scoped - const createNameScoped = (name, config) => - createName(`${scopedName}-${name}`, config); - - // import proxy - const proxy = rds.DatabaseProxy.fromDatabaseProxyAttributes( - scope, - createNameScoped('proxy', props), - { - dbProxyArn: cdk.Fn.importValue(createNameScoped('arn', props)), - dbProxyName: cdk.Fn.importValue(createNameScoped('name', props)), - endpoint: cdk.Fn.importValue(createNameScoped('endpoint', props)), - securityGroups: [securityGroup], - }, - ); - return { proxy }; - } -} diff --git a/infra/constructs/aurora-database-proxy/props/aurora-database-proxy.props.ts b/infra/constructs/aurora-database-proxy/props/aurora-database-proxy.props.ts deleted file mode 100644 index ffde0e5a..00000000 --- a/infra/constructs/aurora-database-proxy/props/aurora-database-proxy.props.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { ISecurityGroup, IVpc } from 'aws-cdk-lib/aws-ec2'; -import { DatabaseCluster } from 'aws-cdk-lib/aws-rds'; -import { ApplicationProps } from '../../../props/application.props'; - -export interface AuroraDatabaseProxyProps extends ApplicationProps { - vpc: IVpc; - securityGroup: ISecurityGroup; - auroraDatabaseCluster: DatabaseCluster; -} diff --git a/infra/constructs/aurora-database/aurora-database.construct.ts b/infra/constructs/aurora-database/aurora-database.construct.ts deleted file mode 100644 index 876c70d5..00000000 --- a/infra/constructs/aurora-database/aurora-database.construct.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { Aspects } from 'aws-cdk-lib'; -import { - InstanceType, - SubnetType, -} from 'aws-cdk-lib/aws-ec2'; -import * as rds from 'aws-cdk-lib/aws-rds'; -import { CfnDBCluster } from 'aws-cdk-lib/aws-rds'; -import { Construct } from 'constructs'; -import { createName } from '../../utils/create-name'; -import { AuroraDatabaseProps } from './props/aurora-database.props'; - -export class AuroraDatabase extends Construct { - databaseCluster: rds.DatabaseCluster; - - constructor( - scope: Construct, - id: string, - auroraDatabaseProps: AuroraDatabaseProps, - ) { - super(scope, id); - - // get vpc and security group - const { securityGroup, vpc } = auroraDatabaseProps; - - // create a aurora db cluster serverless v2 postgres - const DATABASE_CLUSTER_NAME = createName('cluster', auroraDatabaseProps); - this.databaseCluster = new rds.DatabaseCluster( - this, - DATABASE_CLUSTER_NAME, - { - instances: 1, - iamAuthentication: true, - port: 5432, - engine: rds.DatabaseClusterEngine.auroraPostgres({ - version: rds.AuroraPostgresEngineVersion.VER_16_1, - }), - storageEncrypted: true, - instanceProps: { - vpc: vpc, - instanceType: new InstanceType('serverless'), - autoMinorVersionUpgrade: true, - securityGroups: [securityGroup], - vpcSubnets: vpc.selectSubnets({ - subnetType: SubnetType.PUBLIC, // use the public subnet created above for the db - }), - }, - }, - ); - - // add capacity to the db cluster to enable scaling - Aspects.of(this.databaseCluster).add({ - visit(node) { - if (node instanceof CfnDBCluster) { - node.serverlessV2ScalingConfiguration = { - minCapacity: 0.5, // min capacity is 0.5 vCPU - maxCapacity: 1, // max capacity is 1 vCPU (default) - }; - } - }, - }); - } -} diff --git a/infra/constructs/aurora-database/props/aurora-database.props.ts b/infra/constructs/aurora-database/props/aurora-database.props.ts deleted file mode 100644 index 36393c31..00000000 --- a/infra/constructs/aurora-database/props/aurora-database.props.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { ISecurityGroup, IVpc } from 'aws-cdk-lib/aws-ec2'; -import { ApplicationProps } from '../../../props/application.props'; - -export interface AuroraDatabaseProps extends ApplicationProps { - vpc: IVpc; - securityGroup: ISecurityGroup; -} diff --git a/infra/constructs/generic-security-group/generic-security-group.construct.ts b/infra/constructs/generic-security-group/generic-security-group.construct.ts deleted file mode 100644 index 49414a06..00000000 --- a/infra/constructs/generic-security-group/generic-security-group.construct.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* eslint-disable prettier/prettier */ -import * as cdk from 'aws-cdk-lib'; -import { ISecurityGroup, IVpc, Port, SecurityGroup, Vpc } from 'aws-cdk-lib/aws-ec2'; -import { Construct } from 'constructs'; -import { createName } from '../../utils/create-name'; -import { createOutput } from '../../utils/create-output'; -import { GenericSecurityGroupProps } from './props/generic-security-group.props'; - -export class GenericSecurityGroup extends Construct { - vpc: IVpc; - securityGroup: SecurityGroup; - constructor( - scope: Construct, - id: string, - props: GenericSecurityGroupProps, - ) { - super(scope, id); - - //get vpc - this.vpc = props.vpc; - - // create a security group for aurora db - const SECURITY_GROUP_NAME = createName(id, props); - this.securityGroup = new SecurityGroup(this, SECURITY_GROUP_NAME, { - securityGroupName: SECURITY_GROUP_NAME, - vpc: this.vpc, // use the vpc created above - allowAllOutbound: true, // allow outbound traffic to anywhere - }); - - // export security group and vpc - this.exportSecurityGroupAndVpc(props.name, props); - } - - addIngressSecurityGroup(ingressSg: ISecurityGroup, port: Port, description: string) { - this.securityGroup.addIngressRule(ingressSg, port, description); - - } - - // export resources - exportSecurityGroupAndVpc(scopedName: string, props) { - // create name scoped - const createNameScoped = (name, config) => - createName(`${scopedName}-${name}`, config); - - // outputs - console.log('export', createNameScoped('id', props)); - createOutput(this, createNameScoped('id', props), this.vpc.vpcId); - createOutput(this, createNameScoped('subnet-id-1', props), this.vpc.publicSubnets[0].subnetId); - createOutput(this, createNameScoped('subnet-route-table-id-1', props), this.vpc.publicSubnets[0].routeTable.routeTableId); - createOutput(this, createNameScoped('subnet-id-2', props), this.vpc.publicSubnets[1].subnetId); - createOutput(this, createNameScoped('subnet-route-table-id-2', props), this.vpc.publicSubnets[1].routeTable.routeTableId); - createOutput(this, createNameScoped('security-group-id', props), this.securityGroup.securityGroupId); - } - - // import resources - static fromName(scope, scopedName: string, props) { - // create name scoped - const createNameScoped = (name) => - createName(`${scopedName}-${name}`, props); - console.log('import', createNameScoped('id')); - - // vpc resource - const vpc = Vpc.fromVpcAttributes(scope, createNameScoped('id'), { - vpcId: cdk.Fn.importValue(createNameScoped('id')), - availabilityZones: ['sa-east-1'], - publicSubnetIds: [ - cdk.Fn.importValue(createNameScoped('subnet-id-1')), - cdk.Fn.importValue(createNameScoped('subnet-id-2')), - ], - publicSubnetRouteTableIds: [ - cdk.Fn.importValue(createNameScoped('subnet-route-table-id-1')), - cdk.Fn.importValue(createNameScoped('subnet-route-table-id-2')), - ], - }); - const securityGroup = SecurityGroup.fromSecurityGroupId( - scope, - createNameScoped('security-group-id'), - cdk.Fn.importValue(createNameScoped('security-group-id')), - ); - return { - vpc, - securityGroup, - }; - } -} diff --git a/infra/constructs/generic-security-group/props/generic-security-group.props.ts b/infra/constructs/generic-security-group/props/generic-security-group.props.ts deleted file mode 100644 index 1303068e..00000000 --- a/infra/constructs/generic-security-group/props/generic-security-group.props.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { IVpc } from 'aws-cdk-lib/aws-ec2'; -import { ApplicationProps } from '../../../props/application.props'; - -export interface GenericSecurityGroupProps extends ApplicationProps { - name: string; - vpc: IVpc; -} diff --git a/infra/constructs/lambda-database-migration/lambda-database-migration.construct.ts b/infra/constructs/lambda-database-migration/lambda-database-migration.construct.ts deleted file mode 100644 index e6243270..00000000 --- a/infra/constructs/lambda-database-migration/lambda-database-migration.construct.ts +++ /dev/null @@ -1,73 +0,0 @@ -import * as iam from 'aws-cdk-lib/aws-iam'; -import { - NodejsFunction, - NodejsFunctionProps, -} from 'aws-cdk-lib/aws-lambda-nodejs'; -import { Construct } from 'constructs'; -import { join } from 'path'; -import { createName } from '../../utils/create-name'; -import { - DEFAULT_NESTJS_FUNCTION_PROPS, - DEFAULT_NESTJS_LAMBDA_ENVIRONMENT, - createDatabaseAuroraEnvironment, -} from '../lambda-nestjs-function/constants'; -import { LambdaDatabaseMigrationProps } from './props/lambda-database-migration.prop'; - -export class LambdaDatabaseMigration extends Construct { - role: iam.Role; - nodejsFunction: NodejsFunction; - constructor( - scope: Construct, - id: string, - props: LambdaDatabaseMigrationProps, - ) { - super(scope, id); - - // build database name - const AURORA_DATABASE_NAME = createName('aurora-database', props); - - // create database function - const DATABASE_MIGRATION_FUNCTION_NAME = createName('migration', props); - const functionProps: NodejsFunctionProps = { - ...props, - ...DEFAULT_NESTJS_FUNCTION_PROPS, - environment: { - ...DEFAULT_NESTJS_LAMBDA_ENVIRONMENT[props.stageName], - ...createDatabaseAuroraEnvironment(AURORA_DATABASE_NAME), - }, - bundling: { - minify: false, - commandHooks: { - beforeBundling: (inputDir: string, outputDir: string): string[] => { - return [ - `cp -R ${inputDir}/dist ${outputDir}/dist`, - `cp ${inputDir}/tsconfig.json ${outputDir}/tsconfig.json`, - `mkdir ${outputDir}/cert`, - `cp -R ${inputDir}/src/common/config/certs/rds-combined-ca-bundle.pem ${outputDir}/cert`, - ]; - }, - afterBundling: (): string[] => [], - beforeInstall: (): string[] => [], - }, - nodeModules: ['typeorm', 'ts-node', '@nestjs/config', 'pg', 'bcryptjs'], - }, - entry: join( - __dirname, - '..', - '..', - '..', - '..', - 'node_modules', - '@atlas-org', - 'database', - 'index.js', - ), - functionName: DATABASE_MIGRATION_FUNCTION_NAME, - }; - this.nodejsFunction = new NodejsFunction( - this, - DATABASE_MIGRATION_FUNCTION_NAME, - functionProps, - ); - } -} diff --git a/infra/constructs/lambda-database-migration/props/lambda-database-migration.prop.ts b/infra/constructs/lambda-database-migration/props/lambda-database-migration.prop.ts deleted file mode 100644 index e6bc3622..00000000 --- a/infra/constructs/lambda-database-migration/props/lambda-database-migration.prop.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { NodejsFunctionProps } from 'aws-cdk-lib/aws-lambda-nodejs'; -import { ApplicationProps } from '../../../props/application.props'; - -export type LambdaDatabaseMigrationProps = LambdaDatabaseMigrationPropsRequired; -export interface LambdaDatabaseMigrationPropsRequired - extends NodejsFunctionProps, - ApplicationProps { -} diff --git a/infra/constructs/lambda-nestjs-function/constants.ts b/infra/constructs/lambda-nestjs-function/constants.ts index 978e3013..b7a50e9a 100644 --- a/infra/constructs/lambda-nestjs-function/constants.ts +++ b/infra/constructs/lambda-nestjs-function/constants.ts @@ -1,4 +1,3 @@ -import * as cdk from 'aws-cdk-lib'; import { Duration } from 'aws-cdk-lib'; import { Runtime } from 'aws-cdk-lib/aws-lambda'; import { join } from 'path'; @@ -27,16 +26,28 @@ export const DEFAULT_NESTJS_NODE_EXTERNALS = [ 'swagger-ui-express', 'typescript', '@nestjs/cli', + '@babel', ]; -export const NESTJS_SWAGGER_MODULES = ['oidc-provider', 'swagger-ui-express', '@nestjs/swagger', '@babel/plugin-proposal-export-namespace-from', '@babel/plugin-transform-modules-commonjs']; -export const DEFAULT_NESTJS_NODE_MODULE = ['oidc-provider', '@babel/plugin-proposal-export-namespace-from', '@babel/plugin-transform-modules-commonjs']; +export const NESTJS_SWAGGER_MODULES = [ + 'oidc-provider', + 'swagger-ui-express', + '@nestjs/swagger', + '@babel/plugin-proposal-export-namespace-from', + '@babel/plugin-transform-modules-commonjs', +]; +export const DEFAULT_NESTJS_NODE_MODULE = [ + 'oidc-provider', + '@babel/plugin-proposal-export-namespace-from', + '@babel/plugin-transform-modules-commonjs', +]; export const DEFAULT_NESTJS_COMMAND_HOOKS = { beforeBundling: (inputDir: string, outputDir: string): string[] => { return [ - `mkdir ${outputDir}/cert`, - `cp -R ${inputDir}/src/common/config/certs/rds-combined-ca-bundle.pem ${outputDir}/cert`, + `pwd ${inputDir}`, + `mkdir ${outputDir}/certs`, + `cp -R ${inputDir}/src/common/config/certs/rds-combined-ca-bundle.pem ${outputDir}/certs`, `mkdir ${outputDir}/templates`, //`cp -R ${inputDir}/src/common/mail/templates/* ${outputDir}/templates`, `mkdir ${outputDir}/i18n`, @@ -48,14 +59,7 @@ export const DEFAULT_NESTJS_COMMAND_HOOKS = { }; export const DEFAULT_NESTJS_FUNCTION_PROPS = { - depsLockFilePath: join( - __dirname, - '..', - '..', - '..', - '..', - 'package-lock.json', - ), + depsLockFilePath: join(__dirname, '..', '..', '..', 'package-lock.json'), memorySize: 2048, timeout: Duration.seconds(6), runtime: Runtime.NODEJS_20_X, @@ -63,71 +67,10 @@ export const DEFAULT_NESTJS_FUNCTION_PROPS = { bundling: { minify: true, keepNames: true, - sourcemap: true, + sourcemap: false, zip: true, externalModules: DEFAULT_NESTJS_NODE_EXTERNALS, nodeModules: DEFAULT_NESTJS_NODE_MODULE, commandHooks: DEFAULT_NESTJS_COMMAND_HOOKS, }, }; - -export const DEFAULT_NESTJS_LAMBDA_ENVIRONMENT = { - production: { - NODE_ENV: 'production', - APP_PORT: '3000', - APP_NAME: 'NestJS Boilerplate', - APP_FALLBACK_LANGUAGE: 'en', - APP_HEADER_LANGUAGE: 'x-custom-lang', - - FRONTEND_DOMAIN: 'https://localhost:4200', - BACKEND_DOMAIN: 'https://api.yourdomain.com', - SWAGGER_ENABLED: 'true', - I18N_DIRECTORY: 'i18n', - AUTH_JWT_SECRET: 'put-your-secret-here', - AUTH_JWT_TOKEN_EXPIRES_IN: '1d', - - SESSIONS_TABLE_NAME: 'atlas-production-sessions', - - MAIL_TEMPLATES_PATH: 'templates', - MAIL_FROM: 'info@yourdomain.com.br', - - AWS_STORAGE_CREDENTIAL: 'profile', - AWS_STORAGE_REGION: 'us-east-1', - }, - development: { - // App - NODE_ENV: 'development', - APP_PORT: '3000', - APP_NAME: 'NestJS Boilerplate', - APP_FALLBACK_LANGUAGE: 'en', - APP_HEADER_LANGUAGE: 'x-custom-lang', - - FRONTEND_DOMAIN: 'https://localhost:4200', - BACKEND_DOMAIN: 'https://api.yourdomain.com', - SWAGGER_ENABLED: 'true', - I18N_DIRECTORY: 'i18n', - AUTH_JWT_SECRET: 'put-your-secret-here', - AUTH_JWT_TOKEN_EXPIRES_IN: '1d', - - SESSIONS_TABLE_NAME: 'atlas-development-sessions', - - MAIL_TEMPLATES_PATH: 'templates', - MAIL_FROM: 'info@yourdomain.com.br', - - AWS_STORAGE_CREDENTIAL: 'profile', - AWS_STORAGE_REGION: 'us-east-1', - } -}; - -export const createDatabaseAuroraEnvironment = (name: string) => { - return { - DATABASE_TYPE: 'postgres', - DATABASE_HOST: cdk.Fn.importValue(name + '-proxy-host'), - DATABASE_USERNAME: 'postgres', - DATABASE_PORT: '5432', - DATABASE_NAME: 'postgres', - DATABASE_REJECT_UNAUTHORIZED: 'true', - DATABASE_SSL_ENABLED: 'true', - DATABASE_SYNCHRONIZE: 'false', - }; -}; diff --git a/infra/constructs/lambda-nestjs-function/lambda-nestjs-function.construct.ts b/infra/constructs/lambda-nestjs-function/lambda-nestjs-function.construct.ts new file mode 100644 index 00000000..94725846 --- /dev/null +++ b/infra/constructs/lambda-nestjs-function/lambda-nestjs-function.construct.ts @@ -0,0 +1,53 @@ +import { Construct } from 'constructs'; +import { join } from 'path'; +import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; +import { + DEFAULT_NESTJS_FUNCTION_PROPS, + NESTJS_SWAGGER_MODULES, +} from './constants'; +import { LambdaNestJsFunctionProps } from './props/lambda-nestjs-function.props'; +import { createDatabaseAuroraEnvironment } from '../../helpers/create-database-aurora-environment'; + +export class LambdaNestJsFunction extends Construct { + nodejsFunction: NodejsFunction; + constructor(scope: Construct, id: string, props: LambdaNestJsFunctionProps) { + super(scope, id); + + const { functionName, moduleName } = props; + let environment = props.environment; + + if (props.database) { + environment = { + ...environment, + ...createDatabaseAuroraEnvironment(props.database), + }; + } + + const functionProps = { + ...props, + ...DEFAULT_NESTJS_FUNCTION_PROPS, + environment: environment, + entry: join( + __dirname, + '..', + '..', + '..', + 'dist', + 'app', + moduleName, + 'server.js', + ), + functionName: functionName, + }; + + if (functionProps.swaggerBundling) { + functionProps.bundling.nodeModules = NESTJS_SWAGGER_MODULES; + } + + this.nodejsFunction = new NodejsFunction( + this, + functionName + `Service`, + functionProps, + ); + } +} diff --git a/infra/constructs/lambda-nestjs-function/lambda-nestjs-function.constructs.ts b/infra/constructs/lambda-nestjs-function/lambda-nestjs-function.constructs.ts deleted file mode 100644 index ecc9b8bf..00000000 --- a/infra/constructs/lambda-nestjs-function/lambda-nestjs-function.constructs.ts +++ /dev/null @@ -1,62 +0,0 @@ -import * as iam from 'aws-cdk-lib/aws-iam'; -import { Tracing } from 'aws-cdk-lib/aws-lambda'; -import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; -import { Construct } from 'constructs'; -import { join } from 'path'; -import { createName } from '../../utils/create-name'; -import { - DEFAULT_NESTJS_FUNCTION_PROPS, - DEFAULT_NESTJS_LAMBDA_ENVIRONMENT, - NESTJS_SWAGGER_MODULES, - createDatabaseAuroraEnvironment, -} from './constants'; -import { LambdaNestJsFunctionProps } from './props/lambda-nestjs-function.props'; - -export class LambdaNestJsFunction extends Construct { - role: iam.Role; - nodejsFunction: NodejsFunction; - constructor(scope: Construct, id: string, props: LambdaNestJsFunctionProps) { - super(scope, id); - - // build database name - const AURORA_DATABASE_NAME = createName('aurora-database', props); - const { functionName, moduleName } = props; - - // create database function - const NESTJS_FUNCTION_NAME = createName(functionName, props); - - // app path - const APP_PATH = props.appPath?.length - ? props.appPath - : [moduleName, 'server.js']; - - let environment = { - ...DEFAULT_NESTJS_LAMBDA_ENVIRONMENT[props.stageName], - }; - - environment = { - ...environment, - ...createDatabaseAuroraEnvironment(AURORA_DATABASE_NAME), - }; - - const functionProps = { - ...props, - ...DEFAULT_NESTJS_FUNCTION_PROPS, - environment, - tracing: Tracing.ACTIVE, - entry: join(__dirname, '..', '..', '..', 'app', ...APP_PATH), - functionName: NESTJS_FUNCTION_NAME, - }; - - if (functionProps.swaggerBundling) { - console.log('!!!!!!!!!!!!!!!11'); - functionProps.bundling.nodeModules = NESTJS_SWAGGER_MODULES; - } - - this.nodejsFunction = new NodejsFunction( - this, - NESTJS_FUNCTION_NAME, - functionProps, - ); - } -} diff --git a/infra/constructs/lambda-nestjs-function/props/lambda-nestjs-function.props.ts b/infra/constructs/lambda-nestjs-function/props/lambda-nestjs-function.props.ts index e9de7c8a..9667f7a4 100644 --- a/infra/constructs/lambda-nestjs-function/props/lambda-nestjs-function.props.ts +++ b/infra/constructs/lambda-nestjs-function/props/lambda-nestjs-function.props.ts @@ -1,15 +1,16 @@ import { NodejsFunctionProps } from 'aws-cdk-lib/aws-lambda-nodejs'; -import { ApplicationProps } from '../../../props/application.props'; +import { ISecurityGroup } from 'aws-cdk-lib/aws-ec2'; +import { DatabaseCluster } from 'aws-cdk-lib/aws-rds'; export type LambdaNestJsFunctionProps = LambdaNestJsFunctionPropsRequired; -export interface LambdaNestJsFunctionPropsRequired - extends NodejsFunctionProps, - ApplicationProps { +export interface LambdaNestJsFunctionPropsRequired extends NodejsFunctionProps { functionName: string; moduleName: string; swaggerBundling?: boolean; queues?: any; buckets?: any; + securityGroup: ISecurityGroup; + database?: DatabaseCluster; cloudfronts?: any; appPath?: string[]; } diff --git a/infra/constructs/lambda-role/lambda-role.construct.ts b/infra/constructs/lambda-role/lambda-role.construct.ts deleted file mode 100644 index e669a240..00000000 --- a/infra/constructs/lambda-role/lambda-role.construct.ts +++ /dev/null @@ -1,24 +0,0 @@ -import * as iam from 'aws-cdk-lib/aws-iam'; -import { ManagedPolicy } from 'aws-cdk-lib/aws-iam'; -import { Construct } from 'constructs'; - -export class LambdaRole extends Construct { - role: iam.Role; - constructor(scope: Construct, name: string) { - super(scope, name); - this.role = new iam.Role(this, name, { - roleName: name, - assumedBy: new iam.AnyPrincipal(), - managedPolicies: [ - ManagedPolicy.fromAwsManagedPolicyName( - 'service-role/AWSLambdaVPCAccessExecutionRole', - ), - ManagedPolicy.fromAwsManagedPolicyName( - 'service-role/AWSLambdaBasicExecutionRole', - ), - ManagedPolicy.fromAwsManagedPolicyName('SecretsManagerReadWrite'), - ManagedPolicy.fromAwsManagedPolicyName('AmazonSESFullAccess'), - ], - }); - } -} diff --git a/infra/factories/infraestructure.ts b/infra/factories/infraestructure.ts deleted file mode 100644 index a3831923..00000000 --- a/infra/factories/infraestructure.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { App } from 'aws-cdk-lib'; -import { DEFAULT_STAGE_NAME } from '../constants'; -import { ApplicationProps } from '../props/application.props'; -import { createName } from './../utils/create-name'; - -export class AtlasInfraestructure { - app: App; - constructor(environments: { [key: string]: ApplicationProps }) { - // Create a new CDK app - this.app = new App(); - - const environment = - this.app.node.tryGetContext('environment') ?? DEFAULT_STAGE_NAME; - - if (!environments[environment]) { - console.log('This environment does not exists.'); - return; - } - - const applicationProps = environments[environment]; - - const { layersStack } = applicationProps; - const layerCreated = {}; - - // Iterate through each layer in the stack - layersStack.map(({ name, provide, env, dependencies }) => { - // Generate a unique stack name for the current layer - const CORE_STACK_NAME = createName(name, applicationProps); - const config = { ...applicationProps }; - - if (env) { - config.env = env; - } - - config.layersCreated = layerCreated; - - // Create an instance of the current layer and store it in layerCreated object - layerCreated[name] = new provide(this.app, CORE_STACK_NAME, config); - - if (dependencies) { - // Add dependencies to the current layer - dependencies.map((dependenceName) => - layerCreated[name].addDependency(layerCreated[dependenceName]), - ); - } - }); - } - - public synth() { - // Generate CloudFormation templates and artifacts - this.app.synth(); - } -} diff --git a/infra/helpers/aurora.ts b/infra/helpers/aurora.ts new file mode 100644 index 00000000..946eaaed --- /dev/null +++ b/infra/helpers/aurora.ts @@ -0,0 +1,52 @@ +import { Construct } from 'constructs'; +import { IVpc } from 'aws-cdk-lib/aws-ec2'; +import { + ApplicationConfig, + ApplicationProps, +} from '../props/application-props'; +import { + addScalingCapacity, + createDatabaseCluster, +} from './create-database-cluster'; +import { createOutput } from './create-output'; +import { createAndExportGenericSecurityGroup } from './setup-generic-security-group'; + +export const setupAuroraDatabase = ( + scope: Construct, + scopedName: string, + applicationProps: ApplicationProps, + applicationConfig: ApplicationConfig, + vpc: IVpc, +) => { + // Create a security group for the database + const databaseSecurityGroup = createAndExportGenericSecurityGroup( + scope, + scopedName + 'DatabaseSecurityGroup', + { + name: scopedName + 'DatabaseSecurityGroup', + vpc, + ...applicationProps, + }, + ); + + // Create the Aurora database cluster + const databaseCluster = createDatabaseCluster( + scope, + scopedName + 'DatabaseCluster', + { + ...applicationProps, + vpc, + securityGroup: databaseSecurityGroup.securityGroup, + }, + ); + + addScalingCapacity(databaseCluster); + // Create output + createOutput( + scope, + `${scopedName}DatabaseClusterHost`, + databaseCluster.clusterEndpoint.hostname, + ); + + return databaseCluster; +}; diff --git a/infra/helpers/bootstrap.ts b/infra/helpers/bootstrap.ts new file mode 100644 index 00000000..81e0243b --- /dev/null +++ b/infra/helpers/bootstrap.ts @@ -0,0 +1,66 @@ +import { App } from 'aws-cdk-lib'; +import { DEFAULT_STAGE_NAME } from './../constants'; +import { ApplicationProps } from './../props/application-props'; + +const bootstrapInfrastructure = (environments: { + [key: string]: ApplicationProps; +}) => { + const app = new App(); + + // Get the current environment from the context or use the default. + const environment = + app.node.tryGetContext('environment') ?? DEFAULT_STAGE_NAME; + + // Check if the environment exists in the provided configurations. + if (!environments[environment]) { + console.error('This environment does not exist.'); + return null; + } + + const applicationProps = environments[environment]; + const { layersStack } = applicationProps; + const layerCreated: { [key: string]: any } = {}; + + // Iterate over each layer in the stack. + layersStack.forEach(({ name, provide, dependencies }: any) => { + // Generate a unique stack name for the current layer. + const CORE_STACK_NAME = name; + const config = { + ...applicationProps, + env: applicationProps.env, + layersCreated: layerCreated, + }; + // Create an instance of the current layer and store it in the layerCreated object. + layerCreated[name] = new provide(app, CORE_STACK_NAME, config); + + // Add dependencies to the current layer, if any. + if (dependencies) { + dependencies.forEach((dependencyName: string) => + layerCreated[name].addDependency(layerCreated[dependencyName]), + ); + } + }); + + return app; +}; + +/** + * Function to synthesize the infrastructure and generate CloudFormation templates. + * @param app - Instance of the CDK app. + */ +const synthesizeInfrastructure = (app: App) => { + app.synth(); +}; + +/** + * Main execution function. + * @param environments - Object containing application properties for each environment. + */ +export const bootstrap = (environments: { + [key: string]: ApplicationProps; +}) => { + const app = bootstrapInfrastructure(environments); + if (app) { + synthesizeInfrastructure(app); + } +}; diff --git a/infra/helpers/create-database-aurora-environment.ts b/infra/helpers/create-database-aurora-environment.ts new file mode 100644 index 00000000..7044e422 --- /dev/null +++ b/infra/helpers/create-database-aurora-environment.ts @@ -0,0 +1,10 @@ +import { DatabaseCluster } from 'aws-cdk-lib/aws-rds'; + +export const createDatabaseAuroraEnvironment = (cluster: DatabaseCluster) => { + return { + DATABASE_TYPE: 'aurora-postgres', + DATABASE_NAME: cluster.clusterIdentifier, + DATABASE_SECRET_ARN: cluster.secret.secretArn, + DATABASE_RESOURCE_ARN: cluster.clusterArn, + }; +}; diff --git a/infra/helpers/create-database-cluster.ts b/infra/helpers/create-database-cluster.ts new file mode 100644 index 00000000..93352865 --- /dev/null +++ b/infra/helpers/create-database-cluster.ts @@ -0,0 +1,54 @@ +import { Aspects } from 'aws-cdk-lib'; +import { InstanceType, SubnetType } from 'aws-cdk-lib/aws-ec2'; +import * as rds from 'aws-cdk-lib/aws-rds'; +import { CfnDBCluster } from 'aws-cdk-lib/aws-rds'; +import { Construct } from 'constructs'; +import { AuroraDatabaseProps } from '../props/aurora-database-props'; +import { createOutput } from './create-output'; + +export const createDatabaseCluster = ( + scope: Construct, + id: string, + props: AuroraDatabaseProps, +): rds.DatabaseCluster => { + const { securityGroup, vpc } = props; + const clusterProps: rds.DatabaseClusterProps = { + defaultDatabaseName: id, + port: 5432, + engine: rds.DatabaseClusterEngine.auroraPostgres({ + version: rds.AuroraPostgresEngineVersion.VER_14_10, // Aurora PostgreSQL engine version + }), + instances: 1, + storageEncrypted: true, // Enable storage encryption + instanceProps: { + vpc: vpc, // VPC for the database instances + instanceType: new InstanceType('serverless'), // Instance type + autoMinorVersionUpgrade: true, // Enable automatic minor version upgrades + securityGroups: [securityGroup], // Security groups for the instances + vpcSubnets: vpc.selectSubnets({ + subnetType: SubnetType.PUBLIC, // Subnet type for the instances + }), + }, + }; + const cluster = new rds.DatabaseCluster(scope, id, clusterProps); + + // Export secret output + createOutput(scope, `${id}Secret`, cluster.secret?.secretArn); + + return cluster; +}; + +export const addScalingCapacity = (cluster: rds.DatabaseCluster): void => { + // Add scaling configuration to the database cluster + Aspects.of(cluster).add({ + visit(node) { + if (node instanceof CfnDBCluster) { + // Set serverless V2 scaling configuration + node.serverlessV2ScalingConfiguration = { + minCapacity: 0.5, // Minimum capacity is 0.5 vCPU + maxCapacity: 2, // Maximum capacity is 5 vCPU (default) + }; + } + }, + }); +}; diff --git a/infra/helpers/create-output.ts b/infra/helpers/create-output.ts new file mode 100644 index 00000000..e764518f --- /dev/null +++ b/infra/helpers/create-output.ts @@ -0,0 +1,13 @@ +import { CfnOutput } from 'aws-cdk-lib'; + +export const createOutput = ( + self: any, + name: string, + value: any, +): CfnOutput => { + const output = new CfnOutput(self, name, { + value: value, // The value of the output + exportName: name, // The name used for exporting the output value + }); + return output; +}; diff --git a/infra/helpers/create-vpc.ts b/infra/helpers/create-vpc.ts new file mode 100644 index 00000000..4a4b2faf --- /dev/null +++ b/infra/helpers/create-vpc.ts @@ -0,0 +1,25 @@ +import { Construct } from 'constructs'; +import { Vpc, IpAddresses, SubnetType } from 'aws-cdk-lib/aws-ec2'; +import { vpcCDIR } from './../constants'; + +export const createVpc = (scope: Construct, scopedName: string): Vpc => { + return new Vpc(scope, scopedName + 'Vpc', { + vpcName: scopedName + 'Vpc', + ipAddresses: IpAddresses.cidr(vpcCDIR), + maxAzs: 2, + natGateways: 1, + restrictDefaultSecurityGroup: false, + subnetConfiguration: [ + { + subnetType: SubnetType.PUBLIC, + cidrMask: 24, + name: 'subnet-public', + }, + { + subnetType: SubnetType.PRIVATE_WITH_EGRESS, + cidrMask: 24, + name: 'subnet-private', + }, + ], + }); +}; diff --git a/infra/helpers/setup-bastion.ts b/infra/helpers/setup-bastion.ts new file mode 100644 index 00000000..b6f7d21e --- /dev/null +++ b/infra/helpers/setup-bastion.ts @@ -0,0 +1,41 @@ +import { CfnOutput, Fn } from 'aws-cdk-lib'; +import { + BastionHostLinux, + ISecurityGroup, + IVpc, + SubnetType, +} from 'aws-cdk-lib/aws-ec2'; + +export function setupBastion( + scope: any, + scopedName: string, + vpc: IVpc, + securityGroup: ISecurityGroup, +): void { + // Create bastion host instance in public subnet + const bastionHostLinux = new BastionHostLinux( + scope, + `${scopedName}BastionHostLinux`, + { + vpc: vpc, + securityGroup: securityGroup, + subnetSelection: { + subnetType: SubnetType.PUBLIC, + }, + }, + ); + + // Retrieve the host of the database from the output of another stack + const applicationDatabaseHost = Fn.importValue( + `${scopedName}DatabaseClusterHost`, + ); + + // Display command for starting the tunnel session + const startTunnel = `aws ssm start-session \\ + --target ${bastionHostLinux.instanceId} \\ + --document-name AWS-StartPortForwardingSessionToRemoteHost \\ + --parameters '{"host":["${applicationDatabaseHost}"],"portNumber":["5432"], "localPortNumber":["5432"]}'`; + + // Output the command for starting the tunnel session + new CfnOutput(scope, `${scopedName}StartTunnel`, { value: startTunnel }); +} diff --git a/infra/helpers/setup-generic-security-group.ts b/infra/helpers/setup-generic-security-group.ts new file mode 100644 index 00000000..0dd413e1 --- /dev/null +++ b/infra/helpers/setup-generic-security-group.ts @@ -0,0 +1,131 @@ +import * as cdk from 'aws-cdk-lib'; +import { ISecurityGroup, IVpc, SecurityGroup, Vpc } from 'aws-cdk-lib/aws-ec2'; +import { Construct } from 'constructs'; +import { GenericSecurityGroupProps } from '../props/generic-security-group-props'; +import { createOutput } from './create-output'; + +const defaultVpc = (scope: Construct, scopedName: string): IVpc => { + // Lookup and return the default VPC + return Vpc.fromLookup(scope, `${scopedName}DefaultVpc`, { isDefault: true }); +}; + +const createSecurityGroup = ( + scope: Construct, + securityGroupId: string, + vpc: IVpc, + props: GenericSecurityGroupProps, +): SecurityGroup => { + // Create and return a new security group + return new SecurityGroup(scope, securityGroupId, { + securityGroupName: securityGroupId, + vpc, + allowAllOutbound: true, + ...props, + }); +}; + +const exportResources = ( + scope: Construct, + scopedName: string, + vpc: IVpc, + securityGroup: ISecurityGroup, +): void => { + // Export the VPC ID + createOutput(scope, `${scopedName}VpcId`, vpc.vpcId); + + // Export public subnet IDs and their route table IDs + vpc.publicSubnets.forEach((subnet, index) => { + createOutput( + scope, + `${scopedName}GenericSubnetId${index + 1}`, + subnet.subnetId, + ); + createOutput( + scope, + `${scopedName}GenericSubnetRouteTableId${index + 1}`, + subnet.routeTable.routeTableId, + ); + }); + + // Export private subnet IDs and their route table IDs + vpc.privateSubnets.forEach((subnet, index) => { + createOutput( + scope, + `${scopedName}GenericPrivateSubnetId${index + 1}`, + subnet.subnetId, + ); + createOutput( + scope, + `${scopedName}GenericPrivateSubnetRouteTableId${index + 1}`, + subnet.routeTable.routeTableId, + ); + }); + + // Export the security group ID + createOutput( + scope, + `${scopedName}GenericSecurityGroupId`, + securityGroup.securityGroupId, + ); +}; + +export const importGenericSecurityGroupResources = ( + scope: Construct, + scopedName: string, +): { vpc: IVpc; securityGroup: ISecurityGroup } => { + // Import the VPC using its attributes + const vpc = Vpc.fromVpcAttributes(scope, `${scopedName}VpcId`, { + vpcId: cdk.Fn.importValue(`${scopedName}VpcId`), + availabilityZones: [0, 1].map((i) => cdk.Fn.select(i, cdk.Fn.getAzs())), + publicSubnetIds: [ + cdk.Fn.importValue(`${scopedName}GenericSubnetId1`), + cdk.Fn.importValue(`${scopedName}GenericSubnetId2`), + ], + publicSubnetRouteTableIds: [ + cdk.Fn.importValue(`${scopedName}GenericSubnetRouteTableId1`), + cdk.Fn.importValue(`${scopedName}GenericSubnetRouteTableId2`), + ], + privateSubnetIds: [ + cdk.Fn.importValue(`${scopedName}GenericPrivateSubnetId1`), + cdk.Fn.importValue(`${scopedName}GenericPrivateSubnetId2`), + ], + privateSubnetRouteTableIds: [ + cdk.Fn.importValue(`${scopedName}GenericPrivateSubnetRouteTableId1`), + cdk.Fn.importValue(`${scopedName}GenericPrivateSubnetRouteTableId2`), + ], + }); + + // Import the security group using its ID + const securityGroup = SecurityGroup.fromSecurityGroupId( + scope, + `${scopedName}GenericSecurityGroupId`, + cdk.Fn.importValue(`${scopedName}GenericSecurityGroupId`), + ); + + return { + vpc, + securityGroup, + }; +}; + +const createAndExportGenericSecurityGroup = ( + scope: Construct, + securityGroupId: string, + props: GenericSecurityGroupProps, +): { vpc: IVpc; securityGroup: ISecurityGroup } => { + // Create or lookup the VPC + const vpc = props.vpc ?? defaultVpc(scope, securityGroupId); + + // Create the security group + const securityGroup = createSecurityGroup(scope, securityGroupId, vpc, props); + + // Export the resources + exportResources(scope, securityGroupId, vpc, securityGroup); + + return { + vpc, + securityGroup, + }; +}; + +export { createAndExportGenericSecurityGroup }; diff --git a/infra/index.ts b/infra/index.ts index 4456bb6f..84474bf9 100644 --- a/infra/index.ts +++ b/infra/index.ts @@ -1,66 +1,7 @@ -import { AtlasInfraestructure } from './factories/infraestructure'; -import { ApplicationLayerStack } from './layers/application.layer'; -import { CoreLayerStack } from './layers/core.layer'; +import { production } from './configs/production'; +import { bootstrap } from './helpers/bootstrap'; -/** - * Function to create layers stack. It creates 'core-layer' and 'application-layer'. - * - * @returns array of layers. - */ -const createStacks = () => { - return [ - { name: 'core-layer', provide: CoreLayerStack }, - { - name: 'application-layer', - provide: ApplicationLayerStack, - dependencies: ['core-layer'], - } - ]; -}; - -/** - * Configuration for the application. Contains configurations for both production and development. - */ -const infraestructure = new AtlasInfraestructure({ - production: { - // Name of your application - applicationName: 'atlas', - // Stage name for the environment - stageName: 'production', - // Domain name environment - domainName: 'sandbox.slingui.com', - // API domain name for the environment - apiDomainName: 'api.sandbox.slingui.com', - // Public host ID for the environment (AWS Route 53 Hosted Zone ID) - idPublicHostZone: 'Z01545163ANT5OQYS99UY', - env: { - // AWS account ID for the environment - account: '767397837500', - // AWS region for the environment (e.g., 'us-east-1') - region: 'us-east-1', - }, - layersStack: createStacks(), - }, - development: { - // Name of your application - applicationName: 'atlas', - // Stage name for the environment - stageName: 'development', - // Domain name environment - domainName: 'sandbox.slingui.com', - // API domain name for the environment - apiDomainName: 'api.sandbox.slingui.com', - // Public host zone ID for the environment (AWS Route 53 Hosted Zone ID) - idPublicHostZone: 'Z01545163ANT5OQYS99UY', - env: { - // AWS account ID for the environment - account: '767397837500', - // AWS region for the environment (e.g., 'us-east-1') - region: 'us-east-1', - }, - layersStack: createStacks(), - }, +// Execute the infraestructure application +bootstrap({ + production, }); - -// Execute synth method -infraestructure.synth(); \ No newline at end of file diff --git a/infra/layers/application.layer.ts b/infra/layers/application.layer.ts deleted file mode 100644 index 9045f9b8..00000000 --- a/infra/layers/application.layer.ts +++ /dev/null @@ -1,159 +0,0 @@ -import * as cdk from 'aws-cdk-lib'; -import { LambdaIntegration } from 'aws-cdk-lib/aws-apigateway'; -import { - Certificate, - CertificateValidation, -} from 'aws-cdk-lib/aws-certificatemanager'; -import { Table } from 'aws-cdk-lib/aws-dynamodb'; -import { CnameRecord, PublicHostedZone } from 'aws-cdk-lib/aws-route53'; -import { Construct } from 'constructs'; -import { ApiGateway } from '../constructs/api-gateway/api-gateway.construct'; -import { - ApplicationProps, - IApplicationResource, -} from '../props/application.props'; -import { DatabaseMigrationResource } from '../resources/database-migration.resource'; -import { LambdaResource } from '../resources/lambda.resources'; -import { createSessionsTable } from '../tables/sessions'; -import { createName } from '../utils/create-name'; -import { GenericSecurityGroup } from '../constructs/generic-security-group/generic-security-group.construct'; -import { Port } from 'aws-cdk-lib/aws-ec2'; - -export class ApplicationLayerStack extends cdk.Stack { - databaseMigrationResource: DatabaseMigrationResource; - lambdaResource: LambdaResource; - certificate: Certificate; - - constructor( - scope: Construct, - id: string, - applicationProps: ApplicationProps, - ) { - super(scope, id, applicationProps); - - // application for run database migration - const DATABASE_MIGRATION_NAME = createName( - 'aurora-database', - applicationProps, - ); - this.databaseMigrationResource = new DatabaseMigrationResource( - this, - DATABASE_MIGRATION_NAME, - applicationProps, - ); - - const API_GATEWAY_FUNCTION_NAME = createName( - 'api-gateway', - applicationProps, - ); - const publicHostedZone = PublicHostedZone.fromHostedZoneAttributes( - this, - 'Zone', - { - hostedZoneId: applicationProps.idPublicHostZone, - zoneName: applicationProps.domainName, - }, - ); - const certificate = new Certificate(this, 'Certificate', { - domainName: applicationProps.domainName, - subjectAlternativeNames: [ - '*.' + applicationProps.domainName, - applicationProps.domainName, - ], - validation: CertificateValidation.fromDns(publicHostedZone), - }); - this.certificate = certificate; - - // Tables - const sessions = createSessionsTable(this, applicationProps); - - const { api } = new ApiGateway(this, API_GATEWAY_FUNCTION_NAME, { - ...applicationProps, - restApiName: 'api', - domainName: applicationProps.apiDomainName, - certificate: certificate, - }); - - // Security Group - const { vpc, securityGroup: databaseProxySecurityGroup } = GenericSecurityGroup.fromName( - this, - 'database-proxy-sg', - applicationProps, - ); - - //Lambda SG - const lambdaSecurityGroup = new GenericSecurityGroup( - this, - createName('lambda-sg', applicationProps), - { - name: 'lambda-sg', - vpc, - ...applicationProps, - }, - ); - - // Lambda access to proxy - databaseProxySecurityGroup.addIngressRule(lambdaSecurityGroup.securityGroup, Port.tcp(5432), `Allows access from the migrations lambda to the proxy`); - - this.createLambdaResources( - this, - applicationProps, - api, - { - sessions, - }, - [ - { functionName: 'auth', moduleName: 'auth' }, - { functionName: 'users', moduleName: 'users' }, - { - functionName: 'swagger', - moduleName: 'swagger', - swaggerBundling: true, - }, - ], - lambdaSecurityGroup, - ); - - // Finally, add a CName record in the hosted zone with a value of the new custom domain that was created above: - const CNAME_RECORD_SET_NAME = createName( - 'api-gateway-cname-record-set', - applicationProps, - ); - new CnameRecord(this, CNAME_RECORD_SET_NAME, { - zone: publicHostedZone, - recordName: 'api', - domainName: api.domainName.domainNameAliasDomainName - }); - } - - createLambdaResources( - scope: Construct, - applicationProps: ApplicationProps, - api: any, - tables: { sessions: Table }, - resources: IApplicationResource[], - genericSecurityGroup: GenericSecurityGroup, - ): void { - resources.forEach(({ functionName, moduleName, swaggerBundling }) => { - const lambdaName = createName(functionName, applicationProps); - const lambdaResource = new LambdaResource(scope, lambdaName, { - functionName, - moduleName, - swaggerBundling, - genericSecurityGroup, - ...applicationProps, - }); - Object.keys(tables).forEach((key) => - tables[key].grantReadWriteData(lambdaResource.nodejsFunction), - ); - - const integration = new LambdaIntegration(lambdaResource.nodejsFunction, { - proxy: true, - }); - - const resource = api.root.addResource(moduleName); - resource.addResource('{proxy+}').addMethod('ANY', integration); - resource.addMethod('ANY', integration); - }); - } -} diff --git a/infra/layers/core.layer.ts b/infra/layers/core.layer.ts deleted file mode 100644 index db6d42fd..00000000 --- a/infra/layers/core.layer.ts +++ /dev/null @@ -1,27 +0,0 @@ -import * as cdk from 'aws-cdk-lib'; -import { Construct } from 'constructs'; -import { ApplicationProps } from '../props/application.props'; -import { AuroraDatabaseResource } from '../resources/aurora-database.resource'; -import { createName } from '../utils/create-name'; - -export class CoreLayerStack extends cdk.Stack { - auroraDatabaseResource: AuroraDatabaseResource; - constructor( - scope: Construct, - id: string, - applicationProps: ApplicationProps, - ) { - super(scope, id, applicationProps); - - // aurora database - const AURORA_DATABASE_NAME = createName( - 'aurora-database', - applicationProps, - ); - this.auroraDatabaseResource = new AuroraDatabaseResource( - this, - AURORA_DATABASE_NAME, - applicationProps, - ); - } -} diff --git a/infra/props/api-gateway-props.ts b/infra/props/api-gateway-props.ts new file mode 100644 index 00000000..29fd9dc0 --- /dev/null +++ b/infra/props/api-gateway-props.ts @@ -0,0 +1,10 @@ +import { Certificate } from 'aws-cdk-lib/aws-certificatemanager'; + +export interface ApiGatewayProps { + /** The name of the REST API. */ + restApiName: string; + /** The domain name for the API Gateway. */ + domainName: string; + /** The SSL certificate associated with the domain. */ + certificate: Certificate; +} diff --git a/infra/props/application-props.ts b/infra/props/application-props.ts new file mode 100644 index 00000000..da8093d5 --- /dev/null +++ b/infra/props/application-props.ts @@ -0,0 +1,69 @@ +import { StackProps } from 'aws-cdk-lib'; + +/** + * Represents the configuration for a stack. + */ +export interface ConfigStack { + /** The name of the stack. */ + name: string; + /** The resources provided by the stack. */ + provide: any; + /** The environment for the stack. */ + env?: Record; + /** The dependencies of the stack. */ + dependencies?: string[]; +} + +/** + * Represents the configuration for an application. + */ +export interface ApplicationConfig { + /** The domain name for the application. */ + domainName: string; + /** The domain name for the API. */ + apiDomainName: string; + /** The ID of the public hosted zone. */ + idPublicHostZone: string; + /** The environment variables for the application. */ + applicationEnvironment: Env; +} + +/** + * Represents a collection of applications with dynamic keys. + */ +interface Applications { + core: ApplicationConfig; +} + +/** + * Represents the properties for an application stack. + */ +export interface ApplicationProps extends StackProps { + /** The name of the application. */ + applicationName: string; + /** The stage name for the application. */ + stageName: string; + + githubOrganizationId?: string; + /** The environment configuration for the application. */ + env: { + account: string; + region: string; + }; + /** The configuration stacks for the application. */ + layersStack: ConfigStack[]; + /** The applications in the stack. */ + applications: Applications; +} + +/** + * Represents an application resource. + */ +export interface IApplicationResource { + /** The name of the function. */ + functionName: string; + /** The name of the module. */ + moduleName: string; + /** Indicates if Swagger bundling is enabled. */ + swaggerBundling?: boolean; +} diff --git a/infra/props/application.props.ts b/infra/props/application.props.ts deleted file mode 100644 index 63644f8b..00000000 --- a/infra/props/application.props.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { StackProps } from 'aws-cdk-lib'; - -export interface LayerStack { - name: string; - provide: any; - env?: any; - dependencies?: string[]; -} - -export interface ApplicationProps extends StackProps { - applicationName: string; - domainName: string; - apiDomainName: string; - idPublicHostZone: string; - stageName: string; - createNameCustom?: (name: string, config: ApplicationProps) => string; - env: any; - layersStack: LayerStack[]; - layersCreated?: any; -} - -export interface IApplicationResource { - functionName: string; - moduleName: string; - swaggerBundling?: boolean; -} diff --git a/infra/props/aurora-database-props.ts b/infra/props/aurora-database-props.ts new file mode 100644 index 00000000..16c7a5ad --- /dev/null +++ b/infra/props/aurora-database-props.ts @@ -0,0 +1,12 @@ +import { ISecurityGroup, IVpc } from 'aws-cdk-lib/aws-ec2'; +import { ApplicationProps } from './application-props'; + +/** + * Represents the properties for an Aurora database. + */ +export interface AuroraDatabaseProps extends ApplicationProps { + /** The Virtual Private Cloud (VPC) associated with the Aurora database. */ + vpc: IVpc; + /** The security group associated with the Aurora database. */ + securityGroup: ISecurityGroup; +} diff --git a/infra/props/aurora-database-proxy-props.ts b/infra/props/aurora-database-proxy-props.ts new file mode 100644 index 00000000..9ea8ad56 --- /dev/null +++ b/infra/props/aurora-database-proxy-props.ts @@ -0,0 +1,15 @@ +import { ISecurityGroup, IVpc } from 'aws-cdk-lib/aws-ec2'; +import { DatabaseCluster } from 'aws-cdk-lib/aws-rds'; +import { ApplicationProps } from './application-props'; + +/** + * Represents the properties for an Aurora database proxy. + */ +export interface AuroraDatabaseProxyProps extends ApplicationProps { + /** The Virtual Private Cloud (VPC) for the Aurora database proxy. */ + vpc: IVpc; + /** The security group for the Aurora database proxy. */ + securityGroup: ISecurityGroup; + /** The Aurora database cluster associated with the proxy. */ + auroraDatabaseCluster: DatabaseCluster; +} diff --git a/infra/props/generic-security-group-props.ts b/infra/props/generic-security-group-props.ts new file mode 100644 index 00000000..38c74b12 --- /dev/null +++ b/infra/props/generic-security-group-props.ts @@ -0,0 +1,12 @@ +import { IVpc } from 'aws-cdk-lib/aws-ec2'; +import { ApplicationProps } from './application-props'; + +/** + * Represents the properties for a generic security group. + */ +export interface GenericSecurityGroupProps extends ApplicationProps { + /** The name of the security group. */ + name: string; + /** The Virtual Private Cloud (VPC) associated with the security group. */ + vpc?: IVpc; +} diff --git a/infra/props/lambda.props.ts b/infra/props/lambda.props.ts deleted file mode 100644 index ecc71222..00000000 --- a/infra/props/lambda.props.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ApplicationProps } from './application.props'; - -export interface LambdaProps extends ApplicationProps { - functionName: string; - moduleName: string; - queues?: any; - buckets?: any; - genericSecurityGroup: any; - cloudfronts?: any; - swaggerBundling?: boolean; -} diff --git a/infra/resources/aurora-database.resource.ts b/infra/resources/aurora-database.resource.ts deleted file mode 100644 index 9b5eb6ac..00000000 --- a/infra/resources/aurora-database.resource.ts +++ /dev/null @@ -1,78 +0,0 @@ -/* eslint-disable prettier/prettier */ -import { AuroraDatabase } from '../constructs/aurora-database/aurora-database.construct'; -import { GenericSecurityGroup } from '../constructs/generic-security-group/generic-security-group.construct'; -import { AuroraDatabaseProxy } from '../constructs/aurora-database-proxy/aurora-database-proxy.construct'; -import { ApplicationProps } from '../props/application.props'; -import { Construct } from 'constructs'; -import { Port, Vpc } from 'aws-cdk-lib/aws-ec2'; -import { createName as defaultCreateName } from '../utils/create-name'; - -export class AuroraDatabaseResource extends Construct { - auroraDatabaseProxy: AuroraDatabaseProxy; - constructor( - scope: Construct, - id: string, - applicationProps: ApplicationProps, - ) { - super(scope, id); - // setup prefix - const createName = (name, config) => - defaultCreateName(`aurora-stack-${name}`, config); - - // get default account vpc - const DEFAULT_VPC_NAME = createName('default-vpc', applicationProps); - const vpc = Vpc.fromLookup(this, DEFAULT_VPC_NAME, { - isDefault: true, - }); - - // create aurora database security group - const databaseSecurityGroup = new GenericSecurityGroup( - this, - createName('database-sg', applicationProps), - { - name: 'database-sg', - vpc, - ...applicationProps, - }, - ); - - // ProxySG - const databaseProxySecurityGroup = new GenericSecurityGroup( - this, - createName('database-proxy-sg', applicationProps), - { - name: 'database-proxy-sg', - vpc, - ...applicationProps, - }, - ); - - // create aurora database cluster - const DATABASE_CLUSTER_NAME = createName('cluster', applicationProps); - const { databaseCluster } = new AuroraDatabase( - this, - DATABASE_CLUSTER_NAME, - { - ...applicationProps, - vpc, - securityGroup: databaseSecurityGroup.securityGroup, - }, - ); - - // create aurora database proxy - const DATABASE_PROXY_NAME = createName('proxy', applicationProps); - this.auroraDatabaseProxy = new AuroraDatabaseProxy(this, DATABASE_PROXY_NAME, { - ...applicationProps, - auroraDatabaseCluster: databaseCluster, - vpc, - securityGroup: databaseProxySecurityGroup.securityGroup, - }); - - //Add Security Group Rules - databaseProxySecurityGroup.addIngressSecurityGroup( - databaseSecurityGroup.securityGroup, - Port.tcp(databaseCluster.clusterEndpoint.port), - `Access for the ${this.auroraDatabaseProxy.node.id} proxy` - ); - } -} diff --git a/infra/resources/database-migration.resource.ts b/infra/resources/database-migration.resource.ts deleted file mode 100644 index 4146b2f3..00000000 --- a/infra/resources/database-migration.resource.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* eslint-disable prettier/prettier */ -import { ApplicationProps } from '../props/application.props'; -import { Construct } from 'constructs'; -import { LambdaRole } from '../constructs/lambda-role/lambda-role.construct'; -import { AuroraDatabaseProxy } from '../constructs/aurora-database-proxy/aurora-database-proxy.construct'; -import { GenericSecurityGroup } from '../constructs/generic-security-group/generic-security-group.construct'; -import { LambdaDatabaseMigration } from '../constructs/lambda-database-migration/lambda-database-migration.construct'; -import { createName as defaultCreateName } from '../utils/create-name'; -import { Port } from 'aws-cdk-lib/aws-ec2'; - -export class DatabaseMigrationResource extends Construct { - lambdaDatabaseMigration: LambdaDatabaseMigration; - constructor( - scope: Construct, - id: string, - applicationProps: ApplicationProps, - ) { - super(scope, id); - - // setup prefix - const createName = (name, config) => - defaultCreateName(`database-migration-${name}`, config); - - const createAuroraDatabaseName = (name, config) => - defaultCreateName(`aurora-database-${name}`, config); - - // add lambda function for run migrations - const LAMBDA_ROLE_NAME = createName('lambda-role', applicationProps); - const { role } = new LambdaRole(this, LAMBDA_ROLE_NAME); - - // get vpc, security group from db - const { vpc } = GenericSecurityGroup.fromName( - this, - 'database-sg', - applicationProps, - ); - - // get vpc, security group from proxy - const { securityGroup: databaseProxySecurityGroup } = GenericSecurityGroup.fromName( - this, - 'database-proxy-sg', - applicationProps, - ); - - // get proxy - const { proxy } = AuroraDatabaseProxy.fromNameAndSecurityGroup( - this, - 'aurora-database-proxy', - databaseProxySecurityGroup, - applicationProps, - ); - - // grant access to lambda role - proxy.grantConnect(role, 'postgres'); - - //Lambda SG - const lambdaSecurityGroup = new GenericSecurityGroup( - this, - createName('lambda-migration-sg', applicationProps), - { - name: 'lambda-migration-sg', - vpc, - ...applicationProps, - }, - ); - - // Lambda access to proxy - databaseProxySecurityGroup.addIngressRule(lambdaSecurityGroup.securityGroup, Port.tcp(5432), `Allows access from the migrations lambda to the ${proxy.dbProxyName}`); - - - // add lambda function for run migrations - const LAMBDA_DATABASE_MIGRATION_NAME = createAuroraDatabaseName( - 'lambda-database-migration', - applicationProps, - ); - this.lambdaDatabaseMigration = new LambdaDatabaseMigration(this, LAMBDA_DATABASE_MIGRATION_NAME, { - ...applicationProps, - role, - vpc: vpc, - securityGroups: [lambdaSecurityGroup.securityGroup], - }); - } -} diff --git a/infra/resources/lambda.resources.ts b/infra/resources/lambda.resources.ts deleted file mode 100644 index 9fa58b08..00000000 --- a/infra/resources/lambda.resources.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; -import { Construct } from 'constructs'; -import { AuroraDatabaseProxy } from '../constructs/aurora-database-proxy/aurora-database-proxy.construct'; -import { LambdaNestJsFunction } from '../constructs/lambda-nestjs-function/lambda-nestjs-function.constructs'; -import { LambdaRole } from '../constructs/lambda-role/lambda-role.construct'; -import { LambdaProps } from '../props/lambda.props'; -import { createName as defaultCreateName } from '../utils/create-name'; - -export class LambdaResource extends Construct { - nodejsFunction: NodejsFunction; - constructor(scope: Construct, id: string, lambdaProps: LambdaProps) { - super(scope, id); - // setup prefix - const createName = (name, config) => - defaultCreateName(`lambda-${lambdaProps.functionName}-${name}`, config); - - const { functionName, moduleName } = lambdaProps; - - const { proxy } = AuroraDatabaseProxy.fromNameAndSecurityGroup( - this, - 'aurora-database-proxy', - lambdaProps.genericSecurityGroup.securityGroup, - lambdaProps, - ); - - // create iam role - const LAMBDA_ROLE_NAME = createName('role', lambdaProps); - const { role } = new LambdaRole(this, LAMBDA_ROLE_NAME); - - // grant access to lambda connect in aurora database - proxy.grantConnect(role, 'postgres'); - - // create nodejs function - const LAMBDA_NESTJS_FUNCTION_NAME = createName('function', lambdaProps); - const { nodejsFunction } = new LambdaNestJsFunction( - this, - LAMBDA_NESTJS_FUNCTION_NAME, - { - ...lambdaProps, - functionName, - moduleName, - role, - vpc: lambdaProps.genericSecurityGroup.vpc, - securityGroups: [lambdaProps.genericSecurityGroup.securityGroup], - }, - ); - - this.nodejsFunction = nodejsFunction; - } -} diff --git a/infra/stacks.ts b/infra/stacks.ts new file mode 100644 index 00000000..24aade00 --- /dev/null +++ b/infra/stacks.ts @@ -0,0 +1,27 @@ +import { CoreStack } from './stacks/core-stack'; +import { PipelineRoleStack } from './stacks/pipeline-stack'; +import { ApplicationStack } from './stacks/application-stack'; + +export const stacks = [ + { name: 'CoreStack', provide: CoreStack }, + { + name: 'PipelineStack', + provide: PipelineRoleStack, + dependencies: ['CoreStack'], + }, + { + name: 'ApplicationStack', + provide: ApplicationStack, + dependencies: ['CoreStack'], + }, + // { + // name: 'BastionStack', + // provide: BastionStack, + // dependencies: ['CoreStack'], + // }, + // { + // name: 'ClientsStack', + // provide: ClientsStack, + // dependencies: ['CoreStack'], + // } +]; diff --git a/infra/stacks/application-stack.ts b/infra/stacks/application-stack.ts new file mode 100644 index 00000000..801567af --- /dev/null +++ b/infra/stacks/application-stack.ts @@ -0,0 +1,196 @@ +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import * as apigateway from 'aws-cdk-lib/aws-apigateway'; +import * as certificatemanager from 'aws-cdk-lib/aws-certificatemanager'; +import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; +import * as route53 from 'aws-cdk-lib/aws-route53'; +import * as ec2 from 'aws-cdk-lib/aws-ec2'; +import { + ApplicationConfig, + ApplicationProps, +} from '../props/application-props'; +import { importGenericSecurityGroupResources } from '../helpers/setup-generic-security-group'; +import { LambdaNestJsFunctionProps } from '../constructs/lambda-nestjs-function/props/lambda-nestjs-function.props'; +import { LambdaNestJsFunction } from '../constructs/lambda-nestjs-function/lambda-nestjs-function.construct'; +import { + AnyPrincipal, + ManagedPolicy, + PolicyStatement, + Role, +} from 'aws-cdk-lib/aws-iam'; +import { CorsHttpMethod } from 'aws-cdk-lib/aws-apigatewayv2'; + +export class ApplicationStack extends cdk.Stack { + constructor( + scope: Construct, + id: string, + applicationProps: ApplicationProps, + ) { + super(scope, id); + + const applicationConfig = applicationProps.applications.core; + + const publicHostedZone = route53.PublicHostedZone.fromHostedZoneAttributes( + this, + 'ApplicationZone', + { + hostedZoneId: applicationConfig.idPublicHostZone, + zoneName: applicationConfig.domainName, + }, + ); + + const certificate = new certificatemanager.Certificate( + this, + 'ApplicationCertificate', + { + domainName: applicationConfig.domainName, + subjectAlternativeNames: [ + `*.${applicationConfig.domainName}`, + applicationConfig.domainName, + ], + validation: + certificatemanager.CertificateValidation.fromDns(publicHostedZone), + }, + ); + + const api = new apigateway.RestApi(this, 'ApplicationApiGateway', { + restApiName: 'api', + defaultCorsPreflightOptions: { + allowHeaders: ['*'], + allowMethods: [CorsHttpMethod.ANY], + allowOrigins: ['*'], + }, + domainName: { + domainName: applicationConfig.apiDomainName, + certificate: certificate, + }, + }); + + // Import a VPC from database + const { vpc } = importGenericSecurityGroupResources( + this, + 'CoreDatabaseSecurityGroup', + ); + + const lambdaSecurityGroup = new ec2.SecurityGroup( + this, + 'LambdaSecurityGroup', + { + vpc, + allowAllOutbound: true, + }, + ); + + const sessionsTable = new dynamodb.Table(this, 'SessionsTable', { + partitionKey: { name: 'sessionId', type: dynamodb.AttributeType.STRING }, + billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, + }); + + const secretsManagerPolicy = new PolicyStatement({ + actions: ['secretsmanager:GetSecretValue'], + resources: ['*'], + }); + + const dataApiPolicy = new PolicyStatement({ + actions: [ + 'rds-data:BatchExecuteStatement', + 'rds-data:BeginTransaction', + 'rds-data:CommitTransaction', + 'rds-data:ExecuteStatement', + 'rds-data:RollbackTransaction', + ], + resources: ['*'], + }); + + const lambdaRole = new Role(this, 'LambdaRole', { + roleName: 'LambdaRole', + assumedBy: new AnyPrincipal(), + managedPolicies: [ + ManagedPolicy.fromAwsManagedPolicyName( + 'service-role/AWSLambdaVPCAccessExecutionRole', + ), + ManagedPolicy.fromAwsManagedPolicyName( + 'service-role/AWSLambdaBasicExecutionRole', + ), + ManagedPolicy.fromAwsManagedPolicyName('SecretsManagerReadWrite'), + ManagedPolicy.fromAwsManagedPolicyName('AmazonSESFullAccess'), + ], + }); + lambdaRole.addToPolicy(secretsManagerPolicy); + lambdaRole.addToPolicy(dataApiPolicy); + + this.createLambdaResources( + applicationConfig, + applicationProps, + api, + { sessions: sessionsTable }, + [ + { functionName: 'Auth', moduleName: 'auth' }, + { functionName: 'Users', moduleName: 'users' }, + { + functionName: 'Swagger', + moduleName: 'swagger', + swaggerBundling: true, + }, + ], + vpc, + lambdaSecurityGroup, + lambdaRole, + ); + + new route53.CnameRecord(this, 'ApiGatewayCnameRecord', { + zone: publicHostedZone, + recordName: 'api', + domainName: api.domainName?.domainNameAliasDomainName ?? '', + }); + } + + createLambdaResources( + applicationConfig: ApplicationConfig, + applicationProps: ApplicationProps, + api: apigateway.RestApi, + tables: { sessions: dynamodb.Table }, + resources: { + functionName: string; + moduleName: string; + swaggerBundling?: boolean; + }[], + vpc: ec2.IVpc, + securityGroup: ec2.SecurityGroup, + lambdaRole: Role, + ) { + resources.forEach(({ functionName, moduleName, swaggerBundling }) => { + const lambdaFunctionProps: LambdaNestJsFunctionProps = { + functionName: applicationProps.applicationName + functionName, + moduleName, + environment: { + ...applicationConfig.applicationEnvironment, + TABLE_NAME: tables.sessions.tableName, + }, + role: lambdaRole, + vpc, + securityGroup, + swaggerBundling, + }; + + const { nodejsFunction } = new LambdaNestJsFunction( + this, + `${functionName}Function`, + lambdaFunctionProps, + ); + + tables.sessions.grantReadWriteData(nodejsFunction); + + const lambdaIntegration = new apigateway.LambdaIntegration( + nodejsFunction, + { + proxy: true, + }, + ); + + const resource = api.root.addResource(moduleName); + resource.addResource('{proxy+}').addMethod('ANY', lambdaIntegration); + resource.addMethod('ANY', lambdaIntegration); + }); + } +} diff --git a/infra/stacks/bastion-stack.ts b/infra/stacks/bastion-stack.ts new file mode 100644 index 00000000..024ab072 --- /dev/null +++ b/infra/stacks/bastion-stack.ts @@ -0,0 +1,28 @@ +import { Stack } from 'aws-cdk-lib'; +import { importGenericSecurityGroupResources } from '../helpers/setup-generic-security-group'; +import { setupBastion } from '../helpers/setup-bastion'; +import { ApplicationProps } from '../props/application-props'; +import { Peer, Port } from 'aws-cdk-lib/aws-ec2'; + +export class BastionStack extends Stack { + constructor(scope: any, id: string, props: ApplicationProps) { + super(scope, id, props); + const scopedName = 'Core'; + const { vpc, securityGroup } = importGenericSecurityGroupResources( + this, + scopedName + 'DatabaseSecurityGroup', + ); + + if (props.applications.core) { + // enable ssh port + securityGroup.addIngressRule( + Peer.anyIpv4(), + Port.tcp(22), + 'Add public access to ssh', + ); + + // Setup the bastion host using the same security group as the application + setupBastion(this, scopedName, vpc, securityGroup); + } + } +} diff --git a/infra/stacks/core-stack.ts b/infra/stacks/core-stack.ts new file mode 100644 index 00000000..56d6192e --- /dev/null +++ b/infra/stacks/core-stack.ts @@ -0,0 +1,29 @@ +import { Stack } from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import { ApplicationProps } from '../props/application-props'; +import { setupAuroraDatabase } from '../helpers/aurora'; +import { createVpc } from '../helpers/create-vpc'; + +export class CoreStack extends Stack { + constructor( + scope: Construct, + id: string, + applicationProps: ApplicationProps, + ) { + super(scope, id, applicationProps); + + const { applications } = applicationProps; + const scopedName = 'Core'; + + const vpc = createVpc(this, scopedName); + if (applications.core) { + setupAuroraDatabase( + this, + scopedName, + applicationProps, + applicationProps.applications.core, + vpc, + ); + } + } +} diff --git a/infra/stacks/pipeline-stack.ts b/infra/stacks/pipeline-stack.ts new file mode 100644 index 00000000..022b1733 --- /dev/null +++ b/infra/stacks/pipeline-stack.ts @@ -0,0 +1,153 @@ +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import { ApplicationProps } from '../props/application-props'; + +export class PipelineRoleStack extends cdk.Stack { + constructor(scope: Construct, id: string, props: ApplicationProps) { + super(scope, id, props); + + if (props.githubOrganizationId === undefined) { + throw new Error( + 'You need add githubOrganizationId in your application props on configs/.', + ); + } + + // Define o provedor OpenID Connect + const organizationId = props.githubOrganizationId; + const providerUrl = `https://oidc.circleci.com/org/${organizationId}`; + const provider = new iam.OpenIdConnectProvider( + this, + 'PipelineOidcProvider', + { + url: providerUrl, + clientIds: [organizationId], + }, + ); + + // Define a role + const pipelineRole = new iam.Role(this, 'PipelineRole', { + assumedBy: new iam.FederatedPrincipal( + provider.openIdConnectProviderArn, + {}, + 'sts:AssumeRoleWithWebIdentity', + ), + description: 'Role for ECS update and ECR push permissions', + }); + + const account = props.env.account; + const region = props.env.region; + + // Attach policies to allow ECS task execution and image push + pipelineRole.addToPolicy( + new iam.PolicyStatement({ + actions: [ + 'ecs:UpdateService', + 'iam:PassRole', + 'ecs:RegisterTaskDefinition', + 'ecs:ListTaskDefinitions', + 'ecs:DescribeTaskDefinition', + 'ecs:DescribeServices', + 'ecs:ListClusters', + 'ecs:ListServices', + 'ecr:GetDownloadUrlForLayer', + 'ecr:GetAuthorizationToken', + 'ecr:BatchGetImage', + 'ecr:BatchCheckLayerAvailability', + 'ecr:CompleteLayerUpload', + 'ecr:UploadLayerPart', + 'ecr:InitiateLayerUpload', + 'ecr:PutImage', + 'ecs:TagResource', // Adicionada a permissão ecs:TagResource + ], + resources: [ + `arn:aws:ecs:${region}:${account}:service/*`, + `arn:aws:ecs:${region}:${account}:task-definition/*`, + `arn:aws:ecs:${region}:${account}:cluster/*`, + `arn:aws:ecr:${region}:${account}:repository/*`, + ], + }), + ); + + // Add the new policy statement to allow ecr:GetAuthorizationToken + pipelineRole.addToPolicy( + new iam.PolicyStatement({ + actions: ['ecr:GetAuthorizationToken'], + resources: [`arn:aws:ecr:${region}:${account}:repository/*`], + }), + ); + + // Add policies to allow tunneling to the database via SSM + pipelineRole.addToPolicy( + new iam.PolicyStatement({ + actions: [ + 'ssm:StartSession', + 'ssm:SendCommand', + 'ssm:DescribeInstanceInformation', + 'ssm:GetCommandInvocation', + 'ssm:ListCommands', + 'ssm:ListCommandInvocations', + 'ssm:TerminateSession', + ], + resources: [`arn:aws:ssm:${region}:${account}:*`], + }), + ); + + pipelineRole.addToPolicy( + new iam.PolicyStatement({ + actions: [ + 'ec2:DescribeInstances', + 'ec2:DescribeRegions', + 'ec2:DescribeSecurityGroups', + 'ec2:DescribeSubnets', + 'ec2:DescribeVpcs', + 'rds:DescribeDBInstances', + 'rds:DescribeDBClusters', + ], + resources: [`arn:aws:ec2:${region}:${account}:*`, `arn:aws:rds:${region}:${account}:*`], + }), + ); + + // Add policies to allow access to AWS Secrets Manager + pipelineRole.addToPolicy( + new iam.PolicyStatement({ + actions: [ + 'secretsmanager:GetSecretValue', + 'secretsmanager:DescribeSecret', + ], + resources: [`arn:aws:secretsmanager:${region}:${account}:secret:*`], + }), + ); + + // Create a user + const pipelineUser = new iam.User(this, 'PipelineUser', { + userName: 'PipelineUser', + }); + + // Attach inline policy to allow the user to assume the role + pipelineUser.addToPolicy( + new iam.PolicyStatement({ + actions: ['sts:AssumeRole'], + resources: [pipelineRole.roleArn], + }), + ); + + // Output the user name + new cdk.CfnOutput(this, 'PipelineUserName', { + value: pipelineUser.userName, + description: 'User name for the pipeline user', + }); + + // Output the role ARN + new cdk.CfnOutput(this, 'PipelineRoleArn', { + value: pipelineRole.roleArn, + description: 'ARN of the role used by the pipeline', + }); + + // Output the OIDC provider ARN + new cdk.CfnOutput(this, 'OpenIdConnectProviderArn', { + value: provider.openIdConnectProviderArn, + description: 'ARN of the OIDC provider', + }); + } +} diff --git a/infra/utils/cloudfront-utils.ts b/infra/utils/cloudfront-utils.ts new file mode 100644 index 00000000..b237dd3c --- /dev/null +++ b/infra/utils/cloudfront-utils.ts @@ -0,0 +1,37 @@ +import * as cloudfront from 'aws-cdk-lib/aws-cloudfront'; + +export const CLOUDFRONT_ERROR_REDIRECT: { + [key: number]: cloudfront.CfnDistribution.CustomErrorResponseProperty; +} = { + 400: { + errorCode: 400, + responseCode: 200, + responsePagePath: '/index.html', + }, + 403: { + errorCode: 403, + responseCode: 200, + responsePagePath: '/index.html', + }, + 404: { + errorCode: 404, + responseCode: 200, + responsePagePath: '/index.html', + }, +}; + +export function getHttpRedirect(id: number) { + return CLOUDFRONT_ERROR_REDIRECT[id]; +} + +export function getHttpRedirects(errorCodes: number[]): any { + let errorCodesResponse: cloudfront.CfnDistribution.CustomErrorResponseProperty[]; + errorCodes.forEach((code) => { + if (!errorCodesResponse) { + errorCodesResponse = [getHttpRedirect(code)]; + } else { + errorCodesResponse.push(getHttpRedirect(code)); + } + }); + return errorCodesResponse!; +} diff --git a/infra/utils/create-name.ts b/infra/utils/create-name.ts deleted file mode 100644 index f8fec606..00000000 --- a/infra/utils/create-name.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { ApplicationProps } from './../props/application.props'; - -export const createName = (name: string, config: ApplicationProps) => { - if (!config.createNameCustom) - return `${config.applicationName}-${config.stageName}-${name}`; - - return config.createNameCustom(name, config); -}; diff --git a/infra/utils/create-output.ts b/infra/utils/create-output.ts deleted file mode 100644 index e08d63c8..00000000 --- a/infra/utils/create-output.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { CfnOutput } from 'aws-cdk-lib'; - -export const createOutput = (self, name: string, value: any) => { - const output = new CfnOutput(self, name, { - value: value, - exportName: name, - }); - return output; -}; diff --git a/package.json b/package.json index ae610f64..291e9a29 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,6 @@ "scripts": { "prebuild": "rimraf dist/app", "build": "nest build", - "build:infra": "tsc -p tsconfig.infra.json", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "start": "nest start", "start:dev": "nest start --watch", @@ -20,14 +19,14 @@ "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:e2e": "jest --config ./test/jest-e2e.json", "typeorm": "npx env-cmd ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js", - "lambda:typeorm": "npx typeorm-lambda-proxy $LAMBDA_NAME node ./node_modules/typeorm/cli.js --dataSource=dist/app/common/database/lambda-cli.data-source.js", - "migration:generate": "npm run typeorm -- --dataSource=src/common/database/local-cli.data-source.ts migration:generate", + "lambda:typeorm": "npx typeorm-lambda-proxy $LAMBDA_NAME node ./node_modules/typeorm/cli.js --dataSource=dist/app/common/database/cli.data-source.js", + "migration:generate": "npm run typeorm -- --dataSource=src/common/database/cli.data-source.ts migration:generate", "migration:create": "npm run typeorm -- migration:create", - "migration:run": "npm run typeorm -- --dataSource=src/common/database/local-cli.data-source.ts migration:run", - "migration:revert": "npm run typeorm -- --dataSource=src/common/database/local-cli.data-source.ts migration:revert", + "migration:run": "npm run typeorm -- --dataSource=src/common/database/cli.data-source.ts migration:run", + "migration:revert": "npm run typeorm -- --dataSource=src/common/database/cli.data-source.ts migration:revert", "documentation": "npx @compodoc/compodoc -p tsconfig.json --includesName Documentation --includes docs --theme stripe", "documentation:serve": "npx @compodoc/compodoc -p tsconfig.json --includesName Documentation --includes docs --theme stripe -s", - "schema:drop": "npm run typeorm -- --dataSource=src/common/database/local-cli.data-source.ts schema:drop", + "schema:drop": "npm run typeorm -- --dataSource=src/common/database/cli.data-source.ts schema:drop", "seed:run": "ts-node -r tsconfig-paths/register ./src/common/database/seeds/run-seed.ts" }, "dependencies": { diff --git a/src/auth/oidc/adapters/dynamodb.adapter.ts b/src/auth/oidc/adapters/dynamodb.adapter.ts index 2f87e3c6..cfed7735 100644 --- a/src/auth/oidc/adapters/dynamodb.adapter.ts +++ b/src/auth/oidc/adapters/dynamodb.adapter.ts @@ -1,5 +1,17 @@ import { Adapter, AdapterPayload } from 'oidc-provider'; -import { BatchWriteItemCommand, BatchWriteItemInput, DeleteItemCommand, DeleteItemInput, DynamoDBClient, GetItemCommand, GetItemInput, QueryCommand, QueryInput, UpdateItemCommand, UpdateItemInput } from "@aws-sdk/client-dynamodb"; +import { + BatchWriteItemCommand, + BatchWriteItemInput, + DeleteItemCommand, + DeleteItemInput, + DynamoDBClient, + GetItemCommand, + GetItemInput, + QueryCommand, + QueryInput, + UpdateItemCommand, + UpdateItemInput, +} from '@aws-sdk/client-dynamodb'; const TABLE_NAME = process.env.SESSIONS_TABLE_NAME; const TABLE_REGION = process.env.AWS_REGION; @@ -55,13 +67,13 @@ export class DynamoDBAdapter implements Adapter { }; const command = new GetItemCommand(params); + const result = <{ payload?: string; expiresAt?: number } | undefined>( + (await dynamoClient.send(command)).Item + ); - const result = < - { payload?: string; expiresAt?: number } | undefined - >(await dynamoClient.send(command)).Item; - - const payload: AdapterPayload = result.payload ? JSON.parse(result.payload) : undefined; - + const payload: AdapterPayload = result.payload + ? JSON.parse(result.payload) + : undefined; // DynamoDB can take upto 48 hours to drop expired items, so a check is required if (!result || (result.expiresAt && Date.now() > result.expiresAt * 1000)) { @@ -84,11 +96,13 @@ export class DynamoDBAdapter implements Adapter { const command = new QueryCommand(params); - const result = < - { payload?: string; expiresAt?: number } | undefined - >(await dynamoClient.send(command)).Items?.[0]; + const result = <{ payload?: string; expiresAt?: number } | undefined>( + (await dynamoClient.send(command)).Items?.[0] + ); - const payload: AdapterPayload = result.payload ? JSON.parse(result.payload) : undefined; + const payload: AdapterPayload = result.payload + ? JSON.parse(result.payload) + : undefined; // DynamoDB can take upto 48 hours to drop expired items, so a check is required if (!result || (result.expiresAt && Date.now() > result.expiresAt * 1000)) { @@ -111,11 +125,13 @@ export class DynamoDBAdapter implements Adapter { }; const command = new QueryCommand(params); - const result = < - { payload?: string; expiresAt?: number } | undefined - >(await dynamoClient.send(command)).Items?.[0]; + const result = <{ payload?: string; expiresAt?: number } | undefined>( + (await dynamoClient.send(command)).Items?.[0] + ); - const payload: AdapterPayload = result.payload ? JSON.parse(result.payload) : undefined; + const payload: AdapterPayload = result.payload + ? JSON.parse(result.payload) + : undefined; // DynamoDB can take upto 48 hours to drop expired items, so a check is required if (!result || (result.expiresAt && Date.now() > result.expiresAt * 1000)) { @@ -148,7 +164,7 @@ export class DynamoDBAdapter implements Adapter { async destroy(id: string): Promise { const params: DeleteItemInput = { TableName: TABLE_NAME, - Key: { modelId: {S: this.name + '-' + id} }, + Key: { modelId: { S: this.name + '-' + id } }, }; const command = new DeleteItemCommand(params); @@ -164,7 +180,7 @@ export class DynamoDBAdapter implements Adapter { IndexName: 'grantIdIndex', KeyConditionExpression: 'grantId = :grantId', ExpressionAttributeValues: { - ':grantId': {S: grantId}, + ':grantId': { S: grantId }, }, ProjectionExpression: 'modelId', Limit: 25, diff --git a/src/auth/oidc/clients/clients.controller.spec.ts b/src/auth/oidc/clients/clients.controller.spec.ts index ba6818a7..fe7254a5 100644 --- a/src/auth/oidc/clients/clients.controller.spec.ts +++ b/src/auth/oidc/clients/clients.controller.spec.ts @@ -8,9 +8,7 @@ describe('ClientsController', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - imports: [ - ConfigModule, - ], + imports: [ConfigModule], controllers: [ClientsController], providers: [ClientsService], }).compile(); diff --git a/src/auth/oidc/clients/clients.service.spec.ts b/src/auth/oidc/clients/clients.service.spec.ts index eb2198da..e8c47b79 100644 --- a/src/auth/oidc/clients/clients.service.spec.ts +++ b/src/auth/oidc/clients/clients.service.spec.ts @@ -8,9 +8,7 @@ describe('ClientsService', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ClientsService], - imports: [ - ConfigModule, - ], + imports: [ConfigModule], }).compile(); service = module.get(ClientsService); diff --git a/src/common/config/database.config.ts b/src/common/config/database.config.ts index d9511edb..743fc67f 100644 --- a/src/common/config/database.config.ts +++ b/src/common/config/database.config.ts @@ -14,4 +14,6 @@ export default registerAs('database', () => ({ ca: process.env.DATABASE_CA, key: process.env.DATABASE_KEY, cert: process.env.DATABASE_CERT, + secretArn: process.env.DATABASE_SECRET_ARN, + resourceArn: process.env.DATABASE_RESOURCE_ARN, })); diff --git a/src/common/database/README.md b/src/common/database/README.md index 982a0b02..f426f996 100644 --- a/src/common/database/README.md +++ b/src/common/database/README.md @@ -1,7 +1,8 @@ # Configuration Files Documentation +In this folder, we have 2 different configuration files for the database connection: -In this folder, we have 3 different configuration files for the database connection: +'typeorm-config.service.ts' +This configuration is used by the application. It sets up the TypeORM connection options utilizing the TypeOrmConfigService class which sources its settings from environment variables managed by the ConfigService. This ensures that the application connects to the correct database with the proper credentials and settings in a secure and configurable manner. -1. `typeorm-config.service.ts` - This configuration is used by a server both locally and in the lambdas for HTTP requests. -2. `lambda-cli.data-source` - This configuration is used to run the TypeORM CLI within the production environment with the lambdas. -3. `local-cli.data-source.ts` - it is used to run migrations and seeds locally. \ No newline at end of file +'cli.data-source.ts' +This configuration is used by the command line data source and inside of Lambda. It is designed to facilitate database operations through command line tools and to support the execution of database-related tasks within AWS Lambda functions. This configuration ensures that migrations, seeders, and other database utilities can be executed properly in both local and cloud environments. \ No newline at end of file diff --git a/src/common/database/lambda-cli.data-source.ts b/src/common/database/cli.data-source.ts similarity index 91% rename from src/common/database/lambda-cli.data-source.ts rename to src/common/database/cli.data-source.ts index 20adb2f4..dc139f42 100644 --- a/src/common/database/lambda-cli.data-source.ts +++ b/src/common/database/cli.data-source.ts @@ -24,7 +24,7 @@ export const AppDataSource = new DataSource({ extra: config.sslEnabled ? { sslmode: 'verify-full', - sslrootcert: __dirname + '/certs/rds-combined-ca-bundle.pem', + sslrootcert: __dirname + '/certs/rds-combined-ca-bundle.pem', } : {}, } as DataSourceOptions); diff --git a/src/common/database/local-cli.data-source.ts b/src/common/database/local-cli.data-source.ts deleted file mode 100644 index afa7a299..00000000 --- a/src/common/database/local-cli.data-source.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { DataSource, DataSourceOptions } from 'typeorm'; - console.log(process.env.DATABASE_HOST); -export const AppDataSource = new DataSource({ - type: process.env.DATABASE_TYPE, - url: process.env.DATABASE_URL, - host: process.env.DATABASE_HOST, - port: parseInt(process.env.DATABASE_PORT, 10) || 5432, - username: process.env.DATABASE_USERNAME, - password: process.env.DATABASE_PASSWORD, - database: process.env.DATABASE_NAME, - synchronize: process.env.DATABASE_SYNCHRONIZE === 'true', - dropSchema: false, - keepConnectionAlive: true, - logging: process.env.NODE_ENV !== 'production', - entities: [__dirname + '/../../**/*.entity{.ts,.js}'], - migrations: [__dirname + '/migrations/**/*{.ts,.js}'], - cli: { - entitiesDir: 'src', - migrationsDir: 'src/database/migrations', - subscribersDir: 'subscriber', - }, - extra: { - // based on https://node-postgres.com/api/pool - // max connection pool size - max: parseInt(process.env.DATABASE_MAX_CONNECTIONS, 10) || 100, - ssl: - process.env.DATABASE_SSL_ENABLED === 'true' - ? { - rejectUnauthorized: - process.env.DATABASE_REJECT_UNAUTHORIZED === 'true', - ca: process.env.DATABASE_CA ?? undefined, - key: process.env.DATABASE_KEY ?? undefined, - cert: process.env.DATABASE_CERT ?? undefined, - } - : undefined, - }, -} as DataSourceOptions); diff --git a/src/common/database/typeorm-config.service.ts b/src/common/database/typeorm-config.service.ts index ed259564..10ad10f6 100644 --- a/src/common/database/typeorm-config.service.ts +++ b/src/common/database/typeorm-config.service.ts @@ -1,27 +1,31 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { TypeOrmModuleOptions, TypeOrmOptionsFactory } from '@nestjs/typeorm'; +import { AuroraPostgresConnectionOptions } from 'typeorm/driver/aurora-postgres/AuroraPostgresConnectionOptions'; import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions'; -import { generatePasswordWithRdsSigner } from './utils/generatePasswordWithRdsSigner'; @Injectable() export class TypeOrmConfigService implements TypeOrmOptionsFactory { constructor(private configService: ConfigService) {} createTypeOrmOptions(): TypeOrmModuleOptions { - console.log({ - type: this.configService.get('database.type'), - url: this.configService.get('database.url'), - host: this.configService.get('database.host'), - port: this.configService.get('database.port'), - username: this.configService.get('database.username'),}); + if (this.configService.get('database.type') === 'aurora-postgres') { + return { + type: this.configService.get('database.type'), + database: this.configService.get('database.name'), + secretArn: this.configService.get('database.secretArn'), + resourceArn: this.configService.get('database.resourceArn'), + region: this.configService.get('aws.region'), + } as AuroraPostgresConnectionOptions; + } + return { type: this.configService.get('database.type'), url: this.configService.get('database.url'), host: this.configService.get('database.host'), port: this.configService.get('database.port'), username: this.configService.get('database.username'), - password: generatePasswordWithRdsSigner, + password: this.configService.get('database.password'), database: this.configService.get('database.name'), synchronize: this.configService.get('database.synchronize'), dropSchema: false, diff --git a/src/common/database/utils/generatePasswordWithRdsSigner.ts b/src/common/database/utils/generatePasswordWithRdsSigner.ts index f0dc016e..ccb2d8c4 100644 --- a/src/common/database/utils/generatePasswordWithRdsSigner.ts +++ b/src/common/database/utils/generatePasswordWithRdsSigner.ts @@ -8,6 +8,6 @@ export const generatePasswordWithRdsSigner = async () => { username: process.env.DATABASE_USER, }; const signer = new Signer(configSigner); - const password = (await signer.getAuthToken()); + const password = await signer.getAuthToken(); return password; };