你的 Lambda 怎么证明自己是它自己?

写 Serverless 的人早晚会被问一个问题:你那个 Lambda 要读 S3 里的文件,它凭什么能读?

最常见的回答是 “我把 Access Key 塞环境变量了”。这个方案看起来快手,但等一下——如果密钥泄漏了怎么办?你的 AWS 账号等于敞开大门。Access Key 死了就是死的,不轮转,不用了你还得记得去删。

还有一个做法是把密钥写死在代码里。见过一个项目把 .env 顺便提交到 GitHub 的,30 分钟后 AWS 账单就报警了。如果你还在这么做,建议现在就去做一次扫描。

正确的思路是:不要让代码持有长期凭证,让 Lambda 运行环境自动给代码发放临时凭证。 AWS SDK 里自带了一个机制来处理这件事——默认凭证链(Default Credential Provider Chain)。当你的代码跑在 Lambda 上时,SDK 会自己去找 Lambda 执行环境里的临时凭证,开发者一行凭证代码都不用写。

本文就演示怎么用 Rust 写出这样的 Lambda,从 IAM 角色到部署再到验证,全程不走捷径。

传统做法的问题在哪?

简单列三个最常见的坑:

硬编码凭证。把 AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY 写进代码或配置文件。一旦泄漏,除非手动吊销,否则这把钥匙永远有效。GitHub 的 secret scanning 报告里这类泄漏常年排前三。

环境变量存密钥。比硬编码好一点,不写进代码仓库了,但本质一样——长期有效的静态密钥。如果 Lambda 被其他方式攻破,攻击者能直接读取环境变量拿到凭证。

权限过度宽泛。给 Lambda 挂了一个 AdministratorAccess 或者一个 “S3 FullAccess” 角色。方便是方便了,但一个只读 S3 特定前缀的文件 Lambda 凭什么能删整个 bucket?攻击者从单个函数出发横向移动的路径就是这么来的。

理想的方案很简单:最小权限 + 自动轮转 + 零代码显式管理凭证。AWS SDK 的 Workload Credentials Provider 组合起来就是这个答案。

AWS SDK 默认凭证链是怎么工作的?

当你调用 aws_config::load_from_env() 时,SDK 内部会按顺序尝试以下凭证来源:

  1. 环境变量AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY
  2. AWS 配置文件~/.aws/credentials~/.aws/config
  3. Web Identity Token — EKS / ECS 等 OIDC 场景
  4. ECS 容器凭证AWS_CONTAINER_CREDENTIALS_RELATIVE_URI
  5. EC2 Instance Metadata Service (IMDS) — EC2 实例配置文件
  6. Lambda 运行时凭证 — 通过环境变量 AWS_LAMBDA_RUNTIME_API 获取

当代码跑在 Lambda 上时,第 6 项会命中。Lambda 运行时会提前通过 sts:AssumeRole 获取一组临时凭证(Access Key + Secret Key + Session Token),然后通过内部 HTTP 端点暴露给你的代码。SDK 自动去请求、自动刷新、自动在过期前续期——全程开发者无感。

所以你的 Rust 代码唯一要做的事就是创建 SDK 客户端,不需要传凭证。凭的是 Lambda 执行角色本身的权限。

实操第一步:创建 IAM 角色

你的 Lambda 需要一个角色来告诉 AWS “它可以做什么”。这里我们建一个只允许 s3:ListAllMyBuckets 的演示角色。

创建一个信任策略文件 lambda-trust-policy.json

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

再创建一个权限策略文件 s3-list-policy.json

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:ListAllMyBuckets"
      ],
      "Resource": "*"
    }
  ]
}

然后创建角色并绑定策略:

aws iam create-role \
  --role-name RustLambdaS3ListerRole \
  --assume-role-policy-document file://lambda-trust-policy.json

aws iam put-role-policy \
  --role-name RustLambdaS3ListerRole \
  --policy-name S3ListAllMyBucketsPolicy \
  --policy-document file://s3-list-policy.json

创建完成后保存你的角色 ARN,后面部署会用:

aws iam get-role --role-name RustLambdaS3ListerRole --query 'Role.Arn' --output text

实操第二步:写 Rust Lambda 代码

前置条件

  • Rust toolchain(建议用最新 stable,最低要求 1.91.1 以上)
  • cargo-lambda 安装
# macOS / Linux
curl -fsSL https://cargo-lambda.info/install.sh | sh

初始化项目

