AWS CDK | Create a REST API spanning multiple CDK Stacks - aws-cdk

We are using AWS CDK to create our Serverless REST API. However, there are a large number of endpoints and sometimes we have to destroy and redeploy our stack. To prevent the REST API URL from changing with each deployment, I am planning to create the API GATEWAY in one stack and add methods and resources in a separate stack. How can I refer the created rest API in a separate stack?
Tried to implement something from https://github.com/aws/aws-cdk/issues/3705, but all of the resources(API Gateway, resource and methods) are being pushed in a single stack instead of API Gateway in one stack and the resources in other stack.
Relevant codes snippets are provided below:
bts-app-cdk.ts
const first = new FirstStack(app, 'FirstStack', {
env: {
region: 'us-east-1',
account: '1234567890',
}
});
const second = new SecondStack(app, 'SecondStack', {
apiGateway: first.apiGateway,
env: {
region: 'us-east-1',
account: '1234567890',
}
});
second.addDependency(first)
first-stack.ts
export class FirstStack extends cdk.Stack {
public readonly apiGateway: apig.IResource;
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const apiGateway = new apig.RestApi(this, 'BooksAPI', {
restApiName:'Books API',
})
apiGateway.root.addMethod('GET');
this.apiGateway = apiGateway.root;
}
}
second-stack
export interface SecondStackProps extends cdk.StackProps {
readonly apiGateway: apig.IResource;
}
export class SecondStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props: SecondStackProps) {
super(scope, id, props);
props.apiGateway.addMethod('ANY')
}
}

Seems that there is no way at the moment except using Cfn constructs, here's the github issue to track https://github.com/aws/aws-cdk/issues/1477

You have to set the deploy prop to false when instantiating your api-gw.
Then you can pass the restApiId and rootResourceId as enviroment variables to stacks.
Finally you can use a Deployment to deploy everything.
interface ResourceNestedStackProps extends NestedStackProps {
readonly restApiId: string;
readonly rootResourceId: string;
}
export class FirstStack extends cdk.Stack {
public readonly methods: Method[];
constructor(scope: cdk.Construct, props: ResourceNestedStackProps) {
super(scope, 'first-stack', props);
// get a hold of the rest api from attributes
const api = RestApi.fromRestApiAttributes(this, 'RestApi', {
restApiId: props.restApiId,
rootResourceId: props.rootResourceId,
})
// REPEAT THIS FOR METHODS
const method = api.root.addMethod('GET');
this.methods.push(method)
// ---
}
}
Then in the root stack file,
class RootStack extends Stack {
constructor(scope: Construct) {
const restApi = new RestApi(this, 'RestApi', {
deploy: false,
});
restApi.root.addMethod('ANY');
const firstStack = new FirstStack(this, {
restApiId: restApi.restApiId,
rootResourceId: restApi.restApiRootResourceId,
});
new DeployStack(this, {
restApiId: restApi.restApiId,
methods: [...firstStack.methods],
})
}
}
The deploy stack is where you deploy your API:
interface DeployStackProps extends NestedStackProps {
readonly restApiId: string;
readonly methods?: Method[];
}
class DeployStack extends NestedStack {
constructor(scope: Construct, props: DeployStackProps) {
super(scope, 'deploy-stack', props);
const deployment = new Deployment(this, 'Deployment', {
api: RestApi.fromRestApiId(this, 'RestApi', props.restApiId),
});
if (props.methods) {
for (const method of props.methods) {
deployment.node.addDependency(method);
}
}
new Stage(this, 'Stage', { deployment });
}
}
Refer this for more details: https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.NestedStack.html

Related

Using CDK to grant permissions to Dead Letter Queue on SNS Topic Subscription

