I want to deploy AWS CDK stacks from with a Lambda function. The use case is to support similar functionality to that described in Trek10's Serverless CI/CD pipeline, where the relevant code is here.
In a nutshell, AWS CodePipelines only support listening to changes within a single Git branch. I want to listen to GitHub events relating to the creation of repos and branches and create CodePipeline instances in response to these events so that there is a Pipeline for each branch of each Git repository. I want a Lambda to listen to the GitHub events and create CDK stacks. The Trek10 example uses Python and calls CloudFormation directly. I'd like the Lambdas to be much simpler and use the CDK instead.
Here is a simple Lambda using Typescript that is derived from this AWS CDK issue:
// index.ts
import {ScheduledEvent} from 'aws-lambda';
import {CloudFormationDeploymentTarget, DEFAULT_TOOLKIT_STACK_NAME} from 'aws-cdk/lib/api/deployment-target';
import {CdkToolkit} from 'aws-cdk/lib/cdk-toolkit';
import {AppStacks} from 'aws-cdk/lib/api/cxapp/stacks';
import {Configuration} from 'aws-cdk/lib/settings';
import {execProgram} from "aws-cdk/lib/api/cxapp/exec";
import * as yargs from 'yargs';
import {SDK} from 'aws-cdk/lib/api/util/sdk';
export const handleCloudWatchEvent = async (event: ScheduledEvent): Promise<void> => {
try {
const aws = new SDK();
const argv = await yargs.parse(['deploy', '--app', 'bin/pipeline.js', '--staging', '/tmp', '--verbose', '--require-approval', 'never']);
const configuration = new Configuration(argv);
await configuration.load();
const appStacks = new AppStacks({
configuration,
aws,
synthesizer: execProgram,
});
const provisioner = new CloudFormationDeploymentTarget({ aws });
const cli = new CdkToolkit({ appStacks, provisioner });
const toolkitStackName = configuration.settings.get(['toolkitStackName']) || DEFAULT_TOOLKIT_STACK_NAME;
await cli.deploy({
stackNames: [],
exclusively: argv.exclusively as boolean,
toolkitStackName,
roleArn: argv.roleArn as string,
requireApproval: configuration.settings.get(['requireApproval']),
ci: true,
reuseAssets: argv['build-exclude'] as string[],
sdk: aws
});
return;
} catch (e) {
console.error(e);
return;
}
};
However, I get an error as described in the issue mentioned above:
ERROR { Error: Cannot find module '../package.json'
at Function.Module._resolveFilename (internal/modules/cjs/loader.js:636:15)
at Function.Module._load (internal/modules/cjs/loader.js:562:25)
at Module.require (internal/modules/cjs/loader.js:692:17)
at new SDK (/var/task/node_modules/aws-cdk/lib/api/util/sdk.ts:92:39)
at Runtime.exports.handleCloudWatchEvent [as handler] (/resources/index.ts:204:21)
at Runtime.handleOnce (/var/runtime/Runtime.js:66:25)
at process._tickCallback (internal/process/next_tick.js:68:7) code: 'MODULE_NOT_FOUND' }
I don't want to patch sdk.ts: https://github.com/aws/aws-cdk/blob/master/packages/aws-cdk/lib/api/util/sdk.ts#L92 as that seems to be a very dirty solution so I'm looking for another solution.
What is the simplest working example of calling the CDK from within a Lambda function?
Recently I had an ideal use case for a CDK deployer in Lambda but couldn't find any good and full examples of how to do this.
Using kadishmal example from CDK Github I managed to put CDK into Lambda layer, delete AWS SDK module from there (Lambda already has it), include it into a Lambda, and run CDK deploy/destroy from there. There is a size restriction for a Lambda and all its layers of 250mb so I also had to use Webpack to optimize the build size.
Here a Github repository with a basic example - a stack template that deploys S3 bucket and destroys it.
I created this repository which uses a 5 minute approach with Gradle & Docker to install CDK in a targeted location and mounts the Lambda Layer zipfile on your local system which you can use directly to manually upload in the AWS console or use i.e. with CDK.
An example Lambda handler function with NodeJS runtime and with the layer attached can look something like:
exports.handler = async (event) => {
const spawnSync = require('child_process').spawnSync;
const process = spawnSync('cdk', ['--version'], {
stdio: 'pipe',
stderr: 'pipe'
});
console.log(process.status);
console.log(process.stdout.toString());
};
Related
I have a lambda that invokes other lambdas and that's working fine. I want to set this up on my localhost now so I can test the interaction between the lambdas locally before deploying. This is the code I'm using to invoke the other lambda from the first lambda.
exports.route = async (req) => {
const functionName = getFunctionName(req.body.data); // logic involving recursion parses the data to see which type of function it is.
await client.send(
new InvokeCommand({
FunctionName: functionName,
InvocationType: 'Event',
Payload: JSON.stringify({
body: req.body,
headers: req.headers
})
})
);
}
Is there a way to run the lambdas and have this route locally when I'm testing on localhost? I am able to run the first lambda locally, but it invokes the deployed lambda and not my local lambda.
This is how I'm running the first lambda
sam build -m package.json
sam local start-api --host 0.0.0.0 --warm-containers EAGER
This is how I'm running the second lambda
sam build -m package.json
sam local start-lambda --host 0.0.0.0 --warm-containers EAGER
The way I'm planning to test is by running ngrok http on localhost and pointing a dev version of my bot there. Then I can send test commands to the dev bot and that then forwards the events to my localhost that invokes the first lambda that then routes to the second lambda running on localhost.
Is this the wrong way to test lambdas?
Update
To get the routing lambda to invoke the local version of the lambdas I updated the LambdaClient initialization object to point to localhost like this.
const lambdaOptions = process.env.env === "prod" ? undefined : {
apiVersion: '2015-03-31',
endpoint: 'http://192.168.0.xxx:3001', // my internal ip address
sslEnabled: false,
region: 'us-east-1',
accessKeyId: 'any',
secretAccessKey: 'any'
};
const client = new LambdaClient(lambdaOptions);
This points the lambda to my localhost, but the InvocationType: 'Event' feature is not available
Here's a github conversation about the issue.
https://github.com/aws/aws-sam-cli/pull/749
Since InvocationType: 'Event' is not currently supported when running lambdas locally, I ended up creating a simple nodejs express app to mimic the behavior of the first lambda. With express it is possible to send a response and continue processing unlike lambdas. So I changed InvocationType to RequestResponse and moved the invoking of the second lambdas to after the res.send() express call is made.
I am new to AWS and cloud technology in general. So, please bear with me if the use case below is a trivial one.
Well, I have a table in Amazon DynamoDB which I am exporting to Amazon S3 using exportTableToPointInTime API (ExportsToS3) on a scheduled basis everyday at 6 AM. It is being done using an AWS Lambda function in this way -
const AWS = require("aws-sdk");
exports.handler = async (event) => {
const dynamodb = new AWS.DynamoDB({ apiVersion: '2012-08-10' });
const tableParams = {
S3Bucket: '<s3-bucket-name>',
TableArn: '<DynamoDB-Table-ARN>',
ExportFormat: 'DYNAMODB_JSON'
};
await dynamodb.exportTableToPointInTime(tableParams).promise();
};
The CFT template of the AWS Lambda function takes care of creating lambda roles and policies, etc. along with scheduling using Cloudwatch events. This setup works and the table is exported to the target Amazon S3 bucket everyday at the scheduled time.
Now, the next thing I want is that after the export to Amazon S3 is complete, I should be able to invoke an another lambda function and pass the export status to that lambda function which does some processing with it.
The problem I am facing is that the above lambda function finishes execution almost immediately with the exportTableToPointInTime call returning status as IN_PROGRESS.
I tried capturing the response of the above call like -
const exportResponse = await dynamodb.exportTableToPointInTime(tableParams).promise();
console.log(exportResponse);
Output of this is -
{
"ExportDescription": {
"ExportArn": "****",
"ExportStatus": "IN_PROGRESS",
"StartTime": "2021-09-20T16:51:52.147000+05:30",
"TableArn": "****",
"TableId": "****",
"ExportTime": "2021-09-20T16:51:52.147000+05:30",
"ClientToken": "****",
"S3Bucket": "****",
"S3SseAlgorithm": "AES256",
"ExportFormat": "DYNAMODB_JSON"
}
}
I am just obfuscating some values in the log with ****
As can be seen, the exportTableToPointInTime API call does not wait for the table to be exported completely. If it would have, it would have returned ExportStatus as either COMPLETED or FAILED.
Is there a way I can design the above use case to achieve my requirement - invoking an another lambda function only when the export is actually complete?
As of now, I have tried a brute force way to do it and which works but it definitely seems to be inefficient as it puts in a sleep there and also the lambda function is running for the entire duration of the export leading to cost impacts.
exports.handler = async (event) => {
const dynamodb = new AWS.DynamoDB({ apiVersion: '2012-08-10' });
const tableParams = {
S3Bucket: '<s3-bucket-name>',
TableArn: '<DynamoDB-Table-ARN>',
ExportFormat: 'DYNAMODB_JSON'
};
const exportResponse = await dynamodb.exportTableToPointInTime(tableParams).promise();
const exportArn = exportResponse.ExportDescription.ExportArn;
let exportStatus = exportResponse.ExportDescription.ExportStatus;
const sleep = (waitTimeInMs) => new Promise(resolve => setTimeout(resolve, waitTimeInMs));
do {
await sleep(60000); //waiting every 1 min and then calling listExports API
const listExports = await dynamodb.listExports().promise();
const filteredExports = listExports.ExportSummaries.filter(e => e.ExportArn == exportArn);
const currentExport = filteredExports[0];
exportStatus = currentExport.ExportStatus;
}
while (exportStatus == 'IN_PROGRESS');
var lambda = new AWS.Lambda();
var paramsForInvocation = {
FunctionName: 'another-lambda-function',
InvocationType: 'Event',
Payload: JSON.stringify({ 'ExportStatus': exportStatus })
};
await lambda.invoke(paramsForInvocation).promise();
};
What can be done to better it or the above solution is okay?
Thanks!!
One option to achieve this is to define a waiter in order to wait till a "Completed" status is returned from exportTableToPointInTime.
As far I can see there are a few default Waiters for DynamoDB already present, but there is not one for the export, so you'll need to write your own (you can use those already present as an example).
A good post describing how to use and write a waiter could be found here.
This way if the export takes less than 15 minutes you'll be able to catch it within the Lambda limits without the need of a secondary lambda.
If it takes longer than that, you'll need to decouple it, where you have multiple options as suggested by #Schepo and #wahmd:
using an S3 event on the other end
Using AWS EventBridge
Using SNS
combinations of the above.
Context: we want to export the DynamoDB table content into an S3 bucket and trigger a lambda when the export is complete.
In CloudTrail there's an ExportTableToPointInTime event that is sent when the export is started, but no event for when the export is finished.
A way to trigger a lambda once the export is completed is by creating an S3 trigger using this configuration:
In particular:
The creation event type is a complete multi-upload (others do not seem to work, not sure why).
I think the prefix can be omitted, but it's useful. It's composed of:
The first part is the table name, content.
The second part, AWSDynamoDB, is set automatically by the export tool.
This is the most important part. The last files created once the export is complete are manifest-summary.json and manifest-summary.md5. We must set the suffix as one of these files.
For an await call, you are missing "async" keyword on handler.
Change
exports.handler = (event) => {
to
exports.handler = async event => {
Since this is an await call, you need 'async' keyword with it.
Let me know if it fixed your issue.
Also, I suspect you don't need .promise() as it might be already returning promise. Anyways, please try with & without it incase it still doesn't work.
After dynamoDB await call, You can invoke another lambda. It would make sure that your lambda is invoked after dynamoDb export call is completed.
To invoke second lambda,
you can use aws sdk invoke package.
putEvent api using eventBridge.
Later option is better as it decouples both lambdas & also, first lambda does not have to wait until the seconds invocation is completed. (reduces lambda time, hence reduces cost)
I'm writing a lambda (in node.js 6.10) to update an endpoint SageMaker. To do so I have to create a new HyperParamterTuningJob (and then describe it).
I succeeded to call all functions of the service SageMaker from the sdk (like listModels, createTrainingJob, ...) (https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/SageMaker.html) except some of them.
All the functions that are related to HyperParameterTuningJob
(createHyperParameterTuningJob, describeHyperParameterTuningJob, listHyperParameterTuningJobs and stopHyperParameterTuningJob)
are not known in the sdk by the lambda.
I have attached the policy 'AmazonSageMakerFullAccess' to the role IAM used (where all these functions are allowed). So the error can't come from a problem of authorization.
I have already created a HyperParameterTuningJob (by the interface of AWS) called 'myTuningJob'.
I have an error everytime I use the function describeHyperParamterTuningJob.
Here is my lambda code :
const AWS = require('aws-sdk');
const sagemaker = new AWS.SageMaker({region: 'eu-west-1', apiVersion: '2017-07-24'});
var role = 'arn:aws:iam::xxxxxxxxxxxx:role/service-role/AmazonSageMaker-ExecutionRole-xxxxxxxxxxxxxxx';
exports.handler = (event, context, callback) => {
var params = {
HyperParameterTuningJobName: 'myTuningJob'
};
sagemaker.describeHyperParameterTuningJob(params, function(err, data) {
if (err) console.log(err, err.stack);
else console.log(data);
});
};
When I try to test this code in AWS lambda, it returns this result in the console :
Function Logs:
START RequestId: 6e79aaa4-9a18-11e8-8dcd-d58423b413c1 Version: $LATEST
2018-08-07T08:03:56.336Z 6e79aaa4-9a18-11e8-8dcd-d58423b413c1 TypeError: sagemaker.describeHyperParameterTuningJob is not a function
at exports.handler (/var/task/index.js:10:15)
END RequestId: 6e79aaa4-9a18-11e8-8dcd-d58423b413c1
REPORT RequestId: 6e79aaa4-9a18-11e8-8dcd-d58423b413c1 Duration: 50.00 ms
Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 32 MB
RequestId: 6e79aaa4-9a18-11e8-8dcd-d58423b413c1 Process exited before completing request
When I call all other functions of the SageMaker service from the sdk, it runs correctly, whitout any error.
I don't find any explanation in the documentation of why these functions related to HyperParameterTuningJob are not recognized as functions in the sdk.
Does anyone have any idea of why it doesn't work ? Or any solutions to call theses functions ?
In AWS Lambda, only the sdk that have a sable version are available.
The sdk of the SageMaker service is not stable yet, so functions related to HyperParameterTuningJob are not in the version of the sdk included in AWS Lambda.
To use theses functions, you need to install the latest version of the sdk on local on your machine (with npm install aws-sdk).
Then zip the node_modules folder and your script (called index.js), then upload this zip folder into the AWS lambda.
I have written an HTTP triggered function in GCP and it computes some data as expected and after the computation, I would like to publish the result on a MQTT topic.
I added the following snippet of code but it triggers an error:
Error: Error: Cannot find module '#google-cloud/pubsub'
Below is the code added
//decoding worked
const PubSub = require('#google-cloud/pubsub');
// Your Google Cloud Platform project ID
const projectId = 'XXXXX';
// Instantiates a client
const pubsubClient = new PubSub({
projectId: projectId
});
// The name for the new topic
const topicName = 'XXXX';
// Creates the new topic
pubsubClient
.createTopic(topicName)
.then(results => {
const topic = results[0];
console.log(`Topic ${topic.name} created.`);
})
.catch(err => {
console.error('ERROR:', err);
});
If I get rid of the import of library, I get
Error: ReferenceError: PubSub is not defined
So - How can I publish in a topic from an HTTP triggered function in gcp?
You need to install the #google-cloud/pubsub library as dependency so that your Cloud Function can import it successfully. You can do it by running the following command locally:
npm install --save #google-cloud/pubsub
This will include this library in the package.json file you upload with your function code.
If you are writing your function from the Developer Console directly, you'll need to add the following to your package.json file:
"dependencies": {
"#google-cloud/pubsub": "^0.19.0"
}
How can we invoke multiple AWS Lambda functions one after the other ?For example if an AWS Lambda chain consists of 8 separate lambda functions and each simulate a 1 sec processing event and then invoke the next function in the chain.
I wouldn't recommend using direct invoke to launch your functions. Instead you should consider creating an SNS Topic and subscribing your Lambda functions to this topic. Once a message is published to your topic, all functions will fire at the same time. This solution is also easily scalable.
See more information at official documentation Invoking Lambda functions using Amazon SNS notifications
With python:
from boto3 import client as botoClient
import json
lambdas = botoClient("lambda")
def lambda_handler(event, context):
response1 = lambdas.invoke(FunctionName="myLambda1", InvocationType="RequestResponse", Payload=json.dumps(event));
response2 = lambdas.invoke(FunctionName="myLambda2", InvocationType="RequestResponse", Payload=json.dumps(event));
A simple way to do it is to use the AWS sdk to invoke the lambda function.
The solution would look different depending on what sdk you use. If using the Node sdk I would suggest promisifying the sdk with a Promise library like for example Bluebird.
The code would look something like:
const Promise = require('bluebird');
const AWS = require('aws-sdk');
const lambda = Promise.promisifyAll(new AWS.Lambda({ apiVersion: '2015-03-31' }));
lambda.invokeAsync({FunctionName: 'FirstLambdaFunction'})
.then(() => {
// handle successful response from first lambda
return lambda.invokeAsync({FunctionName: 'SecondLambdaFunction'});
})
.then(() => lambda.invokeAsync({FunctionName: 'ThirdLambdaFunction'}))
.catch(err => {
// Handle error response
);
The reason why I like this approach is that you own the context of all the lambdas and can decide to do whatever you like with the different responses.
Just call the next Lambda function at the end of each function?
Use http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Lambda_20141111.html#invokeAsync-property if you are using Node.js/JavaScript.