Field Notes: A Trio of Steps to Migrate Your Containerized Application to AWS Lambda

Field Notes: A Trio of Steps to Migrate Your Containerized Application to AWS LambdaMore Info

on 18 AUG 2021

in Amazon Elastic Container Registry, Architecture, AWS Lambda, Technical How-to

The capability of AWS Lambda to support container images facilitates the transition of containerized web applications to a serverless framework. This transformation grants automatic scaling, inherent high availability, and a cost-efficient billing model, ensuring you are not charged for excess resources. For those already leveraging containers, this support allows you to reap the benefits without necessitating extensive engineering modifications or new development practices. Your team’s proficiency with containers remains intact while you enjoy the operational ease and cost savings associated with Serverless computing.

This article outlines the steps necessary to adapt a containerized web application for deployment on Lambda, requiring only minor adjustments in the development, packaging, and deployment processes. Although we will use Ruby as our example language, the principles apply universally across any programming language.

Overview of the Solution

The sample application we will work with is a containerized web service designed to generate PDF invoices. Our goal is to migrate this service so that its business logic executes within a Lambda function, utilizing Amazon API Gateway to create a Serverless RESTful web API. API Gateway is a managed service that facilitates the creation and execution of API operations at scale.

Walkthrough

In this guide, you will discover how to migrate the containerized web application into a serverless environment through Lambda. The high-level steps include:

  1. Running the containerized application locally for testing
  2. Adapting the application for Lambda
    • Creating a Lambda function handler
    • Modifying the container image for Lambda
    • Testing the containerized Lambda function locally
  3. Deploying and testing on Amazon Web Services (AWS)

Prerequisites

Before we begin, ensure you have the following:

  • An AWS account
  • IAM permissions for creating Lambda functions, API Gateway, IAM roles, and Amazon Elastic Container Registry (ECR)
  • Docker and AWS Command Line Interface (CLI) installed
  • A fundamental understanding of Lambda operations

1. Get the Containerized Application Running Locally for Testing

You can find the sample code for this application on GitHub. Clone the repository to follow along.

git clone https://github.com/aws-samples/aws-lambda-containerized-custom-runtime-blog.git

1.1. Build the Docker Image

Examine the Dockerfile in the root of the cloned repository, which uses Bitnami’s Ruby 3.0 image from the Amazon ECR Public Gallery as its base. It adheres to security best practices by executing the application as a non-root user and exposing the invoice generator service on port 4567. Open your terminal and navigate to the folder where you cloned the repository. Build the Docker image with the following command.

docker build -t ruby-invoice-generator .

1.2. Test Locally

Run the application locally using the Docker run command.

docker run -p 4567:4567 ruby-invoice-generator

In a practical scenario, order and customer details for the invoice would typically be sent through a POST request body or as GET request query parameters. For simplicity, we are randomly selecting from a few hard-coded values inside lib/utils.rb. Open another terminal to test invoice creation using this command.

curl "http://localhost:4567/invoice" 
  --output invoice.pdf 
  --header 'Accept: application/pdf'

This command generates the file invoice.pdf in the directory where you executed the curl command. Feel free to test the URL directly in your browser. Press Ctrl+C to stop the container once you confirm that the application is functioning correctly. We are now ready to migrate it to run on Lambda as a container.

2. Port the Application to Run on Lambda

The operational model and request plane for Lambda remain unchanged. This means the function handler serves as the entry point to application logic when packaging a Lambda function as a container image. By transitioning our business logic to a Lambda function, we can decouple two concerns and replace the web server code in the container image with an HTTP API facilitated by API Gateway. This allows you to concentrate on the business logic in the container, while API Gateway efficiently routes requests.

2.1. Create the Lambda Function Handler

The code for our Lambda function is outlined in function.rb, with the handler function discussed later. The primary distinction to note between the original Sintra-powered code and our Lambda handler version is the necessity to base64 encode the PDF. This step is crucial for returning binary media from API Gateway Lambda proxy integration, which will automatically decode it for the client.

def self.process(event:, context:)
  self.logger.debug(JSON.generate(event))
  invoice_pdf = Base64.encode64(Invoice.new.generate)
  { 
    'headers' => { 'Content-Type': 'application/pdf' }, 
    'statusCode' => 200, 
    'body' => invoice_pdf, 
    'isBase64Encoded' => true 
  }
end

If you need a refresher on the fundamentals of Lambda function handlers, check the documentation on writing a Lambda handler in Ruby. This marks the new addition to the development workflow—creating a Lambda function handler to encapsulate the business logic.

2.2. Modify the Container Image for Lambda

AWS provides open-source base images for Lambda, currently available for Ruby runtime versions 2.5 and 2.7. However, you can incorporate any version of your desired runtime (Ruby 3.0 in this case) by including it with your Docker image. We will utilize Bitnami’s Ruby 3.0 image from the Amazon ECR Public Gallery as the base. It’s essential to note that Lambda only supports running container images stored in Amazon ECR; you cannot upload arbitrary container images directly.

Since the function handler is the entry point to the business logic, the Dockerfile CMD must be adjusted to point to the function handler rather than initiating the web server. In our scenario, because we are utilizing our own base image to include a custom runtime, we must also implement a change. Custom images require the runtime interface client to manage interactions between the Lambda service and the function’s code.

The runtime interface client is an open-source lightweight interface that receives requests from Lambda, forwards them to the function handler, and returns results back to the Lambda service. Here are the relevant changes to the Dockerfile.

ENTRYPOINT ["aws_lambda_ric"]
CMD [ "function.Billing::InvoiceGenerator.process" ]

The Docker command executed when the container runs is: aws_lambda_ric function.Billing::InvoiceGenerator.process. For the complete code, refer to Dockerfile.lambda in the cloned repository. This image adheres to best practices for optimizing Lambda container images through a multi-stage build. The final image employs a tag called 3.0-prod as its base, excluding development dependencies to minimize the image size. Create the Lambda-compatible container image using the following command.

docker build -f Dockerfile.lambda -t lambda-ruby-invoice-generator .

This concludes the modifications to the Dockerfile. We have introduced a new dependency on the runtime interface client and designated it as our container’s entrypoint. Should you wish to delve deeper into this topic, consider checking out another blog post here as well as the authoritative source on the subject here. Additionally, you can find an excellent resource on fulfillment center safety and training here.


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *