Définir des gestionnaires de fonctions Lambda en Rust - AWS Lambda

Définir des gestionnaires de fonctions Lambda en Rust

Note

Le client d’exécution Rust est un package expérimental. Il est susceptible d’être modifié et n’est destiné qu’à des fins d’évaluation.

Le gestionnaire de fonction Lambda est la méthode dans votre code de fonction qui traite les événements. Lorsque votre fonction est invoquée, Lambda exécute la méthode du gestionnaire. Votre fonction s’exécute jusqu’à ce que le gestionnaire renvoie une réponse, se ferme ou expire.

Cette page explique comment utiliser les gestionnaires de fonctions Lambda dans Rust, notamment l’initialisation du projet, les conventions de dénomination et les pratiques exemplaires. Cette page inclut également un exemple de fonction Lambda Rust qui collecte des informations relatives à une commande, produit un reçu sous forme de fichier texte et place ce fichier dans un compartiment Amazon Simple Storage Service (S3). Pour plus d’informations sur le déploiement de votre fonction après l’avoir écrite, consultez Déploiement de fonctions Lambda Rust avec des archives de fichiers .zip.

Configuration de votre projet de gestionnaire Rust

Lorsque vous utilisez des fonctions Lambda en Rust, le processus consiste à écrire votre code, à le compiler et à déployer les artefacts compilés sur Lambda. Le moyen le plus simple de configurer un projet de gestionnaire Lambda dans Rust est d’utiliser l’environnement d’exécution AWS Lambda pour Rust. Malgré son nom, l’environnement d’exécution AWS Lambda pour Rust n’est pas un environnement d’exécution géré au même sens que dans Lambda pour Python, Java ou Node.js. Au lieu de cela, l’environnement d’exécution AWS Lambda pour Rust est un conteneur (lambda_runtime) qui permet d’écrire des fonctions Lambda en Rust et de s’interfacer avec l’environnement d’exécution AWS Lambda.

Utilisez la commande suivante pour installer l’environnement d’exécution AWS Lambda pour Rust :

cargo install cargo-lambda

Après avoir installé cargo-lambda, utilisez la commande suivante pour initialiser un nouveau projet de gestionnaire de fonction Lambda :

cargo lambda new example-rust

Lorsque vous exécutez cette commande, l’interface de ligne de commande (CLI) vous pose quelques questions concernant votre fonction Lambda :

  • Fonction HTTP : si vous avez l’intention d’invoquer votre fonction via API Gateway ou une URL de fonction, répondez Oui. Dans le cas contraire, répondez Non. Dans l’exemple de code de cette page, nous invoquons notre fonction avec un événement JSON personnalisé. Nous répondons donc Non.

  • Type d’événement : si vous avez l’intention d’utiliser une forme d’événement prédéfinie pour invoquer votre fonction, sélectionnez le type d’événement attendu approprié. Sinon, laissez cette option vide. Dans l’exemple de code de cette page, nous invoquons notre fonction avec un événement JSON personnalisé. Nous laissons donc cette option vide.

Une fois la commande exécutée avec succès, entrez dans le répertoire principal de votre projet :

cd example-rust

Cette commande génère un fichier generic_handler.rs et un fichier main.rs dans le répertoire src. generic_handler.rs peut être utilisé pour personnaliser un gestionnaire d’événements générique. Le fichier main.rs contient la logique de votre application principale. Le fichier Cargo.toml contient des métadonnées relatives à votre package et répertorie ses dépendances externes.

Exemple de fonction Lambda Rust

L’exemple de code de fonction Lambda Rust suivant prend en compte les informations relatives à une commande, produit un reçu sous forme de fichier texte et place ce fichier dans un compartiment Amazon S3.