I'm trying to set up an SNS topic, with a subscription to a Queue. I want to put a Dead Letter Queue onto to the SNS Subscription.
This deploys OK, however in the AWS console, when I open the subscription, I see the error "Couldn't check Amazon SQS queue permissions. Make sure that the queue exists and that your account has permission to read the attributes of the queue".
Do I need to somehow grant write permission for SNSTopic to TopicDLQ?
export class SNSToSQSConstruct extends Construct {
public readonly TopicDLQ: IQueue
public readonly SQSQueue: IQueue
public readonly SNSTopic: ITopic
constructor(scope: Construct, id: string) {
super(scope, id);
this.TopicDLQ = new Queue(this, `${id}_TopicDLQ`, {
visibilityTimeout: cdk.Duration.seconds(300),
});
this.SQSQueue = new Queue(this, `${id}_Queue`, {
visibilityTimeout: cdk.Duration.seconds(300),
});
this.SNSTopic = new Topic(this, `${id}_Topic`, {
fifo: false, // fifo support 300tps, standard support almost unlimited
topicName: id,
});
var subscription = this.SNSTopic.addSubscription(new SqsSubscription(this.SQSQueue, {
rawMessageDelivery: true,
deadLetterQueue: this.TopicDLQ
}));
// error Subscription is not IGrantable
//this.TopicDLQ.grantSendMessages(subscription);
}
}
I think you can do this with addToResourcePolicy
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { IQueue, Queue } from 'aws-cdk-lib/aws-sqs'
import { ITopic, Topic } from 'aws-cdk-lib/aws-sns'
import { SqsSubscription } from 'aws-cdk-lib/aws-sns-subscriptions'
import { ServicePrincipal, PolicyStatement, Effect} from 'aws-cdk-lib/aws-iam'
export class SNSToSQSConstruct extends Construct {
public readonly TopicDLQ: IQueue
public readonly SQSQueue: IQueue
public readonly SNSTopic: ITopic
constructor(scope: Construct, id: string) {
super(scope, id);
this.TopicDLQ = new Queue(this, `${id}_TopicDLQ`, {
visibilityTimeout: cdk.Duration.seconds(300),
});
this.SQSQueue = new Queue(this, `${id}_Queue`, {
visibilityTimeout: cdk.Duration.seconds(300),
});
this.SNSTopic = new Topic(this, `${id}_Topic`, {
fifo: false, // fifo support 300tps, standard support almost unlimited
topicName: id,
});
var subscription = this.SNSTopic.addSubscription(new SqsSubscription(this.SQSQueue, {
rawMessageDelivery: true,
deadLetterQueue: this.TopicDLQ
}));
this.TopicDLQ.addToResourcePolicy(
new PolicyStatement({
effect: Effect.ALLOW,
principals: [new ServicePrincipal('sns.amazonaws.com')],
actions: ["sqs:SendMessage"],
resources: [this.TopicDLQ.queueArn],
conditions: {
ArnEquals: {
"aws:SourceArn": this.SNSTopic.topicArn,
},
},
})
);
}
}

NestJS - inject service into typeorm migration

I would like to inject a service into a typeorm migration, so that I can perform data migration based on some logic within a service:
import { MigrationInterface, QueryRunner, Repository } from 'typeorm';
import { MyService } from '../../services/MyService.service';
import { MyEntity } from '../../entities/MyEntity.entity';
export class MyEntityMigration12345678
implements MigrationInterface
{
name = 'MyEntityMigration12345678';
constructor(
private readonly myService: MyService,
) {}
public async up(queryRunner: QueryRunner): Promise<void> {
const myEntityRepository: Repository<MyEntity> =
queryRunner.connection.getRepository<MyEntity>(MyEntity);
const entities = await myEntityRepository.findBy({
myColumn: '',
});
for (const entity of entities) {
const columnValue = this.myService.getColumnValue(myEntity.id);
await myEntityRepository.save({
...entity,
myColumn: columnValue,
});
}
}
// ...
}
Nevertheless
myService is undefined, and
myEntityRepository.findBy(.) gets stuck.
How can I do a migration based on business logic?
Thanks!
One option would be to write whatever query myService.getColumn value does inside your migration. If you're hell bent on using Nest's DI inside your migration then you could do something like this:
import { NestFactory } from '#nestjs/core';
import { MigrationInterface, QueryRunner, Repository } from 'typeorm';
import { AppModule } from '../../app.module'; // assumed path
import { MyService } from '../../services/MyService.service';
import { MyEntity } from '../../entities/MyEntity.entity';
export class MyEntityMigration12345678
implements MigrationInterface
{
name = 'MyEntityMigration12345678';
public async up(queryRunner: QueryRunner): Promise<void> {
const myEntityRepository: Repository<MyEntity> =
queryRunner.connection.getRepository<MyEntity>(MyEntity);
const entities = await myEntityRepository.findBy({
myColumn: '',
});
const appCtx = await NestFactory.createApplicationContext(AppModule);
const myService = app.get(MyService, { strict: false });
for (const entity of entities) {
const columnValue = myService.getColumnValue(myEntity.id);
await myEntityRepository.save({
...entity,
myColumn: columnValue,
});
}
await app.close();
}
// ...
}
You can't use injection inside the migration because the class itself is managed by TypeORM. You can, as shown above, create a NestApplicationContext instance and get the MyService instance from that. This only works, by the way, if MyService is REQUEST scoped

