This article explains how to create a reusable Lambda function construct using CDKTF, including deployment, IAM roles, and automated code packaging.
In this lesson, we encapsulate the behavior for a Lambda function in a reusable construct. This construct not only deploys the Lambda function but also creates the necessary IAM role and attaches the essential permissions.Arthur wants to deploy a Lambda function that randomly selects a name from a list. To achieve this, we are building a reusable Lambda function construct.
The code above sets up our initial Lambda function construct. Notice how we dynamically create a role name based on the function name and define the assume-role policy.
Next, attach a policy that grants basic permissions (like logging to CloudWatch). Modern IDEs can help suggest resource creation commands; for example, you may see an option for creating an IAM Role Policy Attachment:
Copy
Ask AI
// Create IAM role for Lambdaconst lambdaRole = new iamRole.IamRole(this, 'lambda-execution-role', { name: `${functionName}-execution-role`, assumeRolePolicy: JSON.stringify({ Version: '2012-10-17', Statement: [ { Effect: 'Allow', Principal: { Service: 'lambda.amazonaws.com' }, Action: 'sts:AssumeRole', }, ], }),});// Attach policy to the role to grant basic permissions (e.g., logging to CloudWatch)new iamRolePolicyAttachment.IamRolePolicyAttachment(this, 'LambdaExecutionRolePolicy', { role: lambdaRole.name, policyArn: 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole',});
If a required parameter is missing, a type error is thrown, which helps catch issues during development.
Now, create the Lambda function and expose it as a public property for access from the containing stack. The function is created using the CDKTF resource for Lambda:
Copy
Ask AI
// Attach policy to the role to grant Lambda basic permissionsnew iamRolePolicyAttachment.IamRolePolicyAttachment(this, 'LambdaExecutionRolePolicy', { role: lambdaRole.name, policyArn: 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole',});// Create the Lambda function resourcethis.lambdaFunction = new lambdaFunction.LambdaFunction(this, 'lambda-function', { functionName, role: lambdaRole.arn, runtime: 'nodejs18.x', timeout: 30,});
The Lambda function requires several properties:
functionName: The name provided to the construct.
role: The ARN of the IAM role created earlier.
runtime: The execution environment (Node.js 18.x in this case).
timeout: The execution timeout (e.g., 30 seconds).
For more details, use your IDE’s “go to definition” feature to review the CDKTF documentation.
Below is an example that demonstrates how to integrate the Lambda function construct into your main stack:
Copy
Ask AI
import { Construct } from 'constructs';import { App, TerraformStack, TerraformOutput } from 'cdktf';import { iamRole, provider } from '@cdktf/provider-aws';import { LambdaFunction } from './constructs/LambdaFunction';class MyStack extends TerraformStack { constructor(scope: Construct, id: string) { super(scope, id); new provider.AwsProvider(this, 'aws-provider', { region: 'us-east-1', }); new LambdaFunction(this, 'lambda-function', { functionName: 'cdktf-name-picker-api', }); // Define additional resources new TerraformOutput(this, 'lets-go', { value: 'lets go!' }); }}const app = new App();new MyStack(app, 'cdktf-name-picker');app.synth();
Notice that we pass parameters such as the function name (cdktf-name-picker-api). Later, you will pass additional properties like the filename (for the zipped code) and the handler.For example, to deploy your code stored in a zip file, specify the filename and handler properties:
Copy
Ask AI
import * as path from 'path';new LambdaFunction(this, 'lambda-function', { functionName: 'cdktf-name-picker-api', filename: path.join(process.env.INIT_CWD!, './function-name-picker/index.js.zip'), handler: 'index.handler',});
Once deployed, check the AWS Lambda console. Under the Lambda functions list, you should see your new cdktf-name-picker-api function:
Inspecting the configuration details of the deployed function (such as permissions and code settings) should reflect the setup provided in our construct:
When tested, a successful response similar to the JSON below is returned:
Initially, the Lambda function code was manually zipped and referenced directly, which can be cumbersome and error-prone when the business logic changes frequently.
Automating Packaging with execSync (Not Recommended)
One approach is to use Node’s execSync to execute shell commands that package the code. For example:
Copy
Ask AI
import { execSync } from 'child_process';import * as path from 'path';interface LambdaFunctionProps { functionName: string; // Additional properties from LambdaFunctionConfig can be added here bundle: string; handler: string;}export class LambdaFunction extends Construct { public readonly lambdaFunction: lambdaFunction.LambdaFunction; constructor( scope: Construct, id: string, { functionName, bundle, ...rest }: LambdaFunctionProps ) { super(scope, id); // Compute the path for the zip file const filename = path.join(process.env.INIT_CWD!, `./out/${bundle}.zip`); // Zip the bundle using execSync execSync( `rm -rf ./out && mkdir -p ./out && cd ${bundle} && zip -r ${filename} .`, { cwd: process.env.INIT_CWD! } ); // Create the IAM role for Lambda const lambdaRole = new iamRole.IamRole(this, 'lambda-execution-role', { name: `${functionName}-execution-role`, assumeRolePolicy: JSON.stringify({ Version: '2012-10-17', Statement: [ { Effect: 'Allow', Principal: { Service: 'lambda.amazonaws.com' }, Action: 'sts:AssumeRole', }, ], }), }); new iamRolePolicyAttachment.IamRolePolicyAttachment(this, 'LambdaExecutionRolePolicy', { role: lambdaRole.name, policyArn: 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', }); // Create the Lambda function using the generated zip file this.lambdaFunction = new lambdaFunction.LambdaFunction(this, 'lambda-function', { functionName, role: lambdaRole.arn, runtime: 'nodejs18.x', timeout: 30, filename, // Use the generated zip file path ...rest, }); }}
Manual packaging with execSync can lead to issues: Terraform won’t detect changes in the zipped file if the timestamp isn’t updated, potentially leading to outdated deployments.
A better alternative is to use Terraform assets to automatically track changes in your application code. This approach ensures Terraform detects when you modify your business logic and redeploys the updated code.Follow these steps to implement this:
Import the Terraform asset classes:
Copy
Ask AI
import { TerraformAsset, AssetType } from 'cdktf';
Replace manual zipping with a Terraform asset that creates an archive from your code folder:
Copy
Ask AI
interface LambdaFunctionProps extends Omit<LambdaFunctionConfig, 'role' | 'filename'> { bundle: string; functionName: string; handler: string;}export class LambdaFunction extends Construct { public readonly lambdaFunction: lambdaFunction.LambdaFunction; constructor( scope: Construct, id: string, { functionName, bundle, ...rest }: LambdaFunctionProps ) { super(scope, id); // Create a Terraform asset for the Lambda function code const asset = new TerraformAsset(this, 'lambda-asset', { path: path.join(process.env.INIT_CWD!, bundle), type: AssetType.ARCHIVE, }); // Create IAM role for Lambda const lambdaRole = new iamRole.IamRole(this, 'lambda-execution-role', { name: `${functionName}-execution-role`, assumeRolePolicy: JSON.stringify({ Version: '2012-10-17', Statement: [ { Effect: 'Allow', Principal: { Service: 'lambda.amazonaws.com' }, Action: 'sts:AssumeRole', }, ], }), }); new iamRolePolicyAttachment.IamRolePolicyAttachment(this, 'LambdaExecutionRolePolicy', { role: lambdaRole.name, policyArn: 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', }); // Create the Lambda function using the asset file this.lambdaFunction = new lambdaFunction.LambdaFunction(this, 'lambda-function', { functionName, role: lambdaRole.arn, runtime: 'nodejs18.x', timeout: 30, filename: asset.path, // asset.path points to the generated archive ...rest, }); }}
In your main stack file, reference the bundle folder rather than a pre-zipped file:
Copy
Ask AI
import { Construct } from 'constructs';import { App, TerraformStack, TerraformOutput } from 'cdktf';import { provider } from '@cdktf/provider-aws';import { LambdaFunction } from './constructs/LambdaFunction';class MyStack extends TerraformStack { constructor(scope: Construct, id: string) { super(scope, id); new provider.AwsProvider(this, 'aws-provider', { region: 'us-east-1', }); new LambdaFunction(this, 'lambda-function', { functionName: 'cdktf-name-picker-api', bundle: './function-name-picker', // Path to your code directory handler: 'index.handler', }); new TerraformOutput(this, 'lets-go', { value: 'lets go!' }); }}const app = new App();new MyStack(app, 'cdktf-name-picker');app.synth();
With this approach, Terraform tracks changes in the asset. When you update your business logic (for example, by adding new console logs), Terraform will detect the change and update the Lambda function accordingly.Below is an example snippet from the Lambda code reflecting a code change:
Using Terraform assets provides a more robust and change-aware deployment, ensuring that only actual code modifications trigger a redeployment.This concludes the section on the Lambda function construct, encompassing both manual and automated packaging methods. By leveraging Terraform assets, your deployments become more efficient and reliable.Happy coding!