Woodsman
Woodsman

Reputation: 1179

AWS Lambda Timeout trying to connect to AWS Serverless Redis Cache

I'm trying getting a socket connect error trying to reach my AWS Serverless Redis Cache. I defined both the lambda and the redis cache in the same VPC and the same subnets. This code worked when I did the AWS self managed Redis offering. Something seems different about the serverless for me.

Below is my Lambda code, but I believe this is much more related to AWS not allowing the connection from the lambda to the Redis cluster. What things might be blocking this?

My lambda Terraform:

resource "aws_lambda_function" "lambda" {
  function_name = "rfl-${var.deployment_environment}-lambda"
  role          = aws_iam_role.my_role.arn
  timeout = 180

  handler = "lambda/preload_cache.handler"
  runtime = "nodejs18.x"

  filename         = "lambdas/target/lambda.zip"
  source_code_hash = filebase64sha256("lambdas/target/preload-cache-lambda.zip")
  environment {
    variables = {
      MY_TOPIC_ARN = aws_sns_topic.messages_topic.arn
      CACHE_URL                = "redis://${awscc_elasticache_serverless_cache.redis_cluster.endpoint.address}:${awscc_elasticache_serverless_cache.redis_cluster.endpoint.port}"
      LOG_LEVEL = var.log_level
    }
  }
  vpc_config {
    subnet_ids = var.metadata_subnet_ids
    security_group_ids = [aws_security_group.gather_barcode_data_security_group.id]
  }

  depends_on = [aws_sns_topic.messages_topic, awscc_elasticache_serverless_cache.redis_cluster,aws_iam_role.my_role]

  publish = true
}

resource "awscc_elasticache_serverless_cache" "redis_cluster" {
  serverless_cache_name = "rfl-${var.deployment_environment}-my-elasticache-serverless-cache"
  description           = "ElastiCache Redis Serverless"
  engine                = "redis"
  major_engine_version  = "7"

  security_group_ids    = [aws_security_group.redis_security_group.id]
  subnet_ids            = var.metadata_subnet_ids
  depends_on            = [aws_security_group.redis_security_group]
}

An abbreviated version of my lambda:

import { SNS } from 'aws-sdk';
import { APIGatewayProxyHandler, APIGatewayProxyEvent } from 'aws-lambda';
import { loadMetaDataPanels, openCache, closeCache } from './metadata';
import { RedisClientType } from 'redis';

import fs from 'fs';
import path from 'path';
import { request } from 'http';
import { match } from 'assert';
import { endianness } from 'os';

// Function to read JSON file and parse it to an object
export function readJsonFile(fileName: string): any {
    try {
        const filePath = path.join(__dirname, fileName);
        const fileContents = fs.readFileSync(filePath, 'utf8');
        return JSON.parse(fileContents);
    } catch (err) {
        console.error('Error reading file:', err);
        return null;
    }
}
let redisClient : RedisClientType | null = null;

export const handler: APIGatewayProxyHandler = async (event: APIGatewayProxyEvent) => {
    let cacheUrl = process.env.CACHE_URL;
    let metaDataConnection = await openCache(cacheUrl as string);
    await loadMetaDataPanels(metaDataConnection, readJsonFile('./mydata.json'));

    await closeCache(metaDataConnection);
    return {
        statusCode: 200,
        body: JSON.stringify({ error: 'No data provided or data is empty' })
    };

};

export async function openRedisConnection(url : string): Promise<RedisClientType> {
    return new Promise<RedisClientType>(async (resolve,reject)=>{
        try {
            let redisClient : RedisClientType = createClient({
                url: url
            });
            await redisClient.connect();
            resolve(redisClient);
    
        } catch(error) {
            reject(error);
        }
    });
}

type RedisReturnFormat = {
    "data": string;
};

export async function openCache(url : string) : Promise<MetaDataConnection> {
    return {
        connection : await openRedisConnection(url),
        url: url
    }
}

export async function loadMetaDataPanels(metaDataConnection : MetaDataConnection, metaDataPanelsIn: MetaDataPanels): Promise<MetaDataConnection> {
    try {
        const multi = (metaDataConnection.connection as RedisClientType).multi();
    
        for (const [panelCode, metaDataPanel] of Object.entries(metaDataPanelsIn)) {
            let metaDataPanelString : string = JSON.stringify(metaDataPanel);
            multi.hSet(panelCode as string, 'data', metaDataPanelString); // Storing each MetaDataPanelEntryType as a hash
        }
        let redisCommandRawReply  = await multi.exec();
        return metaDataConnection;
    
    } catch(error) {
        console.log('loadMetaDataPanels: Redis error: '+error);
        throw error;
    };
}

export async function getMetaDataPanels(metaDataConnection : MetaDataConnection, panelCodes: string[]) : Promise<MetaDataPanels> {
    return new Promise<MetaDataPanels >((resolve,reject)=>{
        /* Build luascript and insert empty JSON if key is not found */
        const luaScript : string = `
        local results = {}
        for i, key in ipairs(KEYS) do
            local value = redis.call('HGET', key, 'data')
            if value then
                table.insert(results, value)
            else
                table.insert(results, '{}')
            end
        end
        return results
        `;
        try {
            const result : MetaDataPanels = {};
            (async () =>{
                const evalOptions  = {
                    keys: panelCodes,
                    arguments: []
                };
                (async()=>{
                    let value = await (metaDataConnection.connection as RedisClientType).get('1113');
                });
                const metaDataPanelsArray : string[]  = await (metaDataConnection.connection as RedisClientType).eval(luaScript,evalOptions ) as string[];
                metaDataPanelsArray.forEach((json:string,index:number)=>{
                    const panelCode = panelCodes[index];
                    const redisValueReturned : RedisReturnFormat = JSON.parse(json);
                    result[panelCode] = JSON.parse(json);
                });
                resolve(result);
    
            })();
        } catch (error) { 
            reject(error);
        }
    
    })
}

export async function closeCache(metaDataConnection : MetaDataConnection) {
    console.log('Attempting to close cache');
    if(metaDataConnection != null && metaDataConnection.connection != null) {
        console.log('Attempting to close valid cache');
        return (metaDataConnection.connection as RedisClientType).quit();
    }
}

export async function getMetaDataPanelEntryType(metaDataConnection : MetaDataConnection, panelCode: string): Promise<MetaDataPanelEntryType | null> {
    try {
        const metaData = await (metaDataConnection.connection as RedisClientType).hGetAll(panelCode);

        if (Object.keys(metaData).length === 0) {
            return null;
        }

        return metaData as unknown as MetaDataPanelEntryType;
    } catch (error) {
        console.error('Error fetching MetaDataPanelEntryType from Redis:', error);
        return null;
    };
}

Upvotes: 1

Views: 801

Answers (1)

Woodsman
Woodsman

Reputation: 1179

If this helps anyone else, the problem was tls needed to be specified. I changed the createClient to something like:

let redisClient : RedisClientType = createClient({
                socket: {hostname: 'myhost', port: 6379, tls: true}
                });

Upvotes: 1

Related Questions