How to inject dependency into TypeORM ValueTransformer or EntitySchema

In my NestJS project I use TypeORM with external schema definition. I want to use a ValueTransformer to encrypt sensitive data in some database columns, e.g. the email of the user should be encrypted.
The ValueTransformer depends on an encryption service from another module. I can't figure out how to inject that service via DI.
export const UserSchema = new EntitySchema<User>(<EntitySchemaOptions<User>>{
name: 'user',
columns: {
email: {
type: 'varchar',
nullable: false,
transformer: new EncryptedValueTransformer()
} as EntitySchemaColumnOptions,
},
});
export class EncryptedValueTransformer implements ValueTransformer {
#Inject()
private cryptoService: CryptoService
public to(value: unknown): string | unknown {
// error: cryptoService undefined
return this.cryptoService.encrypt(value);
}
public from(value: unknown): unknown {
// error: cryptoService undefined
return this.cryptoService.decrypt(value);
}
}
#Injectable()
export class CryptoService {
public constructor(#Inject('CONFIG_OPTIONS') private options: CryptoModuleOptions) {
// options contain a secret key from process.env
}
public encrypt(data: string): string | undefined | null { ... }
public decrypt(data: string): string | undefined | null { ... }
}
#Module({})
export class CryptoModule {
public static register(options: CryptoModuleOptions): DynamicModule {
return {
module: CryptoModule,
providers: [
{
provide: 'CONFIG_OPTIONS',
useValue: options,
},
CryptoService,
],
exports: [CryptoService],
};
}
}
#Module({
imports: [
DomainModule,
CryptoModule.register({ secretKey: process.env.ENCRYPTION_KEY }),
TypeOrmModule.forFeature([
UserSchema,
UserTypeOrmRepository,
]),
],
providers: [
UserRepositoryProvider,
UserRepositoryTypeOrmAdapter,
],
exports: [
UserRepositoryProvider,
UserRepositoryTypeOrmAdapter,
],
})
export class PersistenceModule {}
With the above code the CryptoService instance in EncryptedValueTransformer is undefined. I searched related issues. According to this post https://stackoverflow.com/a/57593989/11964644 NestJS needs some context for DI to work. If that context is not given, you can manually provide the dependency.
My workaround now is this:
export class EncryptedValueTransformer implements ValueTransformer {
private cryptoService: CryptoService
public constructor() {
// tightly coupled now an process.env.ENCRYPTION_KEY still undefined at instantiation time
this.cryptoService = new CryptoService({ secretKey: process.env.ENCRYPTION_KEY });
}
}
But with this workaround the process.env variable is not yet resolvable at the point where the class is being instantiated, so I need to modify CryptoService in a way that it reads the env variable at runtime itself. And at this point the CryptoService is not reusable anymore.
How can I inject the CryptoService into the EncryptedValueTransformer or into UserSchema with this external schema setup? Or any better way to solve this?

AWS CDK : How do I associate a specified version of Lambda with an alias?

I use AWS CDK to manage Lambda.
I created two alias for the Lambda function, development and production.
But I don't know how to associate a version with each alias.
export class CdkLambdaStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const fnDemo = new NodejsFunction(this, 'demo', {
entry: 'lib/lambda-handler/index.ts',
currentVersionOptions: {
removalPolicy: RemovalPolicy.RETAIN,
retryAttempts: 1
}
});
// In this case, production would be the most recent version
// I want to specify the previous stable version
fnDemo.currentVersion.addAlias('production');
new lambda.Alias(this, 'demo-development-alias', {
aliasName: 'development',
version: fnDemo.latestVersion
});
}
}
I've looked at the AWS CDK documentation, but I can't find a way to get a previous version It was. Do you have any other good ideas?
https://docs.aws.amazon.com/cdk/api/latest/docs/#aws-cdk_aws-lambda.Version.html
resolved
import cdk = require('#aws-cdk/core');
import * as lambda from '#aws-cdk/aws-lambda';
import {NodejsFunction} from '#aws-cdk/aws-lambda-nodejs';
import {RemovalPolicy} from "#aws-cdk/core";
export class CdkLambdaStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const fnDemo = new NodejsFunction(this, 'demo', {
entry: 'lib/lambda-handler/index.ts',
currentVersionOptions: {
removalPolicy: RemovalPolicy.RETAIN,
}
});
const prodVersion = lambda.Version.fromVersionArn(this, 'prodVersion', `${fnDemo.functionArn}:1`);
prodVersion.addAlias('production');
const stgVersion = lambda.Version.fromVersionArn(this, 'stgVersion', `${fnDemo.functionArn}:2`);
stgVersion.addAlias('staging');
const currentVersion = fnDemo.currentVersion;
const development = new lambda.Alias(this, 'demo-development', {
aliasName: 'development',
version: currentVersion
});
}
}

