Skip to content

fix(cli-internal): externalize Gen1 Lambda layer /opt/* requires in gen2-migration#14955

Draft
sarayev wants to merge 1 commit into
devfrom
fix/gen2-migration-lambda-layers
Draft

fix(cli-internal): externalize Gen1 Lambda layer /opt/* requires in gen2-migration#14955
sarayev wants to merge 1 commit into
devfrom
fix/gen2-migration-lambda-layers

Conversation

@sarayev

@sarayev sarayev commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Problem

Gen1→Gen2 migration dropped Lambda layer handling entirely. When a Gen1 function's code references a Lambda layer via a /opt/<layer> runtime path (e.g. require('/opt/GQLUtilities')), the migration tool:

  1. did not migrate the Gen1 Lambda layer package into any Gen2 layer definition, and
  2. did not externalize the /opt/* requires in the generated defineFunction config.

As a result the generated Gen2 app fails at CDK asset bundling — esbuild cannot resolve /opt/* and the deploy fails with FailedToBundleAsset. Observed on customer app bpedsys (ticket V2205182405), whose migrated functions contained require("/opt/GQLUtilities").

Root cause (exact gap)

packages/amplify-cli/src/commands/gen2-migration/generate/amplify/function/function.generator.tsFunctionGenerator.plan() built the FunctionRenderOptions (env vars, runtime, schedule, triggers, IAM grants…) but never inspected the function source for /opt/* layer imports and never emitted a layers map. There was zero layer//opt/ handling anywhere under gen2-migration/.

@aws-amplify/backend-function's factory computes esbuild externals as externalModules = Object.keys(props.layers), so populating defineFunction({ layers }) with a /opt/<layer> key is exactly what externalizes the module and unblocks bundling.

Fix (this PR — minimal, build-unblocking)

  • Detect: FunctionGenerator.detectLayerModules() scans the Gen1 function source (amplify/backend/function/<name>/src, recursive, skips node_modules, .js/.jsx/.ts/.tsx/.mjs/.cjs) for require()/import()/import … from '/opt/<layer>' and normalizes each to its layer root /opt/<layer> (deduped).
  • Externalize: FunctionRenderer.renderLayers() emits a layers map inside defineFunction, keyed by each /opt/<layer> module, with a REPLACE_WITH_LAYER_VERSION_ARN placeholder value and a // TODO comment (link to the layers docs). The key makes esbuild externalize the module → the build now succeeds.
  • Warn: a migration warning is surfaced both in the plan's Operations Summary (describe()) and via logger.warn during execution, telling the developer to replace the placeholder ARN(s) and noting that the Gen1 layer package itself is not migrated automatically.

Scope / follow-up (honest boundaries)

This PR fixes the build-breaking externalization gap so migrated apps build with a single, clearly-documented manual step (supply the layer ARN). It does not migrate the Gen1 layer code (function/<layer>/opt/*) into a Gen2 custom CDK LayerVersion and auto-wire it — that is a larger, riskier change recommended as a follow-up PR.

Tests

Added unit tests in function.generator.test.ts using real on-disk Gen1 function fixtures (temp cwd, no mocking of the detection path):

  • externalizes /opt/* requires via the layers map (and asserts it renders inside defineFunction, before the escape-hatch fn);
  • normalizes nested + multiple /opt/* imports to unique layer roots (no duplicate keys);
  • emits the migration warning in describe();
  • no-op when the function has no /opt/* imports (no layers map, no warning).

Local results

  • function.generator.test.ts: 27 passed (23 pre-existing + 4 new), no snapshot changes.
  • Full gen2-migration/generate suite: 233 passed / 21 suites, zero regressions.
  • tsc --noEmit on the package: clean.

🤖 Draft PR opened via Roko.

…en2-migration

Gen1->Gen2 migration dropped Lambda layer handling entirely: migrated
functions that require '/opt/<layer>' (a Lambda layer runtime path) had
no corresponding layers config, so esbuild could not resolve '/opt/*' at
CDK asset-bundling time and the deploy failed with FailedToBundleAsset
(observed on customer app bpedsys, ticket V2205182405).

FunctionGenerator now scans the Gen1 function source for
require()/import of '/opt/<layer>' specifiers, normalizes them to their
layer root, and emits a defineFunction({ layers }) map keyed by each
'/opt/<layer>' module. In Gen2 the layers key is used to externalize the
module during esbuild bundling, so the build succeeds. The layer ARN is
unknown at migration time, so a REPLACE_WITH_LAYER_VERSION_ARN
placeholder is emitted with a TODO comment, and a migration warning is
surfaced in the plan summary and via the logger.

Scope note: this fixes the build-breaking externalization gap. Full
migration of the Gen1 layer package into a Gen2 custom LayerVersion is a
larger, separate change and is left as a documented follow-up.

Adds unit tests using real on-disk Gen1 function fixtures that exercise
the actual detection path (no mocking).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant