Skip to main content

Pulumi - Lambda

Resources

📘 Pulumi Docs > aws.lambda

📘 Pulumi Docs > AWS Lambda & Serverless Events

Steps

In Palumi we need to create not just a Lambda function but also an IAM role and a policy to attach to that role for every function.

Role

// Create an IAM role for the Lambda function
const lambdaRole = new aws.iam.Role('lambdaRole', {
assumeRolePolicy: aws.iam.assumeRolePolicyForPrincipal({
Service: 'lambda.amazonaws.com',
}),
});

Policy

// Attach the necessary policies to the role
new aws.iam.RolePolicyAttachment('lambdaRolePolicyAttachment', {
role: lambdaRole.name,
policyArn: aws.iam.ManagedPolicies.AWSLambdaBasicExecutionRole,
});

// if you need to access other AWS services like S3 etc etc you will be attaching more policies here

Lambda Function

Then you have a few different options on how to create the Lambda function.

  • Import the function
  • Write the function directly in place

Import

Here is an example of importing:

// Create the Lambda function
const lambdaFunction = new aws.lambda.Function('myLambdaFunction', {
runtime: aws.lambda.NodeJS12dXRuntime,
role: role.arn,
handler: 'index.handler',
code: new pulumi.asset.AssetArchive({
// Specify the path to the zip file or a directory that contains an index.js file
'.': new pulumi.asset.FileArchive('path/to/your/code.zip'),
}),
});

One potential organization (file structure) for this would be to have a lambdas directory in your project index, with sub-folders for each Lambda function. In each sub-folder you will have a generate.ts file which is the Pulumi code to generate the resources, and an index.js file that contains the actual lambda code.

.
├── Pulumi.yaml
├── Pulumi.dev.yaml
├── package.json
├── tsconfig.json
├── ...
└── src
├── index.ts (pulumi index)
└── lambdas
└── some-lambda-title
├── generate.ts
└── index.js

In Place

Here is an example of writing the code in place:

// Create the Lambda function
const lambdaFunction = new aws.lambda.Function('myLambdaFunction', {
runtime: aws.lambda.NodeJS12dXRuntime,
role: role.arn,
handler: 'index.handler',
code: new pulumi.asset.AssetArchive({
'.': new pulumi.asset.FileArchive('path/to/your/code.zip'), // Specify the path to the zip file
}),
});

Writing the functions inline is obviously not preferred, as this does not provide us with a way to develop and test the functions.

Developing and Testing Functions

It's common to develop and test Lambda functions locally using AWS SAM or Serverless Framework and then use the tested functions as imports.

An alternate method is to create an API gateway endpoint that hits the Lambda function and then test using that endpoint on a tool like Postman.

import * as pulumi from '@pulumi/pulumi';
import * as aws from '@pulumi/aws';

// Create an IAM role for the Lambda function
const role = new aws.iam.Role('lambdaRole', {
assumeRolePolicy: aws.iam.assumeRolePolicyForPrincipal({
Service: 'lambda.amazonaws.com',
}),
});

// Attach the AWSLambdaBasicExecutionRole policy to the role
new aws.iam.RolePolicyAttachment('lambdaRolePolicy', {
role: role.name,
policyArn: aws.iam.ManagedPolicies.AWSLambdaBasicExecutionRole,
});

// Create the Lambda function
const lambdaFunction = new aws.lambda.Function('myInlineLambdaFunction', {
runtime: aws.lambda.NodeJS14dXRuntime, // Use Node.js 14.x runtime
role: role.arn,
handler: 'index.handler',
code: new pulumi.asset.AssetArchive({
'index.js': new pulumi.asset.StringAsset(`
exports.handler = async (event) => {
console.log("Event: ", event);
return {
statusCode: 200,
body: JSON.stringify({ message: "Hello, World!" }),
};
};
`),
}),
});

// Create an API Gateway to invoke the Lambda function
const api = new aws.apigateway.RestApi('myApi', {
description: 'API Gateway for Lambda function',
});

const resource = new aws.apigateway.Resource('myResource', {
parentId: api.rootResourceId,
pathPart: 'myresource',
restApi: api.id,
});

const method = new aws.apigateway.Method('myMethod', {
authorization: 'NONE',
httpMethod: 'GET',
resourceId: resource.id,
restApi: api.id,
});

const integration = new aws.apigateway.Integration('myIntegration', {
httpMethod: method.httpMethod,
resourceId: resource.id,
restApi: api.id,
type: 'AWS_PROXY',
integrationHttpMethod: 'POST',
uri: lambdaFunction.invokeArn,
});

const deployment = new aws.apigateway.Deployment(
'myDeployment',
{
restApi: api.id,
stageName: 'dev',
},
{dependsOn: [integration]},
);

// Export the URL of the API Gateway
export const url = deployment.invokeUrl;

Dependencies

Pulumi does not automatically include dependencies for your Lambda function. You need to package your Lambda function with its dependencies before deployment.

For example take the following function:

const {Client} = require('pg');

