Skip to content

Commit

Permalink
feat(synthetics): enable auto delete lambdas via custom resource (#26580
Browse files Browse the repository at this point in the history
)

Synthetics [used](https://aws.amazon.com/about-aws/whats-new/2022/05/amazon-cloudwatch-synthetics-support-canary-resources-deletion/) to have a property `deleteLambdaResourceOnCanaryDeletion` that has since been deprecated and erased from cloudformation docs. Although this property still works today synthetics makes no promises that this is supported in the future.

Here in CDK land, this PR serves as a replacement to the `deleteLambdaResourceOnCanaryDeletion` property (called `enableAutoDeleteLambdas` on the L2 Canary) by implementing a custom resource similar to what we have in S3 and ECR.

**This PR deprecates `enableAutoDeleteLambdas` in favor of `cleanup: cleanup.LAMBDA`, an enum that achieves the same thing but via custom resource**

Closes #18448

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
kaizencc authored Aug 23, 2023
1 parent 27b7201 commit 6d1dc5b
Show file tree
Hide file tree
Showing 33 changed files with 4,270 additions and 1,271 deletions.
26 changes: 17 additions & 9 deletions packages/@aws-cdk/aws-synthetics-alpha/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,27 +102,35 @@ const schedule = synthetics.Schedule.cron({

If you want the canary to run just once upon deployment, you can use `Schedule.once()`.

### Canary DeleteLambdaResourcesOnCanaryDeletion
### Deleting underlying resources on canary deletion

You can specify whether the AWS CloudFormation is to also delete the Lambda functions and layers used by this canary, when the canary is deleted.
When you delete a lambda, the following underlying resources are isolated in your AWS account:

This can be provisioned by setting the `enableAutoDeleteLambdas` property to `true` when we define the canary.
- Lambda Function that runs your canary script
- S3 Bucket for artifact storage
- IAM roles and policies
- Log Groups in CloudWatch Logs.

```ts
const stack = new Stack();
To learn more about these underlying resources, see
[Synthetics Canaries Deletion](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/synthetics_canaries_deletion.html).

In the CDK, you can configure your canary to delete the underlying lambda function when the canary is deleted.
This can be provisioned by setting `cleanup: Cleanup.LAMBDA`. Note that this
will create a custom resource under the hood that takes care of the lambda deletion for you.

const canary = new synthetics.Canary(stack, 'Canary', {
```ts
const canary = new synthetics.Canary(this, 'Canary', {
test: synthetics.Test.custom({
handler: 'index.handler',
code: synthetics.Code.fromInline('/* Synthetics handler code'),
}),
enableAutoDeleteLambdas: true,
cleanup: synthetics.Cleanup.LAMBDA,
runtime: synthetics.Runtime.SYNTHETICS_NODEJS_PUPPETEER_4_0,
});
```

Synthetic Canaries create additional resources under the hood beyond Lambda functions. Setting `enableAutoDeleteLambdas: true` will take care of
cleaning up Lambda functions on deletion, but you still have to manually delete other resources like S3 buckets and CloudWatch logs.
> Note: To properly clean up your canary on deletion, you still have to manually delete other resources
> like S3 buckets and CloudWatch logs.
### Configuring the Canary Script

Expand Down
82 changes: 81 additions & 1 deletion packages/@aws-cdk/aws-synthetics-alpha/lib/canary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ import { Runtime } from './runtime';
import { Schedule } from './schedule';
import { CloudWatchSyntheticsMetrics } from 'aws-cdk-lib/aws-synthetics/lib/synthetics-canned-metrics.generated';
import { CfnCanary } from 'aws-cdk-lib/aws-synthetics';
import { CustomResource, CustomResourceProvider, CustomResourceProviderRuntime } from 'aws-cdk-lib/core';
import * as path from 'path';

const AUTO_DELETE_UNDERLYING_RESOURCES_RESOURCE_TYPE = 'Custom::SyntheticsAutoDeleteUnderlyingResources';
const AUTO_DELETE_UNDERLYING_RESOURCES_TAG = 'aws-cdk:auto-delete-underlying-resources';

/**
* Specify a test that the canary should run
Expand Down Expand Up @@ -50,6 +55,25 @@ export interface CustomTestOptions {
readonly handler: string,
}

/**
* Different ways to clean up underlying Canary resources
* when the Canary is deleted.
*/
export enum Cleanup {
/**
* Clean up nothing. The user is responsible for cleaning up
* all resources left behind by the Canary.
*/
NOTHING = 'nothing',

/**
* Clean up the underlying Lambda function only. The user is
* responsible for cleaning up all other resources left behind
* by the Canary.
*/
LAMBDA = 'lambda',
}

/**
* Options for specifying the s3 location that stores the data of each canary run. The artifacts bucket location **cannot**
* be updated once the canary is created.
Expand Down Expand Up @@ -195,9 +219,18 @@ export interface CanaryProps {
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-synthetics-canary.html#cfn-synthetics-canary-deletelambdaresourcesoncanarydeletion
*
* @default false
* @deprecated this feature has been deprecated by the service team, use `cleanup: Cleanup.LAMBDA` instead which will use a Custom Resource to achieve the same effect.
*/
readonly enableAutoDeleteLambdas?: boolean;

/**
* Specify the underlying resources to be cleaned up when the canary is deleted.
* Using `Cleanup.LAMBDA` will create a Custom Resource to achieve this.
*
* @default Cleanup.NOTHING
*/
readonly cleanup?: Cleanup;

/**
* Lifecycle rules for the generated canary artifact bucket. Has no effect
* if a bucket is passed to `artifactsBucketLocation`. If you pass a bucket
Expand Down Expand Up @@ -248,6 +281,7 @@ export class Canary extends cdk.Resource implements ec2.IConnectable {
* @internal
*/
private readonly _connections?: ec2.Connections;
private readonly _resource: CfnCanary;

public constructor(scope: Construct, id: string, props: CanaryProps) {
if (props.canaryName && !cdk.Token.isUnresolved(props.canaryName)) {
Expand Down Expand Up @@ -285,12 +319,49 @@ export class Canary extends cdk.Resource implements ec2.IConnectable {
code: this.createCode(props),
runConfig: this.createRunConfig(props),
vpcConfig: this.createVpcConfig(props),
deleteLambdaResourcesOnCanaryDeletion: props.enableAutoDeleteLambdas,
});
this._resource = resource;

this.canaryId = resource.attrId;
this.canaryState = resource.attrState;
this.canaryName = this.getResourceNameAttribute(resource.ref);

if (props.cleanup === Cleanup.LAMBDA ?? props.enableAutoDeleteLambdas) {
this.cleanupUnderlyingResources();
}
}

private cleanupUnderlyingResources() {
const provider = CustomResourceProvider.getOrCreateProvider(this, AUTO_DELETE_UNDERLYING_RESOURCES_RESOURCE_TYPE, {
codeDirectory: path.join(__dirname, '..', 'custom-resource-handlers', 'dist', 'aws-synthetics-alpha', 'auto-delete-underlying-resources-handler'),
useCfnResponseWrapper: false,
runtime: CustomResourceProviderRuntime.NODEJS_18_X,
description: `Lambda function for auto-deleting underlying resources created by ${this.canaryName}.`,
policyStatements: [{
Effect: 'Allow',
Action: ['lambda:DeleteFunction'],
Resource: this.lambdaArn(),
}, {
Effect: 'Allow',
Action: ['synthetics:GetCanary'],
Resource: '*',
}],
});

new CustomResource(this, 'AutoDeleteUnderlyingResourcesCustomResource', {
resourceType: AUTO_DELETE_UNDERLYING_RESOURCES_RESOURCE_TYPE,
serviceToken: provider.serviceToken,
properties: {
CanaryName: this.canaryName,
},
});

// We also tag the canary to record the fact that we want it autodeleted.
// The custom resource will check this tag before actually doing the delete.
// Because tagging and untagging will ALWAYS happen before the CR is deleted,
// we can set `autoDeleteLambda: false` without the removal of the CR emptying
// the lambda as a side effect.
cdk.Tags.of(this._resource).add(AUTO_DELETE_UNDERLYING_RESOURCES_TAG, 'true');
}

/**
Expand Down Expand Up @@ -402,6 +473,15 @@ export class Canary extends cdk.Resource implements ec2.IConnectable {
});
}

private lambdaArn() {
return cdk.Stack.of(this).formatArn({
service: 'lambda',
resource: 'function',
arnFormat: cdk.ArnFormat.COLON_RESOURCE_NAME,
resourceName: 'cwsyn-*',
});
}

/**
* Returns the code object taken in by the canary resource.
*/
Expand Down
6 changes: 5 additions & 1 deletion packages/@aws-cdk/aws-synthetics-alpha/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,10 @@
"cdk-build": {
"env": {
"AWSLINT_BASE_CONSTRUCT": true
}
},
"pre": [
"./scripts/airlift-custom-resource-handlers.sh"
]
},
"keywords": [
"aws",
Expand All @@ -84,6 +87,7 @@
"license": "Apache-2.0",
"devDependencies": {
"@aws-cdk/cdk-build-tools": "0.0.0",
"@aws-cdk/custom-resource-handlers": "0.0.0",
"@aws-cdk/integ-runner": "0.0.0",
"@aws-cdk/pkglint": "0.0.0",
"@aws-cdk/integ-tests-alpha": "0.0.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/bin/bash

scriptdir=$(cd $(dirname $0) && pwd)
customresourcedir=$(node -p "path.dirname(require.resolve('@aws-cdk/custom-resource-handlers/package.json'))")
awscdklibdir=${scriptdir}/..

list_custom_resources() {
for file in $customresourcedir/dist/aws-synthetics-alpha/*/index.js; do
echo $file | rev | cut -d "/" -f 2-4 | rev
done
}

customresources=$(list_custom_resources)

echo $customresources

cd $awscdklibdir
mkdir -p $awscdklibdir/custom-resource-handlers

for cr in $customresources; do
mkdir -p $awscdklibdir/custom-resource-handlers/$cr
cp $customresourcedir/$cr/index.js $awscdklibdir/custom-resource-handlers/$cr
done
Loading

0 comments on commit 6d1dc5b

Please sign in to comment.