Exemple main.rsFonction Lambda
use aws_sdk_s3::{Client, primitives::ByteStream}; use lambda_runtime::{run, service_fn, Error, LambdaEvent}; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::env; #[derive(Deserialize, Serialize)] struct Order { order_id: String, amount: f64, item: String, } async fn function_handler(event: LambdaEvent<Value>) -> Result<String, Error> { let payload = event.payload; // Deserialize the incoming event into Order struct let order: Order = serde_json::from_value(payload)?; let bucket_name = env::var("RECEIPT_BUCKET") .map_err(|_| "RECEIPT_BUCKET environment variable is not set")?; let receipt_content = format!( "OrderID: {}\nAmount: ${:.2}\nItem: {}", order.order_id, order.amount, order.item ); let key = format!("receipts/{}.txt", order.order_id); let config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await; let s3_client = Client::new(&config); upload_receipt_to_s3(&s3_client, &bucket_name, &key, &receipt_content).await?; Ok("Success".to_string()) } async fn upload_receipt_to_s3( client: &Client, bucket_name: &str, key: &str, content: &str, ) -> Result<(), Error> { client .put_object() .bucket(bucket_name) .key(key) .body(ByteStream::from(content.as_bytes().to_vec())) // Fixed conversion .content_type("text/plain") .send() .await?; Ok(()) } #[tokio::main] async fn main() -> Result<(), Error> { run(service_fn(function_handler)).await }

Ce fichier main.rs comprend les sections suivantes :

  • Instructions use : utilisez-les pour importer des conteneurs (crates) et méthodes Rust que votre fonction Lambda requiert.

  • #[derive(Deserialize, Serialize)] : définissez la forme de l’événement d’entrée attendu dans cette structure Rust.

  • async fn function_handler(event: LambdaEvent<Value>) -> Result<String, Error> : il s’agit de la méthode de gestion principale, qui contient la logique principale de votre application.

  • async fn upload_receipt_to_s3 (...) : il s’agit d’une méthode auxiliaire référencée par la méthode function_handler principale.

  • #[tokio::main] : il s’agit d’une macro qui marque le point d’entrée d’un programme Rust. Elle met également en place un environnement d’exécution Tokio, qui permet à votre méthode main() d’utiliser async/await et de s’exécuter de manière asynchrone.

  • async fn main() -> Result<(), Error> : la fonction main() est le point d’entrée de votre code. À l’intérieur, nous spécifions function_handler comme méthode de gestionnaire principal.

Le fichier Cargo.toml suivant accompagne cette fonction.

[package] name = "example-rust" version = "0.1.0" edition = "2024" [dependencies] aws-config = "1.5.18" aws-sdk-s3 = "1.78.0" lambda_runtime = "0.13.0" serde = { version = "1", features = ["derive"] } serde_json = "1" tokio = { version = "1", features = ["full"] }

Pour que cette fonction fonctionne correctement, son rôle d’exécution doit autoriser l’action s3:PutObject. Assurez-vous également de définir la variable d’environnement RECEIPT_BUCKET. Après une invocation réussie, le compartiment Amazon S3 doit contenir un fichier de reçu.

Définitions de classe valides pour les gestionnaires Rust

Dans la plupart des cas, les signatures du gestionnaire Lambda que vous définissez dans Rust auront le format suivant :

async fn function_handler(event: LambdaEvent<T>) -> Result<U, Error>

