0

Currently I'm working on a database migration. We use cloudformation to handle our resources and we have some lambda functions which create direct connections to our current database. We use secrets manager to handle the database credentials (username, password, endpoint/host, port, etc...).

What we want to have done is that when I modify the, let's say, the endpoint/host on the secrets, the connection on all the lambda functions we have which make a direct connection to the database would be updated.

I have read this question and its answers and I have tried to force a cold-start using a script which executes the aws lambda update-function-configuration command for the lambdas that I need to refresh their runtime.

The issue with this approach is that it seems to not be enough to completely refresh the lambda runtime because the database connection is still behaving as before making changes on the values stored on the secrets.

We cannot afford the time to make a full deployment of the stacks responsible for the lambdas that we need to "restart".

I'm not sure if the UpdateFunctionCode API endpoint will be useful to me since some of our lamdbas use are image based and others are ZipFile based using a runtime.

halfer
  • 19,824
  • 17
  • 99
  • 186
KenshinApa
  • 13
  • 1
  • 5
  • Save your connection details into env var. Like if it a secrets manager secret to fetch the connection detail, put it into lambda env var – Sándor Bakos May 10 '22 at 17:10
  • By what mechanism specifically are you retrieving your secrets inside your lambda function? Which SDK functions are you calling? – brads3290 May 10 '22 at 17:51
  • @brads3290 I'm using a boto3 client to retrieve the secrets values – KenshinApa May 10 '22 at 18:37
  • @KenshinApa yeah, specifically which functions are you calling? – brads3290 May 10 '22 at 18:38
  • @brads3290 this would be the whole code to get the secrets `client(service_name="secretsmanager", region_name=REGION).get_secret_value(SecretId=SECRET_NAME)` – KenshinApa May 10 '22 at 18:46
  • @KenshinApa hmm.. are you caching that secret in your application code in any way? According to the docs, boto3 shouldn’t be caching it so you should be getting the most up to date secret every time, without needing to restart the lambda – brads3290 May 10 '22 at 18:51
  • @brads3290 I see your point. The thing is that the secret is being retrieved only once at the beginning of the lambda and saved to a constant which stays alive on the lambda runtime while it is still being used – KenshinApa May 10 '22 at 20:25
  • @KenshinApa I think that is your problem. Rather than trying to find a hacky way to force the lambda to restart, just fetch the credentials as needed. If you need to cache, AWS provides a utility for this and allows you to configure cache time, so you could set it to a small timeframe (e.g. 10 seconds), and within 10 seconds of updating your secret, all your lambdas will stop using the old secret. https://docs.aws.amazon.com/secretsmanager/latest/userguide/retrieving-secrets_cache-python.html – brads3290 May 10 '22 at 20:42
  • @brads3290 Thank you so much for the suggestions. However, your approach would not work for my lamdbas since they are constantly being used and there is almost no sleep time for them at all. They are continuosly being called and so they are kept awake – KenshinApa May 10 '22 at 20:48

1 Answers1

0

Reading this helped me solve my issue. Since updating an environment variable forces the lambda runtime to be restarted, I can just retrieve the existing environment variables and add a new one to restart a given lambda's runtime.

I made a little script to cold-start the lambda functions listed on an array:

NOTE: I'm not the best at bash scripting; so, this could be optimized. Also, I did not have time to learn to handle json objects on bash; that's why I used a second script (python) for that.

#!/bin/bash

AWS_PROFILE="dev";
ENV="dev";

echo "Cold starting only lambdas from array";

declare -a LIST_OF_LAMBDAS=( \
    "lambda-name1-$ENV" \
    "lambda-name2-$ENV" \
    "lambda-name3-$ENV"
);

for lambda in ${LIST_OF_LAMBDAS[@]}; do
    echo "Cold-starting lambda: $lambda";
    aws lambda get-function-configuration \
        --function-name $lambda \
        --query '{revisionId: RevisionId, env: Environment}' > original_env.json;

    REVISION_ID=`cat original_env.json | grep revisionId | \
        sed 's/"revisionId": //g;s/ //g;s/://g;s/"//g;s/,//g'`;

    python add_new_env_var.py; # Uses original_env.json to creates a file, called updated_env.json, which contains the updated env. vars.

    aws lambda update-function-configuration \
        --function-name "$lambda" \
        --environment file://updated_env.json \
        --description "Restarting/cold-starting lambda..." \
        --revision-id "$REVISION_ID" \
        --profile $AWS_PROFILE > /dev/null;

    if [ $? -eq 0 ]; then
        echo "OK";
    else
        echo "FAIL";
        echo $lambda >> "lambdas_failed_to_cold_start.txt"
    fi
    rm original_env.json updated_env.json;
    printf "\n";
done

echo "Script finished - DONE"

The python script is pretty simple:

import json
import time


def add_new_env_var(env_dict):
    """This function takes a dictionary which should have the structure of the output produced by
    the aws cli command:
    
    aws lambda get-function-configuration \
        --function-name $lambda \ -> $lambda is a valid name for a lambda function on the given env.
        --query '{revisionId: RevisionId, env: Environment}'
    Args:
        env_dict (dict): Contains the environment variables which already existed on a given lambda.
    Returns:
        dict: Same dict as received, but with one more env. variable which stores the current
        timestamp
    """    
    current_timestamp = str(time.time())

    try:
        env_dict["env"]["Variables"]["COLD_START_TS"] = current_timestamp
    except TypeError:
        env_dict = {"env":{"Variables":{"COLD_START_TS": current_timestamp}}}

    return env_dict


def start_processing():
    original_env_dict = {}

    with open("original_env.json") as original_env_file:
        original_env_dict = json.load(original_env_file)

    updated_env_dict = add_new_env_var(original_env_dict)

    with open("updated_env.json", "w") as outfile:
        json.dump(updated_env_dict["env"], outfile)


if __name__ == "__main__":
    start_processing()

I know that this is kinda hacky, but I wanted to share since it may be useful for someone.

KenshinApa
  • 13
  • 1
  • 5
  • FYI, another way to force a lambda to restart (which may simplify your script, not sure) is to increase the function memory. So you could potentially just `describe-function`, then `update-function` with memory + 1Mb. Obviously you'll reach a point where you can't do that anymore though.. – brads3290 May 10 '22 at 20:53
  • Thank you for the suggestion @brads3290 I'll take that into account – KenshinApa May 11 '22 at 00:11
  • You could also update the memory twice, once to increase it, then immediately a second time, to set it back to what it should be. This way the lambda remains in a consistent state with your config under source control – Brad Oct 13 '22 at 21:47