使用 Terraform 持续部署 Lambda 函数和层

发布: (2025年12月30日 GMT+8 02:07)
10 min read
原文: Dev.to

Source: Dev.to

这篇博客文章的配套代码可以在这里找到。

介绍

将 Lambda 函数使用 Terraform 部署到 AWS 往往相当头疼,尤其是在多个环境(这只会在 dev 和 test 环境中发生,对吧?)进行部署时。

你可能会遇到的一些问题包括:

  • 每次 terraform apply 都会重新部署 Lambda 函数
  • 关于缺少包含 Lambda 函数文件的归档文件的错误
  • Terraform 状态文件中的软锁

在本文中,我将展示一种方法,使 Lambda 函数仅在代码有更改时才进行一致的部署,即使是从多个环境进行部署也能实现。

历史

在使用 Terraform 部署 Lambda 函数时,长期存在一些问题。下面列出了一些外部链接,包含这些问题的示例以及尝试解决的方案。

示例配置

我已经使用 Terraform 成功部署了一个 AWS Lambda:

resource "aws_lambda_function" "lambda" {
  filename                       = "dist/subscriber-lambda.zip"
  function_name                  = "test_get-code"
  role                           = 
  handler                        = "main.handler"
  timeout                        = 14
  reserved_concurrent_executions = 50
  memory_size                    = 128
  runtime                        = "python3.6"
  tags                           = 
  source_code_hash               = "${base64sha256(file("../modules/lambda/lambda-code/main.py"))}"
  kms_key_arn                    = 

  vpc_config {
    subnet_ids         = 
    security_group_ids = 
  }

  environment {
    variables = {
      environment = "dev"
    }
  }
}

当我运行 terraform plan 时,它提示 Lambda 资源需要更新,因为 source_code_hash 已更改,尽管我并未修改 Python 代码库(该代码库在同一仓库中受版本控制):

~ module.app.module.lambda.aws_lambda_function.lambda
    last_modified:   "2018-10-05T07:10:35.323+0000" => 
    source_code_hash: "jd6U44lfe4124vR0VtyGiz45HFzDHCH7+yTBjvr400s=" => "JJIv/AQoPvpGIg01Ze/YRsteErqR0S6JsqKDNShz1w78"

使用 null_resource 触发更新

如果需要更细粒度的控制或想基于其他资源触发更新,可以使用 null_resource

resource "null_resource" "lambda_update" {
  triggers = {
    code_hash = filebase64sha256("my-function.zip")
  }

  provisioner "local-exec" {
    command = "echo 'Code updated, triggering Lambda deployment...'"
  }
}

resource "aws_lambda_function" "example" {
  # ... other configurations
  depends_on = [null_resource.lambda_update]
}

此示例会在 my-function.zip 的哈希值发生变化时触发更新。

Issue with aws_lambda_layer_version

Hi All,

我们使用 Terraform 0.14.6,遇到了以下问题。

我们为 aws_lambda_layer_version 提供了 source_code_hash。Terraform 接受了该值,但在状态文件中写入了完全不同的值。

在计划中 source_code_hashFyN0P9BvuTm023dkHFaWvAGmyD0rlhujGsPCTqaBGyw=;然而,在状态文件中它变成了 c3forIEso3mJh74PY6HrhFK94GfJvQ4zG9rEIgBCBhw=

当我使用 AWS CLI 检查该层时,"CodeSha256"c3forIEso3mJh74PY6HrhFK94GfJvQ4zG9rEIgBCBhw=

基于此,无论我提供什么样的 source_code_hash,都无法覆盖文件名的哈希值。

Terraform 配置:

resource "aws_lambda_layer_version" "loader" {
  layer_name          = "loader"
  compatible_runtimes = ["python3.8"]

  filename         = "lambda_layer.zip"
  source_code_hash = filebase64sha256("lambda_layer.zip")
}

在所有这些示例中,你可以看到会计算哈希以确定代码是否已更改。这本身不是问题,但 问题在于它们全部使用 base64 编码的哈希

如何让你的 Lambda 函数部署跨环境友好

base64 编码的问题在于,同样的数据在不同环境(操作系统、用户设置)下得到的哈希可能不同。

下面的文章 说明了这个问题:

造成此问题的根本原因是不同机器上的打包方式不同以及文档不完善。还有,AWS 那边的一个荒唐设计选择。

source_code_hash 会被 AWS 在响应中提供的数据 覆盖

source_code_hash(又称 output_base64sha256filebase64sha256)的文档具有误导性:

(String) 输出归档文件的 base64 编码的 SHA256 校验和。

为什么要对哈希进行 base64 编码?base64 编码的目的是让二进制数据可打印,而 SHA‑256 哈希本身已经是可打印的(十六进制)。

实际发生的过程是:

  1. 计算归档文件的 SHA‑256。
  2. 将得到的 二进制 摘要视为原始数据的字节,然后对该二进制块进行 base64 编码:
sha256sum lambda.zip | xxd -r -p | base64

问题在于,最近的 zip 版本会存储文件权限,而不同机器上的 umask 值不同会导致权限不同,进而生成不同的归档文件并产生不同的哈希。

当团队中同时使用 Windows 和 macOS/Linux 时,还会面临额外的挑战,因为文件系统(以及归档文件的文件名)差异很大。

让它工作起来

经过一些尝试,我得出了以下解决方案。

(本文的其余部分继续逐步实现可复现的打包流程、自定义脚本生成确定性的 zip,以及使用确定性哈希的 Terraform 配置。请在此处插入您的解决方案。)