Pour ce gestionnaire :

  • Le nom de ce gestionnaire est function_handler.

  • L’entrée singulière du gestionnaire est un événement, et est de type LambdaEvent<T>.

    • LambdaEvent est un wrapper qui vient du conteneur (crate) lambda_runtime. L’utilisation de ce wrapper vous donne accès à l’objet de contexte, qui inclut des métadonnées spécifiques à Lambda, comme l’ID de demande de l’invocation.

    • T est le type d’événement désérialisé. Par exemple, cela peut être serde_json::Value, qui permet au gestionnaire d’accepter n’importe quelle entrée JSON générique. Il peut également s’agir d’un type comme ApiGatewayProxyRequest si votre fonction attend un type d’entrée spécifique et prédéfini.

  • Le type de retour du gestionnaire est Result<U, Error>.

    • U est le type de sortie désérialisé. U doit implémenter la caractéristique serde::Serialize afin que Lambda puisse convertir la valeur de retour en JSON. Par exemple, U peut être un type simple comme String, serde_json::Value ou une structure personnalisée tant qu’il implémente Serialize. Lorsque votre code atteint une instruction Ok(U), cela indique une exécution réussie et votre fonction renvoie une valeur de type U.

    • Lorsque votre code rencontre une erreur (c’est-à-dire Err(Error)), votre fonction enregistre l’erreur dans Amazon CloudWatch et renvoie une réponse d’erreur de type Error.

Dans notre exemple, la signature du gestionnaire ressemble à ce qui suit :

async fn function_handler(event: LambdaEvent<Value>) -> Result<String, Error>

Les autres signatures de gestionnaire valides peuvent présenter les caractéristiques suivantes :

  • Omission du wrapper LambdaEvent : si vous omettez LambdaEvent, vous perdez l’accès à l’objet de contexte Lambda dans votre fonction. Voici un exemple de ce type de signature :

    async fn handler(event: serde_json::Value) -> Result<String, Error>
  • Utilisation du type d’unité comme entrée : pour Rust, vous pouvez utiliser le type d’unité pour représenter une entrée vide. Cela est couramment utilisé pour les fonctions avec des invocations périodiques et planifiées. Voici un exemple de ce type de signature :

    async fn handler(_: ()) -> Result<Value, Error>

Convention de nommage du gestionnaire

Les gestionnaires Lambda dans Rust n’ont pas de restrictions de dénomination strictes. Bien que vous puissiez utiliser n’importe quel nom pour votre gestionnaire, les noms de fonctions en Rust suivent généralement le format de casse snake_case.

Pour les applications plus petites, comme dans cet exemple, vous pouvez utiliser un seul fichier main.rs pour contenir l’ensemble de votre code. Pour les projets plus importants, main.rs doit contenir le point d’entrée de votre fonction, mais vous pouvez avoir des fichiers supplémentaires pour séparer votre code en modules logiques. Par exemple, vous pouvez avoir la structure de fichiers suivante :

/example-rust │── src/ │ ├── main.rs # Entry point │ ├── handler.rs # Contains main handler │ ├── services.rs # [Optional] Back-end service calls │ ├── models.rs # [Optional] Data models │── Cargo.toml

Définition et accès à l’objet d’événement d’entrée

JSON est le format d’entrée le plus courant et standard pour les fonctions Lambda. Dans cet exemple, la fonction exige une entrée similaire à l’exemple suivant :

{ "order_id": "12345", "amount": 199.99, "item": "Wireless Headphones" }

En Rust, vous pouvez définir la forme de l’événement d’entrée attendu dans une structure. Dans cet exemple, nous définissons la structure suivante pour représenter un Order :

#[derive(Deserialize, Serialize)] struct Order { order_id: String, amount: f64, item: String, }

Cette structure correspond à la forme d’entrée attendue. Dans cet exemple, la macro #[derive(Deserialize, Serialize)] génère automatiquement du code pour la sérialisation et la désérialisation. Cela signifie que nous pouvons désérialiser le type JSON d’entrée générique dans notre structure à l’aide de la méthode serde_json::from_value(). Ceci est illustré dans les premières lignes du gestionnaire :

