AWS CDK- Access resource from other stack - aws-cdk

Suppose i have a resource, say stepfunction activity, in one stack. How can i access it's arn in other stack?
I found CfnConstruct suitables for exporting (https://docs.aws.amazon.com/cdk/api/latest/docs/#aws-cdk_core.CfnOutput.html). As of now i have used CfnConstruct to export it:
this.initiateValidationActivityArn = new cdk.CfnOutput(this, 'InitiateValidationActivityArn', {
value: igvsStateMachine.initiateValidationActivity.activityArn
});
Now how can i access it in other file. I have tried this:
ecsService.initiateValidationActivityArn.value
But value is private to construct so can't use it.

If you have the stacks in one deployable cdk-app, you can access the property value from the stack by making it accessible from outside / not-private.
What I recommend doing is to keep it readonly so that it can't be re-initialized from outside the stack.
// file: ./lib/first-stack.ts
// import statements
export class FirstStack extends Stack {
readonly vpc: IVpc;
constructor(...) {
// setup
this.vpc = new Vpc(this, "VPC", {...});
}
}
In the dependent stack, you can pass it via (custom) props.
// file: ./lib/second-stack.ts
export interface SecondStackProps extends StackProps {
importedVpc: IVpc;
}
export class SecondStack extends Stack {
constructor(scope: cdk.Construct, id: string, props: SecondStackProps) {
super(scope, id, props);
const importedVpc = props.importedVpc;
// do sth. with your imported resource
}
}
Why is that working you may ask...?
It works because CDK generates the Resource IDs and during the synth phase it can put the tokens for the resources inside the generated templates.
This doesn't work, when you have separated cdk-apps / deployments of stacks, because CDK can't resolve the (existing) Resource ID Tokens during cdk-synth.
With that, you need to have another approach:
// file: ./lib/first-stack-separated-deployment.ts
// import statements
export class FirstStackSeparatedDeployment extends Stack {
cfnVpcId: CfnOutput;
constructor(...) {
// setup
const vpc = new Vpc(this, "VPC", {...});
this.cfnVpcId= new cdk.CfnOutput(this, "FirstStackCfnVpcId", {
value: vpc.vpcId,
exportName: "UniqueNameForYourVpcId"
});
}
}
In the other stack, which requires the already deployed resource, you do the following:
// file: ./lib/second-stack-separated-deployment.ts
export class SecondStackSeparatedDeployment extends Stack {
constructor(...) {
// setup
const vpcId = Fn.importValue("UniqueNameForYourVpcId")
const importedVpc = ec2.Vpc.fromVpcAttributes(this, "ImportedVpc", {
vpcId: vpcId,
availabilityZones: [this.region],
})
// proceed further...
}
}
Basically, you take the exportName from the CfnOutput Construct as an identifier and import it via the core Fn.importValue.
With that approach, the first stack already needs to be deployed.

Related

Using BullMQ with NestJs, is it possible to use a config variable as part of a Job name?

I'm trying to use an environment variable value in the nestjs/bull module's #Process() decorator, as follows. How should I provide the 'STAGE' variable as part of the job name?
import { Process, Processor } from '#nestjs/bull';
import { Inject } from '#nestjs/common';
import { ConfigService } from '#nestjs/config';
import { Job } from 'bull';
#Processor('main')
export class MqListener {
constructor(
#Inject(ConfigService) private configService: ConfigService<SuperRootConfig>,
) { }
// The reference to configService is not actually allowed here:
#Process(`testjobs:${this.configService.get('STAGE')}`)
handleTestMessage(job: Job) {
console.log("Message received: ", job.data)
}
}
EDITED with answers (below) from Micael and Jay:
Micael Levi answered the initial question: You can't use the NestJS ConfigModule to get your config into a memory variable. However, running dotenv.config() in your bootstrap function will not work either; you get undefined values for the memory variables if you try to access them from within a Method Decorator. To resolve this, Jay McDoniel points out that you have to import the file before you import AppModule. So this works:
// main.ts
import { NestFactory } from '#nestjs/core';
require('dotenv').config()
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(process.env.PORT || 4500);
}
bootstrap();
You cannot use this on that context due to how decorator evaluation works. At that time, there's no instance created for MqListener class, thus, using this.configService doens't make sense.
You'll need to access process.env. directly. And so will call dotenv (or what lib that read & parses your dot env file) in that file.

Pass properties over from one Construct to another with CDK before synth runs

I've got two Constructs being created in one Stack, and during synthesis I would like to merge a bunch of properties and use the merged result.
The reason for this is to split creation of resources into a specific look in the Stack. In other words, I want to initialize two Constructs in the one Stack, but when CDK deploys the stack, I would like properties from both to be merged, with the resource(s) created in one of the Stacks, (Environment in my example below) being able to use the merged properties from both Constructs.
Here is where I've got to so far, but when running a cdk synth the resulting someProperties object does not reflect the items I've sent through from my ExampleThing Construct that was initialized in the same Stack.
export class IExampleEnvironment extends Construct {
readonly someId?: string;
}
export interface ExampleProps {
readonly environment: Environment;
readonly a: string;
readonly b: string;
}
export class Environment extends IExampleEnvironment {
private someProperties: any;
constructor(scope: Construct, id: string, props: StackProps) {
super(scope, id);
new ExampleResourceThatUsesSomeProperties(this, 'Example' {
foo: this.someProperties // this is where I would like to use some properties that are brought in from another Construct (in the same stack as Environment)
});
}
public addSomeProperties(someProps: ExampleProps) {
this.someProperties = { A: someProps.a, B: someProps.b };
}
}
export class ExampleThing extends Construct {
constructor(scope: Construct, id: string, props: ExampleProps) {
super(scope, id);
props.environment.addSomeProperties(props);
}
}
// Creating the Stack...
const environment = new Environment(this, 'Environment', { someId: "123" });
const exampleThing = new ExampleThing(this, 'ExampleThing', props);
Currently, I'm using a reference to the Environment property sent in, and then calling the internal addSomeProperties() method on it, hoping that would work. I suspect this is my issue, but I'm not sure of how to achieve this.
The important thing here is that the Environment stack must be created first, as the ExampleThing stack needs to supply a environment property as part of its ExampleProps and pass the Environment in for that.
Is there a better way that would work?
So I'll admit this is a very niche requirement, and not really the way CDK is meant to be used, but I wanted to get this very specific use case working.
I dug in a little more and found that it is possible by overriding the prepare() method in the Construct. Here is my working example, which shows how one construct can create an S3 bucket, and another can update this instance's S3 bucket purely by creating an instance and passing in the other instance:
thing.ts:
import { Construct } from "#aws-cdk/core";
import { Foo } from "./foo";
export interface ExampleThingProps {
readonly foo: Foo;
readonly a: string;
readonly b?: string;
}
export class ExampleThing extends Construct {
constructor(scope: Construct, id: string, props: ExampleThingProps) {
super(scope, id);
props.foo.addSomeProperties(props);
}
}
foo.ts:
import { Construct, StackProps } from "#aws-cdk/core";
import * as s3 from "#aws-cdk/aws-s3";
import { ExampleThingProps } from "./thing";
import { IBucket } from "#aws-cdk/aws-s3";
export class IExampleFoo extends Construct {
readonly someId?: string;
}
export interface FooProps {
bucketName: string;
}
export class Foo extends IExampleFoo {
private someProperties: any;
private theBucket: IBucket;
constructor(scope: Construct, id: string, props: FooProps) {
super(scope, id);
this.someProperties = { A: props.bucketName };
this.theBucket = new s3.Bucket(this, 'ExampleBucket', {
bucketName: this.someProperties.A
});
}
prepare() {
const firstBucket = this.node.tryFindChild("ExampleBucket");
if (firstBucket) {
console.log('found first bucket node: ' + firstBucket.node.uniqueId);
const removed = this.node.tryRemoveChild(firstBucket.node.id);
console.log('removed the found node: ' + removed);
}
this.theBucket = new s3.Bucket(this, 'ExampleBucket', {
bucketName: this.someProperties.A
});
}
public addSomeProperties(someProps: ExampleThingProps) {
console.log('updates the existing properties...');
this.someProperties = { A: someProps.a, B: someProps.b };
}
}
Usage:
const foo = new Foo(this, 'Foo', {
bucketName: "this-is-the-name-set-from-foo-construction"
});
const thing = new ExampleThing(this, 'Thing', {
foo: foo,
a: "shiny-new-name"
});
If you don't initialize a new ExampleThing then the bucket will be named this-is-the-name-set-from-foo-construction.
If you initialize a new ExampleThing, then the bucket's name will be changed for synthesize (and will apply in a cdk diff or deploy and be named shiny-new-name.
I think you are looking to create dependencies between the resources, here is an example
const app = new App();
const myFirstStack = new MyFirstStack(
app,
stackNameone
);
const mySecondStack = new MySecondStack(
app,
stackNametwo
);
mySecondStack.addDependency(myFirstStack, "Depends on resources");
You can do this with most resources in CDK so whichever resources you want created first add it as a dependency like the above example.

How do I get a list of subnet IDs from one stack to another using SSM when StringListParameter doesn't work?

According to this bug and this bug, ssm.StringListParameter doesn't work in the CDK due to some issues with CFT.
I need to be able to export an arbitrarily length list of subnetIds from one stack and import the list into another using SSM, as the lifetime of subnetIds is completely different than the lifetime of the consumer of subnetIds in a second stack (especially in production, though not in a sandbox). I cannot hard code the subnet IDs, as when creating a sandbox the IDs would vary from sandbox to sandbox. I want the latest version of whatever is in the SSM key.
However, the bugs appear to not be resolvable, and I cannot find a work around.
I tried serializing using JSON, but the item passed around the code is is a late binding Token, which is treated as a string, and the deserialized items is a string [], so it's not possible to get such code to compile.
Here's an example of what I attempted. It doesn't compile:
export function getOtherStackOutputList(stack: cdk.Stack, mangledOtherStackName: string, key: string): string [] {
const globallyUniqueKey = `/${mangledOtherStackName}/${key}`;
const jsonResult = ssm.StringParameter.fromStringParameterName(stack, key + 'SSM', globallyUniqueKey).stringValue;
if (cdk.Token.isUnresolved(jsonResult)) {
return jsonResult;
} else {
const result = JSON.parse(jsonResult);
return result;
}
};
which would be used by code that looks like this:
const efsVpcIsolatedSubnetsIds = StackValueShare.getOtherStackOutputList(this, mangledStackName + efsDbStackSuffix,
'efsTimeSeriesDatabaseVpcIsolatedSubnets');
This works for me:
import { Construct } from '#aws-cdk/core';
import { AwsCustomResource, AwsSdkCall } from '#aws-cdk/custom-resources';
import iam = require("#aws-cdk/aws-iam");
interface SSMParameterReaderProps {
parameterName: string;
region: string;
}
export class SSMParameterReader extends AwsCustomResource {
constructor(scope: Construct, name: string, props: SSMParameterReaderProps) {
const { parameterName, region } = props;
const ssmAwsSdkCall: AwsSdkCall = {
service: 'SSM',
action: 'getParameter',
parameters: {
Name: parameterName
},
region,
physicalResourceId: {id:Date.now().toString()} // Update physical id to always fetch the latest version
};
super(scope, name, { onUpdate: ssmAwsSdkCall,policy:{
statements:[new iam.PolicyStatement({
resources : ['*'],
actions : ['ssm:GetParameter'],
effect:iam.Effect.ALLOW,
}
)]
}});
}
public getParameterValue(): string {
return this.getResponseField('Parameter.Value').toString();
}
}

NestJs: inject service (or anything) into imported module

