How to Use the Twilio Java Helper Library and AWS CDK to Deploy AWS Lambda Functions

June 27, 2022
Written by
Vivek Kumar Maskara
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

header - How to Use the Twilio Java Helper Library and AWS CDK to Deploy AWS Lambda Functions

In this post, learn how to leverage the AWS Cloud Development Kit (CDK) to build and deploy a serverless AWS Lambda function that uses Twilio’s Programmable SMS API to send SMS to users. We will use Java for all components of this post to define the AWS CDK application and AWS Lambda Handler, and we will use Twilio’s Java Helper Library to work with the SMS APIs.

Prerequisites

For this tutorial, you will need to set up Java on your system, since we will be using it to define our AWS CDK application and the AWS Lambda handler. In addition to it, you will need to install AWS CLI and AWS CDK CLI, to configure AWS credentials and to build your CDK application respectively. Finally, you will need to create an AWS account for deploying the application, and a Twilio account with an SMS-enabled phone number to send SMS. Refer to the list below to set up everything required for this tutorial:

Build the CDK application

Create a new AWS CDK project

To create a new AWS CDK project, you need the CDK CLI installed on your system. First create a new directory for our CDK project and navigate into it.

mkdir twilio-java-helper-cdk-lambda
cd twilio-java-helper-cdk-lambda

Next, using the CDK CLI run the cdk init command to create a new CDK project using Typescript. The app parameter specifies the template that we are going to use for initializing the project.

cdk init app --language java

Executing the above command creates a new CDK project with a single CDK stack. The generated project is a Java Maven project. Open it in IntelliJ IDE or another IDE of your choice.

CDK supports multiple languages such as TypeScript, Python, Java, and C#. You can choose to use any language that you are comfortable with.

Define the AWS Lambda function

In this section, we will define a Java-based AWS Lambda function that will use the Twilio Java Helper Library to interact with SMS APIs.

Create a new Maven Project for the Lambda function

Create a lambda directory at the root of the CDK project by right clicking on the project name and choosing New > Directory. With the lambda directory selected, click File > New project from the toolbar to create a new Maven project, naming it as twilio-java-helper-send-sms. Choose Java Language for the project and use JDK version 11.

create a new project named "twilio-java-helper-send-sms"

Click on Create to create the project. Choose New window when the IDE prompts you to open the project.

pop up window to open "twilio-java-helper-send-sms" project

During the remainder of this section, we will work in the new window on the twilio-java-helper-send-sms project that was just created.

Define a group ID for the Maven project

As a convention Maven projects use the reversed domain name for group ID. Add a group ID based on the domain that you own. This tutorial uses com.maskaravivek as group ID. Update your pom.xml file with it.

<project
…
    <groupId>com.maskaravivek</groupId>
…
</project>

Create the com/maskaravivek directories inside the src/main/java directory to match the group ID of the project.

Add dependencies in pom.xml

Define the required dependencies in the pom.xml file. To start off, add dependencies for the AWS Java SDK and the AWS Lambda Java core libraries. Open pom.xml and add the following definitions inside the <project> tag:

<dependencies>
    <dependency>
        <groupId>com.amazonaws</groupId>
        <artifactId>aws-java-sdk-core</artifactId>
        <version>1.12.213</version>
    </dependency>
    <dependency>
        <groupId>com.amazonaws</groupId>
        <artifactId>aws-lambda-java-core</artifactId>
        <version>1.2.1</version>
    </dependency>
</dependencies>

Since we will be exposing the Lambda function through API Gateway, the function needs to proxy API Gateway’s request and response. So, also add a dependency for the AWS Lambda Java Events library inside the <dependencies> section of the pom.xml file.

<dependency>
    <groupId>com.amazonaws</groupId>
    <artifactId>aws-lambda-java-events</artifactId>
    <version>3.11.0</version>
</dependency>

We also need the Gson library for working with JSON.

<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.8.6</version>
</dependency>

Finally, add a dependency for the Twilio Java Helper Library, to work with the SMS APIs.

