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 =
相关产品推荐
相关产品推荐

