Skip to content

feat(lambda): optional version-pinned provisioned concurrency#1110

Merged
despock merged 2 commits into
mainfrom
feat/lambda-pc-on-version
Jun 19, 2026
Merged

feat(lambda): optional version-pinned provisioned concurrency#1110
despock merged 2 commits into
mainfrom
feat/lambda-pc-on-version

Conversation

@despock

@despock despock commented Jun 19, 2026

Copy link
Copy Markdown
Member

Why

When provisioned concurrency is attached to a Lambda alias, every CloudFormation alias update that changes FunctionVersion triggers Lambda's built-in canary deploy behaviour — CFN sets RoutingConfig.AdditionalVersionWeights to keep traffic on the old version until PC allocates on the new one. If the new version's init fails (FUNCTION_ERROR_INIT_FAILURE), those routing weights persist at 100% on old, and every subsequent deploy fails with:

Resource handler returned message: "Invalid alias configuration for
Provisioned Concurrency (Service: Lambda, Status Code: 400)"

because Lambda forbids RoutingConfig and ProvisionedConcurrencyConfig on the same alias update. The stack ends up wedged until an operator manually runs aws lambda update-alias --routing-config '{}'.

We hit this on keystone-storefront: a layer-shrink dropped fast-safe-stringify from the Next standalone bundle, v85 failed init, alias canary pinned to v84, and v86/v87 both failed with the same alias error even after fixing the bundle. Required manual alias clearing on five lambdas across two deploys to unstick.

What

Add onVersion?: boolean to ProvisionedConcurrencyProps (default false). When true:

  • PC attaches inline on the published CfnVersion (no standalone AWS::Lambda::ProvisionedConcurrencyConfig L1 exists in CDK).
  • An AWS::ApplicationAutoScaling::ScalableTarget is created with ResourceId: function:<fn>:<version> and a target-tracking policy using PredefinedMetric.LAMBDA_PROVISIONED_CONCURRENCY_UTILIZATION.
  • The alias carries no PC config — so CFN alias updates are atomic, never set routing weights, and the wedge can't happen.
lambdaAliases: [{
  aliasName: 'latest',
  provisionedConcurrency: {
    minCapacity: 1,
    maxCapacity: 2,
    utilizationTarget: 0.8,
    onVersion: true,   // <-- attach PC to the version, not the alias
  },
}]

Trade-off

The autoscaling target's ResourceId embeds the version number, so one target accumulates per deploy. Regional soft limit is 2,500 scalable targets per region per account — fine for normal cadence; worth watching for runaway-rebuild scenarios.

Backwards compatibility

Default is false. Existing consumers that don't set onVersion keep alias-based PC behaviour exactly as before.

despock added 2 commits June 19, 2026 06:41
Add ProvisionedConcurrencyProps.onVersion (default false). When true,
PC + autoscaling attach to the function's published version
(function:<fn>:<version>) instead of the alias
(function:<fn>:<aliasName>).

Why
---
When PC is on the alias, every CFN alias update that changes
FunctionVersion triggers Lambda's built-in canary behaviour: CFN sets
RoutingConfig.AdditionalVersionWeights to keep traffic on the old
version until PC is allocated on the new one. If the new version's
init fails (FUNCTION_ERROR_INIT_FAILURE), the routing weights persist
at 100% on old, and every subsequent deploy fails with "Invalid alias
configuration for Provisioned Concurrency" because Lambda forbids
PC + routing config on the same alias update. The stack wedges until
an operator manually `update-alias --routing-config '{}'`s.

We hit this on keystone-storefront: a layer-shrink dropped
fast-safe-stringify from the standalone bundle, v85 failed init, alias
canary pinned to v84, all subsequent deploys (v86, v87) failed with
the same alias error even after fixing the bundle. Required manual
alias clearing on five lambdas across two deploys.

Attaching PC to the version sidesteps this entirely: the alias
carries no PC, so version swaps are atomic and never set routing
weights. PC warms the new version because CDK publishes a new
currentVersion on every code change and the inline PC config +
autoscaling target attach to that version.

Trade-off
---------
ApplicationAutoScaling target's resource ID embeds the version
number, so one target accumulates per deploy. Regional soft limit is
2,500 targets — fine for normal cadence; worth noting for runaway
rebuild scenarios. Old PC configs get cleaned up when CDK detaches
the underlying CfnVersion.

Implementation
--------------
- types.ts: extend ProvisionedConcurrencyProps with the onVersion flag
  and a paragraph documenting the canary-trap rationale.
- main.ts: branch in createLambdaFunction's alias loop. When
  onVersion is set, call a new attachProvisionedConcurrencyToVersion
  helper that:
    1. Sets provisionedConcurrencyConfig inline on the published
       CfnVersion (CDK doesn't expose a standalone
       AWS::Lambda::ProvisionedConcurrencyConfig L1).
    2. Creates a ScalableTarget with resource id
       function:<fn>:<version> and a target-tracking policy using
       PredefinedMetric.LAMBDA_PROVISIONED_CONCURRENCY_UTILIZATION.

Tests
-----
- New testLambdaWithVersionPinnedConcurrency fixture in
  lambdas.json + matching create in TestCommonConstruct.
- Four new assertions:
    1. CfnVersion carries ProvisionedConcurrencyConfig
       (ProvisionedConcurrentExecutions: 1).
    2. The lambda's alias has NO ProvisionedConcurrencyConfig.
    3. ScalableTarget exists with resource id built from a
       Fn::GetAtt to <CurrentVersion>.Version (i.e. version-scoped).
    4. ScalingPolicy uses
       LambdaProvisionedConcurrencyUtilization with target 0.7.
- Bumped existing resourceCountIs: AWS::Lambda::Function 7 → 8,
  AWS::Lambda::Alias 2 → 3.

1,285 tests pass, lint clean, build clean.
@despock despock merged commit beec021 into main Jun 19, 2026
8 checks passed
@despock despock deleted the feat/lambda-pc-on-version branch June 19, 2026 05:54
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