You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Terraform单apply实现Lambda代码更新打包上传问题排查

问题描述

我正在编写一个支持容器化与非容器化部署的Terraform AWS Lambda模块,通过布尔变量containerization区分部署方式。当前聚焦非容器化Lambda的实现,需要满足以下需求:

  • 首次执行apply时,若order_verification.zip未创建,自动生成压缩包、上传至S3并创建Lambda函数;
  • 当./lambda_code目录内代码变更时,自动重新打包、上传新压缩包至S3并更新Lambda,且所有操作在单次apply完成。

但尝试多种配置后均出现问题:要么能重建压缩包但无法更新Lambda,要么移除压缩包后Terraform无法识别,导致Lambda未使用新包。使用Edit #1配置执行apply时,遇到以下错误:

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create
-/+ destroy and then create replacement

Terraform will perform the following actions:

  # module.order_verification_lambda.aws_s3_object.lambda_zip will be created
  + resource "aws_s3_object" "lambda_zip" {
      + acl                    = "private"
      + bucket                 = "order-verification-code"
      + bucket_key_enabled     = (known after apply)
      + content_type           = (known after apply)
      + etag                   = "taLJYlBhI2bqJy/6xtl0Sq9LRarNlqp8/Lkx7jtVglk="
      + force_destroy          = false
      + id                     = (known after apply)
      + key                    = "order_verification.zip"
      + kms_key_id             = (known after apply)
      + server_side_encryption = (known after apply)
      + storage_class          = (known after apply)
      + tags_all               = (known after apply)
      + version_id             = (known after apply)
    }

  # module.order_verification_lambda.null_resource.ensure_zip_exists[0] will be created
  + resource "null_resource" "ensure_zip_exists" {
      + id       = (known after apply)
      + triggers = {
          + "missing_zip" = "missing"
        }
    }

  # module.order_verification_lambda.null_resource.zip_and_upload[0] must be replaced
-/+ resource "null_resource" "zip_and_upload" {
      ~ id       = "8133341959139985635" -> (known after apply)
      ~ triggers = { # forces replacement
          - "combined_status" = "933f13bed7d8850565965991daabb9a3715951e43eaea47b26edb2154aa3ce6e" -> null
          + "zip_hash"        = "07aa0c2506298283c26eb5a825943dd2ae031cb6b8383345c81d1fc6dd4794d2"
        }
    }

Plan: 3 to add, 0 to change, 1 to destroy.
module.order_verification_lambda.null_resource.zip_and_upload[0]: Destroying... [id=8133341959139985635]
module.order_verification_lambda.null_resource.zip_and_upload[0]: Destruction complete after 0s
module.order_verification_lambda.null_resource.ensure_zip_exists[0]: Creating...
module.order_verification_lambda.null_resource.ensure_zip_exists[0]: Provisioning with 'local-exec'...
module.order_verification_lambda.null_resource.ensure_zip_exists[0] (local-exec): Executing: ["/bin/sh" "-c" "      cd lambda_code &\\\n      zip -rX order_verification.zip order_verification.py requirements.txt &\\\n      mv order_verification.zip ../\n"]
module.order_verification_lambda.null_resource.ensure_zip_exists[0] (local-exec):   adding: order_verification.py (deflated 67%)
module.order_verification_lambda.null_resource.ensure_zip_exists[0] (local-exec):   adding: requirements.txt (deflated 16%)
module.order_verification_lambda.null_resource.ensure_zip_exists[0]: Creation complete after 0s [id=1949417230852622518]
module.order_verification_lambda.null_resource.zip_and_upload[0]: Creating...
module.order_verification_lambda.null_resource.zip_and_upload[0]: Provisioning with 'local-exec'...
module.order_verification_lambda.null_resource.zip_and_upload[0] (local-exec): Executing: ["/bin/sh" "-c" "      cd lambda_code &\\\n      zip -rX order_verification.zip order_verification.py requirements.txt &\\\n      mv order_verification.zip ../\n"]
module.order_verification_lambda.null_resource.zip_and_upload[0] (local-exec):   adding: order_verification.py (deflated 67%)
module.order_verification_lambda.null_resource.zip_and_upload[0] (local-exec):   adding: requirements.txt (deflated 16%)
module.order_verification_lambda.null_resource.zip_and_upload[0]: Creation complete after 0s [id=5217531696015301099]
╷
│ Error: Provider produced inconsistent final plan
│ 
│ When expanding the plan for module.order_verification_lambda.aws_s3_object.lambda_zip to include new values learned so far during apply, provider
│ "registry.terraform.io/hashicorp/aws" produced an invalid new value for .source: was null, but now cty.StringVal("./order_verification.zip").
│ 
│ This is a bug in the provider, which should be reported in the provider's own issue tracker.
╵
╷
│ Error: Provider produced inconsistent final plan
│ 
│ When expanding the plan for module.order_verification_lambda.aws_s3_object.lambda_zip to include new values learned so far during apply, provider
│ "registry.terraform.io/hashicorp/aws" produced an invalid new value for .etag: was cty.StringVal("taLJYlBhI2bqJy/6xtl0Sq9LRarNlqp8/Lkx7jtVglk="), but now
│ cty.StringVal("gDlsnAlsiBpZK9s2tACPmCufe5Lk+ubCoUbcLSNBIcA=").
│ 
│ This is a bug in the provider, which should be reported in the provider's own issue tracker.