TypeORM STI inheritance issues

I´m trying to setup a STI inheritance hierarchy, similar to described on the official docs here, but getting the following error
TypeError: Cannot read property 'ownColumns' of undefined
at /Users/luizrolim/workspace/nqa/src/metadata-builder/EntityMetadataBuilder.ts:320:64
at Array.map (<anonymous>)
at EntityMetadataBuilder.computeEntityMetadataStep1 (/Users/luizrolim/workspace/nqa/src/metadata-builder/EntityMetadataBuilder.ts:316:14)
at /Users/luizrolim/workspace/nqa/src/metadata-builder/EntityMetadataBuilder.ts:108:45
at Array.forEach (<anonymous>)
Here are me entities:
#ChildEntity()
export class ChildExam extends BaseExam {
}
#Entity('exm_exams')
#TableInheritance({ column: { type: 'varchar', name: 'type' } })
export abstract class BaseExam extends NQBaseEntity {
#Column()
public alias: string
#Column()
public description: string
}
import { BaseEntity, Column, PrimaryGeneratedColumn } from 'typeorm'
import { DateColumns } from '#infra/util/db/DateColumns'
export abstract class NQBaseEntity extends BaseEntity {
#PrimaryGeneratedColumn()
public id: number
#Column(type => DateColumns)
public dates: DateColumns
}
I am running at "typeorm": "^0.2.12",
I also ran into this error when trying to make Single Table Inheritance work:
(node:22416) UnhandledPromiseRejectionWarning: TypeError: Cannot read
property 'ownColumns' of undefined at
metadata-builder\EntityMetadataBuilder.ts:356:64
My problem was, that I only listed my child entities in the connection options but I forgot to also add my parent entity.
Here is how I solved it (using TypeORM v0.3.0-rc.19):
ParentEntity.ts
import {BaseEntity, Entity} from 'typeorm';
#Entity()
#TableInheritance({ column: { type: 'varchar', name: 'type' } })
export abstract class ParentEntity extends BaseEntity {
// ...
}
ChildEntity.ts
import {ChildEntity} from 'typeorm';
#ChildEntity()
export class ChildEntity extends ParentEntity {
constructor() {
super();
}
// ...
}
initDatabase.ts
import 'reflect-metadata';
import {Connection, createConnection} from 'typeorm';
import {SqliteConnectionOptions} from 'typeorm/driver/sqlite/SqliteConnectionOptions';
import {PostgresConnectionOptions} from 'typeorm/driver/postgres/PostgresConnectionOptions';
import {ConnectionOptions} from 'typeorm/connection/ConnectionOptions';
import {ParentEntity, ChildEntity} from './entity/index';
function getConnectionOptions(env?: string): ConnectionOptions {
// Here is the important part! Listing both, parent and child entity!
const entities = [ParentEntity, ChildEntity];
const test: SqliteConnectionOptions = {
database: ':memory:',
dropSchema: true,
entities,
logging: false,
name: 'default',
synchronize: true,
type: 'sqlite',
};
const development: SqliteConnectionOptions = {
database: 'test.db3',
type: 'sqlite',
};
const production: PostgresConnectionOptions = {
type: 'postgres',
url: process.env.DATABASE_URL,
};
const devProd = {
name: 'default',
entities,
logging: false,
migrations: [],
subscribers: [],
synchronize: true,
};
switch (env) {
case 'production':
return Object.assign(production, devProd);
case 'test':
return test;
default:
return Object.assign(development, devProd);
}
}
export default function initDatabase(): Promise<Connection> {
const options = getConnectionOptions(process.env.NODE_ENV);
return createConnection(options);
}
In my application I now have to call await initDatabase() at first, to properly initialize the database schema.
You should make class BaseExam not abstract to make it work.

Resources