9
votes

I'm looking for the ability to programmatically schedule a lambda function to run a single time with another lambda function. For example, I made a request to myFirstFunction with date and time parameters, and then at that date and time, have mySecondFunction execute. Is that possible only with stateless AWS services? I'm trying to avoid an always-on ec2 instance.

Most of the results I'm finding for scheduling a lambda functions have to do with cloudwatch and regularly scheduled events, not ad-hoc events.

5

5 Answers

3
votes

This is a perfect use case for aws step functions.

Use Wait state with SecondsPath or TimestampPath to add the required delay before executing the Next State.

0
votes

What you're tring to do (schedule Lambda from Lambda) it's not possible with the current AWS services.

So, in order to avoid an always-on ec2 instance, there are other options:

1) Use AWS default or custom metrics. You can use, for example, ApproximateNumberOfMessagesVisible or CPUUtilization (if your app fires a big CPU utilization when process a request). You can also create a custom metric and fire it when your instance is idle (depending on the app that's running in your instance).

The problem with this option is that you'll waste already paid minutes (AWS always charge a full-hour, no matter if you used your instance for 15 minutes).

2) A better option, in my opinion, would be to run a Lambda function once per minute to check if your instances are idle and shut them down only if they are close to the full hour.

import boto3
from datetime import datetime

def lambda_handler(event, context):
    print('ManageInstances function executed.')
    environments = [['instance-id-1', 'SQS-queue-url-1'], ['instance-id-2', 'SQS-queue-url-2'], ...]
    ec2_client = boto3.client('ec2')
    for environment in environments:
        instance_id = environment[0]
        queue_url = environment[1]
        print 'Instance:', instance_id
        print 'Queue:', queue_url
        rsp = ec2_client.describe_instances(InstanceIds=[instance_id])
        if rsp:
            status = rsp['Reservations'][0]['Instances'][0]
            if status['State']['Name'] == 'running':
                current_time = datetime.now()
                diff = current_time - status['LaunchTime'].replace(tzinfo=None)
                total_minutes = divmod(diff.total_seconds(), 60)[0]
                minutes_to_complete_hour = 60 - divmod(total_minutes, 60)[1]
                print 'Started time:', status['LaunchTime']
                print 'Current time:', str(current_time)
                print 'Minutes passed:', total_minutes
                print 'Minutes to reach a full hour:', minutes_to_complete_hour
                if(minutes_to_complete_hour <= 2):
                    sqs_client = boto3.client('sqs')
                    response = sqs_client.get_queue_attributes(QueueUrl=queue_url, AttributeNames=['All'])
                    messages_in_flight = int(response['Attributes']['ApproximateNumberOfMessagesNotVisible'])
                    messages_available = int(response['Attributes']['ApproximateNumberOfMessages'])
                    print 'Messages in flight:', messages_in_flight
                    print 'Messages available:', messages_available
                    if(messages_in_flight + messages_available == 0):
                        ec2_resource = boto3.resource('ec2')
                        instance = ec2_resource.Instance(instance_id)
                        instance.stop()
                        print('Stopping instance.')
            else:
                print('Status was not running. Nothing is done.')
        else:
            print('Problem while describing instance.')
0
votes

UPDATE - I wouldn't recommend using this approach. Things changed in when TTL deletions happen and they are not close to TTL time. The only guarantee is that the item will be deleted after the TTL. Thanks @Mentor for highlighting this.

2 months ago AWS announced DynamoDB item TTL, which allows you to insert an item and mark when you wish for it to be deleted. It will be deleted automatically when the time comes.

You can use this feature in conjunction with DynamoDB Streams to achieve your goal - your first function inserts an item to a DynamoDB table. The record TTL should be when you want the second lambda triggered. Setup a stream that triggers your second lambda. In this lambda you will identify deletion events and if that's a delete then run your logic.

Bonus point - you can use the table item as a mechanism for the first lambda to pass parameters to the second lambda.

About DynamoDB TTL: https://aws.amazon.com/blogs/aws/new-manage-dynamodb-items-using-time-to-live-ttl/

0
votes

It does depend on your use case, but the idea that you want to trigger something at a later date is a common pattern. The way I do it serverless is I have a react application that triggers an action to store a date in the future. I take the date format like 24-12-2020 and then convert it using date(), having researched that the date format mentioned is correct, so I might try 12-24-2020 and see what I get(!). When I am happy I convert it to a Unix number in javascript React I use this code:

new Date(action.data).getTime() / 1000

where action.data is the date and maybe the time for the action.

I run React in Amplify (serverless), I store that to dynamodb (serverless). I then run a Lambda function (serverless) to check my dynamodb for any dates (I actually use the Unix time for now) and compare the two Unix dates now and then (stored) which are both numbers, so the comparison is easy. This seems to me to be super easy and very reliable.

I just set the crontab on the Lambda to whatever is needed depending on the approximate frequency required, in most cases running a lambda every five minutes is pretty good, although if I was only operating this in a certain time zone for a business weekday application I would control the Lambda a little more. Lambda is free for the first 1m functions per month and running it every few minutes will cost nothing. Obviously things change, so you will need to look that up in your area.

You will never get perfect timing in this scenario. It will, however, for the vast majority of use cases be close enough according to the timing settings of the Lambda function, you could set it up to check every minute or just once per day, it all depends on your application.

Alternatively, If I wanted an instant reaction to an event I might use SMS, SQS, or Kinesis to instantly stream a message, it all depends on your use case.

-1
votes

I'd opt for enqueuing deferred work to SQS using message timers in myFirstFunction.

Currently, you can't use SQS as a Lambda event source, but you can either periodically schedule mySecondFunction to check the queue via scheduled CloudWatch Events (somewhat of a variant of the other options you've found) or use a CloudWatch alarm on the ApproximateNumberOfMessagesVisible to fire an SNS message to a Lambda and avoid constant polling for queues that are frequently inactive for long periods.