Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions docs/infra.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,25 @@ deploy gets done, just no longer the routine way changes ship.
`BtfpDev` serves `dev.badthingsforpets.com`; `BtfpProd` serves `badthingsforpets.com` and
`www.badthingsforpets.com`.

### One-time: migrating a function onto SAM's `AutoPublishAlias`

If a Lambda function previously got its `live` alias from a CDK-managed `fn.addAlias('live')`
(pre-SAM canary setup), SAM's `AutoPublishAlias` cannot create its own `live` alias while
that one still exists — CloudFormation has no built-in way to order "delete the old alias"
before "create the new one" without an ordering dependency that cycles back on itself (the
migration needs the function's name via `Ref`, so it always depends on the function; adding
the reverse dependency propagates through SAM to the function's generated `Version` and
`Alias` too, forming a direct cycle). Delete the stale alias by hand, once, before deploying:

```bash
aws lambda delete-alias --function-name <physical-function-name> --name live
```

Safe to run even if the alias doesn't exist (returns `ResourceNotFoundException`). Do this
right before the deploy that introduces `publishLiveAlias` for that function — the alias is
briefly gone until SAM recreates it during that same deploy. Fine for dev (Basic-Auth-walled);
for prod, do it immediately before approving `deploy-prod` to minimize the gap.

## Budget (rough, at low/unknown traffic)

| Item | Cost |
Expand Down
64 changes: 12 additions & 52 deletions infra/cdk/lib/lambda-canary.ts
Original file line number Diff line number Diff line change
@@ -1,60 +1,10 @@
import * as cdk from 'aws-cdk-lib';
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
import {
AwsCustomResource,
AwsCustomResourcePolicy,
PhysicalResourceId,
} from 'aws-cdk-lib/custom-resources';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as sam from 'aws-cdk-lib/aws-sam';

const CANARY_PERIOD = cdk.Duration.minutes(1);

/**
* CDK `addAlias('live')` → SAM `AutoPublishAlias`: the old alias can linger in
* Lambda while SAM tries to create the same name. One-time DeleteAlias on
* create; scoped to the stack (not the function construct) so API Gateway
* integrations do not form a CFN dependency cycle with the function.
*/
function deleteOrphanLiveAliasBeforeSam(fn: lambda.Function): void {
const stack = cdk.Stack.of(fn);
new AwsCustomResource(stack, `${fn.node.id}SamLiveAliasMigration`, {
onCreate: {
service: '@aws-sdk/client-lambda',
action: 'DeleteAliasCommand',
parameters: {
FunctionName: fn.functionName,
Name: 'live',
},
physicalResourceId: PhysicalResourceId.of(
`${stack.stackName}-${fn.node.id}-sam-live-alias-migration`,
),
ignoreErrorCodesMatching: 'ResourceNotFoundException|NotFound',
},
onUpdate: {
service: '@aws-sdk/client-lambda',
action: 'GetFunctionCommand',
parameters: { FunctionName: fn.functionName },
physicalResourceId: PhysicalResourceId.of(
`${stack.stackName}-${fn.node.id}-sam-live-alias-migration`,
),
},
policy: AwsCustomResourcePolicy.fromStatements([
new iam.PolicyStatement({
actions: ['lambda:DeleteAlias', 'lambda:GetFunction'],
resources: [
stack.formatArn({
service: 'lambda',
resource: 'function',
resourceName: '*',
}),
],
}),
]),
});
}

/**
* Turns a CDK `Function` into an `AWS::Serverless::Function` so we get SAM's
* AutoPublishAlias / AutoPublishAliasAllProperties / DeploymentPreference
Expand All @@ -64,6 +14,18 @@ function deleteOrphanLiveAliasBeforeSam(fn: lambda.Function): void {
* `type` starts the traffic shift; `alarms` (if any) only roll it back.
*
* Traffic must go to the returned `:live` alias, not `$LATEST`.
*
* One-time migration note: a function that previously used a CDK-managed
* `fn.addAlias('live')` (pre-SAM) has an existing `live` alias outside this
* stack's control. SAM's `AutoPublishAlias` cannot create it while it
* exists — CloudFormation has no reliable way to order that deletion
* before this creation without a Ref-based custom resource depending on
* the function, which cycles back through SAM's DependsOn propagation to
* every resource generated from it (Version, Alias). Delete it manually,
* once, before deploying this change:
* aws lambda delete-alias --function-name <physical-function-name> --name live
* Safe to run against a function with no alias (returns ResourceNotFoundException).
* See docs/infra.md.
*/
export function publishLiveAlias(
fn: lambda.Function,
Expand All @@ -79,8 +41,6 @@ export function publishLiveAlias(

fn.stack.addTransform('AWS::Serverless-2016-10-31');

deleteOrphanLiveAliasBeforeSam(fn);

const cfn = fn.node.defaultChild as lambda.CfnFunction;
cfn.addOverride('Type', 'AWS::Serverless::Function');

Expand Down
Loading