Repository: cargo-lambda/cargo-lambda (language: python)

Issue:
Improve configuration changes on deploys
Hello,

I see that we can provide the IAM role on the command line, but don't see how to provide the timeout, memory... & other lambda config. 
Is it possible to set those as metadata in the Cargo.toml file? The issue right now is that if I update a function, it set it with the default 30s and create a new version. Could we also set it so that it does not publish a new version?

_Originally posted by @Chandrian in https://github.com/cargo-lambda/cargo-lambda/discussions/243_


Discussion / hints:

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index 6c0af97b..105c7e87 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -772,6 +772,9 @@ dependencies = [
  "remove_dir_all 0.7.0",
  "serde",
  "serde_json",
+ "strum",
+ "strum_macros",
+ "thiserror",
  "tracing",
 ]
 
diff --git a/crates/cargo-lambda-deploy/src/functions.rs b/crates/cargo-lambda-deploy/src/functions.rs
index 855418ce..b4025346 100644
--- a/crates/cargo-lambda-deploy/src/functions.rs
+++ b/crates/cargo-lambda-deploy/src/functions.rs
@@ -2,6 +2,10 @@ use super::DeployResult;
 use crate::roles;
 use aws_sdk_s3::{types::ByteStream, Client as S3Client};
 use cargo_lambda_interactive::progress::Progress;
+use cargo_lambda_metadata::{
+    cargo::function_deploy_metadata,
+    lambda::{Memory, Timeout, Tracing},
+};
 use cargo_lambda_remote::{
     aws_sdk_config::SdkConfig,
     aws_sdk_lambda::{
@@ -23,11 +27,11 @@ use clap::{Args, ValueHint};
 use miette::{IntoDiagnostic, Result, WrapErr};
 use serde::Serialize;
 use std::{
+    collections::HashMap,
     fs::File,
     io::{BufRead, BufReader},
     path::PathBuf,
 };
-use strum_macros::{Display, EnumString};
 use tokio::time::{sleep, Duration};
 use uuid::Uuid;
 
@@ -36,18 +40,11 @@ enum FunctionAction {
     Update(Box<GetFunctionOutput>),
 }
 
-#[derive(Clone, Debug, Display, EnumString)]
-#[strum(ascii_case_insensitive)]
-pub enum Tracing {
-    Active,
-    Passthrough,
-}
-
 #[derive(Args, Clone, Debug)]
 pub struct FunctionDeployConfig {
     /// Memory allocated for the function
-    #[clap(long)]
-    pub memory_size: Option<i32>,
+    #[clap(long, alias = "memory-size")]
+    pub memory: Option<Memory>,
 
     /// Enable function URL for this function
     #[clap(long)]
@@ -58,8 +55,8 @@ pub struct FunctionDeployConfig {
     pub disable_function_url: bool,
 
     /// How long the function can be running for, in seconds
-    #[clap(long, default_value = "30")]
-    pub timeout: i32,
+    #[clap(long)]
+    pub timeout: Option<Timeout>,
 
     /// Option to add one or many environment variables, allows multiple repetitions
     /// Use VAR_KEY=VAR_VALUE as format
@@ -71,16 +68,16 @@ pub struct FunctionDeployConfig {
     pub env_file: Option<PathBuf>,
 
     /// Tracing mode with X-Ray
-    #[clap(long, default_value_t = Tracing::Active)]
-    pub tracing: Tracing,
+    #[clap(long)]
+    pub tracing: Option<Tracing>,
 
     /// IAM Role associated with the function
-    #[clap(long)]
-    pub iam_role: Option<String>,
+    #[clap(long, alias = "iam-role")]
+    pub role: Option<String>,
 
     /// Lambda Layer ARN to associate the deployed function with
-    #[clap(long)]
-    pub layer_arn: Option<Vec<String>>,
+    #[clap(long, alias = "layer-arn")]
+    pub layer: Option<Vec<String>>,
 }
 
 #[derive(Serialize)]
@@ -102,6 +99,8 @@ impl std::fmt::Display for DeployOutput {
 #[allow(clippy::too_many_arguments)]
 pub(crate) async fn deploy(
     name: &str,
+    binary_name: &str,
+    manifest_path: &PathBuf,
     function_config: &FunctionDeployConfig,
     remote_config: &RemoteConfig,
     sdk_config: &SdkConfig,
@@ -114,6 +113,8 @@ pub(crate) async fn deploy(
 
     let (function_arn, version) = upsert_function(
         name,
+        binary_name,
+        manifest_path,
         &client,
         function_config,
         remote_config,
@@ -154,6 +155,8 @@ pub(crate) async fn deploy(
 #[allow(clippy::too_many_arguments)]
 async fn upsert_function(
     name: &str,
+    binary_name: &str,
+    manifest_path: &PathBuf,
     client: &LambdaClient,
     function_config: &FunctionDeployConfig,
     remote_config: &RemoteConfig,
@@ -165,6 +168,42 @@ async fn upsert_function(
 ) -> Result<(String, String)> {
     let current_function = client.get_function().function_name(name).send().await;
 
+    let mut deploy_metadata = function_deploy_metadata(manifest_path, binary_name)?;
+
+    if let Some(tracing) = &function_config.tracing {
+        if &deploy_metadata.tracing != tracing {
+            deploy_metadata.tracing = tracing.clone();
+        }
+    }
+
+    if function_config.role.is_some() {
+        deploy_metadata.iam_role = function_config.role.clone();
+    }
+
+    if function_config.memory.is_some() {
+        deploy_metadata.memory = function_config.memory.clone();
+    }
+
+    if let Some(timeout) = &function_config.timeout {
+        if !timeout.is_zero() {
+            deploy_metadata.timeout = timeout.clone()
+        }
+    }
+
+    if function_config.env_file.is_some() {
+        deploy_metadata.env_file = function_config.env_file.clone();
+    }
+
+    let environment = function_environment(
+        deploy_metadata.env.clone(),
+        &deploy_metadata.env_file,
+        &function_config.env_var,
+    )?;
+
+    if function_config.layer.is_some() {
+        deploy_metadata.layers = function_config.layer.clone();
+    }
+
     let action = match current_function {
         Ok(fun) => FunctionAction::Update(Box::new(fun)),
         Err(no_fun) if function_doesnt_exist_error(&no_fun) => FunctionAction::Create,
@@ -176,17 +215,17 @@ async fn upsert_function(
     };
 
     let tracing_config = TracingConfig::builder()
-        .mode(function_config.tracing.to_string().as_str().into())
+        .mode(deploy_metadata.tracing.to_string().as_str().into())
         .build();
 
     let (arn, version) = match action {
         FunctionAction::Create => {
-            let (iam_role, is_new_role) = match &function_config.iam_role {
+            let (iam_role, is_new_role) = match &deploy_metadata.iam_role {
                 None => (roles::create(sdk_config, progress).await?, true),
                 Some(role) => (role.clone(), false),
             };
 
-            tracing::debug!(role_arn = ?iam_role, "using iam role");
+            tracing::debug!(role_arn = ?iam_role, config = ?deploy_metadata, "creating new function");
             progress.set_message("deploying function");
 
             let code = match &s3_bucket {
@@ -216,6 +255,9 @@ async fn upsert_function(
 
             let mut output = None;
             for attempt in 2..5 {
+                let memory = deploy_metadata.memory.clone().map(Into::into);
+                let timeout = deploy_metadata.timeout.clone().into();
+
                 let result = client
                     .create_function()
                     .runtime(Runtime::Providedal2)
@@ -225,14 +267,11 @@ async fn upsert_function(
                     .architectures(architecture.clone())
                     .code(code.clone())
                     .publish(true)
-                    .set_memory_size(function_config.memory_size)
-                    .timeout(function_config.timeout)
+                    .set_memory_size(memory)
+                    .timeout(timeout)
                     .tracing_config(tracing_config.clone())
-                    .environment(function_environment(
-                        &function_config.env_file,
-                        &function_config.env_var,
-                    )?)
-                    .set_layers(function_config.layer_arn.clone())
+                    .environment(environment.clone())
+                    .set_layers(deploy_metadata.layers.clone())
                     .send()
                     .await;
 
@@ -290,31 +329,30 @@ async fn upsert_function(
             let mut update_config = false;
             let mut builder = client.update_function_configuration().function_name(name);
 
-            if let Some(iam_role) = &function_config.iam_role {
+            if let Some(iam_role) = &deploy_metadata.iam_role {
                 builder = builder.role(iam_role);
             }
 
-            if function_config.memory_size.is_some()
-                && conf.memory_size != function_config.memory_size
-            {
+            let memory = deploy_metadata.memory.clone().map(Into::into);
+            if memory.is_some() && conf.memory_size != memory {
                 update_config = true;
-                builder = builder.set_memory_size(function_config.memory_size);
+                builder = builder.set_memory_size(memory);
             }
 
-            if conf.timeout.unwrap_or_default() != function_config.timeout {
+            let timeout: i32 = deploy_metadata.timeout.clone().into();
+            if conf.timeout.unwrap_or_default() != timeout {
                 update_config = true;
-                builder = builder.timeout(function_config.timeout);
+                builder = builder.timeout(timeout);
             }
 
-            if should_update_layers(&function_config.layer_arn, &conf) {
+            if should_update_layers(&deploy_metadata.layers, &conf) {
                 update_config = true;
-                builder = builder.set_layers(function_config.layer_arn.clone());
+                builder = builder.set_layers(deploy_metadata.layers.clone());
             }
 
-            let env = function_environment(&function_config.env_file, &function_config.env_var)?;
-            if env.variables != conf.environment.map(|e| e.variables).unwrap_or_default() {
+            if environment.variables != conf.environment.map(|e| e.variables).unwrap_or_default() {
                 update_config = true;
-                builder = builder.environment(env);
+                builder = builder.environment(environment);
             }
 
             if tracing_config.mode != conf.tracing_config.map(|t| t.mode).unwrap_or_default() {
@@ -329,6 +367,9 @@ async fn upsert_function(
                     .await
                     .into_diagnostic()
                     .wrap_err("failed to update function configuration")?;
+
+                wait_for_ready_state(client, name, &remote_config.alias, progress).await?;
+                progress.set_message("deploying function");
             }
 
             let mut builder = client.update_function_code().function_name(name);
@@ -571,10 +612,12 @@ pub(crate) async fn delete_function_url_config(
 }
 
 pub(crate) fn function_environment(
+    variables: HashMap<String, String>,
     env_file: &Option<PathBuf>,
     env_vars: &Option<Vec<String>>,
 ) -> Result<Environment> {
-    let mut env = Environment::builder();
+    let mut env = Environment::builder().set_variables(Some(variables));
+
     if let Some(path) = env_file {
         let file = File::open(path)
             .into_diagnostic()
diff --git a/crates/cargo-lambda-deploy/src/lib.rs b/crates/cargo-lambda-deploy/src/lib.rs
index e34649ac..9437af5d 100644
--- a/crates/cargo-lambda-deploy/src/lib.rs
+++ b/crates/cargo-lambda-deploy/src/lib.rs
@@ -144,8 +144,11 @@ impl Deploy {
             )
             .await
         } else {
+            let binary_name = self.binary_name.clone().unwrap_or_else(|| name.clone());
             functions::deploy(
                 &name,
+                &binary_name,
+                &self.manifest_path,
                 &self.function_config,
                 &self.remote_config,
                 &sdk_config,
diff --git a/crates/cargo-lambda-metadata/Cargo.toml b/crates/cargo-lambda-metadata/Cargo.toml
index 21a2aa04..4cbeab3f 100644
--- a/crates/cargo-lambda-metadata/Cargo.toml
+++ b/crates/cargo-lambda-metadata/Cargo.toml
@@ -22,4 +22,7 @@ miette = "4.7.1"
 remove_dir_all = "0.7.0"
 serde.workspace = true
 serde_json.workspace = true
+strum.workspace = true
+strum_macros.workspace = true
+thiserror.workspace = true
 tracing.workspace = true
diff --git a/crates/cargo-lambda-metadata/src/cargo/mod.rs b/crates/cargo-lambda-metadata/src/cargo.rs
similarity index 57%
rename from crates/cargo-lambda-metadata/src/cargo/mod.rs
rename to crates/cargo-lambda-metadata/src/cargo.rs
index 3e8b6a1d..9b5b861c 100644
--- a/crates/cargo-lambda-metadata/src/cargo/mod.rs
+++ b/crates/cargo-lambda-metadata/src/cargo.rs
@@ -1,11 +1,16 @@
 use cargo_metadata::{Metadata as CargoMetadata, Package};
-use miette::{IntoDiagnostic, Result, WrapErr};
+use miette::{IntoDiagnostic, Result};
 use serde::Deserialize;
 use std::{
     collections::{HashMap, HashSet},
     path::{Path, PathBuf},
 };
 
+use crate::{
+    error::MetadataError,
+    lambda::{Memory, Timeout, Tracing},
+};
+
 #[derive(Default, Deserialize)]
 #[non_exhaustive]
 pub struct Metadata {
@@ -13,20 +18,40 @@ pub struct Metadata {
     pub lambda: LambdaMetadata,
 }
 
-#[derive(Clone, Default, Deserialize)]
+#[derive(Clone, Debug, Default, Deserialize)]
 #[non_exhaustive]
 pub struct LambdaMetadata {
-    #[serde(default)]
-    pub env: HashMap<String, String>,
+    #[serde(flatten)]
+    pub package: PackageMetadata,
     #[serde(default)]
     pub bin: HashMap<String, PackageMetadata>,
 }
 
-#[derive(Clone, Default, Deserialize)]
+#[derive(Clone, Debug, Default, Deserialize)]
 #[non_exhaustive]
 pub struct PackageMetadata {
     #[serde(default)]
     pub env: HashMap<String, String>,
+    #[serde(default)]
+    pub deploy: DeployConfig,
+}
+
+#[derive(Clone, Debug, Default, Deserialize)]
+pub struct DeployConfig {
+    #[serde(default)]
+    pub memory: Option<Memory>,
+    #[serde(default)]
+    pub timeout: Timeout,
+    #[serde(default)]
+    pub env: HashMap<String, String>,
+    #[serde(default)]
+    pub env_file: Option<PathBuf>,
+    #[serde(default)]
+    pub tracing: Tracing,
+    #[serde(default, alias = "role")]
+    pub iam_role: Option<String>,
+    #[serde(default)]
+    pub layers: Option<Vec<String>>,
 }
 
 /// Extract all the binary target names from a Cargo.toml file
@@ -62,13 +87,12 @@ fn load_metadata<P: AsRef<Path>>(manifest_path: P) -> Result<CargoMetadata> {
 
     // try to split manifest path and assign current_dir to enable parsing a project-specific
     // cargo config
-    match (
-        manifest_path.as_ref().parent(),
-        manifest_path.as_ref().file_name(),
-    ) {
+    let manifest_ref = manifest_path.as_ref();
+    let parent = manifest_ref.parent().map(|p| p.to_path_buf());
+    match (parent, manifest_ref.file_name()) {
         (Some(mut project_dir), Some(manifest_file)) => {
-            if project_dir == Path::new("") {
-                project_dir = Path::new("./")
+            if !project_dir.is_dir() {
+                project_dir = std::env::current_dir().into_diagnostic()?;
             }
             metadata_cmd.current_dir(project_dir);
             metadata_cmd.manifest_path(manifest_file);
@@ -76,7 +100,7 @@ fn load_metadata<P: AsRef<Path>>(manifest_path: P) -> Result<CargoMetadata> {
         _ => {
             // fall back to using the manifest_path without changing the dir
             // this means there will not be any proejct-specific config parsing
-            metadata_cmd.manifest_path(manifest_path.as_ref());
+            metadata_cmd.manifest_path(manifest_ref);
         }
     }
 
@@ -86,7 +110,7 @@ fn load_metadata<P: AsRef<Path>>(manifest_path: P) -> Result<CargoMetadata> {
 /// Create a HashMap of environment varibales from the package and workspace manifest
 /// See the documentation to learn about how we use this metadata:
 /// https://www.cargo-lambda.info/commands/watch.html#environment-variables
-pub fn function_metadata<P: AsRef<Path>>(
+pub fn function_environment_metadata<P: AsRef<Path>>(
     manifest_path: P,
     name: Option<&str>,
 ) -> Result<HashMap<String, String>> {
@@ -95,7 +119,7 @@ pub fn function_metadata<P: AsRef<Path>>(
         serde_json::from_value(metadata.workspace_metadata).unwrap_or_default();
 
     let mut env = HashMap::new();
-    env.extend(ws_metadata.env);
+    env.extend(ws_metadata.package.env);
 
     if let Some(name) = name {
         if let Some(res) = ws_metadata.bin.get(name) {
@@ -119,10 +143,9 @@ pub fn function_metadata<P: AsRef<Path>>(
 
             if target_matches {
                 let package_metadata: Metadata = serde_json::from_value(pkg.metadata.clone())
-                    .into_diagnostic()
-                    .wrap_err("invalid lambda metadata in Cargo.toml file")?;
+                    .map_err(MetadataError::InvalidCargoMetadata)?;
 
-                env.extend(package_metadata.lambda.env);
+                env.extend(package_metadata.lambda.package.env);
                 if let Some(res) = package_metadata.lambda.bin.get(name) {
                     env.extend(res.env.clone());
                 }
@@ -134,6 +157,48 @@ pub fn function_metadata<P: AsRef<Path>>(
     Ok(env)
 }
 
+/// Create a `DeployConfig` struct from Cargo metadata.
+/// This configuration can be overwritten by flags from the cli.
+pub fn function_deploy_metadata<P: AsRef<Path>>(
+    manifest_path: P,
+    name: &str,
+) -> Result<DeployConfig> {
+    let metadata = load_metadata(manifest_path)?;
+    let ws_metadata: LambdaMetadata =
+        serde_json::from_value(metadata.workspace_metadata).unwrap_or_default();
+
+    let mut config = ws_metadata.package.deploy;
+
+    if let Some(package_metadata) = ws_metadata.bin.get(name) {
+        merge_deploy_config(&mut config, &package_metadata.deploy);
+    }
+
+    for pkg in &metadata.packages {
+        for target in &pkg.targets {
+            let target_matches = target.name == name
+                && target.kind.iter().any(|kind| kind == "bin")
+                && pkg.metadata.is_object();
+
+            tracing::debug!(
+                name = name,
+                target_matches = target_matches,
+                "searching package metadata"
+            );
+
+            if target_matches {
+                let package_metadata: Metadata = serde_json::from_value(pkg.metadata.clone())
+                    .map_err(MetadataError::InvalidCargoMetadata)?;
+                let package_deploy = package_metadata.lambda.package.deploy;
+
+                merge_deploy_config(&mut config, &package_deploy);
+            }
+        }
+    }
+
+    tracing::debug!(config = ?config, "using deploy configuration from metadata");
+    Ok(config)
+}
+
 /// Load the main package in the project.
 /// It returns an error if the project includes from than one package.
 /// Use this function when the user didn't provide any funcion name
@@ -141,18 +206,39 @@ pub fn function_metadata<P: AsRef<Path>>(
 pub fn root_package<P: AsRef<Path>>(manifest_path: P) -> Result<Package> {
     let metadata = load_metadata(manifest_path)?;
     if metadata.packages.len() > 1 {
-        Err(miette::miette!(
-            "there are more than one package in the project, you must specify a function name"
-        ))
+        Err(MetadataError::MultiplePackagesInProject)?;
     } else if metadata.packages.is_empty() {
-        Err(miette::miette!("there are no packages in this project"))
-    } else {
-        Ok(metadata
-            .packages
-            .into_iter()
-            .next()
-            .expect("failed to extract the root package from the metadata"))
+        Err(MetadataError::MissingPackageInProject)?;
+    }
+
+    Ok(metadata
+        .packages
+        .into_iter()
+        .next()
+        .expect("failed to extract the root package from the metadata"))
+}
+
+fn merge_deploy_config(base: &mut DeployConfig, package_deploy: &DeployConfig) {
+    if package_deploy.memory.is_some() {
+        base.memory = package_deploy.memory.clone();
+    }
+    if !package_deploy.timeout.is_zero() {
+        base.timeout = package_deploy.timeout.clone();
+    }
+    base.env.extend(package_deploy.env.clone());
+    if package_deploy.env_file.is_some() && base.env_file.is_none() {
+        base.env_file = package_deploy.env_file.clone();
     }
+    if package_deploy.tracing != Tracing::default() {
+        base.tracing = package_deploy.tracing.clone();
+    }
+    if package_deploy.iam_role.is_some() {
+        base.iam_role = package_deploy.iam_role.clone();
+    }
+    if package_deploy.layers.is_some() {
+        base.layers = package_deploy.layers.clone();
+    }
+    tracing::debug!(ws_metadata = ?base, package_metadata = ?package_deploy, "finished merging deploy metadata");
 }
 
 #[cfg(test)]
@@ -208,37 +294,70 @@ mod tests {
     #[test]
     fn test_metadata_packages() {
         let env =
-            function_metadata(fixture("single-binary-package"), Some("basic-lambda")).unwrap();
+            function_environment_metadata(fixture("single-binary-package"), Some("basic-lambda"))
+                .unwrap();
 
         assert_eq!(env.get("FOO").unwrap(), "BAR");
     }
 
+    #[test]
+    fn test_deploy_metadata_packages() {
+        let env =
+            function_deploy_metadata(fixture("single-binary-package"), "basic-lambda").unwrap();
+
+        let layers = [
+            "arn:aws:lambda:us-east-1:xxxxxxxx:layers:layer1".to_string(),
+            "arn:aws:lambda:us-east-1:xxxxxxxx:layers:layer2".to_string(),
+        ];
+
+        let mut vars = HashMap::new();
+        vars.insert("VAR1".to_string(), "VAL1".to_string());
+
+        assert_eq!(Some(Memory::Mb512), env.memory);
+        assert_eq!(Timeout::new(60), env.timeout);
+        assert_eq!(Some(Path::new(".env.production")), env.env_file.as_deref());
+        assert_eq!(Some(layers.to_vec()), env.layers);
+        assert_eq!(Tracing::Active, env.tracing);
+        assert_eq!(vars, env.env);
+        assert_eq!(
+            Some("arn:aws:lambda:us-east-1:xxxxxxxx:iam:role1".to_string()),
+            env.iam_role
+        );
+    }
+
     #[test]
     fn test_metadata_multi_packages() {
-        let env = function_metadata(fixture("multi-binary-package"), Some("get-product")).unwrap();
+        let env =
+            function_environment_metadata(fixture("multi-binary-package"), Some("get-product"))
+                .unwrap();
 
         assert_eq!(env.get("FOO").unwrap(), "BAR");
 
         let env =
-            function_metadata(fixture("multi-binary-package"), Some("delete-product")).unwrap();
+            function_environment_metadata(fixture("multi-binary-package"), Some("delete-product"))
+                .unwrap();
 
         assert_eq!(env.get("BAZ").unwrap(), "QUX");
     }
 
     #[test]
     fn test_metadata_workspace_packages() {
-        let env = function_metadata(fixture("workspace-package"), Some("basic-lambda-1")).unwrap();
+        let env =
+            function_environment_metadata(fixture("workspace-package"), Some("basic-lambda-1"))
+                .unwrap();
 
         assert_eq!(env.get("FOO").unwrap(), "BAR");
 
-        let env = function_metadata(fixture("workspace-package"), Some("basic-lambda-2")).unwrap();
+        let env =
+            function_environment_metadata(fixture("workspace-package"), Some("basic-lambda-2"))
+                .unwrap();
 
         assert_eq!(env.get("FOO").unwrap(), "BAR");
     }
 
     #[test]
     fn test_metadata_packages_without_name() {
-        let env = function_metadata(fixture("single-binary-package"), None).unwrap();
+        let env = function_environment_metadata(fixture("single-binary-package"), None).unwrap();
 
         assert_eq!(env.get("FOO").unwrap(), "BAR");
     }
diff --git a/crates/cargo-lambda-metadata/src/error.rs b/crates/cargo-lambda-metadata/src/error.rs
new file mode 100644
index 00000000..44d90c98
--- /dev/null
+++ b/crates/cargo-lambda-metadata/src/error.rs
@@ -0,0 +1,20 @@
+use std::num::ParseIntError;
+
+use miette::Diagnostic;
+use thiserror::Error;
+
+#[derive(Debug, Diagnostic, Error)]
+pub enum MetadataError {
+    #[error("invalid memory value `{0}`")]
+    InvalidMemory(i32),
+    #[error("invalid lambda metadata in Cargo.toml file")]
+    InvalidCargoMetadata(#[from] serde_json::Error),
+    #[error("invalid timeout value")]
+    InvalidTimeout(#[from] ParseIntError),
+    #[error("invalid tracing option `{0}`")]
+    InvalidTracing(String),
+    #[error("there are more than one package in the project, you must specify a function name")]
+    MultiplePackagesInProject,
+    #[error("there are no packages in this project")]
+    MissingPackageInProject,
+}
diff --git a/crates/cargo-lambda-metadata/src/lambda.rs b/crates/cargo-lambda-metadata/src/lambda.rs
new file mode 100644
index 00000000..aa22758c
--- /dev/null
+++ b/crates/cargo-lambda-metadata/src/lambda.rs
@@ -0,0 +1,202 @@
+use serde::{
+    de::{Deserializer, Error, Visitor},
+    Deserialize,
+};
+use std::{fmt, str::FromStr};
+use strum_macros::{Display, EnumString};
+
+use crate::error::MetadataError;
+
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
+pub struct Timeout(u32);
+
+impl Timeout {
+    pub fn new(i: u32) -> Timeout {
+        Timeout(i)
+    }
+
+    pub fn is_zero(&self) -> bool {
+        self.0 == 0
+    }
+}
+
+impl Default for Timeout {
+    fn default() -> Self {
+        Timeout(30)
+    }
+}
+
+impl FromStr for Timeout {
+    type Err = MetadataError;
+
+    fn from_str(t: &str) -> Result<Timeout, Self::Err> {
+        let t = u32::from_str(t).map_err(MetadataError::InvalidTimeout)?;
+
+        Ok(Timeout(t))
+    }
+}
+
+impl From<Timeout> for i32 {
+    fn from(t: Timeout) -> i32 {
+        t.0 as i32
+    }
+}
+
+#[derive(Clone, Debug, Display, EnumString, Eq, PartialEq)]
+pub enum Memory {
+    #[strum(to_string = "128")]
+    Mb128,
+    #[strum(to_string = "512")]
+    Mb512,
+    #[strum(to_string = "1024")]
+    Mb1024,
+    #[strum(to_string = "1536")]
+    Mb1536,
+    #[strum(to_string = "2048")]
+    Mb2048,
+    #[strum(to_string = "3072")]
+    Mb3072,
+    #[strum(to_string = "4096")]
+    Mb4096,
+    #[strum(to_string = "5120")]
+    Mb5120,
+    #[strum(to_string = "6144")]
+    Mb6144,
+    #[strum(to_string = "7168")]
+    Mb7168,
+    #[strum(to_string = "8192")]
+    Mb8192,
+    #[strum(to_string = "9216")]
+    Mb9216,
+    #[strum(to_string = "10240")]
+    Mb10240,
+}
+
+impl From<Memory> for i32 {
+    fn from(m: Memory) -> i32 {
+        match m {
+            Memory::Mb128 => 128,
+            Memory::Mb512 => 512,
+            Memory::Mb1024 => 1024,
+            Memory::Mb1536 => 1536,
+            Memory::Mb2048 => 2048,
+            Memory::Mb3072 => 3072,
+            Memory::Mb4096 => 4096,
+            Memory::Mb5120 => 5120,
+            Memory::Mb6144 => 6144,
+            Memory::Mb7168 => 7168,
+            Memory::Mb8192 => 8192,
+            Memory::Mb9216 => 9216,
+            Memory::Mb10240 => 10240,
+        }
+    }
+}
+
+impl TryFrom<i32> for Memory {
+    type Error = MetadataError;
+
+    fn try_from(m: i32) -> Result<Memory, Self::Error> {
+        match m {
+            128 => Ok(Memory::Mb128),
+            512 => Ok(Memory::Mb512),
+            1024 => Ok(Memory::Mb1024),
+            1536 => Ok(Memory::Mb1536),
+            2048 => Ok(Memory::Mb2048),
+            3072 => Ok(Memory::Mb3072),
+            4096 => Ok(Memory::Mb4096),
+            5120 => Ok(Memory::Mb5120),
+            6144 => Ok(Memory::Mb6144),
+            7168 => Ok(Memory::Mb7168),
+            8192 => Ok(Memory::Mb8192),
+            9216 => Ok(Memory::Mb9216),
+            10240 => Ok(Memory::Mb10240),
+            other => Err(MetadataError::InvalidMemory(other)),
+        }
+    }
+}
+
+impl<'de> Deserialize<'de> for Memory {
+    fn deserialize<D>(deserializer: D) -> Result<Memory, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        struct MemoryVisitor;
+        impl<'de> Visitor<'de> for MemoryVisitor {
+            type Value = Memory;
+
+            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+                formatter.write_str("an integer that matches Lambda's memory values")
+            }
+
+            fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
+            where
+                E: Error,
+            {
+                Memory::try_from(value as i32).map_err(|e| Error::custom(e.to_string()))
+            }
+
+            fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
+            where
+                E: serde::de::Error,
+            {
+                self.visit_i64(value as i64)
+            }
+        }
+
+        deserializer.deserialize_i64(MemoryVisitor)
+    }
+}
+
+#[derive(Clone, Debug, Default, Display, EnumString, Eq, PartialEq)]
+#[strum(ascii_case_insensitive)]
+pub enum Tracing {
+    Active,
+    #[default]
+    PassThrough,
+}
+
+impl TryFrom<String> for Tracing {
+    type Error = MetadataError;
+
+    fn try_from(s: String) -> Result<Tracing, Self::Error> {
+        match s.to_lowercase().as_str() {
+            "active" => Ok(Self::Active),
+            "passthrough" => Ok(Self::PassThrough),
+            _ => Err(MetadataError::InvalidTracing(s)),
+        }
+    }
+}
+
+impl<'de> Deserialize<'de> for Tracing {
+    fn deserialize<D>(deserializer: D) -> Result<Tracing, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        struct TracingVisitor;
+        impl<'de> Visitor<'de> for TracingVisitor {
+            type Value = Tracing;
+
+            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+                formatter.write_str(
+                    "a string that matches Lambda's tracing options: `active` or `passthrough`",
+                )
+            }
+
+            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+            where
+                E: Error,
+            {
+                self.visit_string(v.to_string())
+            }
+
+            fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
+            where
+                E: Error,
+            {
+                Tracing::try_from(v).map_err(|e| Error::custom(e.to_string()))
+            }
+        }
+
+        deserializer.deserialize_string(TracingVisitor)
+    }
+}
diff --git a/crates/cargo-lambda-metadata/src/lib.rs b/crates/cargo-lambda-metadata/src/lib.rs
index 938c64b5..49cb366f 100644
--- a/crates/cargo-lambda-metadata/src/lib.rs
+++ b/crates/cargo-lambda-metadata/src/lib.rs
@@ -1,2 +1,4 @@
 pub mod cargo;
+pub mod error;
 pub mod fs;
+pub mod lambda;
diff --git a/crates/cargo-lambda-watch/src/scheduler.rs b/crates/cargo-lambda-watch/src/scheduler.rs
index a6a274cf..bd5810d4 100644
--- a/crates/cargo-lambda-watch/src/scheduler.rs
+++ b/crates/cargo-lambda-watch/src/scheduler.rs
@@ -5,7 +5,7 @@ use crate::{
 use axum::{body::Body, response::Response};
 use cargo_lambda_interactive::command::new_command;
 use cargo_lambda_invoke::DEFAULT_PACKAGE_FUNCTION;
-use cargo_lambda_metadata::cargo::function_metadata;
+use cargo_lambda_metadata::cargo::function_environment_metadata;
 use std::{
     collections::{hash_map::Entry, HashMap, VecDeque},
     sync::Arc,
@@ -200,7 +200,7 @@ async fn start_function(
         None
     };
 
-    let env = function_metadata(cargo_options.manifest_path, bin_name)
+    let env = function_environment_metadata(cargo_options.manifest_path, bin_name)
         .map_err(|err| {
             warn!(error = %err, "ignoring invalid function metadata");
             err
diff --git a/docs/commands/deploy.md b/docs/commands/deploy.md
index 9011cc42..2a24249f 100644
--- a/docs/commands/deploy.md
+++ b/docs/commands/deploy.md
@@ -67,6 +67,23 @@ cargo lambda build --release --extension
 cargo lambda deploy --extension
 ```
 
+## Deploy configuration in Cargo's Metadata
+
+You can keep some deploy configuration options in your project's `Cargo.toml` file. This give you a more "configuration as code" approach since you can store that configuration along side your project. The following example shows the options that you can specify in the metadata, all of them are optional:
+
+```toml
+[package.metadata.lambda.deploy]
+memory = 512                   # Function's memory
+timeout = 60                   # Function's execution timeout
+tracing = "active"             # Tracing mode
+role = "role-full-arn"         # Function's execution role
+env_file = ".env.production"   # File to load environment variables from
+env = { "VAR1" = "VAL1" }      # Additional environment variables
+layers = [                     # List of layers to deploy with your function
+    "layer-full-arn"
+]
+```
+
 ## Other options
 
 Use the `--help` flag to see other options to configure the function's deployment.


Test patch:
diff --git a/test/fixtures/single-binary-package/Cargo.toml b/test/fixtures/single-binary-package/Cargo.toml
index 81a2feb5..8fe56bf2 100644
--- a/test/fixtures/single-binary-package/Cargo.toml
+++ b/test/fixtures/single-binary-package/Cargo.toml
@@ -10,3 +10,15 @@ tokio = { version = "1", features = ["macros"] }
 
 [package.metadata.lambda.env]
 FOO = "BAR"
+
+[package.metadata.lambda.deploy]
+memory = 512
+timeout = 60
+env_file = ".env.production"
+layers = [
+    "arn:aws:lambda:us-east-1:xxxxxxxx:layers:layer1",
+    "arn:aws:lambda:us-east-1:xxxxxxxx:layers:layer2",
+]
+tracing = "active"
+role = "arn:aws:lambda:us-east-1:xxxxxxxx:iam:role1"
+env = { "VAR1" = "VAL1" }