AWS Lambda NodeJS launch EMR Spark Problem - amazon-web-services

I am trying to launch an EMR cluster with the following script in Lambda (NodeJS-12).
var AWS = require('aws-sdk');
var emr = new AWS.EMR({apiVersion: '2009-03-31', region: 'ap-northeast-1'});
exports.handler = (event, context, callback) => {
var params = {
"Name": "LaunchEMR",
"Instances": {
"KeepJobFlowAliveWhenNoSteps": true,
"TerminationProtected": false,
"Ec2SubnetId": "subnet-XXXXXXXX",
"EmrManagedMasterSecurityGroup": "ElasticMapReduce-master",
"EmrManagedSlaveSecurityGroup": "ElasticMapReduce-slave",
"HadoopVersion": "2.8.5",
"InstanceGroups": [{
"Name": "Master",
"InstanceRole": "MASTER",
"InstanceCount": 1,
"InstanceType": "m3.xlarge",
"Market": "ON_DEMAND"
}, {
"Name": "Core",
"InstanceRole": "CORE",
"InstanceCount": 1,
"InstanceType": "m3.xlarge",
"Market": "ON_DEMAND"
}]
},
"Applications": [{
"Name": "Hadoop"
}, {
"Name": "Spark"
}],
"ServiceRole": "EMR_DefaultRole",
"JobFlowRole": "EMR_EC2_DefaultRole",
"ReleaseLabel": "emr-5.28.0"
};
emr.runJobFlow(params)
.on('success', function(response){ console.log("success => " + response); console.log(response); })
.on('error', function(response){ console.log("error => " + response); console.log(response); })
.on('complete', function(response){ console.log("complete => " + response); console.log(response); })
.send( function(err, data){
if (err) console.log(err, err.stack); // an error occurred
else console.log(data); // successful response
callback(null, {
statusCode: 200,
body: JSON.stringify(err),
});
});
};
I had tuned the IAM policies can I seem to be able to submit the request successfully to EMR, as there is no error in the Lambda cloud-watch log, and we can see the request in EMR events.
However, we will get a cluster termination event right after the request event.
Error Log
Amazon EMR Cluster j-31OE0F3OXGO4K (LaunchEMR) has terminated with errors at 2019-12-02 06:32 UTC with a reason of VALIDATION_ERROR.
The SubnetID and Security-Group are the same as my manually started clusters.
I added the "iam:PassRole" in "EMR_DefaultRole".
I also tried some other instance types.
Is there anything that I am missing?