自动在源码更改时重新部署 Lambda 函数

当我将 Lambda 函数代码提供为一个目录(包含所需文件)时,会使用 archive_file 数据源将该目录压缩为归档文件。

1. 生成用于触发重新部署的随机 UUID

首先,我们基于源码目录(及其子目录)中的 所有 文件(不包括 ZIP 文件)创建一个随机 UUID。会为每个文件计算 MD5 哈希;只要其中任意一个哈希发生变化,UUID 就会改变,从而强制 aws_lambda_function 资源重新部署。

# Create a random UUID which is used to trigger a redeploy of the function.
# The MD5 hash for each file (except ZIP files) will be calculated and,
# if any of those changes, it will trigger a redeploy of the
# aws_lambda_function resource `lambda_function`.
# We cannot rely on a base64 hash, because the seed for that is environment dependent.
resource "random_uuid" "lambda_function" {
  keepers = {
    for filename in setunion(
      toset([for fn in fileset("${path.root}/lambda_function/", "**") : fn if !endswith(fn, ".zip")])
    ) :
    filename => filemd5("${path.root}/lambda_function/${filename}")
  }
}

注意: 这里使用 MD5 仅用于检测变化,而非加密用途。如果需要,可以改用 SHA256SHA512(额外的开销通常可以忽略不计)。

2. 创建 ZIP 归档

我们将函数目录压缩到 .gitignore 忽略的路径下。

# Create an archive file of the function directory
data "archive_file" "lambda_function" {
  type        = "zip"
  source_dir  = "${path.root}/lambda_function"
  output_path = "${path.root}/lambda_output/${var.function_name}.zip"
}

3. 部署 Lambda 函数

random_uuid 资源用作 Lambda 函数的替换触发器。
我们还会忽略 filename 属性的变化,因为归档文件的绝对路径在不同机器之间可能不同。

# Create the Lambda function
resource "aws_lambda_function" "lambda_function" {
  function_name = var.function_name
  role          = aws_iam_role.lambda_execution_role.arn
  handler       = "${var.function_name}.${var.handler_name}"
  runtime       = var.runtime
  timeout       = var.timeout
  architectures = var.architectures

  # Use the filename of the archive file as input for the function
  filename = data.archive_file.lambda_function.output_path

  depends_on = [
    aws_iam_role.lambda_execution_role
  ]

  lifecycle {
    replace_triggered_by = [
      # Trigger a replace of the function when any of the function source files changes.
      random_uuid.lambda_function
    ]
    ignore_changes = [
      # Ignore the source filename of the object itself, because that can change between
      # users/machines/operating systems.
      filename
    ]
  }
}

应用此配置后,在不同环境中运行相同代码 不会 导致意外的重新部署。

4. 部署 Lambda Layer(使用中间 S3 对象)

相同的模式同样适用于 Lambda Layer。区别在于 Layer 版本是从 S3 对象构建的,只要源码文件变化,该对象就会被替换。

# Random UUID for the layer (change detection)
resource "random_uuid" "lambda_layer" {
  keepers = {
    for filename in setunion(
      toset([for fn in fileset("${path.root}/lambda_layer/", "**") : fn if !endswith(fn, ".zip")])
    ) :
    filename => filemd5("${path.root}/lambda_layer/${filename}")
  }
}

# Archive the layer directory
data "archive_file" "lambda_layer" {
  type        = "zip"
  source_dir  = "${path.root}/lambda_layer"
  output_path = "${path.root}/lambda_output/${var.layer_name}.zip"
}

# Store the archive in S3
resource "aws_s3_object" "this" {
  depends_on          = [data.archive_file.lambda_layer]
  key                 = join("/", [for x in [var.s3_key, "${var.layer_name}.zip"] : x if x != null && x != ""])
bucket              = var.s3_bucket
source              = data.archive_file.lambda_layer.output_path
checksum_algorithm  = "SHA256"

lifecycle {
  replace_triggered_by = [
    random_uuid.lambda_layer
  ]
  ignore_changes = [
    # Ignore the source of the object itself, because that can change between machines/operating systems
    source
  ]
}
# Create the Lambda layer version
resource "aws_lambda_layer_version" "lambda_layer" {
  layer_name          = var.layer_name
  compatible_runtimes = [var.runtime]
  source_code_hash    = aws_s3_object.this.checksum_sha256
  s3_bucket           = aws_s3_object.this.bucket
  s3_key              = aws_s3_object.this.key
}

5. 为什么这很重要

通过管道运行 IaC 更改(正如在生产和预发布环境中应该做的)可以消除因贡献者机器之间差异导致的错误计划/应用更改。

流行的 Lambda module by Anton Babenko 使用 base64 哈希和归档文件名,这可能导致上述问题。使用此处展示的方法,这些问题可以避免。

PR Update

正在处理一个 PR,以修复该模块的 Base64 处理。

结论

本文的目标是向您展示如何解决在使用 Terraform 部署 Lambda 函数和/或层时可能遇到的(至少)两个问题。

希望这能帮助您了解这些问题的成因,并帮助您就如何解决它们做出明智的决定。

Back to Blog

相关文章

阅读更多 »

为什么传统 DevOps 停止扩展

传统的 DevOps 运作良好……直到组织规模扩大。 在小规模时,一个集中式的 DevOps 团队负责部署、修复和处理所有问题,感觉很高效……

Terraform 堆栈

概述:一组可投入生产的 Terraform Stacks,展示了跨完整应用程序、多区域 fan‑out 和 Kubernetes 平台的企业模式。