async fn function_handler(event: LambdaEvent<Value>) -> Result<String, Error> { let payload = event.payload; // Deserialize the incoming event into Order struct let order: Order = serde_json::from_value(payload)?; ... }

Vous pouvez ensuite accéder aux champs de l’objet. Par exemple, order.order_id récupère la valeur de order_id à partir de l’entrée d’origine.

Types d’événements d’entrée prédéfinis

De nombreux types d’événements d’entrée prédéfinis sont disponibles dans le conteneur (crate) aws_lambda_events. Par exemple, si vous avez l’intention d’invoquer votre fonction avec API Gateway, y compris l’importation suivante :

use aws_lambda_events::event::apigw::ApiGatewayProxyRequest;

assurez-vous que votre gestionnaire principal utilise la signature suivante :

async fn handler(event: LambdaEvent<ApiGatewayProxyRequest>) -> Result<String, Error> { let body = event.payload.body.unwrap_or_default(); ... }

Reportez-vous au conteneur aws_lambda_events pour plus d’informations sur les autres types d’événements d’entrée prédéfinis.

Accès et utilisation de l’objet de contexte Lambda

L’objet de contexte Lambda contient des informations sur l’invocation, la fonction et l’environnement d’exécution. En Rust, le wrapper LambdaEvent inclut l’objet de contexte. Par exemple, vous pouvez utiliser l’objet de contexte pour récupérer l’ID de demande de l’invocation en cours à l’aide du code suivant :

async fn function_handler(event: LambdaEvent<Value>) -> Result<String, Error> { let request_id = event.context.request_id; ... }

Pour plus d’informations sur la copie d’objets, consultez Utilisation de l’objet de contexte Lambda pour récupérer les informations de la fonction Rust.

Utilisation d’Kit AWS SDK pour Rust dans votre gestionnaire

Vous utiliserez souvent les fonctions Lambda pour interagir avec d’autres ressources AWS ou pour les mettre à jour. Le moyen le plus simple d’interagir avec ces ressources est d’utiliser Kit AWS SDK pour Rust.

Pour ajouter des dépendances du SDK à votre fonction, ajoutez-les dans votre fichier Cargo.toml. Nous vous recommandons de n’ajouter que les bibliothèques dont vous avez besoin pour votre fonction. Dans l’exemple de code précédent, nous avons utilisé aws_sdk_s3::Client. Dans le fichier Cargo.toml, vous pouvez ajouter cette dépendance en ajoutant la ligne suivante sous la section [dependencies] :

aws-sdk-s3 = "1.78.0"
Note

La version n’est peut-être pas la plus récente. Choisissez la version adéquate pour votre application.

Importez ensuite les dépendances directement dans votre code :

use aws_sdk_s3::{Client, primitives::ByteStream};

L’exemple de code initialise ensuite un client Amazon S3 comme suit :

let config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await; let s3_client = Client::new(&config);

Après avoir initialisé votre client SDK, vous pouvez l’utiliser pour interagir avec d’autres services AWS. L’exemple de code appelle l’API PutObject Amazon S3 dans la fonction d’assistance upload_receipt_to_s3.

Accès aux variables d’environnement

Dans le code de votre gestionnaire, vous pouvez référencer n’importe quelle variable d’environnement à l’aide de la méthode env::var. Dans cet exemple, nous référençons la variable d’environnement RECEIPT_BUCKET définie à l’aide de la ligne de code suivante :

let bucket_name = env::var("RECEIPT_BUCKET") .map_err(|_| "RECEIPT_BUCKET environment variable is not set")?;

Utilisation de l’état partagé

Vous pouvez déclarer des variables partagées indépendantes du code du gestionnaire de votre fonction Lambda. Ces variables peuvent vous aider à charger des informations d’état pendant le Phase d’initialisation, avant que votre fonction ne reçoive des événements. Par exemple, vous pouvez modifier le code de cette page pour utiliser l’état partagé lors de l’initialisation du client Amazon S3 en mettant à jour la fonction main et la signature du gestionnaire :

async fn function_handler(client: &Client, event: LambdaEvent<Value>) -> Result<String, Error> { ... upload_receipt_to_s3(client, &bucket_name, &key, &receipt_content).await?; ... } ... #[tokio::main] async fn main() -> Result<(), Error> { let shared_config = aws_config::from_env().load().await; let client = Client::new(&shared_config); let shared_client = &client; lambda_runtime::run(service_fn(move |event: LambdaEvent<Request>| async move { handler(&shared_client, event).await })) .await

Pratiques exemplaires en matière de code pour les fonctions Lambda Rust

Respectez les directives de la liste suivante pour utiliser les pratiques exemplaires de codage lors de la création de vos fonctions Lambda :

  • Séparez le gestionnaire Lambda de votre logique principale. Cela vous permet de créer une fonction testable plus unitaire.

  • Réduisez la complexité de vos dépendances. Privilégiez les infrastructures plus simples qui se chargent rapidement au démarrage de l’environnement d’exécution.

  • Réduisez la taille de votre package de déploiement selon ses besoins relatifs à l’environnement d’exécution. Cela contribue à réduire le temps nécessaire au téléchargement et à la décompression de votre package de déploiement avant l'invocation.

Tirez parti de la réutilisation de l’environnement d’exécution pour améliorer les performances de votre fonction. Initialisez les clients SDK et les connexions à la base de données en dehors du gestionnaire de fonctions et mettez en cache les actifs statiques localement dans le répertoire /tmp. Les invocations ultérieures traitées par la même instance de votre fonction peuvent réutiliser ces ressources. Cela permet d’économiser des coûts, tout en réduisant le temps d’exécution de la fonction.

Pour éviter des éventuelles fuites de données entre les invocations, n’utilisez pas l’environnement d’exécution pour stocker des données utilisateur, des événements ou d’autres informations ayant un impact sur la sécurité. Si votre fonction repose sur un état réversible qui ne peut pas être stocké en mémoire dans le gestionnaire, envisagez de créer une fonction distincte ou des versions distinctes d’une fonction pour chaque utilisateur.

Utilisez une directive keep-alive pour maintenir les connexions persistantes. Lambda purge les connexions inactives au fil du temps. Si vous tentez de réutiliser une connexion inactive lorsque vous invoquez une fonction, cela entraîne une erreur de connexion. Pour maintenir votre connexion persistante, utilisez la directive Keep-alive associée à votre environnement d’exécution. Pour obtenir un exemple, consultez Réutilisation des connexions avec Keep-Alive dans Node.js.

Utilisez des variables d’environnement pour transmettre des paramètres opérationnels à votre fonction. Par exemple, si vous écrivez dans un compartiment Amazon S3 au lieu de coder en dur le nom du compartiment dans lequel vous écrivez, configurez le nom du compartiment comme variable d’environnement.

Évitez d’utiliser des invocations récursives dans votre fonction Lambda, lorsque la fonction s’invoque elle-même ou démarre un processus susceptible de l’invoquer à nouveau. Cela peut entraîner un volume involontaire d’invocations de fonction et des coûts accrus. Si vous constatez un volume involontaire d’invocations, définissez immédiatement la simultanéité réservée à la fonction sur 0 afin de limiter toutes les invocations de la fonction, pendant que vous mettez à jour le code.

N’utilisez pas d’API non publiques non documentées dans le code de votre fonction Lambda. Pour les exécutions gérées AWS Lambda, Lambda applique périodiquement des mises à jour de sécurité et fonctionnelles aux API internes de Lambda. Ces mises à jour internes de l’API peuvent être incompatibles avec les versions antérieures, entraînant des conséquences imprévues telles que des échecs d’invocation si votre fonction dépend de ces API non publiques. Consultez la Référence d’API pour obtenir la liste des API accessibles au public.

Écriture du code idempotent. L’écriture de code idempotent pour vos fonctions garantit ne gestion identique des événements dupliqués. Votre code doit valider correctement les événements et gérer correctement les événements dupliqués. Pour de plus amples informations, veuillez consulterComment faire en sorte que ma fonction Lambda soit idempotente ?.