Skip to content
Open
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
13 changes: 11 additions & 2 deletions docs/guides/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -722,15 +722,24 @@ functions:

By default, osls creates function versions for every deploy. This behavior is optional, and can be turned off in cases where you don't invoke past versions by their qualifier. If you would like to do this, you can invoke your functions as `arn:aws:lambda:....:function/myFunc:3` to invoke version 3 for example.

Versions are not cleaned up by osls, so make sure you use a plugin or other tool to prune sufficiently old versions. osls can't clean up versions because it doesn't have information about whether older versions are invoked or not. This feature adds to the number of total stack outputs and resources because a function version is a separate resource from the function it refers to.
Older versions are not removed automatically unless you enable `provider.pruneFunctionVersions`. When enabled, osls deletes function and layer versions beyond the configured limit after each deploy, while keeping versions referenced by aliases. Aliased versions are never deleted.

To turn off function versioning, set the provider-level option `versionFunctions`.
To turn off function versioning, set the provider-level option `versionFunctions`. `pruneFunctionVersions` cannot be used when `versionFunctions` is `false`.

```yml
provider:
versionFunctions: false
```

Enable automatic pruning after deploy:

```yml
provider:
pruneFunctionVersions: true # keeps 10 versions (default)
# pruneFunctionVersions:
# number: 20
```

## Dead Letter Queue (DLQ)

