How to Fix Terraform 'Provider Produced an Unexpected New Value' for aws_lambda_function
Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 15 mins
TL;DR
- What broke: Terraform planned a Lambda resource state, but the AWS provider returned a different computed value post-apply — triggering an unresolvable diff loop that halts your pipeline.
- How to fix it: Identify the drifting attribute (
filename/source_code_hashconflict, unmanagedlast_modified, computedqualified_arn, or environment variable map ordering) and either remove the conflicting argument, add alifecycle { ignore_changes }block, or pin the deployment mechanism to S3-only orfilename-only. - Auto-refactor: Use our Client-Side Sandbox below to paste your
aws_lambda_functionblock and get the corrected HCL instantly.
The Incident (What Does the Error Mean?)
Raw error output from a failed terraform apply:
Error: Provider produced an unexpected new value for aws_lambda_function.my_function
on main.tf line 12, in resource "aws_lambda_function" "my_function":
12: resource "aws_lambda_function" "my_function" {
The root object is not consistent after apply:
- .last_modified: was cty.StringVal("2024-01-15T10:30:00.000+0000"),
but now cty.StringVal("2024-01-15T10:30:01.000+0000")
(or alternatively)
- .source_code_hash: planned value cty.StringVal("abc123...")
does not match config value cty.StringVal("")
Immediate consequence: terraform apply exits non-zero. Your CI/CD pipeline is dead. If this is a terraform apply -auto-approve in a deployment workflow, the Lambda function may be in a partial update state — new code deployed, but environment variables or aliases not updated — causing silent runtime failures.
The Attack Vector / Blast Radius
This error is a state corruption vector, not just a cosmetic drift. Here is the cascading failure chain:
Partial apply state: Terraform marks the resource as tainted internally. Subsequent runs may attempt a
destroy+recreatecycle, causing Lambda cold-start downtime and wiping any out-of-band configuration (concurrency limits, reserved concurrency set via console).filename+s3_bucketmutual exclusivity violation: Specifying bothfilenameands3_bucket/s3_keyin the same resource block is the #1 root cause. The provider resolves the deployment package from S3 at apply time, but thesource_code_hashwas computed from the localfilenamezip during plan — they never match.last_modifiedtimestamp churn: AWS updateslast_modifiedon every API describe call in some regions. If your provider version is < 5.x and you have noignore_changesblock, everyplanshows a diff, and everyapplytriggers this error.Environment variable map ordering: Terraform's
maptype does not guarantee key ordering in older provider versions. A Lambda with 10 env vars can show a perpetual diff because the provider returns keys in alphabetical order but your HCL defines them in insertion order.Blast radius: In a monorepo with a
for_eachLambda factory pattern, one broken Lambda resource blocks the entire module apply, leaving 10–50 other functions undeployed.
How to Fix It (The Solution)
Root Cause 1: filename and s3_bucket Conflict (Most Common)
resource "aws_lambda_function" "my_function" {
function_name = "my-api-handler"
role = aws_iam_role.lambda_exec.arn
handler = "index.handler"
runtime = "nodejs20.x"
- filename = "${path.module}/builds/function.zip"
- source_code_hash = filebase64sha256("${path.module}/builds/function.zip")
- s3_bucket = "my-deployment-bucket"
- s3_key = "builds/function.zip"
+ # PICK ONE deployment method. S3 is preferred for CI/CD pipelines.
+ s3_bucket = "my-deployment-bucket"
+ s3_key = "builds/function.zip"
+ source_code_hash = data.aws_s3_object.function_hash.etag
}
+
+data "aws_s3_object" "function_hash" {
+ bucket = "my-deployment-bucket"
+ key = "builds/function.zip"
+}
Root Cause 2: last_modified and Computed Attribute Drift
resource "aws_lambda_function" "my_function" {
function_name = "my-api-handler"
role = aws_iam_role.lambda_exec.arn
handler = "index.handler"
runtime = "nodejs20.x"
s3_bucket = "my-deployment-bucket"
s3_key = "builds/function.zip"
+ lifecycle {
+ ignore_changes = [
+ # last_modified is AWS-controlled; never manage it in Terraform
+ last_modified,
+ # If layers are updated out-of-band by a separate pipeline:
+ # layers,
+ ]
+ }
}
Enterprise Best Practice: Immutable S3-Based Deployments with Version Pinning
The correct enterprise pattern decouples the artifact pipeline from the infrastructure pipeline. Terraform should only manage configuration, not upload code.
-resource "aws_lambda_function" "my_function" {
- function_name = "my-api-handler"
- filename = "${path.module}/dist/handler.zip"
- source_code_hash = filebase64sha256("${path.module}/dist/handler.zip")
- role = aws_iam_role.lambda_exec.arn
- handler = "index.handler"
- runtime = "nodejs20.x"
-
- environment {
- variables = {
- LOG_LEVEL = "info"
- DB_HOST = var.db_host
- API_KEY = var.api_key
- }
- }
-}
+# Artifact version is injected as a variable from the CI/CD artifact pipeline
+variable "lambda_s3_version_id" {
+ type = string
+ description = "S3 VersionId of the Lambda deployment package. Set by the artifact pipeline."
+}
+
+resource "aws_lambda_function" "my_function" {
+ function_name = "my-api-handler"
+ role = aws_iam_role.lambda_exec.arn
+ handler = "index.handler"
+ runtime = "nodejs20.x"
+
+ s3_bucket = "my-deployment-bucket"
+ s3_key = "builds/handler.zip"
+ s3_object_version = var.lambda_s3_version_id
+
+ # source_code_hash derived from S3 ETag — not local filesystem
+ source_code_hash = data.aws_s3_object.function_pkg.etag
+
+ environment {
+ variables = {
+ # Alphabetical order prevents map-ordering drift in older provider versions
+ API_KEY = var.api_key
+ DB_HOST = var.db_host
+ LOG_LEVEL = "info"
+ }
+ }
+
+ lifecycle {
+ ignore_changes = [last_modified]
+ }
+}
+
+data "aws_s3_object" "function_pkg" {
+ bucket = "my-deployment-bucket"
+ key = "builds/handler.zip"
+ version_id = var.lambda_s3_version_id
+}
💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.
Prevention in CI/CD
1. Checkov Policy (blocks filename + s3_bucket co-existence):
# .checkov.yaml
checks:
- id: CKV_AWS_50 # Lambda X-Ray tracing — already built-in
custom_checks:
- name: NO_LAMBDA_DUAL_SOURCE
resource_type: aws_lambda_function
check_type: attribute
# Use a custom Python check for mutual exclusivity validation
For this specific pattern, use a Sentinel or OPA policy in Terraform Cloud/Enterprise:
# opa/lambda_source_policy.rego
package terraform.aws.lambda
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_lambda_function"
config := resource.change.after
# Deny if both filename and s3_bucket are non-null
config.filename != null
config.s3_bucket != null
msg := sprintf(
"Lambda function '%s' specifies both 'filename' and 's3_bucket'. Use one deployment method only.",
[resource.name]
)
}
2. terraform validate + tflint in pre-commit:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/antonbabenko/pre-commit-terraform
rev: v1.88.0
hooks:
- id: terraform_validate
- id: terraform_tflint
args:
- --args=--enable-rule=aws_lambda_function_invalid_runtime
- id: terraform_checkov
args:
- --args=--framework terraform
3. Pin AWS Provider Version: The unexpected new value bug has known fixes in specific provider releases. Lock your provider version to prevent regression:
# versions.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
# Minimum version with Lambda state drift fixes
version = ">= 5.31.0, < 6.0.0"
}
}
}
4. terraform plan artifact diffing in PR pipelines: Store the plan output as a structured JSON artifact (terraform show -json tfplan.binary > plan.json) and diff it against the previous run in your PR bot. Any attribute showing perpetual drift is a signal to add ignore_changes or fix the source conflict before it hits production.