Skip to content

Commit 0621df4

Browse files
committed
lambda and cdk
1 parent 17f37fe commit 0621df4

22 files changed

+1433
-1127
lines changed

.github/workflows/ab-testing-ci.yml

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,29 @@ jobs:
5555
name: ab-testing-build
5656
path: ab-testing/dist
5757

58+
build-lambda:
59+
name: Build Lambda
60+
runs-on: ubuntu-latest
61+
defaults:
62+
run:
63+
working-directory: ab-testing/deploy-dictionary-lambda
64+
permissions:
65+
contents: read
66+
steps:
67+
- uses: actions/checkout@v5
68+
69+
- name: Set up Node environment
70+
uses: ./.github/actions/setup-node-env
71+
72+
- name: Build Lambda
73+
run: pnpm build
74+
75+
- name: Save build
76+
uses: actions/upload-artifact@v5
77+
with:
78+
name: ab-testing-lambda-build
79+
path: ab-testing/deploy-dictionary-lambda/dist
80+
5881
build-ui:
5982
name: UI build
6083
runs-on: ubuntu-latest
@@ -81,7 +104,7 @@ jobs:
81104

82105
riff-raff:
83106
runs-on: ubuntu-latest
84-
needs: [build, build-ui]
107+
needs: [build, build-ui, build-lambda]
85108
permissions:
86109
id-token: write
87110
contents: read
@@ -108,6 +131,12 @@ jobs:
108131
name: ui-build
109132
path: output/ab-tests.html
110133

134+
- name: Fetch Lambda build
135+
uses: actions/download-artifact@v6.0.0
136+
with:
137+
name: ab-testing-lambda-build
138+
path: ab-testing/deploy-dictionary-lambda/dist
139+
111140
- name: Riff-Raff Upload
112141
uses: guardian/actions-riff-raff@v4.1.9
113142
with:
@@ -118,5 +147,7 @@ jobs:
118147
contentDirectories: |
119148
ab-testing:
120149
- ab-testing/dist
150+
ab-testing-deploy-dictionary-lambda:
151+
- ab-testing/deploy-dictionary-lambda/dist
121152
admin/ab-testing:
122153
- output/ab-tests.html

ab-testing/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
cdk.out

ab-testing/cdk.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"app": "node cdk/bin/cdk.ts",
3+
"context": {
4+
"aws-cdk:enableDiffNoFail": "true",
5+
"@aws-cdk/core:stackRelativeExports": "true"
6+
}
7+
}

ab-testing/cdk/bin/cdk.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import "source-map-support/register.js";
2+
import { RiffRaffYamlFile } from "@guardian/cdk/lib/riff-raff-yaml-file/index.js";
3+
import { App } from "aws-cdk-lib";
4+
import { DictionaryDeployLambda } from "../lib/dictionaryDeployLambda.ts";
5+
6+
const app = new App();
7+
8+
const appName = "ab-testing-deploy";
9+
10+
new DictionaryDeployLambda(app, "DictionaryDeployLambdaCode", {
11+
stack: "frontend",
12+
stage: "CODE",
13+
env: {
14+
region: "eu-west-1",
15+
},
16+
app: appName,
17+
});
18+
19+
new DictionaryDeployLambda(app, "DictionaryDeployLambdaProd", {
20+
stack: "frontend",
21+
stage: "PROD",
22+
env: {
23+
region: "eu-west-1",
24+
},
25+
app: appName,
26+
});
27+
28+
const riffRaff = new RiffRaffYamlFile(app);
29+
const {
30+
riffRaffYaml: { deployments },
31+
} = riffRaff;
32+
33+
const abTestingArtifactDeployment = "ab-testing-dictionary-artifact";
34+
35+
deployments.set(abTestingArtifactDeployment, {
36+
app: abTestingArtifactDeployment,
37+
contentDirectory: "dictionary-deploy-lambda/artifacts",
38+
type: "aws-s3",
39+
regions: new Set(["eu-west-1"]),
40+
stacks: new Set(["frontend"]),
41+
parameters: {
42+
bucketSsmKey: "/account/services/dotcom-store.bucket",
43+
prefixStack: false,
44+
publicReadAcl: false,
45+
},
46+
});
47+
48+
deployments.set("ab-testing-ui-artifact", {
49+
app: "ab-testing-ui-artifact",
50+
contentDirectory: "admin/ab-testing",
51+
type: "aws-s3",
52+
regions: new Set(["eu-west-1"]),
53+
stacks: new Set(["frontend"]),
54+
parameters: {
55+
bucketSsmKey: "/account/services/dotcom-store.bucket",
56+
cacheControl: "public, max-age=315360000",
57+
prefixStack: false,
58+
publicReadAcl: false,
59+
},
60+
});
61+
62+
deployments
63+
.get(`lambda-update-eu-west-1-frontend-${appName}`)
64+
?.dependencies?.push(abTestingArtifactDeployment);
65+
66+
riffRaff.synth();
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import type { GuStackProps } from "@guardian/cdk/lib/constructs/core/stack.js";
2+
import { GuStack } from "@guardian/cdk/lib/constructs/core/stack.js";
3+
import { GuLambdaFunction } from "@guardian/cdk/lib/constructs/lambda/index.js";
4+
import { GuS3Bucket } from "@guardian/cdk/lib/constructs/s3/index.js";
5+
import type { App } from "aws-cdk-lib";
6+
import { CustomResource } from "aws-cdk-lib";
7+
import { Runtime } from "aws-cdk-lib/aws-lambda";
8+
import { StringParameter } from "aws-cdk-lib/aws-ssm";
9+
10+
type Props = GuStackProps & {
11+
app: string;
12+
};
13+
14+
export class DictionaryDeployLambda extends GuStack {
15+
constructor(scope: App, id: string, props: Props) {
16+
super(scope, id, props);
17+
18+
const { app } = props;
19+
20+
const s3Bucket = GuS3Bucket.fromBucketName(
21+
this,
22+
"DictionaryDeployBucket",
23+
StringParameter.valueForStringParameter(
24+
this,
25+
`/account/services/dotcom-store.bucket`,
26+
),
27+
);
28+
29+
const lambda = new GuLambdaFunction(this, "ID5BatonLambda", {
30+
functionName: `${app}-${this.stage}`,
31+
fileName: "lambda.zip",
32+
handler: "index.handler",
33+
app,
34+
runtime: Runtime.NODEJS_22_X,
35+
memorySize: 256,
36+
environment: {
37+
FASTLY_API_TOKEN: StringParameter.valueForStringParameter(
38+
this,
39+
`/${app}/${this.stage}/fastly-api-token`,
40+
),
41+
FASTLY_AB_TESTING_CONFIG:
42+
StringParameter.valueForStringParameter(
43+
this,
44+
`/${app}/${this.stage}/fastly-ab-testing-config`,
45+
),
46+
STAGE: this.stage,
47+
ARTIFACT_BUCKET_NAME: s3Bucket.bucketName,
48+
},
49+
});
50+
51+
s3Bucket.grantRead(lambda);
52+
53+
// Trigger the Lambda to run upon deployment
54+
new CustomResource(this, "InvokeDictionaryDeployLambda", {
55+
serviceToken: lambda.functionArn,
56+
});
57+
}
58+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"name": "@guardian/dictionary-deploy-lambda",
3+
"version": "1.0.0",
4+
"description": "A/B test definitions and configuration",
5+
"main": "index.ts",
6+
"type": "module",
7+
"scripts": {
8+
"build": "rollup -c rollup.config.js",
9+
"test": "node --test",
10+
"lint": "eslint .",
11+
"prettier:check": "prettier . --check --cache",
12+
"prettier:fix": "prettier . --write --cache"
13+
},
14+
"dependencies": {
15+
"@rollup/plugin-json": "6.1.0",
16+
"cfn-response": "1.0.1",
17+
"esbuild": "0.27.0",
18+
"rollup-plugin-esbuild": "6.2.1",
19+
"superstruct": "2.0.2"
20+
},
21+
"devDependencies": {
22+
"@aws-sdk/client-s3": "3.931.0",
23+
"@guardian/cdk": "62.0.1",
24+
"@guardian/eslint-config": "12.0.1",
25+
"@guardian/tsconfig": "1.0.1",
26+
"@rollup/plugin-commonjs": "29.0.0",
27+
"@rollup/plugin-node-resolve": "16.0.3",
28+
"@rollup/plugin-typescript": "12.3.0",
29+
"@types/aws-lambda": "8.10.158",
30+
"@types/cfn-response": "1.0.8",
31+
"@types/node": "22.17.0",
32+
"aws-cdk-lib": "2.220.0",
33+
"aws-lambda": "1.0.7",
34+
"eslint": "9.39.1",
35+
"prettier": "3.0.3",
36+
"rollup": "4.53.2",
37+
"typescript": "5.9.3"
38+
}
39+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import commonjs from "@rollup/plugin-commonjs";
2+
import json from "@rollup/plugin-json";
3+
import { nodeResolve } from "@rollup/plugin-node-resolve";
4+
import esbuild from "rollup-plugin-esbuild";
5+
6+
/** @type {import('rollup').RollupOptions} */
7+
const rollupConfig = {
8+
input: `src/index.ts`,
9+
output: {
10+
dir: "dist",
11+
format: "module",
12+
preserveModules: true,
13+
preserveModulesRoot: "src",
14+
sourcemap: true,
15+
entryFileNames: (chunkInfo) => {
16+
if (chunkInfo.name.includes("node_modules")) {
17+
// Simplify node_modules paths, if it's in node_modules without a package.json
18+
// it'll be assumed to be a commonjs module, which is incorrect and causes issues.
19+
return (
20+
chunkInfo.name.replace(
21+
/node_modules\/\.pnpm\/.*\/node_modules/,
22+
"external",
23+
) + ".js"
24+
);
25+
}
26+
27+
return "[name].js";
28+
},
29+
},
30+
external: ["@aws-sdk/*"],
31+
plugins: [
32+
commonjs(),
33+
nodeResolve({
34+
preferBuiltins: true,
35+
exportConditions: ["node"],
36+
resolveOnly: (moduleName) => !moduleName.startsWith("@aws-sdk"),
37+
}),
38+
json(),
39+
esbuild({
40+
include: /\.[jt]s?$/,
41+
}),
42+
],
43+
};
44+
45+
export default rollupConfig;
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import {
2+
calculateUpdates,
3+
getDictionaryItems,
4+
updateDictionaryItems,
5+
verifyDictionaryName,
6+
} from "../../lib/fastly-api.ts";
7+
import type { KeyValue } from "./fetch-artifact.ts";
8+
9+
/**
10+
* Deploys key-value pairs to a Fastly edge dictionary.
11+
* Uses `calculateUpdates` to determine necessary CRUD operations.
12+
*
13+
* @param config Configuration for the dictionary deployment.
14+
* @param keyValues An array of key-value pairs to deploy to the dictionary.
15+
*/
16+
export const deployDictionary = async (
17+
{
18+
dictionaryName,
19+
dictionaryId,
20+
serviceId,
21+
activeVersion,
22+
}: {
23+
dictionaryName: string;
24+
dictionaryId: string;
25+
serviceId: string;
26+
activeVersion: number;
27+
},
28+
keyValues: KeyValue[],
29+
) => {
30+
await verifyDictionaryName({
31+
serviceId,
32+
activeVersion,
33+
dictionaryName,
34+
dictionaryId,
35+
});
36+
37+
const currentKeyValues = await getDictionaryItems({
38+
dictionaryId,
39+
});
40+
41+
const updates = calculateUpdates(keyValues, currentKeyValues);
42+
43+
if (updates.length === 0) {
44+
console.log(`No key-values need updating in '${dictionaryName}'`);
45+
} else {
46+
Map.groupBy(updates, (item) => item.op).forEach((items, op) => {
47+
console.log(
48+
`Performing ${items.length} ${op} operations in '${dictionaryName}'`,
49+
);
50+
});
51+
52+
console.log(
53+
`Performing ${updates.length} total operations in '${dictionaryName}'`,
54+
);
55+
56+
const response = await updateDictionaryItems({
57+
dictionaryId,
58+
items: updates,
59+
});
60+
61+
if (response.status !== "ok") {
62+
throw new Error(`Failed to update mvt groups dictionary`);
63+
}
64+
}
65+
};

0 commit comments

Comments
 (0)