Introduction: Why Rust for Serverless is a Breakthrough
\n\nServerless architecture has long become the standard for building scalable and cost-effective applications. However, the traditional choice of languages (Python, Node.js, Java) often faces the problem of cold start and high memory consumption. Rust, with its zero-cost abstractions and lack of a garbage collector, offers a revolutionary solution. In this article, we will break down how to create, configure, and deploy serverless functions in Rust for AWS Lambda — from tool installation to production releases.
\n\nBy the end of this article, you will be able to:
\n- \n
- Set up the environment for Rust + AWS Lambda development \n
- Write an efficient function with event handling \n
- Optimize the binary for minimal cold start time \n
- Deploy the function to AWS using Cargo Lambda \n
Environment Setup: Cargo Lambda and AWS CLI
\n\nTo work with Rust on AWS Lambda, we need a special tool — cargo-lambda. It automates building, testing, and deployment. Let's install it via Cargo:
cargo install cargo-lambda\n\nAlso, make sure you have aws-cli (configured with IAM keys) and Docker (for local testing) installed. Let's create a new project:
cargo lambda new my-rust-lambda\ncd my-rust-lambda\n\nThe command will create a basic structure with a Cargo.toml file and src/main.rs. Pay attention to the dependencies in Cargo.toml:
[package]\nname = "my-rust-lambda"\nversion = "0.1.0"\nedition = "2021"\n\n[dependencies]\nlambda_runtime = "0.11"\ntokio = { version = "1", features = ["macros", "rt-multi-thread"] }\nserde = { version = "1.0", features = ["derive"] }\nserde_json = "1.0"\ntracing = { version = "0.1", features = ["log"] }\ntracing-subscriber = { version = "0.3", features = ["fmt"] }\n\nThe key library is lambda_runtime. It provides the LambdaHandler trait, which we will implement.
Writing the First Function: Handling an API Gateway Request
\n\nLet's consider a classic scenario: a function that receives JSON with a user's name and returns a greeting. Open src/main.rs and write the following code:
use lambda_runtime::{run, service_fn, Error, LambdaEvent};\nuse serde::{Deserialize, Serialize};\n\n#[derive(Deserialize)]\nstruct Request {\n name: String,\n}\n\n#[derive(Serialize)]\nstruct Response {\n message: String,\n}\n\nasync fn function_handler(event: LambdaEvent<Request>) -> Result<Response, Error> {\n let name = event.payload.name;\n let message = format!("Hello, {}! You just launched Rust on Lambda!", name);\n Ok(Response { message })\n}\n\n#[tokio::main]\nasync fn main() -> Result<(), Error> {\n tracing_subscriber::fmt()\n .with_max_level(tracing::Level::INFO)\n .init();\n\n run(service_fn(function_handler)).await\n}\n\nLet's break down the key points:
\n- \n
LambdaEvent<Request>— a structure containing the request body and execution context. \n #[tokio::main]— the asynchronous runtime required for the Lambda runtime to work. \n serde— deserialization of incoming JSON and serialization of the response. \n
Let's test locally:
\n\ncargo lambda watch\n\nThen in another terminal:
\n\ncargo lambda invoke --data-ascii '{"name": "Alexey"}'\n\nYou should see: {"message":"Hello, Alexey! You just launched Rust on Lambda!"}
Optimizing Cold Start and Binary Size
\n\nThe main advantage of Rust is minimal cold start. However, to achieve this, you need to build the bi