diff --git a/docs/infra.md b/docs/infra.md index 757d7bc..6cfb081 100644 --- a/docs/infra.md +++ b/docs/infra.md @@ -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 --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 | diff --git a/infra/cdk/lib/lambda-canary.ts b/infra/cdk/lib/lambda-canary.ts index 483de48..11b7fd0 100644 --- a/infra/cdk/lib/lambda-canary.ts +++ b/infra/cdk/lib/lambda-canary.ts @@ -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 @@ -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 --name live + * Safe to run against a function with no alias (returns ResourceNotFoundException). + * See docs/infra.md. */ export function publishLiveAlias( fn: lambda.Function, @@ -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');