How to make an external api call within an AWS Lambda function

Koop4 picture Koop4 · Apr 30, 2018 · Viewed 25.9k times · Source

I'm having problem with a Lambda function which for some reason it goes on timeout.

I tried to add some console.log to understand what was going on and everything works properly until the external http request.

I use the npm module request

I can see the log before the request, but the one inside the callback never shows up, like if the endpoint doesn't respond.

Am I missing something? Thanks in advance!

var mysql = require('mysql'); 
var request = require('request');
var config = require('./config.json'); 

var pool = mysql.createPool({
  host: config.dbhost,
  user: config.dbuser,
  password: config.dbpassword,
  database: config.dbname
});

var openWeatherOptions = (device) => {
    let domain = 'https://api.openweathermap.org/data/2.5/';
    let apiEndpoint = 'weather?';
    let params = `lat=${device.lat}&lon=${device.lng}&APPID=${process.env.WEATHER_APP_ID}&units=metric`;
    let url = [domain, apiEndpoint, params].join('').toString();
    return {
      url: url,
      method: 'GET',
      headers: { 'Content-Type': 'application/json'},
    };
};

exports.handler = (event, context, callback) => {
    context.callbackWaitsForEmptyEventLoop = false;
    // Connect to DB
    console.log('fallback connecting to DB')
    pool.getConnection( (errOnConnection, connection) => {
        if(errOnConnection) {
            callback({"err": errOnConnection });
        }
        console.log('fallback connected to DB')
        // Retrieve device position
        let queryOne = 'SELECT lat, lng FROM T03_DevicesPosition WHERE deviceCode = (?);';
        console.log('fallback retrieving device position')
        connection.query( queryOne, [event.device], (errOnQ1, results, fields) => { 
            if(errOnQ1) {
                connection.release();
                callback({"err": errOnQ1 });
            }
            console.log('fallback device position retrieved')

            // Call openweather 
            let device = results[0];
            let options = openWeatherOptions(device);
            console.log('fallback calling openWeather with following data: ', device, options);

            request( options, (errOpenWeather, response, body) => {
                console.log('fallback openweather response received');
                if(errOpenWeather || (response.statusCode !== 200 && response.statusCode !== 201) ) {
                    connection.release();
                    callback({"err": errOpenWeather });
                }
                let meteo = JSON.parse(body).main;
                meteo.date = new Date(event.time*1000);
                meteo.pressure = Math.floor( meteo.pressure );
                console.log('fallback storing data', meteo);
                let query = `INSERT INTO T02_DevicesTransmissions (deviceCode, transmissionDate, temperature, humidity, pressure, unixDate, rawData) VALUES ( ?, ?, ?, ?, ?, ?, ?);`;
                let queryValues = [ event.device, meteo.date, meteo.temp, meteo.humidity, meteo.pressure, event.time, 'fallback']; 
                connection.query( query, queryValues, (errInsert, results, fields) => { 
                    connection.release();
                    console.log('fallback completed with', errInsert ? '' : 'out');
                    if (errInsert) callback({"err": errInsert });
                    else callback();
                });
            });

        });
    });
}

Answer

Jules Olléon picture Jules Olléon · Apr 17, 2020

This is an old question but I spent a few hours banging my head figuring out how to set this up properly, so here's hoping someone else's time will be saved :)

If your lambdas are in a VPC they need to go through a NAT Gateway in order to reach external services.

One way to achieve this is:

  • configure your lambda to use a (or multiple) specific subnets (the Lambda console will suggest you associate at least 2 subnets in different availability zones to your lambda to ensure availability)
  • create a NAT Gateway in a different subnet
  • have the route table associated with your lambda's subnets send all outbound traffic (0.0.0.0/0) to the NAT Gateway you created (you do that by choosing the NAT in the target field)
  • have the route table in the NAT's subnet send all outbound traffic to an Internet Gateway

This will route traffic from your lambdas properly through the NAT and Internet Gateway so they can reach external services.

Note that if you're also using RDS from your lambdas AND you intend to also connect to RDS from outside (e.g. to manage the DB from your local machine), you can't put the RDS instance in the lambdas' subnet, or you won't be able to connect to it from outside (the NAT is outbound only). In that case you should make sure your RDS instance is associated with a subnet that is accessible, for instance the one the NAT is in (i.e. the one that sends outbound traffic to the Internet Gateway).