Skip to content

Commit

Permalink
Merge pull request #452 from wellcomecollection/upgrade-cache-invalid…
Browse files Browse the repository at this point in the history
…ation-lambdas-to-node20

Upgrade cache invalidation lambdas to node20
  • Loading branch information
agnesgaroux authored Dec 2, 2024
2 parents 326b9cd + 72093e5 commit 12d9b18
Show file tree
Hide file tree
Showing 8 changed files with 1,088 additions and 307 deletions.
7 changes: 5 additions & 2 deletions cloudfront/invalidation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

Lambda function for invalidating CloudFront caches, triggered by SNS topic.

Message sent to SNS topic is an array of paths to invalidate, e.g. `["/path/to/invalidate", "/wildcard/path*"]`
SNS message body is an array of paths to invalidate, e.g.
```
{ "paths": ["/path/to/invalidate", "/wildcard/path*", "/catalogue/v2/works/z869w74a"] }
```

Current timestamp is used for [`CallerReference`](https://docs.aws.amazon.com/cloudfront/latest/APIReference/API_CreateInvalidation.html#API_CreateInvalidation_RequestSyntax) parameter to `CreateInvalidation`.
SNS timestamp, ie. the time (GMT) when the notification was published, is used for [`CallerReference`](https://docs.aws.amazon.com/cloudfront/latest/APIReference/API_CreateInvalidation.html#API_CreateInvalidation_RequestSyntax) parameter to `CreateInvalidationCommand`.
2 changes: 1 addition & 1 deletion cloudfront/invalidation/lambda/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM public.ecr.aws/docker/library/node:16-slim
FROM public.ecr.aws/docker/library/node:20-slim

RUN apt-get update && apt-get install -yq --no-install-recommends zip

Expand Down
9 changes: 4 additions & 5 deletions cloudfront/invalidation/lambda/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,16 @@
"description": "Lambda for handling CloudFront invalidation requests",
"main": "index.js",
"engines": {
"node": "16.*.*"
"node": "20.*.*"
},
"license": "MIT",
"devDependencies": {
"@aws-sdk/types": "^3.10.0",
"@types/aws-lambda": "^8.10.73",
"@types/jest": "^26.0.22",
"@types/node": "^14.14.37",
"@typescript-eslint/eslint-plugin": "^4.17.0",
"@typescript-eslint/parser": "^4.17.0",
"aws-sdk-mock": "^5.1.0",
"aws-sdk-client-mock": "^4.1.0",
"eslint": "^7.21.0",
"eslint-config-prettier": "^8.1.0",
"eslint-config-standard": "^16.0.2",
Expand All @@ -34,12 +33,12 @@
"build": "tsc -p tsconfig.build.json && (cd dist && zip -r sns_handler.zip .)",
"deploy": "yarn build && yarn test && node deploy",
"lint": "eslint src/*.ts --fix",
"dockerLoginLocal": "aws ecr get-login-password --region eu-west-1 --profile platform | docker login --username AWS --password-stdin 760097843905.dkr.ecr.eu-west-1.amazonaws.com",
"dockerLoginLocal": "aws ecr get-login-password --region eu-west-1 --profile platform-developer | docker login --username AWS --password-stdin 760097843905.dkr.ecr.eu-west-1.amazonaws.com",
"dockerBuildLocal": "yarn dockerLoginLocal && docker build . -t weco_invalidation_lambda",
"dockerTestLocal": "yarn dockerBuildLocal && docker run weco_invalidation_lambda yarn test",
"dockerDeployLocal": "yarn dockerBuildLocal && docker run weco_invalidation_lambda -v ~/.aws:/root/.aws yarn deploy"
},
"dependencies": {
"aws-sdk": "^2.881.0"
"@aws-sdk/client-cloudfront": "^3.699.0"
}
}
107 changes: 49 additions & 58 deletions cloudfront/invalidation/lambda/src/cacheInvalidation.test.ts
Original file line number Diff line number Diff line change
@@ -1,73 +1,64 @@
import { expect, test } from '@jest/globals';
import { SNSEvent, SNSEventRecord } from 'aws-lambda/trigger/sns';
import { Context } from 'aws-lambda';

import { SNSEvent, Context } from 'aws-lambda';
import { CloudFrontClient } from '@aws-sdk/client-cloudfront'
import { mockClient } from 'aws-sdk-client-mock';
import { handler } from './cacheInvalidation';

import AWS from 'aws-sdk';
import {
CreateInvalidationRequest,
InvalidationBatch,
} from 'aws-sdk/clients/cloudfront';
import AWSMock from 'aws-sdk-mock';
const timeStamp = new Date("2019-01-02T12:45:07.000Z").toISOString()

test('makes correct invalidation request', async () => {
process.env = {
DISTRIBUTION_ID: "Distro McDistrFace",
};
const message = {
paths: ['/path/to/invalidate', '/path/with/wildcard*'],
reference: 'my-test-reference',
};
const fakeEvent = getFakeEvent(JSON.stringify(message));

AWSMock.setSDKInstance(AWS);
let calledWith: CreateInvalidationRequest;
AWSMock.mock(
'CloudFront',
'createInvalidation',
(params: CreateInvalidationRequest, callback: Function) => {
calledWith = params;
callback(null, null);
}
);
const mockEvent = getMockEvent(JSON.stringify(message));

const expectedInvalidation = {
CallerReference: 'my-test-reference',
Paths: {
Quantity: 2,
Items: ['/path/to/invalidate', '/path/with/wildcard*'],
},
} as InvalidationBatch;
const cloudFrontMock = mockClient(CloudFrontClient);

await handler(fakeEvent, {} as Context, () => {});
expect(calledWith!.InvalidationBatch).toStrictEqual(expectedInvalidation);
const expectedInvalidationCommand = {
DistributionId: "Distro McDistrFace",
InvalidationBatch: {
Paths: { Quantity: message.paths.length, Items: message.paths },
CallerReference: timeStamp
}
};

AWSMock.restore('CloudFront');
await handler(mockEvent, {} as Context, () => {});
expect(cloudFrontMock.call(0).args[0].input).toStrictEqual(expectedInvalidationCommand)
});

// note - this is boilerplate from aws docs
function getFakeEvent(message: string): SNSEvent {
function getMockEvent(message: string): SNSEvent {
return {
Records: [
"Records": [
{
EventVersion: '1.0',
EventSubscriptionArn:
'arn:aws:sns:us-east-2:123456789012:sns-lambda:21be56ed-a058-49f5-8c98-aedd2564c486',
EventSource: 'aws:sns',
Sns: {
SignatureVersion: '1',
Timestamp: '2019-01-02T12:45:07.000Z',
Signature:
'tcc6faL2yUC6dgZdmrwh1Y4cGa/ebXEkAi6RibDsvpi+tE/1+82j...65r==',
SigningCertUrl:
'https://sns.us-east-2.amazonaws.com/SimpleNotificationService-ac565b8b1a6c5d002d285f9598aa1d9b.pem',
MessageId: '95df01b4-ee98-5cb9-9903-4c221d41eb5e',
Type: 'Notification',
UnsubscribeUrl:
'https://sns.us-east-2.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-2:123456789012:test-lambda:21be56ed-a058-49f5-8c98-aedd2564c486',
TopicArn: 'arn:aws:sns:us-east-2:123456789012:sns-lambda',
Subject: 'TestInvoke',
Message: message,
},
} as SNSEventRecord,
],
} as SNSEvent;
}
"EventVersion": "1.0",
"EventSubscriptionArn": "arn:aws:sns:us-east-1:123456789012:sns-lambda:21be56ed-a058-49f5-8c98-aedd2564c486",
"EventSource": "aws:sns",
"Sns": {
"SignatureVersion": "1",
"Timestamp": timeStamp,
"Signature": "tcc6faL2yUC6dgZdmrwh1Y4cGa/ebXEkAi6RibDsvpi+tE/1+82j...65r==",
"SigningCertUrl": "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-ac565b8b1a6c5d002d285f9598aa1d9b.pem",
"MessageId": "95df01b4-ee98-5cb9-9903-4c221d41eb5e",
"Message": message,
"MessageAttributes": {
"Test": {
"Type": "String",
"Value": "TestString"
},
"TestBinary": {
"Type": "Binary",
"Value": "TestBinary"
}
},
"Type": "Notification",
"UnsubscribeUrl": "https://sns.us-east-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-1:123456789012:test-lambda:21be56ed-a058-49f5-8c98-aedd2564c486",
"TopicArn":"arn:aws:sns:us-east-1:123456789012:sns-lambda",
"Subject": "TestInvoke"
}
}
]
}
}
79 changes: 22 additions & 57 deletions cloudfront/invalidation/lambda/src/cacheInvalidation.ts
Original file line number Diff line number Diff line change
@@ -1,62 +1,27 @@
import { SNSHandler, SNSMessage } from 'aws-lambda/trigger/sns';
import * as AWS from 'aws-sdk';
import { CreateInvalidationRequest as CloudFrontInvalidationRequest } from 'aws-sdk/clients/cloudfront';
import {CloudFront} from "aws-sdk";

