Initializing Enclave...

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_hash conflict, unmanaged last_modified, computed qualified_arn, or environment variable map ordering) and either remove the conflicting argument, add a lifecycle { ignore_changes } block, or pin the deployment mechanism to S3-only or filename-only.
  • Auto-refactor: Use our Client-Side Sandbox below to paste your aws_lambda_function block 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:

  1. Partial apply state: Terraform marks the resource as tainted internally. Subsequent runs may attempt a destroy + recreate cycle, causing Lambda cold-start downtime and wiping any out-of-band configuration (concurrency limits, reserved concurrency set via console).

  2. filename + s3_bucket mutual exclusivity violation: Specifying both filename and s3_bucket/s3_key in the same resource block is the #1 root cause. The provider resolves the deployment package from S3 at apply time, but the source_code_hash was computed from the local filename zip during plan — they never match.

  3. last_modified timestamp churn: AWS updates last_modified on every API describe call in some regions. If your provider version is < 5.x and you have no ignore_changes block, every plan shows a diff, and every apply triggers this error.

  4. Environment variable map ordering: Terraform's map type 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.

  5. Blast radius: In a monorepo with a for_each Lambda 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.

Related Diagnostics

"Part of the Syntax Utility Matrix."

View all 153 Syntax Tools →