When AWS lambda functions fail, they are [retried](http://docs.aws.amazon.com/lambda/latest/dg/retries-on-errors.html). If the retries also fail, AWS has a feature to send information about the failed request to a SNS topic or SQS queue, called the [Dead Letter Queue](http://docs.aws.amazon.com/lambda/latest/dg/dlq.html), which you can use to track and diagnose and react to lambda failures.
Expand Down
4 changes: 4 additions & 0 deletions docs/guides/serverless.yml.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ provider:
kmsKeyArn: arn:aws:kms:us-east-1:XXXXXX:key/some-hash
# Use function versioning (enabled by default)
versionFunctions: false
# After deploy, delete older Lambda function and layer versions (disabled by default)
pruneFunctionVersions: true # keeps 10 versions
# pruneFunctionVersions:
# number: 20 # explicit number of versions to keep
# Processor architecture: 'x86_64' or 'arm64' via Graviton2 (default: x86_64)
architecture: x86_64
```
Expand Down
14 changes: 14 additions & 0 deletions lib/plugins/aws/provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,19 @@ class AwsProvider {
awsLambdaTimeout: { type: 'integer', minimum: 1, maximum: 900 },
awsLambdaTracing: { anyOf: [{ enum: ['Active', 'PassThrough'] }, { type: 'boolean' }] },
awsLambdaVersioning: { type: 'boolean' },
awsPruneFunctionVersions: {
anyOf: [
{ type: 'boolean' },
{
type: 'object',
properties: {
number: { type: 'integer', minimum: 0 },
},
additionalProperties: false,
required: ['number'],
},
],
},
awsLambdaVpcConfig: {
type: 'object',
properties: {
Expand Down Expand Up @@ -1237,6 +1250,7 @@ class AwsProvider {
vpc: { $ref: '#/definitions/awsLambdaVpcConfig' },
vpcEndpointIds: { $ref: '#/definitions/awsCfArrayInstruction' },
versionFunctions: { $ref: '#/definitions/awsLambdaVersioning' },
pruneFunctionVersions: { $ref: '#/definitions/awsPruneFunctionVersions' },
websocket: {
type: 'object',
properties: {
Expand Down
257 changes: 257 additions & 0 deletions lib/plugins/aws/prune.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
'use strict';

const {
LambdaClient,
DeleteFunctionCommand,
DeleteLayerVersionCommand,
ListAliasesCommand,
ListLayerVersionsCommand,
ListVersionsByFunctionCommand,
} = require('@aws-sdk/client-lambda');
const ServerlessError = require('../../serverless-error');
const { log } = require('../../utils/serverless-utils/log');
const {
getAwsErrorMessage,
getAwsErrorStatusCode,
isLambdaResourceNotFoundError,
} = require('../../aws/aws-sdk-v3-error');

const DEFAULT_PRUNE_VERSIONS = 10;

class AwsPrune {
constructor(serverless, options) {
this.serverless = serverless;
this.options = options || {};
this.provider = this.serverless.getProvider('aws');

this.hooks = {
'before:deploy:deploy': async () => this.validateConfiguration(),
'after:deploy:deploy': async () => this.postDeploy(),
};
}

resolvePruneConfig() {
const setting = this.serverless.service.provider.pruneFunctionVersions;
if (setting == null || setting === false) return null;
if (setting === true) return { number: DEFAULT_PRUNE_VERSIONS };
if (typeof setting === 'object' && setting.number != null) {
const number = parseInt(setting.number, 10);
if (!isNaN(number) && number >= 0) return { number };
}
throw new ServerlessError(
'provider.pruneFunctionVersions must be true or an object with a non-negative number property',
'INVALID_PRUNE_FUNCTION_VERSIONS_CONFIG'
);
}

getNumber() {
const config = this.resolvePruneConfig();
return config ? config.number : undefined;
}

shouldVersionFunction(functionKey) {
const functionObject = this.serverless.service.getFunction(functionKey);
if (functionObject.versionFunction != null) {
return functionObject.versionFunction;
}
return this.serverless.service.provider.versionFunctions !== false;
}

validateConfiguration() {
const config = this.resolvePruneConfig();
if (!config) return;

if (this.serverless.service.provider.versionFunctions === false) {
throw new ServerlessError(
'provider.pruneFunctionVersions cannot be used when provider.versionFunctions is false',
'PRUNE_INCOMPATIBLE_WITH_VERSION_FUNCTIONS'
);
}
}

async postDeploy() {
const config = this.resolvePruneConfig();
if (!config) return;

this.validateConfiguration();

await Promise.all([this.pruneFunctions(), this.pruneLayers()]);
}

async getLambdaClient() {
this.lambdaClientPromise ||= this.provider
.getAwsSdkV3Config()
.then((lambdaConfig) => new LambdaClient(lambdaConfig));
return this.lambdaClientPromise;
}

async pruneLayers() {
const layerNames = this.serverless.service
.getAllLayers()
.map((key) => this.serverless.service.getLayer(key).name || key);

let prunedCount = 0;
for (const layerName of layerNames) {
const versions = await this.listVersionsForLayer(layerName);
if (!versions.length) continue;

const deletionCandidates = this.selectPruneVersionsForLayer(versions);
prunedCount += await this.deleteVersionsForLayer(layerName, deletionCandidates);
}

if (prunedCount > 0) {
log.notice.success(`Pruned ${prunedCount} layer version${prunedCount === 1 ? '' : 's'}`);
}
}

async pruneFunctions() {
let prunedCount = 0;

for (const functionKey of this.serverless.service.getAllFunctions()) {
if (!this.shouldVersionFunction(functionKey)) continue;

const functionName = this.serverless.service.getFunction(functionKey).name;
const [versions, aliases] = await Promise.all([
this.listVersionsForFunction(functionName),
this.listAliasesForFunction(functionName),
]);
if (!versions.length) continue;

const deletionCandidates = this.selectPruneVersionsForFunction(versions, aliases);
prunedCount += await this.deleteVersionsForFunction(functionName, deletionCandidates);
}

if (prunedCount > 0) {
log.notice.success(`Pruned ${prunedCount} function version${prunedCount === 1 ? '' : 's'}`);
}
}

async deleteVersionsForLayer(layerName, versions) {
const lambda = await this.getLambdaClient();
let deletedCount = 0;

for (const version of versions) {
log.info(`Deleting layer version ${layerName}:${version}.`);
await lambda.send(
new DeleteLayerVersionCommand({
LayerName: layerName,
VersionNumber: version,
})
);
deletedCount++;
}

return deletedCount;
}

async deleteVersionsForFunction(functionName, versions) {
const lambda = await this.getLambdaClient();
let deletedCount = 0;

for (const version of versions) {
log.info(`Deleting function version ${functionName}:${version}.`);
try {
await lambda.send(
new DeleteFunctionCommand({
FunctionName: functionName,
Qualifier: version,
})
);
deletedCount++;
} catch (error) {
const statusCode = getAwsErrorStatusCode(error);
const message = getAwsErrorMessage(error) || '';
if (
statusCode === 400 &&
message.startsWith('Lambda was unable to delete') &&
message.includes('because it is a replicated function.')
) {
log.warning(
`Unable to delete replicated Lambda@Edge function version ${functionName}:${version}.`
);
} else {
throw error;
}
}
}

return deletedCount;
}

async listAliasesForFunction(functionName) {
try {
return await this.paginateLambda(
ListAliasesCommand,
{ FunctionName: functionName },
'Aliases'
);
} catch (error) {
if (isLambdaResourceNotFoundError(error)) return [];
throw error;
}
}

async listVersionsForFunction(functionName) {
try {
return await this.paginateLambda(
ListVersionsByFunctionCommand,
{ FunctionName: functionName },
'Versions'
);
} catch (error) {
if (isLambdaResourceNotFoundError(error)) return [];
throw error;
}
}

async listVersionsForLayer(layerName) {
try {
return await this.paginateLambda(
ListLayerVersionsCommand,
{ LayerName: layerName },
'LayerVersions'
);
} catch (error) {
if (isLambdaResourceNotFoundError(error)) return [];
throw error;
}
}

async paginateLambda(Command, params, resultKey) {
const lambda = await this.getLambdaClient();
const results = [];
let input = params;

do {
const response = await lambda.send(new Command(input));
results.push(...(response[resultKey] || []));
input = response.NextMarker ? { ...params, Marker: response.NextMarker } : null;
} while (input);

return results;
}

selectPruneVersionsForFunction(versions, aliases) {
const aliasedVersions = new Set(aliases.map((alias) => String(alias.FunctionVersion)));

return versions
.map((versionEntry) => versionEntry.Version)
.filter((version) => version !== '$LATEST')
.filter((version) => !aliasedVersions.has(String(version)))
.sort((a, b) =>
parseInt(a, 10) === parseInt(b, 10) ? 0 : parseInt(a, 10) > parseInt(b, 10) ? -1 : 1
)
.slice(this.getNumber());
}

selectPruneVersionsForLayer(versions) {
return versions
.map((versionEntry) => versionEntry.Version)
.sort((a, b) =>
parseInt(a, 10) === parseInt(b, 10) ? 0 : parseInt(a, 10) > parseInt(b, 10) ? -1 : 1
)
.slice(this.getNumber());
}
}

module.exports = AwsPrune;
1 change: 1 addition & 0 deletions lib/plugins/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ module.exports = [
require('./aws/remove/index.js'),
require('./aws/rollback.js'),
require('./aws/rollback-function.js'),
require('./aws/prune.js'),
require('./aws/package/compile/layers.js'),
require('./aws/package/compile/functions.js'),
require('./aws/package/compile/events/schedule.js'),
Expand Down
Loading
Loading