I am able to successfully run it by taking these 3 lines off.
"Ec2SubnetId": "subnet-XXXXXXXX",
"EmrManagedMasterSecurityGroup": "ElasticMapReduce-master",
"EmrManagedSlaveSecurityGroup": "ElasticMapReduce-slave",
But I still have no idea why it works. (and why it doesn't)

Related

Publishing message from Lambda function to AWS IoT is not working with aws-sdk

I'm trying to publish a message from a lambda function to my AWS IoT Thing, by using the aws-sdk which should use HTTP by default instead of MQTT to publish a message.
Then the Function gets invoked no error message is logged to cloudwatch unfortunately which makes it even harder to debug.
Steps I took:
created a Policy for the message
created a small React frontend to read messages and test it with IoT test client.
wrote lambda and passed the Thing url as env var like this: "https://xxxxxdi.eu-central-1.amazonaws.com"
The Lambda function has also a attached Policy which should grant it to write to AWS IoT.
{
"arn": null,
"document": {
"Version": "2012-10-17",
"Statement": [
{
"Action": "iot:Publish",
"Resource": "arn:aws:iot:eu-central-1:*:*:topic/PairCreated",
"Effect": "Allow"
}
]
},
"id": null,
"name": "PublishMessage",
"type": "inline"
}
Here is the code of the lambda function. The credentials for the object of type IotData are looking ok
import wrap from '#dazn/lambda-powertools-pattern-basic'
import Log from '#dazn/lambda-powertools-logger'
import { IotData } from 'aws-sdk'
export const handler = wrap(async (event: any = {}, context: any) => {
Log.info('event', event)
const iotData = new IotData({endpoint: process.env.IOT_ENDPOINT, region: 'eu-central-1', logger: new Log})
const params: IotData.PublishRequest = {
topic: process.env.IOT_THING_NAME || '',
payload: JSON.stringify(event),
qos: 0,
retain: false
}
Log.debug('params', { params })
iotData.publish(params, (err, res) => {
if (err) {
Log.error("Error publishing to IOT", { err })
return context.fail(err)
};
Log.info('res', res)
return context.succeed();
});
})
The version I'm using: "aws-sdk": "^2.1053.0"
What am I missing?
I found out why the message was not sent.
The callback inside of iotData.publish was not executed correctly because the lambda function was terminated before it could be executed. Here are my changes to the code.
await iotData.publish(params, (err, res) => {
if (err) {
Log.error("Error publishing to IOT", { err })
return context.fail(err)
};
return context.succeed(res);
}).promise()
This returns a Promise we can await so the callback gets executed

API Gateway -> SQS HTTP POST MessageAttributes

I have an API gateway setup which sends to SQS which fires a Lambda, I am trying to pass message attributes to the SQS but when I hit the endpoint in postman I keep getting a 400 Bad Request.. what is the right way to send the attributes over a JSON POST body
here is body from postman (have tried a few options based on this link)
"message": "Message",
"MessageAttributes": {
"Name": "Name",
"Type": "String",
"Value": "my value"
}
}
Here is how API Gateway is configured
Incase someone stumbles on this later here is worked from the CDK side
let intergation = new apiGateway.CfnIntegration(this, 'Integration', {
apiId: props.httpApi.httpApiId,
payloadFormatVersion: '1.0',
integrationType: 'AWS_PROXY',
credentialsArn: apigwRole.roleArn,
integrationSubtype: 'SQS-SendMessage',
requestParameters: {
QueueUrl: sqsqueue.queueUrl,
MessageBody: '$request.body',
MessageAttributes: '$request.body.MessageAttributes'
}
})
new apiGateway.CfnRoute(this, 'Route', {
apiId: props.httpApi.httpApiId,
routeKey: apiGateway.HttpRouteKey.with('/url/foo', apiGateway.HttpMethod.POST).key,
target: `integrations/${intergation .ref}`
}).addDependsOn(intergation);
and the cloudformation
MessageBody: $request.body
MessageAttributes: $request.body.MessageAttribute
then in post man the POST body content type as application/json
{
"message": "Message",
"MessageAttributes": {
"Attributes": {
"DataType": "String",
"StringValue": "my value"
}
}
}
the lamba would log out both separate for each Record from the event body
Records: [
{
....
body: 'Message',
attributes: [Object],
messageAttributes: [Object]
}
]
}
the messageAttributes object from above:
{
Attributes: {
stringValue: 'my value',
stringListValues: [],
binaryListValues: [],
dataType: 'String'
}
}
This is using AWS API Gateway v2 HTTP API also

403 forbidden error when uploading to S3 bucket

I'm pretty new with AWS but im fairly certain I had my IAM user set up properly... are there any other permissions i need to add other than AmazonS3FullAccess? the name implies that it should be enough... either its a permissions issue or I messed up somewhere with my code.
I was trying to follow along with the guide at https://devcenter.heroku.com/articles/s3-upload-node. any help would be appreciated. :)
Here is my relevant code:
//server side code
router.get('/sign-s3', (req, res) => {
const s3 = new aws.S3();
const { fileName, fileType } = req.query;
s3.getSignedUrl('putObject', {
Bucket: S3BUCKET,
Key: fileName,
Expires: 60,
ContentType: fileType,
ACL: 'public-read'
}, (err, data) => {
if (err) {
console.log(err);
res.status(500).json(err)
}
res.json({
signedRequest: data,
url: `https://${S3BUCKET}.s3.amazonaws.com/${fileName}`
});
});
});
//client side code
const onChangeHandler = (e) => {
const file = e.target.files[0];
axios
.get(`/api/bucket/sign-s3?fileName=${file.name}&fileType=${file.type}`)
.then(signedResponse => {
axios
.put(signedResponse.data.signedRequest,file, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
.then(response => {
console.log("upload successful");
props.addImages([signedResponse.data.url]);
})
.catch(error => console.error(error));
})
.catch(error => console.error(error));
}
and a screenshot of my error:
UPDATE:
Removing the line ACL: 'public-read' from my sign route allows the upload to go through but then nobody can access the images. :P based on johns comments down below i assumed it was some kind of header issue so i added 'x-amz-acl': 'public-read' header to my put request on the client side but its still giving me the same issue of an invalid signature
I was receiving same error with an IAM user with "AmazonS3FullAccess". What worked for me was adding this CORS configuration
[
{
"AllowedHeaders": [
"*"
],
"AllowedMethods": [
"GET",
"PUT",
"POST"
],
"AllowedOrigins": [
"*"
],
"ExposeHeaders": []
}
]

Alexa Skill ARN - The remote endpoint could not be called, or the response it returned was invalid

I've created a simple Lambda function to call a webpage, this works fine when I test it from the functions page however when trying to create a skill to call this function I end up with a "The remote endpoint could not be called, or the response it returned was invalid." error.
Lambda Function
var http = require('http');
exports.handler = function(event, context) {
console.log('start request to ' + event.url)
http.get(event.url, function(res) {
console.log("Got response: " + res.statusCode);
context.succeed();
}).on('error', function(e) {
console.log("Got error: " + e.message);
context.done(null, 'FAILURE');
});
console.log('end request to ' + event.url);
}
The Test Event code looks like this:
{
"url": "http://mywebsite.co.uk"
}
and I've added a trigger for the "Alexa Skills Kit".
The ARN for this function is showing as:
arn:aws:lambda:us-east-1:052516835015:function:CustomFunction
Alexa Skill (Developer Portal)
I've then created a skill with a simple Intent:
{
"intents": [
{
"intent": "CustomFunction"
}
]
}
and created an Utterance as:
CustomFunction execute my custom function
In the Configuration section for my skill I have selected the "AWS Lambda ARN (Amazon Resource Name)" option and entered the ARN into the box for North America.
In the Test -> Service Simulator section, I've added "execute my custom function" as the Text and this changes the Lambda Request to show:
{
"session": {
"sessionId": "SessionId.a3e8aee0-acae-4de5-85df-XXXXXXXXX",
"application": {
"applicationId": "amzn1.ask.skill.XXXXXXXXX"
},
"attributes": {},
"user": {
"userId": "amzn1.ask.account.XXXXXXXXX"
},
"new": true
},
"request": {
"type": "IntentRequest",
"requestId": "EdwRequestId.445267bd-2b4a-45ef-8566-XXXXXXXXX",
"locale": "en-GB",
"timestamp": "2016-11-27T22:54:07Z",
"intent": {
"name": "RunWOL",
"slots": {}
}
},
"version": "1.0"
}
but when I run the test I get the following error:
The remote endpoint could not be called, or the response it returned was invalid.
Does anyone have any ideas on why the skill can't connect to the function?
Thanks
The Service Simulator built into the Amazon Alexa Developer Console has known issues. Try copying the JSON generated by the Simulator and pasting it into your lambda function's test event. To access lambda's test events first find the blue 'Test' button. Next to that button select the (Actions Drop down menu) -> (Configure Test Event) -> Paste the provided JSON into the code area -> (Save and Test). Lambda's built in testing features are much more reliable than Alexa's.
If this does not solve the problem lambda's testing event returns a complete stackTrace and error codes. It becomes much easier to trouble shoot when every error isn't "The remote endpoint could not be called, or the response it returned was invalid."
{
"session": {
"sessionId": "SessionId.a3e8aee0-acae-4de5-85df-XXXXXXXXX",
"application": {
"applicationId": "amzn1.ask.skill.XXXXXXXXX"
},
"attributes": {},
"user": {
"userId": "amzn1.ask.account.XXXXXXXXX"
},
"new": true
},
"request": {
"type": "IntentRequest",
"requestId": "EdwRequestId.445267bd-2b4a-45ef-8566-XXXXXXXXX",
"locale": "en-GB",
"timestamp": "2016-11-27T22:54:07Z",
"intent": {
"name": "RunWOL",
"slots": {}
}
},
"version": "1.0"
}
​While uploading .zip, do not compress the folder into .zip.
Instead, go into the folder, select package.json, index.js and node modules & then compress them and then upload the .zip.
This error message is very broad and may imply a lot of different issues. I was getting this error and in my case it was a timeout issue. How long does that website you are pinging taking to respond? The timeout doesn't seem to be properly documented, see my original question here: Troubleshooting Amazon's Alexa Skill Kit (ASK) Lambda interaction

How to get the name of the stage in an AWS Lambda function linked to API Gateway

I have the following Lambda function configured in AWS Lambda :
var AWS = require('aws-sdk');
var DOC = require('dynamodb-doc');
var dynamo = new DOC.DynamoDB();
exports.handler = function(event, context) {
var item = { id: 123,
foo: "bar"};
var cb = function(err, data) {
if(err) {
console.log(err);
context.fail('unable to update hit at this time' + err);
} else {
console.log(data);
context.done(null, data);
}
};
// This doesn't work. How do I get current stage ?
tableName = 'my_dynamo_table_' + stage;
dynamo.putItem({TableName:tableName, Item:item}, cb);
};
Everything works as expected (I insert an item in DynamoDB every time I call it).
I would like the dynamo table name to depend on the stage in which the lambda is deployed.
My table would be:
my_dynamo_table_staging for stage staging
my_dynamo_table_prod for stage prod
However, how do I get the name of the current stage inside the lambda ?
Edit: My Lambda is invoked by HTTP via an endpoint defined with API Gateway
If you have checked "Lambda Proxy Integration" in your Method Integration Request on API Gateway, you should receive the stage from API Gateway, as well as any stageVariable you have configured.
Here's an example of an event object from a Lambda function invoked by API Gateway configured with "Lambda Proxy Integration":
{
"resource": "/resourceName",
"path": "/resourceName",
"httpMethod": "POST",
"headers": {
"header1": "value1",
"header2": "value2"
},
"queryStringParameters": null,
"pathParameters": null,
"stageVariables": null,
"requestContext": {
"accountId": "123",
"resourceId": "abc",
"stage": "dev",
"requestId": "456",
"identity": {
"cognitoIdentityPoolId": null,
"accountId": null,
"cognitoIdentityId": null,
"caller": null,
"apiKey": null,
"sourceIp": "1.1.1.1",
"accessKey": null,
"cognitoAuthenticationType": null,
"cognitoAuthenticationProvider": null,
"userArn": null,
"userAgent": "agent",
"user": null
},
"resourcePath": "/resourceName",
"httpMethod": "POST",
"apiId": "abc123"
},
"body": "body here",
"isBase64Encoded": false
}
I managed it after much fiddling. Here is a walkthrough:
I assume that you have API Gateway and Lambda configured. If not, here's a good guide. You need part-1 and part-2. You can skip the end of part-2 by clicking the newly introduced button "Enable CORS" in API Gateway
Go to API Gateway.
Click here:
Click here:
Then expand Body Mapping Templates, enter application/json as content type, click the add button, then select mapping template, click edit
And paste the following content in "Mapping Template":
{
"body" : $input.json('$'),
"headers": {
#foreach($param in $input.params().header.keySet())
"$param": "$util.escapeJavaScript($input.params().header.get($param))" #if($foreach.hasNext),#end
#end
},
"stage" : "$context.stage"
}
Then click the button "Deploy API" (this is important for changes in API Gateway to take effect)
You can test by changing the Lambda function to this:
var AWS = require('aws-sdk');
var DOC = require('dynamodb-doc');
var dynamo = new DOC.DynamoDB();
exports.handler = function(event, context) {
var currentStage = event['stage'];
if (true || !currentStage) { // Used for debugging
context.fail('Cannot find currentStage.' + ' stage is:'+currentStage);
return;
}
// ...
}
Then call your endpoint. You should have a HTTP 200 response, with the following response body:
{"errorMessage":"Cannot find currentStage. stage is:development"}
Important note:
If you have a Body Mapping Template that is too simple, like this: {"stage" : "$context.stage"}, this will override the params in the request. That's why body and headers keys are present in the Body Mapping Template. If they are not, your Lambda has not access to it.
For those who use the serverless framework it's already implemented and they can access to event.stage without any additional configurations.
See this issue for more information.
You can get it from event variable. I logged my event object and got this.
{ ...
"resource": "/test"
"stageVariables": {
"Alias": "beta"
}
}