<dependency>
    <groupId>com.twilio.sdk</groupId>
    <artifactId>twilio</artifactId>
    <version>8.30.0</version>
</dependency>

Note: After adding the dependencies, you need to sync maven dependencies by right-clicking on the project and selecting Maven > Reload project.

Define request and response classes

The Lambda function proxies the HTTP request and response from the API Gateway, therefore we need to create Java bean classes for them.

Create a SendSmsRequest.java class in the src/main/java/com/maskaravivek package for the Lambda input. In IntelliJ IDE you can create a new Java class by right clicking on the package name and choosing New > Java Class. Add the following code to this file.

package com.maskaravivek;

public class SendSmsRequest {
    private String phoneNumber;
    private String message;

    public SendSmsRequest(String phoneNumber, String message) {
        this.phoneNumber = phoneNumber;
        this.message = message;
    }

    public String getPhoneNumber() {
        return phoneNumber;
    }

    public String getMessage() {
        return message;
    }
}

The Lambda handler uses the request class to parse the JSON input body from the APIGatewayV2HTTPEvent event.

Also, create a SendSmsResponse.java class in the src/main/java/com/maskaravivek package for the Lambda output.

package com.maskaravivek;

public class SendSmsResponse {
    private String messageSid;

    public SendSmsResponse(String messageSid) {
        this.messageSid = messageSid;
    }
}

The class is used by the Lambda handler to set the body of the APIGatewayV2HTTPResponse.

Define the Lambda handler

Next, define the AWS Lambda Handler that takes a JSON input payload containing the receiver phone number and message, and uses Twilio’s Programmable SMS API to send the message.

Create a TwilioJavaHelperSendSmsHandler.java class in src/main/java/com/maskaravivek package and add the following code snippet to it.

package com.maskaravivek;

import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.twilio.Twilio;
import com.twilio.rest.api.v2010.account.Message;
import com.twilio.type.PhoneNumber;
import com.amazonaws.services.lambda.runtime.Context;

import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPResponse;

import java.util.HashMap;

public class TwilioJavaHelperSendSmsHandler implements RequestHandler<APIGatewayV2HTTPEvent, APIGatewayV2HTTPResponse> {

    Gson gson = new GsonBuilder().setPrettyPrinting().create();

    public APIGatewayV2HTTPResponse handleRequest(APIGatewayV2HTTPEvent event, Context context) {
        
    }
}

Note that the Lambda handler implements the handleRequest method that takes APIGatewayV2HTTPEvent as input and returns APIGatewayV2HTTPResponse.

Next add the implementation for the above Lambda handler.

public APIGatewayV2HTTPResponse handleRequest(APIGatewayV2HTTPEvent event, Context context) {
        final String ACCOUNT_SID = System.getenv("TWILIO_ACCOUNT_SID");
        final String AUTH_TOKEN = System.getenv("TWILIO_AUTH_TOKEN");
        final String TWILIO_PHONE_NUMBER = System.getenv("TWILIO_PHONE_NUMBER");

        context.getLogger().log("Input: " + event.getBody());

        SendSmsRequest sendSmsRequest = gson.fromJson(event.getBody(), SendSmsRequest.class);

        Twilio.init(ACCOUNT_SID, AUTH_TOKEN);

        Message message = Message.creator(new PhoneNumber(sendSmsRequest.getPhoneNumber()),
                new PhoneNumber(TWILIO_PHONE_NUMBER),
                sendSmsRequest.getMessage()).create();

        System.out.println(message.getSid());

        SendSmsResponse sendSmsResponse = new SendSmsResponse(message.getSid());

        APIGatewayV2HTTPResponse response = new APIGatewayV2HTTPResponse();
        response.setIsBase64Encoded(false);
        response.setStatusCode(200);

        HashMap<String, String> headers = new HashMap<String, String>();
        headers.put("Content-Type", "application/json");
        response.setHeaders(headers);
        response.setBody(gson.toJson(sendSmsResponse));
        return response;
    }

The handler uses the Message.creator(...).create() function exposed by the Twilio Java helper library to send an SMS to the receiver's phone number.

Note that the handler retrieves the values for TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN and TWILIO_PHONE_NUMBER from environment variables. In a later section, we will see how these environment variables can be set while defining the Lambda function.

Build the Maven Project

Build the Maven project to check if it compiles without errors by clicking on Build > Build Project in the IDE. Alternatively you can use mvn to compile the application using the following command.

mvn clean compile

After verifying that the project compiles, you can optionally update the version of the Lambda Maven project by modifying the <version> tag from snapshot to a release version in the pom.xml file. 

<version>1.0</version>

Note, updating to a release version is completely optional but its a good practice to use a release version tag for deployments.

Define the AWS CDK stack

Now that the Lambda handler is defined, switch back to the CDK Maven project i.e., twilio-java-helper-cdk-lambda window in the IntelliJ IDE.

Add CDK dependencies

Add the following dependencies in the pom.xml file of the CDK project. The pom.xml file would be auto-generated by CDK when you initialize the project.

<dependencies>
        <!-- Add after existing dependencies -->
        <dependency>
            <groupId>software.amazon.jsii</groupId>
            <artifactId>jsii-runtime</artifactId>
            <version>1.58.0</version>
        </dependency>
        <dependency>
            <groupId>software.amazon.awscdk</groupId>
            <artifactId>apigatewayv2-alpha</artifactId>
            <version>2.22.0-alpha.0</version>
        </dependency>
        <dependency>
            <groupId>software.amazon.awscdk</groupId>
            <artifactId>apigatewayv2-integrations-alpha</artifactId>
            <version>2.22.0-alpha.0</version>
        </dependency>
    </dependencies>

Add a CDK construct for AWS Lambda function

AWS CDK constructs are building blocks for cloud components that encapsulate the configuration detail and gluing logic for using one or more AWS services. AWS CDK provides constructs for most of its popular AWS services.

At the start of this tutorial, when we generated the CDK project using the app template, a TwilioJavaHelperCdkLambdaStack.java file was generated in src/main/java/com/myorg directory. We will now define the constructs for the AWS Lambda function and the API Gateway HTTP APIs. First, add the following imports in the src/main/java/com/myorg/TwilioJavaHelperCdkLambdaStack.java file.

import software.amazon.awscdk.BundlingOptions;
import software.amazon.awscdk.CfnOutput;
import software.amazon.awscdk.CfnOutputProps;
import software.amazon.awscdk.DockerVolume;
import software.amazon.awscdk.Duration;
import software.amazon.awscdk.services.apigatewayv2.alpha.AddRoutesOptions;
import software.amazon.awscdk.services.apigatewayv2.alpha.HttpApi;
import software.amazon.awscdk.services.apigatewayv2.alpha.HttpApiProps;
import software.amazon.awscdk.services.apigatewayv2.alpha.HttpMethod;
import software.amazon.awscdk.services.apigatewayv2.alpha.PayloadFormatVersion;
import software.amazon.awscdk.services.apigatewayv2.integrations.alpha.HttpLambdaIntegration;
import software.amazon.awscdk.services.apigatewayv2.integrations.alpha.HttpLambdaIntegrationProps;
import software.amazon.awscdk.Stack;
import software.amazon.awscdk.StackProps;

import software.amazon.awscdk.services.lambda.Code;
import software.amazon.awscdk.services.lambda.Function;
import software.amazon.awscdk.services.lambda.FunctionProps;
import software.amazon.awscdk.services.lambda.Runtime;
import software.amazon.awscdk.services.logs.RetentionDays;
import software.amazon.awscdk.services.s3.assets.AssetOptions;
import software.constructs.Construct;

import static java.util.Collections.singletonList;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;

import static software.amazon.awscdk.BundlingOutput.ARCHIVED;

To avoid any manual intervention, we will define packaging instructions for the Lambda function in the CDK stack. We will use the Code.fromAsset API along with bundling commands for the Maven project. The commands will build the Maven project and copy the jar to the asset-output directory. These commands will be executed inside a Docker container created using Java 11’s official image.

Add the following code snippet in the constructor of the TwilioJavaHelperLambdaStack.java file. The generated class should have two constructors and we need to add the code in the constructor with StackProps in its arguments.

public TwilioJavaHelperLambdaCdkStack(final Construct scope, final String id, final StackProps props) {
    super(scope, id, props);

    List<String> sendSmsFnPackagingInstructions = Arrays.asList(
            "/bin/sh",
            "-c",
            "cd twilio-java-helper-send-sms " +
                    "&& mvn clean install " +
                    "&& cp /asset-input/twilio-java-helper-send-sms/target/twilio-java-helper-send-sms.jar /asset-output/"
    );

    BundlingOptions.Builder builderOptions = BundlingOptions.builder()
            .command(sendSmsFnPackagingInstructions)
            .image(Runtime.JAVA_11.getBundlingImage())
            .volumes(singletonList(
                    // Mount local .m2 repo to avoid download all the dependencies again inside the container
                    DockerVolume.builder()
                            .hostPath(System.getProperty("user.home") + "/.m2/")
                            .containerPath("/root/.m2/")
                            .build()
            ))
            .user("root")
            .outputType(ARCHIVED);
}

Next, define the CDK construct for the AWS Lambda function, right after the code you defined above. We will pass TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN and TWILIO_PHONE_NUMBER as environment variables to the Lambda function. Also, as discussed above, we use the Code.fromAsset API to bundle the Lambda code. It uploads the zipped archive to a temporary AWS S3 bucket for deployment.

HashMap<String, String> environmentMap = new HashMap<>();
        environmentMap.put("TWILIO_ACCOUNT_SID", System.getenv("TWILIO_ACCOUNT_SID"));
        environmentMap.put("TWILIO_AUTH_TOKEN", System.getenv("TWILIO_AUTH_TOKEN"));
        environmentMap.put("TWILIO_PHONE_NUMBER", System.getenv("TWILIO_PHONE_NUMBER"));

        Function sendSmsFunction = new Function(this, "TwilioJavaHelperSendSms", FunctionProps.builder()
                .runtime(Runtime.JAVA_11)
                .code(Code.fromAsset("lambda/", AssetOptions.builder()
                        .bundling(builderOptions
                                .command(sendSmsFnPackagingInstructions)
                                .build())
                        .build()))
                .handler("com.maskaravivek.TwilioJavaHelperSendSmsHandler")
                .memorySize(1024)
                .timeout(Duration.minutes(1))
                .environment(environmentMap)
                .logRetention(RetentionDays.ONE_WEEK)
                .build());

Note that the FunctionProps.builder() can be used to set various properties of the Lambda function like memory, timeout, runtime and environment variables.

Adding a CDK construct for API Gateway API

Now that we have defined the AWS Lambda function, go ahead and expose the function using an HTTP API. We will use AWS API Gateway to expose our API(s) over HTTP. Define the service with the following construct, which you should add below the code added in the previous section, and still in the same constructor.

HttpApi httpApi = new HttpApi(this, "twilio-java-helper-demo", HttpApiProps.builder()
    .apiName("twilio-java-helper-demo")
    .build());

For the last section of this constructor, we will use HttpLambdaIntegration to expose the Lambda as an HTTP API and expose it as a POST method on the /sendSms endpoint.

HttpLambdaIntegration httpLambdaIntegration = new HttpLambdaIntegration(
    "this",
    sendSmsFunction,
    HttpLambdaIntegrationProps.builder()
            .payloadFormatVersion(PayloadFormatVersion.VERSION_2_0)
            .build()
);

httpApi.addRoutes(AddRoutesOptions.builder()
    .path("/sendSms")
    .methods(singletonList(HttpMethod.POST))
    .integration(httpLambdaIntegration)
    .build()
);