type IncomingMessage = {
reference: string;
paths: string[];
};

type InvalidationRequest = {
distribution: string;
reference: string;
paths: string[];
};

function createCloudFrontRequest(
invalidationRequest: InvalidationRequest
): CloudFrontInvalidationRequest {
return {
DistributionId: invalidationRequest.distribution,
InvalidationBatch: {
CallerReference: invalidationRequest.reference,
Paths: {
import { SNSHandler, SNSEvent } from 'aws-lambda';
import {
CloudFrontClient,
CreateInvalidationCommand
} from '@aws-sdk/client-cloudfront'

export const handler: SNSHandler = async (event: SNSEvent) => {
const cloudFrontClient = new CloudFrontClient();

const distributionId = String(process.env.DISTRIBUTION_ID);
const invalidationRequest = JSON.parse(event.Records[0].Sns.Message)

const createInvalidationCommandInput = {
DistributionId: distributionId,
InvalidationBatch: {
Paths: {
Quantity: invalidationRequest.paths.length,
Items: invalidationRequest.paths,
},
// we use the time (GMT) when the notification was published to identify the invalidation
CallerReference: event.Records[0].Sns.Timestamp
},
} as CloudFrontInvalidationRequest;
}

function createInvalidationRequest(
distribution: string,
message: SNSMessage
): InvalidationRequest {
const incomingMessage = JSON.parse(message.Message) as IncomingMessage;
return {
distribution: distribution,
reference: incomingMessage.reference,
paths: incomingMessage.paths,
} as InvalidationRequest;
}

function runInvalidation(
cloudfront:CloudFront,
invalidationRequest: InvalidationRequest
) {
const cloudFrontRequest = createCloudFrontRequest(invalidationRequest);
return cloudfront.createInvalidation(cloudFrontRequest).promise();
}

export const handler: SNSHandler = async (event) => {
const cloudfront = new AWS.CloudFront();

const distro = String(process.env.DISTRIBUTION_ID);
const invalidationRequest = createInvalidationRequest(
distro,
event.Records[0].Sns
);
}

await runInvalidation(cloudfront, invalidationRequest);
};
const command = new CreateInvalidationCommand(createInvalidationCommandInput);
await cloudFrontClient.send(command);
};
Loading

0 comments on commit 12d9b18

Please sign in to comment.