DISCLAIMER: Image is generated using ChatGPT
.
1. Introduction
2. Setup LocalStack
3. Lambda Function
4. Using aws
5. Using Python
6. Using Perl
Introduction
This post comes nearly a month and a half after the last one. The previous post on an AWS
related topic was about AWS DynamoDB. The break wasn’t intentional, though.
AWS Lambda
is a serverless compute service that allows you to run code without having to manage servers. You simply upload your code and Lambda
takes care of the scaling, patching and execution, charging you only for the duration your code is executed.
It’s like hiring a chef who only cooks when you request it, cleans up afterward and charges you only for the minutes spent cooking.
Setup LocalStack
Here is the docker compose configuration: docker-compose.yml
version: '3.8'
services:
localstack:
image: localstack/localstack
container_name: localstack
ports:
- "4566:4566"
- "4510-4559:4510-4559"
environment:
- PERSISTENCE=1
- SERVICES=s3,ec2,lambda,sts,iam,apigateway,logs
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- localstack_localstack_data:/var/lib/localstack
- /tmp/localstack_host:/host_tmp
volumes:
localstack_localstack_data:
Start the container like below:
$ docker-compose up -d
Lambda Function
For the purpose of this post, let’s create basic Lambda
function in Python
.
File: lambda_function.py
def handler(event, context):
return {
"statusCode": 200,
"body": "Hello from Lambda!",
"headers": {"Content-Type": "text/plain"}
}
AWS Lambda
requires code packages to be uploaded in a compressed archive format, typically a .zip
file.
(myenv) $ zip lambda_function.zip lambda_function.py
Using aws
Here is the aws
configuration:
(myenv) $ aws configure list
Name Value Type Location
---- ----- ---- --------
profile <not set> None None
access_key ****************VW55 env
secret_key ****************+ljT env
region eu-west-1 config-file ~/.aws/config
Let’s create AWS Lambda
function as below:
(myenv) $ aws lambda create-function \
--function-name my-local-lambda \
--runtime python3.9 \
--handler lambda_function.handler \
--zip-file fileb://lambda_function.zip \
--role arn:aws:iam::000000000000:role/lambda-role
You should see detailed response as below:
{
"FunctionName": "my-local-lambda",
"FunctionArn": "arn:aws:lambda:eu-west-1:000000000000:function:my-local-lambda",
"Runtime": "python3.9",
"Role": "arn:aws:iam::000000000000:role/lambda-role",
"Handler": "lambda_function.handler",
"CodeSize": 309,
"Description": "",
"Timeout": 3,
"MemorySize": 128,
"LastModified": "2025-08-05T21:35:58.309347+0000",
"CodeSha256": "KjcYzGqZ2FlY/x0/mU4Sq5bk2kZ3mcUQp9Mq69D/3ms=",
"Version": "$LATEST",
"TracingConfig": {
"Mode": "PassThrough"
},
"RevisionId": "4352d333-bceb-49cd-84e0-a9cf6cd7f421",
"State": "Pending",
"StateReason": "The function is being created.",
"StateReasonCode": "Creating",
"PackageType": "Zip",
"Architectures": [
"x86_64"
],
...
...
...
}
The most important bit in the response is "State": "Pending"
.
An AWS Lambda
function typically becomes active within a few seconds after creation, often under 10 seconds
, but it depends on how you’re deploying it and what it’s doing.
We check the status like below:
(myenv) $ aws lambda get-function --function-name my-local-lambda
{
"Configuration": {
"FunctionName": "my-local-lambda",
"FunctionArn": "arn:aws:lambda:eu-west-1:000000000000:function:my-local-lambda",
"Runtime": "python3.9",
"Role": "arn:aws:iam::000000000000:role/lambda-role",
"Handler": "lambda_function.handler",
"CodeSize": 309,
"Description": "",
"Timeout": 3,
"MemorySize": 128,
"LastModified": "2025-08-05T21:35:58.309347+0000",
"CodeSha256": "KjcYzGqZ2FlY/x0/mU4Sq5bk2kZ3mcUQp9Mq69D/3ms=",
"Version": "$LATEST",
"TracingConfig": {
"Mode": "PassThrough"
},
"RevisionId": "3081467c-a0ea-48a4-89fd-70dad2b85be2",
"State": "Active",
"LastUpdateStatus": "Successful",
"PackageType": "Zip",
"Architectures": [
"x86_64"
],
...
...
...
}
The function is now in active state as confirmed above.
Time to invoke the function.
(myenv) $ aws lambda invoke --function-name my-local-lambda output.json
{
"StatusCode": 200,
"ExecutedVersion": "$LATEST"
}
Let’s check the output.json
.
(myenv) $ cat output.json
{"statusCode": 200, "body": "Hello from Lambda!", "headers": {"Content-Type": "text/plain"}}
Using Python
Let’s repeat the process using Python
.
Here we will provide the script, lambda_function.py
as input and we would zip it first, followed by deploy and invoke.
The following function to zip lambda file.
def zip_lambda_file(lambda_script):
if not os.path.exists(lambda_script):
raise FileNotFoundError(f"Lambda script '{lambda_script}' not found")
zip_filename = f"{os.path.splitext(lambda_script)[0]}.zip"
with zipfile.ZipFile(zip_filename, 'w') as zipf:
zipf.write(lambda_script, os.path.basename(lambda_script))
return zip_filename
Now the function to deploy lambda function. Please note, it deletes the function first, if already deployed then deploy it fresh.
def deploy_lambda_function(lambda_client, lambda_script, function_name, handler_name, runtime='python3.9'):
zip_filename = zip_lambda_file(lambda_script)
try:
lambda_client.delete_function(FunctionName=function_name)
print(f"Deleted existing function: {function_name}")
except ClientError as e:
if e.response['Error']['Code'] != 'ResourceNotFoundException':
raise
with open(zip_filename, 'rb') as f:
try:
response = lambda_client.create_function(
FunctionName=function_name,
Runtime=runtime,
Handler=f"{os.path.splitext(lambda_script)[0]}.{handler_name}",
Role='arn:aws:iam::000000000000:role/lambda-role',
Code={'ZipFile': f.read()}
)
print(f"Successfully created function: {function_name}")
return response
except ClientError as e:
print(f"Failed to create function: {e}")
raise
To handle the function state, here is the function to wait until it become active.
def wait_for_lambda_active(lambda_client, function_name, max_retries=10, delay=1):
for _ in range(max_retries):
try:
response = lambda_client.get_function_configuration(
FunctionName=function_name
)
if response['State'] == 'Active':
return True
time.sleep(delay)
except ClientError as e:
print(f"Waiting for function to become active... (Current state: {response.get('State', 'Unknown')})")
if e.response['Error']['Code'] == 'ResourceNotFoundException':
raise
raise TimeoutError(f"Function {function_name} did not become active within {max_retries * delay} seconds")
Finally the function to invoke the lambda function.
def invoke_lambda_function(lambda_client, function_name):
lambda_client = boto3.client('lambda')
try:
response = lambda_client.invoke(
FunctionName=function_name,
Payload=b'{}',
InvocationType='RequestResponse'
)
output = response['Payload'].read().decode('utf-8')
print(f"Lambda invocation output:\n{output}")
return output
except ClientError as e:
print(f"Failed to invoke function: {e}")
raise
We are almost ready, here is the complete script: run-lambda.py
.
import argparse
import boto3
import zipfile
import os
import sys
import time
from botocore.exceptions import ClientError
def zip_lambda_file(lambda_script):
if not os.path.exists(lambda_script):
raise FileNotFoundError(f"Lambda script '{lambda_script}' not found")
zip_filename = f"{os.path.splitext(lambda_script)[0]}.zip"
with zipfile.ZipFile(zip_filename, 'w') as zipf:
zipf.write(lambda_script, os.path.basename(lambda_script))
return zip_filename
def wait_for_lambda_active(lambda_client, function_name, max_retries=10, delay=1):
for _ in range(max_retries):
try:
response = lambda_client.get_function_configuration(
FunctionName=function_name
)
if response['State'] == 'Active':
return True
time.sleep(delay)
except ClientError as e:
print(f"Waiting for function to become active: (Current state: {response.get('State', 'Unknown')})")
if e.response['Error']['Code'] == 'ResourceNotFoundException':
raise
raise TimeoutError(f"Function {function_name} did not become active within {max_retries * delay} seconds")
def deploy_lambda_function(lambda_client, lambda_script, function_name, handler_name, runtime='python3.9'):
zip_filename = zip_lambda_file(lambda_script)
try:
lambda_client.delete_function(FunctionName=function_name)
print(f"Deleted existing function: {function_name}")
except ClientError as e:
if e.response['Error']['Code'] != 'ResourceNotFoundException':
raise
with open(zip_filename, 'rb') as f:
try:
response = lambda_client.create_function(
FunctionName=function_name,
Runtime=runtime,
Handler=f"{os.path.splitext(lambda_script)[0]}.{handler_name}",
Role='arn:aws:iam::000000000000:role/lambda-role',
Code={'ZipFile': f.read()}
)
print(f"Successfully created function: {function_name}")
print("Waiting for function to become active...")
wait_for_lambda_active(lambda_client, function_name)
return response
except ClientError as e:
print(f"Failed to create function: {e}")
raise
def invoke_lambda_function(lambda_client, function_name):
lambda_client = boto3.client('lambda')
try:
response = lambda_client.invoke(
FunctionName=function_name,
Payload=b'{}',
InvocationType='RequestResponse'
)
output = response['Payload'].read().decode('utf-8')
print(f"Lambda invocation output:\n{output}")
return output
except ClientError as e:
print(f"Failed to invoke function: {e}")
raise
def main():
parser = argparse.ArgumentParser(description='Deploy and invoke AWS Lambda function')
parser.add_argument('lambda_script',
help='Path to the Lambda Python script')
parser.add_argument('--function-name', default='my-local-lambda',
help='Name of the Lambda function (default: my-local-lambda)')
parser.add_argument('--handler', default='handler',
help='Name of the handler function (default: handler)')
args = parser.parse_args()
lambda_client = boto3.client('lambda')
try:
deploy_lambda_function(
lambda_client,
args.lambda_script,
args.function_name,
args.handler
)
wait_for_lambda_active(lambda_client, args.function_name)
invoke_lambda_function(lambda_client, args.function_name)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == '__main__':
main()
This is how the interface looks like:
(myenv) $ py run-lambda.py --help
usage: run-lambda.py [-h] [--function-name FUNCTION_NAME] [--handler HANDLER] lambda_script
Deploy and invoke AWS Lambda function
positional arguments:
lambda_script Path to the Lambda Python script
options:
-h, --help show this help message and exit
--function-name FUNCTION_NAME
Name of the Lambda function (default: my-local-lambda)
--handler HANDLER Name of the handler function (default: handler)
Time for some action:
(myenv) $ py run-lambda.py lambda_function.py
Successfully created function: my-local-lambda
Waiting for function to become active:
Lambda invocation output:
{"statusCode": 200, "body": "Hello from Lambda!", "headers": {"Content-Type": "text/plain"}}
Using Perl
Finally time to repeat the process in Perl
now.
As before, here is function to zip the lambda function file.
sub zip_lambda_file {
my ($script_path) = @_;
die "Lambda script '$script_path' not found" unless -e $script_path;
my ($name, $path, $suffix) = fileparse($script_path, qr/\.[^.]*/);
my $zip_filename = $name . '.zip';
my $zip = Archive::Zip->new();
$zip->addFile($script_path, basename($script_path))
or die "Failed to add file to zip: $!";
unless ($zip->writeToFileNamed($zip_filename) == AZ_OK) {
die "Failed to create zip file: $!";
}
return $zip_filename;
}
Now the function to deploy lambda.
sub deploy_function {
my ($lambda_client, $script_path, $function_name, $handler_name) = @_;
my $zip_file = zip_lambda_file($script_path);
my $zip_content = read_zip_file($zip_file);
try {
$lambda_client->DeleteFunction(FunctionName => $function_name);
print "Deleted existing function: $function_name\n";
} catch {
unless ($_->isa('Paws::Exception') && $_->code eq 'ResourceNotFoundException') {
die "Error deleting function: $_";
}
print "No existing function to delete\n";
};
my ($base_name) = fileparse($script_path, qr/\.[^.]*/);
my $handler = "$base_name.$handler_name";
try {
my $response = $lambda_client->CreateFunction(
FunctionName => $function_name,
Runtime => $config->{runtime},
Handler => $handler,
Role => $config->{role_arn},
Code => { ZipFile => $zip_content } # <-- FIXED HERE
);
print "Successfully created function: $function_name\n";
return $response;
} catch {
die "Failed to create function: $_\n";
};
}
sub zip_lambda_file {
my ($script_path) = @_;
die "Lambda script '$script_path' not found" unless -e $script_path;
my ($name, $path, $suffix) = fileparse($script_path, qr/\.[^.]*/);
my $zip_filename = $name . '.zip';
my $zip = Archive::Zip->new();
$zip->addFile($script_path, basename($script_path))
or die "Failed to add file to zip: $!";
unless ($zip->writeToFileNamed($zip_filename) == AZ_OK) {
die "Failed to create zip file: $!";
}
return $zip_filename;
}
Last but not least, function to invoke the lambda.
sub invoke_function {
my ($lambda_client, $function_name) = @_;
sleep 2;
try {
my $response = $lambda_client->Invoke(
FunctionName => $function_name,
Payload => '{}',
InvocationType => 'RequestResponse'
);
my $output = $response->Payload;
print "Lambda invocation output:\n$output\n";
return decode_json($output) if $output =~ /^{/;
return $output;
} catch {
die "Failed to invoke function: $_\n";
};
}
The complete source file: run-lambda.pl
#!/usr/bin/perl
use strict;
use warnings;
use Archive::Zip qw(:ERROR_CODES :CONSTANTS);
use File::Basename;
use File::Spec;
use Try::Tiny;
use Getopt::Long;
use JSON;
use MIME::Base64 ();
use Paws;
my $config = {
function_name => 'my-local-lambda',
handler => 'handler',
runtime => 'python3.9',
role_arn => 'arn:aws:iam::000000000000:role/lambda-role',
endpoint => 'http://localhost:4566',
region => 'eu-west-1',
max_retries => 5,
retry_delay => 1
};
my $lambda = Paws->service('Lambda',
region => $config->{region},
endpoint => $config->{endpoint},
retries => $config->{max_retries},
);
my %options = (
'function-name=s' => \$config->{function_name},
'handler=s' => \$config->{handler},
'help|h' => \my $help
);
GetOptions(%options) or die "Error in command line arguments\n";
if ($help) {
print <<"END_USAGE";
Usage: $0 <script-file> [options]
Options:
--function-name NAME Set Lambda function name
--handler NAME Set handler name
-h, --help Show this help message and exit
END_USAGE
exit;
}
my $script_file = shift @ARGV or die "Please specify a Lambda script file\n";
try {
my $response = deploy_function(
$lambda,
$script_file,
$config->{function_name},
$config->{handler}
);
invoke_function($lambda, $config->{function_name});
} catch {
print STDERR "Error: $_\n";
exit 1;
};
#
#
# SUBROUTINES
sub zip_lambda_file {
my ($script_path) = @_;
die "Lambda script '$script_path' not found" unless -e $script_path;
my ($name, $path, $suffix) = fileparse($script_path, qr/\.[^.]*/);
my $zip_filename = $name . '.zip';
my $zip = Archive::Zip->new();
$zip->addFile($script_path, basename($script_path))
or die "Failed to add file to zip: $!";
unless ($zip->writeToFileNamed($zip_filename) == AZ_OK) {
die "Failed to create zip file: $!";
}
return $zip_filename;
}
sub read_zip_file {
my ($zip_file) = @_;
open(my $fh, '<:raw', $zip_file) or die "Cannot open zip file: $!";
my $content = do { local $/; <$fh> };
close($fh);
return MIME::Base64::encode_base64($content, '');
}
sub deploy_function {
my ($lambda_client, $script_path, $function_name, $handler_name) = @_;
my $zip_file = zip_lambda_file($script_path);
my $zip_content = read_zip_file($zip_file);
try {
$lambda_client->DeleteFunction(FunctionName => $function_name);
print "Deleted existing function: $function_name\n";
} catch {
unless ($_->isa('Paws::Exception') && $_->code eq 'ResourceNotFoundException') {
die "Error deleting function: $_";
}
print "No existing function to delete\n";
};
my ($base_name) = fileparse($script_path, qr/\.[^.]*/);
my $handler = "$base_name.$handler_name";
try {
my $response = $lambda_client->CreateFunction(
FunctionName => $function_name,
Runtime => $config->{runtime},
Handler => $handler,
Role => $config->{role_arn},
Code => { ZipFile => $zip_content } # <-- FIXED HERE
);
print "Successfully created function: $function_name\n";
return $response;
} catch {
die "Failed to create function: $_\n";
};
}
sub invoke_function {
my ($lambda_client, $function_name) = @_;
sleep 2;
try {
my $response = $lambda_client->Invoke(
FunctionName => $function_name,
Payload => '{}',
InvocationType => 'RequestResponse'
);
my $output = $response->Payload;
print "Lambda invocation output:\n$output\n";
return decode_json($output) if $output =~ /^{/;
return $output;
} catch {
die "Failed to invoke function: $_\n";
};
}
Here is the interface looks like below:
$ perl run-lambda.pl -h
Usage: run-lambda.pl <script-file> [options]
Options:
--function-name NAME Set Lambda function name
--handler NAME Set handler name
-h, --help Show this help message and exit
Action time now:
$ perl run-lambda.pl lambda_function.py
Deleted existing function: my-local-lambda
Successfully created function: my-local-lambda
Lambda invocation output:
{"statusCode": 200, "body": "Hello from Lambda!", "headers": {"Content-Type": "text/plain"}}
Happy Hacking !!!