cargo lambda new rust-s3-lister --runtime rust
cd rust-s3-lister

Cargo.toml

截至 2026 年 6 月,以下版本是稳定的。实际用之前请去 crates.io 确认最新版本。

[dependencies]
lambda_runtime = "1.2.1"
tokio = { version = "1", features = ["macros"] }
tracing = { version = "0.1", features = ["log"] }
tracing-subscriber = { version = "0.3", features = ["fmt"] }
serde_json = "1.0"

# AWS SDK for Rust
aws-config = { version = "1.1.7", features = ["behavior-version-latest"] }
aws-sdk-s3 = "1.134.0"

main.rs

关键代码只有几行。不需要手动读环境变量,不需要手动调用 STS——一句 aws_config::load_from_env() 搞定。

use lambda_runtime::{service_fn, LambdaEvent, Error};
use serde_json::{json, Value};
use aws_config::BehaviorVersion;
use aws_sdk_s3::Client;

#[tokio::main]
async fn main() -> Result<(), Error> {
    tracing_subscriber::fmt()
        .with_max_level(tracing::Level::INFO)
        .without_time()
        .init();

    // 这一行就是全部——SDK 自动从 Lambda 运行时拿到临时凭证
    let config = aws_config::load_from_env(BehaviorVersion::latest()).await;
    let s3_client = Client::new(&config);

    lambda_runtime::run(service_fn(|event: LambdaEvent<Value>| {
        function_handler(event, &s3_client)
    }))
    .await?;

    Ok(())
}

async fn function_handler(event: LambdaEvent<Value>, s3_client: &Client) -> Result<Value, Error> {
    tracing::info!("Received event: {:?}", event);

    let response = s3_client.list_buckets().send().await;

    match response {
        Ok(output) => {
            let buckets: Vec<String> = output
                .buckets()
                .unwrap_or_default()
                .iter()
                .filter_map(|b| b.name().map(String::from))
                .collect();
            tracing::info!("Successfully listed S3 buckets: {:?}", buckets);

            Ok(json!({
                "message": format!("Found {} buckets: {:?}", buckets.len(), buckets)
            }))
        }
        Err(e) => {
            tracing::error!("Failed to list S3 buckets: {:?}", e);
            Err(format!("Error listing S3 buckets: {}", e).into())
        }
    }
}

为什么选 BehaviorVersion::latest()

AWS SDK for Rust 的 aws-config 有一个 BehaviorVersion 机制。当 SDK 有 breaking change 时(比如某个默认行为变了),设置这个参数能够让你的应用明确知道自己用的是哪个行为版本。传 latest() 表示"跟随最新版本",更稳妥的做法是将来锁定到具体版本号,避免部署环境升级导致行为变化。

没有 panic 兜底

list_buckets() 返回 Result,这里直接 match 处理了。如果你的生产代码希望更简洁的 “成功即继续,失败即返回” 模式,可以考虑用 anyhowcolor-eyre 替换 Box<dyn Error>,但不要用 unwrap()。Lambda 函数的每次 panic 都不会妥善处理错误上下文,CloudWatch 里的输出也不如 tracing::error 来得干净。

实操第三步:部署

编译

Graviton(ARM64)在 Lambda 上有性价比优势——性能不输 x86,费用低 20%。推荐用 ARM64 编译:

cargo lambda build --release --target aarch64-unknown-linux-musl

如果你的开发机是 x86_64 但想交叉编译到 ARM,cargo lambda build 会自动用 Zig 做交叉编译,不需要额外配置。

部署

cargo lambda deploy rust-s3-lister \
  --role arn:aws:iam::YOUR_AWS_ACCOUNT_ID:role/RustLambdaS3ListerRole \
  --region us-east-1 \
  --arm64

YOUR_AWS_ACCOUNT_ID 换成你的 AWS 账号 ID,region 换成你实际使用的区域。

cargo lambda deploy 做了三件事:

  1. 把编译产物打包成 ZIP(或者使用容器镜像格式,取决于你的运行时选择)
  2. 调用 CreateFunctionUpdateFunctionCode 上传到 Lambda
  3. --role 参数设置函数执行角色

如果要更新已有函数的代码:

cargo lambda deploy rust-s3-lister

不加 role 和 region 参数时会复用上次部署的配置。

实操第四步:验证

用 AWS CLI 调用一下:

aws lambda invoke \
  --function-name rust-s3-lister \
  --payload '{}' \
  response.json