Is there a way better than below to inject a service or component inside an imported module?
export interface AmqpInterceptor{
after(message:any):Promise<void>;
}
export class AmqpInterceptors extends Array<AmqpInterceptor>{
}
//generic library module
#Module({
providers:[{
provide: AmqpInterceptors,
useValue: []
}]
}
export class AMQPModule implements OnModuleInit{
static register(options: AMQPOptions): DynamicModule {
const providers = options.providers || []
return {
module: AMQPModule,
providers: [
...providers,
OtherProvider
]
}
}
}
//end user module
#Module({
imports: [
AMQPModule.register(({
// I had to create a factory method to pass providers as an argument.
// I would think that it is not a good practice
providers: [{
provide:AmqpInterceptors,
useValue:[MyCustomInterceptor]
}]
})
],
providers: [
]
})
export class QueueModule {
}
Current working solution: I declare a default empty array in the generic module and a factory method that allows to pass custom value in module construction.(In my happiest world I declare multiple instances of an interface and then, DI collects all of these, but I thinks this is really impossible in NestJs)
you can use a package like #golevelup/nestjs-discovery to help you with.
Basically you've to do the following:
// AMQP Module
// amqp-interceptor.decorator.ts
import { SetMetadata } from '#nestjs/common';
export function AmqpInterceptor() {
return SetMetadata('AMQP_INTERCEPTOR', true)
}
// amqp-explorer.ts
import { OnModuleInit } from '#nestjs/common'
#Injectable()
export class AmqpExplorer implements OnModuleInit {
constructor(
private readonly discoveryService: DiscoveryService,
) {}
async onModuleInit(): Promise<void> {
const amqpInterceptorProviders = await this.discoveryService.providers('AMQP_INTERCEPTOR')
// you can store this list, to be queried by some
// other provider to use interceptors, etc.
}
}
// amqp.module.ts
#Module({ providers:[AmqpExplorer]})
export class AMQPModule { }
// end user module
// end-user.module.ts
#Module({ imports:[AMQPModule], providers: [SomeAmqpInterceptor] })
export class EndUserModule { }
// some-amqp.interceptor.ts
#Injectable()
#AmqpInterceptor()
export class SomeAmqpInterceptor {
run(): void { console.log('intercepting') }
}
Obs:
Interceptor decorator: you can add any params which would help you to improve your api
I suggest this (list of providers) instead of injecting decorated providers into your module, because I don't know how do you plan to use them. This way, you've a list of providers and can invoke each one.
AMQP_INTERCEPTOR string can collide with some other metadata, it's a good practice add something more specific to avoid two metadata with same one in a module
To invoke the interceptors later:
import { ExternalContextCreator } from '#nestjs/core/helpers/external-context-creator'
export class Runner {
constructor(
private readonly handler: DiscoveredMethodWithMeta,
private readonly externalContextCreator: ExternalContextCreator,
) { }
run(): void {
const handler = this.externalContextCreator.create(
this.handler.discoveredMethod.parentClass.instance,
this.handler.discoveredMethod.handler,
this.handler.discoveredMethod.methodName,
)
const handlerResult = await handler(content)
}
Didn't run local, but I think this is a good way to start

#Inject outside Angular2 application

Is it possible to retrieve any #Injectable like:
var config = #Inject(Config);
Thanks
If you want to get new instances from dependency injection, you need a reference to the Injector then you can acquire new instances with
injector.get(Config);
How you can get a reference to Injector depends on where your code is.
In an Angular component or service, just inject it
constructor(private injector:Injector) {}
You can also just create your own injector like
var injector = Injector.resolveAndCreate([Car, Engine]);
where Car and Engine are the providers the injector can create instances for.
To get the injector of your Angular application to be used outside your Angular application you can use
Example from https://github.com/angular/angular/issues/4112#issuecomment-153811572
let appInjectorRef: Injector;
export const appInjector = (injector?: Injector):Injector => {
if (injector) {
appInjectorRef = injector;
}
return appInjectorRef;
};
bootstrap(App, [
Auth,
HTTP_PROVIDERS,
ROUTER_PROVIDERS,
Car,
Engine
]).then((appRef: ComponentRef) => {
// store a reference to the application injector
appInjector(appRef.injector);
});
let injector: Injector = appInjector();
injector.get(Car);
I found it easier to declare injector as a global variable so I could use it a bit easier.
In the file where you bootstrap angular:
declare global {
var injector: Injector;
}
bootstrap(/* your bootstrap stuff */).then((appRef) => {
injector = appRef.injector;
});
The above will give you access to an injector variable anywhere else in your code.
In the file where you need an instance of Config:
import { Config } from './path/to/config.service';
class TestClass {
private config: Config;
constructor() {
this.config = injector.get(Config);
}
}

Resources