Using a resource created in one cdk construct in another construct - amazon-web-services

I'm building the infrastructure for an application using AWS-CDK.
I have a construct that builds multiple S3 buckets and another construct that creates a lambda function that fetches data from these buckets.
In order to be able to give my lambda permissions to fetch data from the bucket I need the buckets ARN.
Is there a way in which I could export the bucket arn from the construct that produces the buckets and import it into the lambda construct?

Sure, maybe something like this:
export class ConsumingStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const producingStack = new BucketProducingStack(this, 'BucketProducingStack');
const { bucket1, bucket2 } = producingStack;
//Create new lambda stack here
//const lambdaStack = new LambdaStack(this, { bucket1, bucket2} );
}
}
export class BucketProducingStack extends cdk.NestedStack {
bucket1: string;
bucket2: string;
constructor(scope: cdk.Construct, id: string, props?: cdk.NestedStackProps) {
const bucket1 = new Bucket(this, 'BucketOne');
const bucket2 = new Bucket(this, 'BucketTwo');
this.bucket1 = bucket1.bucketArn;
this.bucket2 = bucket2.bucketArn;
}
}
No guarantee this compiles as it was written entirely in this window, but hopefully conveys the idea.

If you are using python, you can add
#property
def main_source_bucket(self) -> _s3.IBucket:
return self.bucket
Reference it in your app stack like this ..
bucket = S3Construct(self, "bucket", "bucket1")
LambdaConstruct(self, "lambda1", "dev", bucket.main_source_bucket

Related

AWS CDK: No export named XYZ found

I've following stack which deploys the two constructs within. The construct DynamoDBConstruct exports the table name, and the construct IAMRoleConstruct consumes it. However, during deployment, it fails stating No export named dbTableName found, despite the fact that dependency is added/specified, the IAMRoleConstruct gets deployed first, why?
Stack:
public AllStacks(Construct scope, string id, IStackProps props = null) : base(scope, id, props)
{
var db = new DynamoDBConstruct(this, "DynamoDB");
var iam = new IAMRoleConstruct(this, "IAMRole");
iam.Node.AddDependency(db);
}
DynamoDBConstruct
public DynamoDBConstruct(Construct scope, string id): base(scope, id)
{
var dbTable = new Table(this, "dbTable", new TableProps()
{
PartitionKey = new Attribute
{
Name = "contactID",
Type = AttributeType.STRING
},
TableClass = TableClass.STANDARD,
TableName = (string)Node.TryGetContext("dbTableName"),
RemovalPolicy = RemovalPolicy.DESTROY
});
new CfnOutput(this, "OutputTableName", new CfnOutputProps()
{
ExportName = "dbTableName",
Value = dbTable.TableName
});
}
IAMRoleConstruct
public IAMRoleConstruct(Construct scope, string id) : base(scope, id)
{
var dbTableName = Fn.ImportValue("dbTableName");
/*
Some code
.
*/
}
With the disclaimer that I am not sure what language your code is in, I'm going to write in CDK's native language (which I recommend you to do as well) - Typescript.
The problem comes most likely from the fact that you are using the export within the same CDK/CFN stack. The export won't be available during stack creation, as that is part of the stack creation itself.
When you're working within a single stack, the simplest, most intuitive way of "moving data" from one construct to another is to just expose values through a public member of your class, e.g.:
class DynamoDBConstruct extends Construct {
public readonly tableName: string;
constructor(scope: Construct, id: string, props: Whatever) {
super(scope, id);
const table = new Table(this, 'Table', {
partitionKey: { name: 'id', type: AttributeType.STRING },
billingMode: BillingMode.PAY_PER_REQUEST,
// omitting table name on purpose - it will be generated by CDK
});
this.tableName = table.tableName;
}
}
Now inside your stack, you can simply use that table name:
class MyStack extends Stack {
constructor(scope: App, id: string, props: Whatever) {
const table = new DynamoDBConstruct(...);
const myOtherConstruct = new MyOtherConstruct(this, 'myOtherConstruct', {
// using table name here
tableName: table.tableName,
});
}
}
The reason for the error is that you are trying to produce and consume a Stack Output in the same stack. That won't work:
Docs: Output values are available after the stack operation is complete. Stack output values aren't available when a stack status is in any of the IN_PROGRESS status.
No worries! As #Victor says, there is a much easier alternative. Get rid of the Outputs. Instead, share data between your custom constructs by declaring public fields (e.g. public Table table) in the providing class, passing the references as props to the consuming class. This is what the CDK constructs do.
See the C# example stacks in the aws-cdk-examples repo.

Create Step Functions from JSON File in CDK

With an existing Step Functions definition JSON file, how can I use it directly in CDK to create a Step Function?
Use the L1 CfnStateMachine construct. It has a definitionString prop that accepts a stringified JSON definition.
Here is the code snippet if it is useful to anyone.
private createStepFunction(props: {
stepfunction_name: string;
stepfunctions_role_arn: string;
}): stepfunctions.CfnStateMachine {
const file = fs.readFileSync("../step_functions/definition.asl.json");
const stepFunction = new stepfunctions.CfnStateMachine(
this,
"cfnStepFunction",
{
roleArn: props.stepfunctions_role_arn,
definitionString: file.toString(),
stateMachineName: props.stepfunction_name,
}
);
return stepFunction;
}

How to parse input from AWS event rule in lambda?

If I use the input in an events rule.. how am I supposed parse it in the lambda?
Now I have:
MyJobScheduledRule:
Type: AWS::Events::Rule
Properties:
Description: Scheduled Rule
ScheduleExpression: !Sub "rate(${IntervalMinutes} minutes)"
State: "ENABLED"
Targets:
- Id: "MyJobLambda"
Arn: !GetAtt MyJobLambda.Arn
Input: "\"{\\\"key\\\":\\\"value\\\"}\"
With the following lambda:
public class MyJobLambda implements RequestHandler<Map<String, String>, Void> {
private static Logger LOGGER = LoggerFactory.getLogger(MyJobLambda.class);
#Override
public Void handleRequest(Map<String, String> event, Context context) {
LOGGER.debug("MyJob got value {} from input", event.get("key"));
return null;
}
}
But I get the following runtime exception:
An error occurred during JSON parsing: java.lang.RuntimeException
java.lang.RuntimeException: An error occurred during JSON parsing
Caused by: java.io.UncheckedIOException: com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `java.util.LinkedHashMap` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('
{
"key": "value"
}
I have also tried using a POJO as input to the lambda. Any ideas?
Use a ScheduledEvent object, and parse the detail field of the event
import com.amazonaws.services.lambda.runtime.events.ScheduledEvent;
public class MyJobLambda implements RequestHandler<Map<String, String>, Void> {
private static Logger LOGGER = LoggerFactory.getLogger(MyJobLambda.class);
#Override
public Void handleRequest(ScheduledEvent event, Context context) {
LOGGER.debug("MyJob got value {} from input", mapScheduledEventDetail(event.getDetail());
return null;
}
private String mapScheduledEventDetail(Map<String,Object> detailObject) {
return ParserUtil.parseObjectToJson(detailObject.get("clientId"));
}
}

AWS CDK: how do I reference cross-stack resources in same app?

I have an App that has two stacks, both within the same region/account. One of those stacks requires the ARN of a lambda that exists in the other stack. How do I reference this?
// within stackA constructor
public StackA(Construct scope, String id, StackProps props) {
SingletonFunction myLambda = SingletonFunction.Builder.create(this, "myLambda")
// some code here
.build()
CfnOutput myLambdaArn = CfnOutput.Builder.create(this, "myLambdaArn")
.exportName("myLambdaArn")
.description("ARN of the lambda that I want to use in StackB")
.value(myLambda.getFunctionArn())
.build();
}
App app = new App();
Stack stackA = new StackA(app, "stackA", someAProps);
Stack stackB = new StackB(app, "stackB", someBProps);
stackB.dependsOn(stackA);
How do pass the ARN into StackB?
CDK's official documentation has a complete example for sharing a S3 bucket between stacks. I copied it below for quicker reference.
/**
* Stack that defines the bucket
*/
class Producer extends cdk.Stack {
public readonly myBucket: s3.Bucket;
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const bucket = new s3.Bucket(this, 'MyBucket', {
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
this.myBucket = bucket;
}
}
interface ConsumerProps extends cdk.StackProps {
userBucket: s3.IBucket;
}
/**
* Stack that consumes the bucket
*/
class Consumer extends cdk.Stack {
constructor(scope: cdk.App, id: string, props: ConsumerProps) {
super(scope, id, props);
const user = new iam.User(this, 'MyUser');
props.userBucket.grantReadWrite(user);
}
}
const producer = new Producer(app, 'ProducerStack');
new Consumer(app, 'ConsumerStack', { userBucket: producer.myBucket });
You can access resources in a different stack, as long as they are in the same account and AWS Region. The following example defines the stack stack1, which defines an Amazon S3 bucket. Then it defines a second stack, stack2, which takes the bucket from stack1 as a constructor property.
// Helper method to build an environment
static Environment makeEnv(String account, String region) {
return Environment.builder().account(account).region(region)
.build();
}
App app = new App();
Environment prod = makeEnv("123456789012", "us-east-1");
StackThatProvidesABucket stack1 = new StackThatProvidesABucket(app, "Stack1",
StackProps.builder().env(prod).build());
// stack2 will take an argument "bucket"
StackThatExpectsABucket stack2 = new StackThatExpectsABucket(app, "Stack,",
StackProps.builder().env(prod).build(), stack1.getBucket());
Option 1:
pass the data from Stack A to Stack B using the constructor :
You can extend cdk.stack and create a new class that will contain stackA.
In that stack, expose the relevant data you want by using public XXX: string\number (etc) ( See line 2 in the example).
Later, just pass this data into StackB constructor ( you can pass it using props as well).
Working code snippet:
Stack A:
export class StackA extends cdk.Stack {
public YourKey: KEY_TYPE;
constructor(scope: cdk.Construct, id: string, props: cdk.StackProps ) {
super(scope, id, props);
Code goes here...
// Output the key
new cdk.CfnOutput(this, 'KEY', { value: this.YourKey });
}
}
Stack B:
export class StackB extends cdk.Stack {
constructor(scope: cdk.Construct, id: string,importedKey: KEY_TYPE, props: cdk.props) {
super(scope, id, props)
Code goes here...
console.log(importedKey)
}
}
bin ts:
const importedKey = new StackA(app, 'id',props).YourKey;
new StackB(app, 'id',importedKey,props);
Option 2:
Sometimes it's just better to save this kind of stuff in the parameter store and read it from there.
More info here.
I found all of the answers to be on the right path, but none explained it fully and/or well. In this example, I'm passing a VPC from a VPC stack to an ECS cluster.
First, add a property to the originating stack. This property is set whenever the asset is created:
export class VpcStack extends cdk.Stack {
readonly vpc: Vpc;
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Here
this.vpc = new Vpc(this, 'vpc', {
maxAzs: 3,
cidr: '10.0.0.0/16',
});
});
}
Next, require this property as a parameter to the consuming stack:
// Create an interface that extends cdk.StackProps
// The VPC property is added here
interface EcsClusterStackProps extends cdk.StackProps {
vpc: Vpc,
}
export class EcsClusterStack extends cdk.Stack {
// Use your interface instead of the regular cdk.StackProps
constructor(scope: cdk.Construct, id: string, props: EcsClusterStackProps) {
super(scope, id, props);
// Use the passed-in VPC where you need it
new Cluster(this, "myCluster", {
capacity: {
instanceType: InstanceType.of(InstanceClass.M6I, InstanceSize.LARGE)
},
clusterName: "myCluster",
vpc: props.vpc, // Here
});
}
}
Third, pass the reference in your app file:
const app = new cdk.App();
// Create the VPC stack
const vpcStack = new VpcStack(app, 'vpc-stack', {
env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },
});
// Pass the VPC directly to the consuming stack's constructor
const ecsClusterStack = new EcsClusterStack(app, 'ecs-cluster-stack', {
vpc: vpcStack.vpc, // Here
});
Hopefully this helps clarify some of the ambiguous areas.

Ionic AWS Mobile Hub Starter issues

I followed the instructions on the github readme and have imported the app into my AWS Mobile Hub project and downloaded my projects aws-config.js to src/assets. When I attempt to serve the app I get a runtime error:
Runtime Error:
aws_cognito_region is not defined
Stack:
ReferenceError: aws_cognito_region is not defined
at new Cognito (http://localhost:8100/build/main.js:112:36)
at _createClass (http://localhost:8100/build/vendor.js:10975:20)
at _createProviderInstance$1 (http://localhost:8100/build/vendor.js:10949:26)
at resolveNgModuleDep (http://localhost:8100/build/vendor.js:10934:17)
at _createClass (http://localhost:8100/build/vendor.js:10977:29)
at _createProviderInstance$1 (http://localhost:8100/build/vendor.js:10949:26)
at resolveNgModuleDep (http://localhost:8100/build/vendor.js:10934:17)
at NgModuleRef_.get (http://localhost:8100/build/vendor.js:12159:16)
at resolveDep (http://localhost:8100/build/vendor.js:12655:45)
at createClass (http://localhost:8100/build/vendor.js:12525:32)
Any insight would be greatly appreciated.
Edit: I have added below my app.config.ts code as well as a segment of my aws-config.js file (omitting the constant declarations at the top that contain my AWS mobile hub project details)
app.config.ts:
import { Injectable } from '#angular/core';
declare var AWS: any;
declare const aws_mobile_analytics_app_id;
declare const aws_cognito_region;
declare const aws_cognito_identity_pool_id;
declare const aws_user_pools_id;
declare const aws_user_pools_web_client_id;
declare const aws_user_files_s3_bucket;
#Injectable()
export class AwsConfig {
public load() {
// Expects global const values defined by aws-config.js
const cfg = {
"aws_mobile_analytics_app_id": aws_mobile_analytics_app_id,
"aws_cognito_region": aws_cognito_region,
"aws_cognito_identity_pool_id": aws_cognito_identity_pool_id,
"aws_user_pools_id": aws_user_pools_id,
"aws_user_pools_web_client_id": aws_user_pools_web_client_id,
"aws_user_files_s3_bucket": aws_user_files_s3_bucket
};
AWS.config.customUserAgent = AWS.config.customUserAgent + ' Ionic';
return cfg;
}
}
aws-config.js:
const 'aws_cognito_region' = 'us-east-1';
... etc
AWS.config.region = aws_project_region;
AWS.config.credentials = new AWS.CognitoIdentityCredentials({
IdentityPoolId: aws_cognito_identity_pool_id
}, {
region: aws_cognito_region
});
AWS.config.update({customUserAgent: 'MobileHub v0.1'});
I fixed this issue by going to the aws-config.js and removing the single quotes on each of the variables defined. So if you have this:
const 'aws_cognito_region' = 'us-east-1';
Change to this:
const aws_cognito_region = 'us-east-1';
You need to configure src/app/app.config with your AWS information (app id, pool id, etc.).