exports.handler = async (event) => {
console.log('🌎 event', event);

const client = new Client({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
});

await client.connect();

try {
const res = await client.query('SELECT * FROM mytable'); // Replace with your query
return {
statusCode: 200,
body: JSON.stringify(res.rows),
};
} catch (error) {
return {
statusCode: 500,
body: JSON.stringify(error),
};
} finally {
await client.end();
}
};

We will get an error that pg is not found. To resolve this do the following.

Navigate to the directory that contains the lambda index file. Then initialize a node.js project and install the packages you need.

npm init -y
npm install pg

Samples

Lambda To Connect to RDS PostgreSQL Database

Assuming we follow the pattern described above where we separate the code into a Pulumi file that generates the assets, and a Lambda file that executes the request.

generate.ts
import * as aws from '@pulumi/aws';
import * as pulumi from '@pulumi/pulumi';
import {sampleDataDB} from '../../databases/sampleDataPostGreSQL';

// Initialize the Pulumi config (to retrieve variables)
const config = new pulumi.Config();

// Retrieve the database variables
// ** these values should be moved to a secret manager
const db_username = config.require('db_username');
const db_password = config.require('db_password');
const db_name = config.require('db_name');

// Create an IAM role for the Lambda function
const lambdaRole = new aws.iam.Role('lambdaRole', {
assumeRolePolicy: aws.iam.assumeRolePolicyForPrincipal({
Service: 'lambda.amazonaws.com',
}),
});

// Attach the necessary policies to the role
new aws.iam.RolePolicyAttachment('lambdaRolePolicyAttachment', {
role: lambdaRole.name,
policyArn: aws.iam.ManagedPolicies.AWSLambdaBasicExecutionRole,
});
new aws.iam.RolePolicyAttachment('lambdaRoleVPCAccessPolicyAttachment', {
role: lambdaRole.name,
policyArn: aws.iam.ManagedPolicies.AWSLambdaVPCAccessExecutionRole,
});

// Define the RDS instance ID
const dbInstanceId = sampleDataDB.id;

// Fetch the RDS database instance details
const dbInstance = dbInstanceId.apply((id) =>
aws.rds.getInstance({
dbInstanceIdentifier: id,
}),
);

// Extract the DB subnet group name
const dbSubnetGroupName = dbInstance.apply(
(instance) => instance.dbSubnetGroup,
);

// Fetch the subnet IDs from the DB subnet group
const dbSubnetGroup = dbSubnetGroupName.apply((name) =>
aws.rds.getSubnetGroup({
name: name,
}),
);

const subnetIds = dbSubnetGroup.apply((group) => group.subnetIds);

// Create the Lambda function
export const getSampleData = new aws.lambda.Function('getSampleData', {
runtime: aws.lambda.Runtime.NodeJS18dX,
code: new pulumi.asset.AssetArchive({
'.': new pulumi.asset.FileArchive('./lambdas/get-sample-data'), // Path to lambda index file
}),
handler: 'index.handler',
role: lambdaRole.arn,
environment: {
variables: {
DB_HOST: sampleDataDB.endpoint.apply((e) => e.split(':')[0]),
DB_USER: db_username,
DB_PASS: db_password,
DB_NAME: db_name,
},
},
vpcConfig: {
subnetIds: subnetIds,
securityGroupIds: dbInstance.apply((instance) =>
instance.vpcSecurityGroups.map((sg) => sg),
),
},
});
index.js
const { Client } = require("pg");

exports.handler = async (event, context) => {
console.log("🌎 event", event);

// standardized error handler
const handleError = (error) => {
console.error("⚠ Full Error Code", JSON.stringify(error));

const errorResponse = {
statusCode: 400,
message: error.message,
requestId: context.awsRequestId,
function_name: process.env.AWS_LAMBDA_FUNCTION_NAME,
function_version: process.env.AWS_LAMBDA_FUNCTION_VERSION,
};

console.log("🚧 Custom Error Response", errorResponse);

// must throw error response in actual error for API Gateway to recognize
// and handle it properly
throw new Error(JSON.stringify(errorResponse));
};

async function connectToDatabase(client) {
try {
await client.connect();
} catch (error) {
handleError(error);
}
}

// 📝: i've confirmed that the environment variables are being passed in correctly

const client = new Client({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
// I cannot emphasize how important this ssl option is
ssl: {
rejectUnauthorized: false, // For self-signed certificates
},
});

await connectToDatabase(client);

try {
const res = await client.query(`
SELECT * FROM "advertising-data".daily
ORDER BY date ASC
`);

console.log("res", res);

return {
statusCode: 200,
body: JSON.stringify(res.rows),
};
} catch (error) {
handleError(error);
} finally {
await client.end();
}
};
tip

If you are getting connection rejection issues, be sure that you are handling encryption in your client options.

Comments

Recent Work

Free desktop AI Chat client, designed for developers and businesses. Unlocks advanced model settings only available in the API. Includes quality of life features like custom syntax highlighting.

Learn More

BidBear

bidbear.io

Bidbear is a report automation tool. It downloads Amazon Seller and Advertising reports, daily, to a private database. It then merges and formats the data into beautiful, on demand, exportable performance reports.

Learn More