new CfnOutput(this, "HttApi", CfnOutputProps.builder()
    .description("Url for Http Api")
    .value(httpApi.getApiEndpoint())
    .build());

Deploy the CDK application

Now that we have defined the resources for our cloud application in the CDK stack, go ahead and deploy it to an AWS account. Set the Twilio Account SID and Auth Token as environment variables before deploying the application. You can obtain these credentials from the Twilio Console. In addition to the credentials, set your twilio phone number as an environment variable. If you don’t have a twilio phone number, you can get a phone number following this guide.

export TWILIO_ACCOUNT_SID=<account_sid>
export TWILIO_AUTH_TOKEN=<auth_token>
export TWILIO_PHONE_NUMBER=<your_twilio_phone_number>

There are three steps for deploying a CDK application. Make sure that Docker is running and  AWS credentials are configured on your system before deploying the application.

Bootstrap the application

The first step is to bootstrap the application. Bootstrapping provisions the resources that might be required by AWS CDK to deploy your application. These resources might include an S3 bucket for storing deployment-related files and IAM roles for granting deployment permissions. Execute the following command in the root directory of your CDK application to bootstrap the stack.

cdk bootstrap

Synthesize the application

The second step is to synthesize the stack by running the cdk synth command. This command executes your CDK application, which causes the resources defined in it to be translated into a YAML-based AWS CloudFormation template. Run the following command in the root directory of your CDK application to synthesize the stack.

cdk synth

Note that if your CDK application contains multiple stacks then you need to specify the name of the stack while executing the synth command. We don’t need to worry about it since our demo application contains just one stack. Also, the synth command catches logical errors in the stack and throws an exception if the stack isn’t defined correctly.

Deploy the application

Once the above command executes successfully, we can go ahead and deploy the application by executing the following command. You should have AWS credentials configured locally for the deploy command to execute successfully. The deploy command will look for the AWS access key, secret, and region to automatically deploy the application to the associated AWS account.

cdk deploy

Note that you might be prompted to confirm the IAM role/policy changes that would be applied to your account.

The application should deploy successfully and it should output the base URL of the HTTP API that we deployed. Keep this URL handy as we will need it while testing the application.

Test the application

Now that the application is deployed to the AWS account, we can get to the fun part - testing the API and checking that we are able to send messages to the specified phone numbers.

Remember that if you are using a trial Twilio account then you can send messages only to numbers that are verified under your account.

Our API accesses a POST JSON request with the receiver phone number and the message. Here’s a sample request object.

{
    "phoneNumber": "+12222222222",
    "message": "Hello from the other side"
}

You can try the following curl request in your terminal by replacing the base URL to test the API. Replace the fake phone number with your own mobile number.

curl --location --request POST '<BASE_URL>/sendSms' \
--header 'Content-Type: application/json' \
--data-raw '{
    "phoneNumber": "+12222222222",
    "message": "Test message"
}'

On executing the above curl request, you should get a JSON response with the message SID.

​​{
    "messageSid": "SMf54164c8df7149239f95a3c8fcd11ba4"
}

The number you provided in the request should receive the message in a short moment.

Conclusion

In this post, we learned how to use AWS CDK to programmatically deploy a Lambda function using Infrastructure as Code (IaC) constructs. AWS CDK simplifies the process of provisioning cloud resources for your application using familiar programming languages. It allows you to use object-oriented techniques and makes the infrastructure code testable. This tutorial demonstrated how we can use the Twilio Java helper library in our AWS Lambda function and deploy it to AWS. The Twilio Java helper library makes it easier to interact with various Twilio services with easy-to-consume APIs. With the power of Twilio Java Helper Library and AWS CDK, you can build your services much faster and deliver a rich experience to your users. You can check out the full source code used in this tutorial on GitHub.

Vivek Kumar Maskara is an Associate Software Engineer at JP Morgan.  He loves writing code, developing apps, creating websites, and writing technical blogs about his experiences. His profile and contact information can be found at maskaravivek.com.