该模块将用于CI流程,需解决此错误以保证流程正常运行。

模块目录结构

.
├── ./lambda_code
│   ├── ./lambda_code/order_verification.py
│   └── ./lambda_code/requirements.txt
├── ./locals.tf
├── ./main.tf
├── ./order_verification.zip
├── ./outputs.tf
├── ./providers.tf

当前main.tf代码

# IAM Role for the Lambda Function
resource "aws_iam_role" "lambda_execution_role" {
  name               = var.role_name
  assume_role_policy = jsonencode({
    Version = "2012-10-17",
    Statement: [
      {
        Effect    = "Allow",
        Principal = { Service = "lambda.amazonaws.com" },
        Action    = "sts:AssumeRole"
      }
    ]
  })

  tags = var.tags
}

resource "aws_iam_role_policy" "lambda_policy" {
  name   = "${replace(var.function_name, "_", "-")}-policy"
  role   = aws_iam_role.lambda_execution_role.id

  policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Effect   = "Allow",
        Action   = ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"],
        Resource = "arn:aws:logs:*:*:*"
      },
      {
        Effect   = "Allow",
        Action   = ["s3:GetObject"],
        Resource = "arn:aws:s3:::ordering-system/*"  # Grant access to all objects in the bucket
      },
      {
        Effect   = "Allow",
        Action   = ["sqs:SendMessage"],
        Resource = var.sqs_queue_arn
      },
      {
        Effect   = "Allow",
        Action   = ["dynamodb:PutItem", "dynamodb:GetItem", "dynamodb:UpdateItem"],
        Resource = var.dynamodb_table_arn
      }
    ]
  })
}

resource "aws_lambda_permission" "allow_s3_trigger" {
  count = var.monitored_bucket_name != "" ? 1 : 0

  statement_id  = "AllowS3Invoke"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.lambda.function_name
  principal     = "s3.amazonaws.com"

  source_arn = "arn:aws:s3:::${var.monitored_bucket_name}"
}

resource "aws_s3_bucket_notification" "monitored_bucket_notification" {
  bucket = var.monitored_bucket_name

  lambda_function {
    lambda_function_arn = aws_lambda_function.lambda.arn
    events              = ["s3:ObjectCreated:*"]
    filter_suffix       = ".json"
  }

  depends_on = [
    aws_lambda_permission.allow_s3_trigger
  ]
}

# Null resource to zip and upload code if containerization is false
resource "null_resource" "zip_and_upload" {
  count = !var.containerization && var.function_directory != "" ? 1 : 0

  provisioner "local-exec" {
    command = <<EOT
      cd ${var.function_directory} && \
      if [ -f "../${var.function_name}.zip" ]; then rm ../${var.function_name}.zip; fi && \
      zip -rX ${var.function_name}.zip ${var.function_name}.py requirements.txt && \
      mv ${var.function_name}.zip ../
EOT
  }

  triggers = {
    zip_hash = sha256(join("", [
      filebase64sha256("${var.function_directory}/${var.function_name}.py"),
      fileexists("${var.function_directory}/requirements.txt") ? filebase64sha256("${var.function_directory}/requirements.txt") : "no-reqs"
    ]))
  }
}

# Upload the zip file to S3
resource "aws_s3_object" "lambda_zip" {
  bucket = var.s3_bucket_name
  key    = "${var.function_name}.zip"
  source = "./${var.function_name}.zip"
  etag   = filebase64sha256("./${var.function_name}.zip")

  lifecycle {
    ignore_changes = [etag]
  }

  depends_on = [null_resource.zip_and_upload]
}

# Lambda function resource
resource "aws_lambda_function" "lambda" {
  function_name = var.function_name
  role          = aws_iam_role.lambda_execution_role.arn

  # Use S3 deployment if containerization is false
  runtime          = !var.containerization ? var.runtime : null
  handler          = !var.containerization ? var.handler : null
  s3_bucket        = !var.containerization ? var.s3_bucket_name : null
  s3_key           = !var.containerization ? "${var.function_name}.zip" : null
  source_code_hash = !var.containerization && fileexists("./${var.function_name}.zip") 
                     ? filebase64sha256("./${var.function_name}.zip") 
                     : base64sha256("dummy")

  # Use container image if containerization is true
  image_uri = var.containerization ? var.image_uri : null

  environment {
    variables = var.environment
  }

  tags = var.tags

  # Ensure Lambda function doesn't get recreated for no reason
  lifecycle {
    ignore_changes = [source_code_hash]
  }

  depends_on = [
    aws_s3_object.lambda_zip
  ]
}

Edit #1配置代码

# Null resource to ensure the zip file is created if missing
resource "null_resource" "ensure_zip_exists" {
  count = !var.containerization && var.function_directory != "" && !fileexists("./${var.function_name}.zip") ? 1 : 0

  provisioner "local-exec" {
    command = <<EOT
      cd ${var.function_directory} && \
      zip -rX ${var.function_name}.zip ${var.function_name}.py requirements.txt && \
      mv ${var.function_name}.zip ../
EOT
  }

  triggers = {
    missing_zip = !fileexists("./${var.function_name}.zip") ? "missing" : "exists"
  }
}

# Null resource to re-zip and upload if code changes
resource "null_resource" "zip_and_upload" {
  count = !var.containerization && var.function_directory != "" ? 1 : 0

  provisioner "local-exec" {
    command = <<EOT
      cd ${var.function_directory} && \
      zip -rX ${var.function_name}.zip ${var.function_name}.py requirements.txt && \
      mv ${var.function_name}.zip ../
EOT
  }

  triggers = {
    zip_hash = sha256(join("", [
      filebase64sha256("${var.function_directory}/${var.function_name}.py"),
      fileexists("${var.function_directory}/requirements.txt") ? filebase64sha256("${var.function_directory}/requirements.txt") : "no-reqs"
    ]))
  }

  depends_on = [null_resource.ensure_zip_exists]
}

# Upload the zip file to S3
resource "aws_s3_object" "lambda_zip" {
  bucket = var.s3_bucket_name
  key    = "${var.function_name}.zip"
  source = fileexists("./${var.function_name}.zip") ? "./${var.function_name}.zip" : null
  etag   = fileexists("./${var.function_name}.zip") ? filebase64sha256("./${var.function_name}.zip") : base64sha256("dummy")

  lifecycle {
    ignore_changes = [etag]
  }

  depends_on = [null_resource.zip_and_upload]
}

# Lambda function resource
resource "aws_lambda_function" "lambda" {
  function_name = var.function_name
  role          = aws_iam_role.lambda_execution_role.arn

  # Use S3 deployment if containerization is false
  runtime          = !var.containerization ? var.runtime : null
  handler          = !var.containerization ? var.handler : null
  s3_bucket        = !var.containerization ? var.s3_bucket_name : null
  s3_key           = !var.containerization ? "${var.function_name}.zip" : null ? filebase64sha256("./${var.function_name}.zip") : base64sha256("dummy")

  # Use container image if containerization is true
  image_uri = var.containerization ? var.image_uri : null

  environment {
    variables = var.environment
  }

  tags = var.tags

  depends_on = [
    aws_s3_object.lambda_zip
  ]
}

解决方案

错误根源在于aws_s3_object的source和etag字段在Terraform计划阶段与执行阶段的值不一致:计划阶段zip文件尚未生成,执行阶段才创建,导致AWS Provider检测到值冲突报错。以下是修复后的配置:

1. 移除冗余的ensure_zip_exists资源

zip_and_upload资源已经可以处理首次创建和代码变更的场景,无需额外检查zip文件是否存在。

2. 修改zip_and_upload资源

简化命令逻辑,确保每次触发时都重新生成zip:

resource "null_resource" "zip_and_upload" {
  count = !var.containerization && var.function_directory != "" ? 1 : 0

  provisioner "local-exec" {
    command = <<EOT
      cd ${var.function_directory} && \
      rm -f ../${var.function_name}.zip && \
      zip -rX ../${var.function_name}.zip ${var.function_name}.py requirements.txt
EOT
  }

  triggers = {
    zip_hash = sha256(join("", [
      filebase64sha256("${var.function_directory}/${var.function_name}.py"),
      fileexists("${var.function_directory}/requirements.txt") ? filebase64sha256("${var.function_directory}/requirements.txt") : "no-reqs"
    ]))
  }
}

3. 修复aws_s3_object配置

使用null_resource的触发器哈希作为etag,避免计划阶段与执行阶段的不一致:

resource "aws_s3_object" "lambda_zip" {
  bucket = var.s3_bucket_name
  key    = "${var.function_name}.zip"
  source = "./${var.function_name}.zip"

  # 关联null_resource的触发器哈希,确保代码变更时S3对象自动更新
  etag =
相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 01:32:20