cat response.json

预期输出:

{
  "message": "Found 12 buckets: [\"my-first-bucket\", \"another-project-bucket\", ...]"
}

然后去 CloudWatch Logs 确认日志:

INFO Received event: LambdaEvent { ... }
INFO Successfully listed S3 buckets: ["my-first-bucket", "another-project-bucket"]

如果你的输出是 “Access Denied”,检查两点:

  • IAM 角色是否正确绑定了 s3:ListAllMyBuckets 策略
  • 角色信任关系是否正确——lambda.amazonaws.com 是否是 Principal

进阶话题

冷启动和运行时选择

Rust 在 Lambda 上的冷启动时间已经有不少 benchmark 数据。在 2026 年的实践中,ARM64 架构下 Rust Lambda 的冷启动中位数在 20ms 左右(完全冷启动,包含 SDK 初始化),x86_64 稍微高一点但也在 30ms 以内。对比 Python 的 100-200ms 和 Node.js 的 80-150ms,差距非常明显。

原因不复杂:

  • Rust 是原生编译,不需要 JIT 预热
  • SDK 初始化轻量,不需要下载或解析额外模块
  • aws_config::load_from_env() 内部只做一次 HTTP 请求到 Lambda 运行时的本地端点(127.0.0.1 内),延迟可以忽略

AWS 官方在 2025 年 11 月正式 GA 了 Rust Lambda 运行时支持,之后生态一直在完善。如果你追逐最低延迟,Rust + ARM64 是目前 Lambda 上的最优组合。

Lambda Managed Instances 并发

2026 年 3 月,AWS Lambda 发布了 Managed Instances 对 Rust 的支持。这让同一个执行环境可以同时处理多个请求,对于高吞吐场景能大幅减少冷启动频次。如果你的 Lambda 流量较高,可以研究 lambda_runtimerun_concurrent() 模式。核心思路:多个 Tokio 任务共享同一个执行环境,复用的 SDK 连接池和缓存数据。

交叉账号场景

本文讨论的是单一账号下 Lambda 访问同账号资源。如果你需要跨账号访问,通常的做法是在目标账号里创建一个 IAM 角色,然后在 Lambda 里手动调用 sts:AssumeRole 获取临时凭证。AWS SDK for Rust 的 aws-sts crate 支持这个模式。不过跨账号的坑更多(信任策略、外部 ID、权限边界),这里不展开。

常见问题

Q: 为什么不用 aws-sdk-s3BehaviorVersion::v2024_11_15() 这种固定版本?

latest() 在当前场景下没问题,但生产环境建议固定到某个已经验证过的版本,防止 SDK 更新后默认行为不一致导致线上问题。上线前在测试环境验证过再切过去。

Q: 本地调试怎么办?

本地开发时直接用 AWS CLI 配置的开发者凭证(~/.aws/credentials)。SDK 的默认凭证链在本地会命中第 1 或第 2 项。用环境变量 AWS_PROFILE 切换不同账号的配置也支持。参考我们之前的文章 了解用 LocalStack 做本地集成测试。

Q: cargo-lambda 和手动打包有什么区别?

cargo-lambda 帮你封装了编译 → 打包 → 上传链路。手动做需要用 strip 去掉符号表、用 musl-gcc 做静态链接,然后自建 ZIP 上传。cargo-lambda 把这些都做了,且内置了 Zig 做跨平台交叉编译。除非你有特殊的自定义运行时需求,否则推荐直接用。

Q: Rust 什么时候不适合 Lambda?

  • 团队对 Rust 不熟,上线后没人能维护
  • 代码逻辑极其简单,Python/Node 写 50 行就能搞定,引入 Rust 的构建和部署复杂度不划算
  • 依赖了大量 C 库且没有 Rust 绑定,编译结果体积爆炸(Lambda 部署包限制 50MB 压缩包 / 250MB 解压后)

Q: 如果我的 Lambda 需要调用 DynamoDB、SQS、SES 多个服务呢?

加依赖就行。AWS SDK for Rust 是树状依赖,你只加实际用到的 aws-sdk-* crate,不会把几百个服务都编译进去。初始化多个 Client 也共用一个 SdkConfig,不会重复凭证获取。


对 Rust 云原生开发感兴趣?关注梦兽编程 每周更新 Rust 深度技术内容。

也推荐阅读我们的《Rust 在生产环境的日志采集实践》 了解 Rust 在 Serverless 之外的实战经验。