Skip to content

LambdaRestApi does not trigger new deployment when Lambda is replaced via PhysicalName.GENERATE_IF_NEEDED #37296

@scottsandersdev

Description

@scottsandersdev

LambdaRestApi does not trigger new deployment when Lambda is replaced via PhysicalName.GENERATE_IF_NEEDED

Describe the bug

When a LambdaRestApi is backed by a Lambda function using PhysicalName.GENERATE_IF_NEEDED (or any CDK-generated name), changes that cause CloudFormation to replace the Lambda function do not trigger a new API Gateway deployment. The API Gateway continues pointing at the old (now-deleted) Lambda ARN, returning 500 Internal Server Error on every request.

This is a gap in the fix for #5306 (PR #8813). That fix added deployment-token tracking for the Lambda function name, but it's guarded by Token.isUnresolved():

// aws-apigateway/lib/integrations/lambda.js — LambdaIntegration.bind()
let deploymentToken;
Token.isUnresolved(functionName) || (deploymentToken = JSON.stringify({ functionName }));

When functionName is a Token (which it is for GENERATE_IF_NEEDED, cross-stack references, or the default unnamed case), deploymentToken remains undefined. Downstream in Method, addToLogicalId receives integrationToken: undefined, so the deployment's logical ID never changes regardless of what happens to the Lambda.

Expected Behavior

Any change to the backing Lambda function — including replacement triggered by a physical name change — should produce a new API Gateway deployment so the stage points at the new function.

Current Behavior

  1. Deploy a LambdaRestApi with a Lambda using PhysicalName.GENERATE_IF_NEEDED — works fine
  2. Make an unrelated change that alters the Lambda's generated physical name (e.g., changing environment resolution in a cross-stack reference, or modifying the construct tree)
  3. CloudFormation replaces the Lambda (delete old + create new with different name)
  4. CloudFormation updates the API Gateway Method integration URI to point at the new Lambda ARN
  5. No new Deployment is created — the stage still references the old deployment
  6. All requests return 500 Internal Server Error because the old deployment's cached integration points at the deleted Lambda

There is no warning or error during cdk synth or cdk deploy. The Lambda and API Gateway both appear healthy in the console. The only symptom is silent 500s.

Reproduction Steps

import { Stack, PhysicalName, Duration } from "aws-cdk-lib";
import { LambdaRestApi } from "aws-cdk-lib/aws-apigateway";
import { Function, Runtime, Code } from "aws-cdk-lib/aws-lambda";

// Stack A: Lambda with generated name
const fn = new Function(this, "MyFunction", {
  functionName: PhysicalName.GENERATE_IF_NEEDED,
  runtime: Runtime.NODEJS_20_X,
  handler: "index.handler",
  code: Code.fromInline('exports.handler = async () => ({ statusCode: 200, body: "ok" })'),
});

// Stack A: API Gateway
const api = new LambdaRestApi(this, "MyApi", { handler: fn });

// Stack B (references fn from Stack A):
// Any change that alters how Stack B resolves the cross-stack reference
// to fn will change the generated physical name, triggering replacement.
// After deployment: API Gateway returns 500.

Real-world trigger: In our case, changing super(scope, id) to super(scope, id, props) in a downstream stack that references the Lambda altered CDK's environment-aware name generation, producing a different physical name and triggering replacement.

Workaround

Add addToLogicalId manually after creating the LambdaRestApi:

const api = new LambdaRestApi(this, "MyApi", { handler: fn });

if (api.latestDeployment) {
  api.latestDeployment.addToLogicalId({
    lambdaFunctionName: fn.functionName,
  });
}

This works because addToLogicalId accepts Tokens — it serializes them into the logical ID hash via CloudFormation intrinsics, so when the resolved function name changes, the deployment hash changes too.

Suggested Fix

In LambdaIntegration.bind(), remove the Token.isUnresolved guard and always include the function name in the deployment token. Alternatively, use the Lambda function's logical ID (which is always a concrete string) instead of the physical name:

// Option A: Always include functionName (works with Tokens via CFN intrinsics)
const deploymentToken = JSON.stringify({ functionName });

// Option B: Use the logical resource ID (always concrete)
const deploymentToken = JSON.stringify({
  functionLogicalId: this.handler.node.defaultChild?.logicalId
});

Environment

  • CDK CLI Version: 2.1007.0
  • Framework Version: 2.1007.0
  • Node.js Version: v24.13.0
  • OS: Amazon Linux 2 (CI/CD) / macOS (local)
  • Language: TypeScript

Other Information

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions