├── .gitignore ├── README.md ├── aws-blog ├── .gitignore ├── .npmignore ├── bin │ └── aws-blog.ts ├── cdk.json ├── jest.config.js ├── lib │ ├── aws-blog-stack.ts │ └── config │ │ ├── config.ts │ │ └── index.ts ├── package-lock.json ├── package.json ├── postman │ └── serverless-caching-aws-blogs.postman_collection.json ├── src │ └── blogs │ │ ├── create-blogs-table │ │ └── create-blogs-table.ts │ │ ├── get-blog │ │ └── get-blog.ts │ │ ├── list-blogs-cache-in-memory │ │ └── list-blogs-cache-in-memory.ts │ │ ├── list-blogs-cache-in-tmp │ │ └── list-blogs-cache-in-tmp.ts │ │ ├── list-blogs │ │ └── list-blogs.ts │ │ └── types.ts └── tsconfig.json ├── docs └── images │ ├── header-2.png │ ├── header-3.png │ ├── header-4.png │ └── header.png └── serverless-blog ├── .gitignore ├── .npmignore ├── bin └── serverless-blog.ts ├── cdk.json ├── jest.config.js ├── lib └── serverless-blog-stack.ts ├── package-lock.json ├── package.json ├── src ├── blogs │ ├── create-blogs-table │ │ └── create-blogs-table.ts │ ├── get-blog-no-dax │ │ └── get-blog-no-dax.ts │ ├── get-blog │ │ └── get-blog.ts │ └── list-blogs │ │ └── list-blogs.ts ├── schema.graphql └── types.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # apple 2 | .DS_store 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Serverless Caching Strategies (Series) 🚀 2 | 3 | ## Introduction 4 | 5 | How to use serverless caching strategies within your solutions, with code examples and visuals, written in TypeScript and the CDK, and with associated code repository in GitHub. 6 | 7 | ### Series Parts 8 | 9 | You can view part one of the article here: [Serverless Caching Strategies - Part 1 (Amazon API Gateway) 🚀](https://leejamesgilmore.medium.com/serverless-caching-strategies-part-1-amazon-api-gateway-c2d680d5b3b) 10 | 11 | ![image](./docs/images/header.png) 12 | 13 | You can view part two of the article here: [Serverless Caching Strategies - Part 2 (Amazon DynamoDB DAX) 🚀](https://leejamesgilmore.medium.com/serverless-caching-strategies-part-2-amazon-dynamodb-dax-d841e1e1ad0e) 14 | 15 | ![image](./docs/images/header-2.png) 16 | 17 | You can view part three of the article here: [Serverless Caching Strategies - Part 3 (Lambda Runtime) 🚀](https://leejamesgilmore.medium.com/serverless-caching-strategies-part-3-lambda-runtime-b3d21250927b) 18 | 19 | ![image](./docs/images/header-3.png) 20 | 21 | You can view part four of the article here: [Serverless Caching Strategies - Part 4 (AWS AppSync) 🚀](https://leejamesgilmore.medium.com/serverless-caching-strategies-part-4-appsync-7fe6ede93183) 22 | 23 | ![image](./docs/images/header-4.png) 24 | 25 | > This is a minimal set of code to demonstrate the points discussed in the article, so coding and architecture best practices have not been adhered too (inc unit testing) 26 | 27 | ## Getting started 28 | 29 | **Note: This will incur costs in your AWS account on running the load tests which you should account for.** 30 | 31 | Please view the detailed deployment steps in the article. 32 | 33 | ## Removing the services 34 | 35 | To remove the services run the following command in the two main folders: `npm run remove` 36 | -------------------------------------------------------------------------------- /aws-blog/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | 10 | # apple 11 | .DS_store 12 | 13 | cdk-outputs.json -------------------------------------------------------------------------------- /aws-blog/.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /aws-blog/bin/aws-blog.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import "source-map-support/register"; 4 | 5 | import * as cdk from "@aws-cdk/core"; 6 | 7 | import { AwsBlogStack } from "../lib/aws-blog-stack"; 8 | 9 | const app = new cdk.App(); 10 | new AwsBlogStack(app, "AwsBlogStack", {}); 11 | -------------------------------------------------------------------------------- /aws-blog/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/aws-blog.ts", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "**/*.d.ts", 11 | "**/*.js", 12 | "tsconfig.json", 13 | "package*.json", 14 | "yarn.lock", 15 | "node_modules", 16 | "test" 17 | ] 18 | }, 19 | "context": { 20 | "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true, 21 | "@aws-cdk/core:stackRelativeExports": true, 22 | "@aws-cdk/aws-rds:lowercaseDbIdentifier": true, 23 | "@aws-cdk/aws-lambda:recognizeVersionProps": true, 24 | "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /aws-blog/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | roots: ['/test'], 4 | testMatch: ['**/*.test.ts'], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /aws-blog/lib/aws-blog-stack.ts: -------------------------------------------------------------------------------- 1 | import * as apigw from "@aws-cdk/aws-apigateway"; 2 | import * as cdk from "@aws-cdk/core"; 3 | import * as customResources from "@aws-cdk/custom-resources"; 4 | import * as ec2 from "@aws-cdk/aws-ec2"; 5 | import * as iam from "@aws-cdk/aws-iam"; 6 | import * as lambda from "@aws-cdk/aws-lambda"; 7 | import * as nodeLambda from "@aws-cdk/aws-lambda-nodejs"; 8 | import * as path from "path"; 9 | import * as rds from "@aws-cdk/aws-rds"; 10 | import * as s3 from "@aws-cdk/aws-s3"; 11 | import * as secretsmanager from "@aws-cdk/aws-secretsmanager"; 12 | 13 | import { config } from "./config"; 14 | 15 | export class AwsBlogStack extends cdk.Stack { 16 | constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { 17 | super(scope, id, props); 18 | 19 | // create the vpc for the database to sit in 20 | const vpc: ec2.Vpc = new ec2.Vpc(this, "aws-blog-vpc", { 21 | cidr: "10.0.0.0/16", 22 | natGateways: 0, 23 | maxAzs: 3, 24 | subnetConfiguration: [ 25 | { 26 | cidrMask: 24, 27 | name: "private-subnet-1", 28 | subnetType: ec2.SubnetType.PRIVATE_ISOLATED, 29 | }, 30 | ], 31 | }); 32 | 33 | // provision serverless aurora in our vpc with the data api enabled 34 | const cluster: rds.ServerlessCluster = new rds.ServerlessCluster( 35 | this, 36 | "blogs-serverless-db", 37 | { 38 | vpc, 39 | vpcSubnets: { 40 | subnetType: ec2.SubnetType.PRIVATE_ISOLATED, 41 | }, 42 | engine: rds.DatabaseClusterEngine.AURORA_MYSQL, 43 | enableDataApi: true, // Optional - will be automatically set if you call grantDataApiAccess() 44 | backupRetention: cdk.Duration.days(1), 45 | clusterIdentifier: "blogs-serverless-db", 46 | defaultDatabaseName: config.database, 47 | deletionProtection: false, 48 | scaling: { 49 | autoPause: cdk.Duration.minutes(10), // default is to pause after 5 minutes of idle time 50 | minCapacity: rds.AuroraCapacityUnit.ACU_1, // default is 2 Aurora capacity units (ACUs) 51 | maxCapacity: rds.AuroraCapacityUnit.ACU_1, // default is 16 Aurora capacity units (ACUs) 52 | }, 53 | credentials: { 54 | username: "admin", 55 | password: cdk.SecretValue.plainText(config.databasePassword), 56 | usernameAsString: true, 57 | secretName: "blogsdbSecret", 58 | }, 59 | } 60 | ); 61 | 62 | cluster.node.addDependency(vpc); 63 | 64 | // generate a secrets manager secret for accessing the database 65 | const secret = new secretsmanager.Secret(this, "blogs-db-secrets", { 66 | description: "blogs-db-secrets", 67 | secretName: "blogs-db-secrets", 68 | generateSecretString: { 69 | generateStringKey: "blogs-db-secrets", 70 | secretStringTemplate: JSON.stringify({ 71 | username: config.databaseUserName, 72 | password: config.databasePassword, 73 | engine: "mysql", 74 | host: cluster.clusterEndpoint.hostname, 75 | port: 3306, 76 | dbClusterIdentifier: cluster.clusterIdentifier, 77 | }), 78 | }, 79 | }); 80 | 81 | secret.node.addDependency(cluster); 82 | 83 | // create an s3 bucket for the logo files 84 | const logoBucket: s3.Bucket = new s3.Bucket( 85 | this, 86 | "company-logo-bucket-caching", 87 | { 88 | removalPolicy: cdk.RemovalPolicy.DESTROY, 89 | autoDeleteObjects: true, 90 | bucketName: "company-logo-bucket-caching", 91 | } 92 | ); 93 | 94 | // list blogs endpoint handler 95 | const listBlogsHandler: nodeLambda.NodejsFunction = 96 | new nodeLambda.NodejsFunction(this, "list-blogs", { 97 | functionName: "list-blogs", 98 | runtime: lambda.Runtime.NODEJS_14_X, 99 | entry: path.join(__dirname, "/../src/blogs/list-blogs/list-blogs.ts"), 100 | memorySize: 1024, 101 | handler: "listBlogsHandler", 102 | bundling: { 103 | minify: true, 104 | externalModules: ["aws-sdk"], 105 | }, 106 | environment: { 107 | REGION: cdk.Stack.of(this).region, 108 | CLUSTER_ARN: cluster.clusterArn, 109 | HOSTNAME: cluster.clusterEndpoint.hostname, 110 | DB: config.database, 111 | SECRET_ARN: secret.secretFullArn as string, 112 | }, 113 | }); 114 | 115 | // create-blogs-table handler 116 | const createBlogsTableHandler: nodeLambda.NodejsFunction = 117 | new nodeLambda.NodejsFunction(this, "create-blogs-table", { 118 | functionName: "create-blogs-table", 119 | runtime: lambda.Runtime.NODEJS_14_X, 120 | entry: path.join( 121 | __dirname, 122 | "/../src/blogs/create-blogs-table/create-blogs-table.ts" 123 | ), 124 | memorySize: 1024, 125 | handler: "createBlogsTableHandler", 126 | timeout: cdk.Duration.minutes(15), 127 | bundling: { 128 | minify: true, 129 | externalModules: ["aws-sdk"], 130 | }, 131 | environment: { 132 | REGION: cdk.Stack.of(this).region, 133 | CLUSTER_ARN: cluster.clusterArn, 134 | HOSTNAME: cluster.clusterEndpoint.hostname, 135 | DB: config.database, 136 | SECRET_ARN: secret.secretFullArn as string, 137 | }, 138 | }); 139 | 140 | const getBlogHandler: nodeLambda.NodejsFunction = 141 | new nodeLambda.NodejsFunction(this, "get-blog", { 142 | functionName: "get-blog", 143 | runtime: lambda.Runtime.NODEJS_14_X, 144 | entry: path.join(__dirname, "/../src/blogs/get-blog/get-blog.ts"), 145 | memorySize: 1024, 146 | handler: "getBlogHandler", 147 | bundling: { 148 | minify: true, 149 | externalModules: ["aws-sdk"], 150 | }, 151 | environment: { 152 | REGION: cdk.Stack.of(this).region, 153 | CLUSTER_ARN: cluster.clusterArn, 154 | HOSTNAME: cluster.clusterEndpoint.hostname, 155 | DB: config.database, 156 | SECRET_ARN: secret.secretFullArn as string, 157 | }, 158 | }); 159 | 160 | // list blogs (cached in memory) endpoint handler 161 | const listBlogsCachedInMemoryHandler: nodeLambda.NodejsFunction = 162 | new nodeLambda.NodejsFunction(this, "list-blogs-cached-in-memory", { 163 | functionName: "list-blogs-cached-in-memory", 164 | runtime: lambda.Runtime.NODEJS_14_X, 165 | entry: path.join( 166 | __dirname, 167 | "/../src/blogs/list-blogs-cache-in-memory/list-blogs-cache-in-memory.ts" 168 | ), 169 | memorySize: 1024, 170 | handler: "listBlogsCachedInMemoryHandler", 171 | bundling: { 172 | minify: true, 173 | externalModules: ["aws-sdk"], 174 | }, 175 | environment: { 176 | REGION: cdk.Stack.of(this).region, 177 | CLUSTER_ARN: cluster.clusterArn, 178 | HOSTNAME: cluster.clusterEndpoint.hostname, 179 | DB: config.database, 180 | SECRET_ARN: secret.secretFullArn as string, 181 | }, 182 | }); 183 | 184 | // list logos (cached in tmp) endpoint handler 185 | const listLogosCachedInTmpHandler: nodeLambda.NodejsFunction = 186 | new nodeLambda.NodejsFunction(this, "list-logos-cached-in-tmp", { 187 | functionName: "list-logos-cached-in-tmp", 188 | runtime: lambda.Runtime.NODEJS_14_X, 189 | entry: path.join( 190 | __dirname, 191 | "/../src/blogs/list-blogs-cache-in-tmp/list-blogs-cache-in-tmp.ts" 192 | ), 193 | memorySize: 1024, 194 | handler: "listLogosCachedInTmpHandler", 195 | bundling: { 196 | minify: true, 197 | externalModules: ["aws-sdk"], 198 | }, 199 | environment: { 200 | REGION: cdk.Stack.of(this).region, 201 | BUCKET: logoBucket.bucketName, 202 | }, 203 | }); 204 | 205 | // create a policy statement 206 | const s3ListBucketsPolicy: iam.PolicyStatement = new iam.PolicyStatement({ 207 | actions: ["s3:*"], 208 | resources: ["arn:aws:s3:::*"], 209 | }); 210 | 211 | // add the policy to the functions role 212 | listLogosCachedInTmpHandler.role?.attachInlinePolicy( 213 | new iam.Policy(this, "list-buckets-policy", { 214 | statements: [s3ListBucketsPolicy], 215 | }) 216 | ); 217 | 218 | // grant read access to our secret from our lambdas 219 | secret.grantRead(createBlogsTableHandler.role as iam.IRole); 220 | secret.grantRead(getBlogHandler.role as iam.IRole); 221 | secret.grantRead(listBlogsHandler.role as iam.IRole); 222 | secret.grantRead(listBlogsCachedInMemoryHandler.role as iam.IRole); 223 | 224 | // create the rest API for accessing our lambdas 225 | const api: apigw.RestApi = new apigw.RestApi(this, "blogs-api", { 226 | description: "blogs api gateway", 227 | deploy: true, 228 | deployOptions: { 229 | // this enables caching on our api gateway, with a ttl of five minutes (unless overridden per method) 230 | cachingEnabled: true, 231 | cacheClusterEnabled: true, 232 | cacheDataEncrypted: true, 233 | stageName: "prod", 234 | dataTraceEnabled: true, 235 | loggingLevel: apigw.MethodLoggingLevel.INFO, 236 | cacheTtl: cdk.Duration.minutes(5), 237 | throttlingBurstLimit: 100, 238 | throttlingRateLimit: 100, 239 | tracingEnabled: true, 240 | metricsEnabled: true, 241 | // Method deployment options for specific resources/methods. (override common options defined in `StageOptions#methodOptions`) 242 | methodOptions: { 243 | "/blogs/GET": { 244 | throttlingRateLimit: 10, 245 | throttlingBurstLimit: 10, 246 | cacheDataEncrypted: true, 247 | cachingEnabled: true, 248 | cacheTtl: cdk.Duration.minutes(10), 249 | loggingLevel: apigw.MethodLoggingLevel.INFO, 250 | dataTraceEnabled: true, 251 | metricsEnabled: true, 252 | }, 253 | "/blogs/{id}/GET": { 254 | throttlingRateLimit: 20, 255 | throttlingBurstLimit: 20, 256 | cachingEnabled: true, 257 | cacheDataEncrypted: true, 258 | cacheTtl: cdk.Duration.minutes(1), 259 | loggingLevel: apigw.MethodLoggingLevel.INFO, 260 | dataTraceEnabled: true, 261 | metricsEnabled: true, 262 | }, 263 | // blogs cached in memory so we want to turn off api gateway caching 264 | "/blogs-cached-in-memory/GET": { 265 | throttlingRateLimit: 10, 266 | throttlingBurstLimit: 10, 267 | cacheDataEncrypted: true, 268 | cachingEnabled: false, 269 | cacheTtl: cdk.Duration.minutes(0), 270 | loggingLevel: apigw.MethodLoggingLevel.INFO, 271 | dataTraceEnabled: true, 272 | metricsEnabled: true, 273 | }, 274 | // logos cached in tmp so we want to turn off api gateway caching 275 | "/logos-cached-in-tmp/GET": { 276 | throttlingRateLimit: 10, 277 | throttlingBurstLimit: 10, 278 | cacheDataEncrypted: true, 279 | cachingEnabled: false, 280 | cacheTtl: cdk.Duration.minutes(0), 281 | loggingLevel: apigw.MethodLoggingLevel.INFO, 282 | dataTraceEnabled: true, 283 | metricsEnabled: true, 284 | }, 285 | }, 286 | }, 287 | defaultCorsPreflightOptions: { 288 | allowHeaders: ["Content-Type", "X-Amz-Date"], 289 | allowMethods: ["GET"], 290 | allowCredentials: true, 291 | allowOrigins: apigw.Cors.ALL_ORIGINS, // we wouldn't do this in production 292 | }, 293 | }); 294 | 295 | // add a /blogs resource 296 | const blogs: apigw.Resource = api.root.addResource("blogs"); 297 | 298 | // add a /blogs-cached-in-memory resource 299 | const blogsCachedInMemory: apigw.Resource = api.root.addResource( 300 | "blogs-cached-in-memory" 301 | ); 302 | 303 | // add a /logos-cached-in-tmp resource 304 | const logosCachedInTmp: apigw.Resource = api.root.addResource( 305 | "logos-cached-in-tmp" 306 | ); 307 | 308 | // integrate the lambda to the method - GET /blogs 309 | blogs.addMethod( 310 | "GET", 311 | new apigw.LambdaIntegration(listBlogsHandler, { 312 | proxy: true, 313 | allowTestInvoke: true, 314 | }) 315 | ); 316 | 317 | // integrate the lambda to the method - GET /blogs-cached-in-memory 318 | blogsCachedInMemory.addMethod( 319 | "GET", 320 | new apigw.LambdaIntegration(listBlogsCachedInMemoryHandler, { 321 | proxy: true, 322 | allowTestInvoke: true, 323 | }) 324 | ); 325 | 326 | // integrate the lambda to the method - GET /logos-cached-in-tmp 327 | logosCachedInTmp.addMethod( 328 | "GET", 329 | new apigw.LambdaIntegration(listLogosCachedInTmpHandler, { 330 | proxy: true, 331 | allowTestInvoke: true, 332 | }) 333 | ); 334 | 335 | // integrate the lambda to the method - GET /blog/{id} 336 | const blog: apigw.Resource = blogs.addResource("{id}"); 337 | blog.addMethod( 338 | "GET", 339 | new apigw.LambdaIntegration(getBlogHandler, { 340 | proxy: true, 341 | allowTestInvoke: true, 342 | // ensure that our caching is done on the id path parameter 343 | cacheKeyParameters: ["method.request.path.id"], 344 | cacheNamespace: "blogId", 345 | requestParameters: { 346 | "integration.request.path.id": "method.request.path.id", 347 | }, 348 | }), 349 | { 350 | requestParameters: { 351 | "method.request.path.id": true, 352 | }, 353 | } 354 | ); 355 | 356 | // add the permissions to the funtions to use the data api 357 | cluster.grantDataApiAccess(createBlogsTableHandler); 358 | cluster.grantDataApiAccess(getBlogHandler); 359 | cluster.grantDataApiAccess(listBlogsHandler); 360 | cluster.grantDataApiAccess(listBlogsCachedInMemoryHandler); 361 | 362 | // add a custom sdk call to invoke our create table lambda (this will create the table and dummy data on deploy) 363 | const lambdaInvokeSdkCall: customResources.AwsSdkCall = { 364 | service: "Lambda", 365 | action: "invoke", 366 | parameters: { 367 | FunctionName: "create-blogs-table", 368 | Payload: `{"path": "${path}"}`, 369 | }, 370 | physicalResourceId: customResources.PhysicalResourceId.of("BlogsTable"), 371 | }; 372 | 373 | // custom resource function role for our custom resource 374 | const customResourceFnRole: iam.Role = new iam.Role( 375 | this, 376 | "custom-resource-role", 377 | { 378 | assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"), 379 | } 380 | ); 381 | 382 | // allow the custom role to invoke our create blogs table lambda 383 | customResourceFnRole.addToPolicy( 384 | new iam.PolicyStatement({ 385 | resources: [ 386 | `arn:aws:lambda:${cdk.Stack.of(this).region}:${ 387 | cdk.Stack.of(this).account 388 | }:function:create-blogs-table`, 389 | ], 390 | actions: ["lambda:InvokeFunction"], 391 | }) 392 | ); 393 | 394 | // run a custom resource lambda on deploy to create the table and dummy data to play with 395 | const customResource: customResources.AwsCustomResource = 396 | new customResources.AwsCustomResource( 397 | this, 398 | "create-blog-table-custom-resource", 399 | { 400 | policy: customResources.AwsCustomResourcePolicy.fromSdkCalls({ 401 | resources: customResources.AwsCustomResourcePolicy.ANY_RESOURCE, 402 | }), 403 | functionName: "invoke-create-table-lambda", 404 | onCreate: lambdaInvokeSdkCall, 405 | onUpdate: lambdaInvokeSdkCall, 406 | timeout: cdk.Duration.minutes(15), 407 | role: customResourceFnRole, 408 | } 409 | ); 410 | 411 | customResource.node.addDependency(cluster); 412 | customResource.node.addDependency(createBlogsTableHandler); 413 | 414 | // add some outputs to use later 415 | new cdk.CfnOutput(this, "BlogsEndpointUrl", { 416 | value: `${api.url}blogs`, 417 | exportName: "BlogsEndpointUrl", 418 | }); 419 | 420 | new cdk.CfnOutput(this, "BlogsdbEndpoint", { 421 | value: cluster.clusterEndpoint.hostname, 422 | exportName: "BlogsdbEndpoint", 423 | }); 424 | 425 | new cdk.CfnOutput(this, "BlogsdbSecret", { 426 | value: secret.secretFullArn || "", 427 | exportName: "BlogsdbSecret", 428 | }); 429 | 430 | new cdk.CfnOutput(this, "BlogsdbHostname", { 431 | value: cluster.clusterEndpoint.hostname, 432 | exportName: "BlogsdbHostname", 433 | }); 434 | } 435 | } 436 | -------------------------------------------------------------------------------- /aws-blog/lib/config/config.ts: -------------------------------------------------------------------------------- 1 | export const config = { 2 | // you would typically pass these as env vars 3 | database: "blogsdb", 4 | databasePassword: "super-secret-passwsord!", 5 | databaseUserName: "admin", 6 | }; 7 | -------------------------------------------------------------------------------- /aws-blog/lib/config/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./config"; 2 | -------------------------------------------------------------------------------- /aws-blog/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-blog", 3 | "version": "0.1.0", 4 | "bin": { 5 | "aws-blog": "bin/aws-blog.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "cdk": "cdk", 12 | "deploy": "cdk deploy --outputs-file ./cdk-outputs.json", 13 | "remove": "cdk destroy" 14 | }, 15 | "devDependencies": { 16 | "@aws-cdk/aws-apigateway": "^1.125.0", 17 | "@aws-cdk/aws-apigatewayv2-integrations": "^1.125.0", 18 | "@aws-cdk/aws-ec2": "^1.125.0", 19 | "@aws-cdk/aws-iam": "^1.125.0", 20 | "@aws-cdk/aws-lambda": "^1.125.0", 21 | "@aws-cdk/aws-lambda-nodejs": "^1.125.0", 22 | "@aws-cdk/aws-logs": "^1.125.0", 23 | "@aws-cdk/aws-rds": "^1.125.0", 24 | "@aws-cdk/aws-s3": "^1.125.0", 25 | "@aws-cdk/aws-secretsmanager": "^1.125.0", 26 | "@aws-cdk/core": "^1.125.0", 27 | "@aws-cdk/custom-resources": "^1.125.0", 28 | "@types/aws-lambda": "^8.10.89", 29 | "@types/data-api-client": "^1.2.3", 30 | "@types/jest": "^26.0.10", 31 | "@types/node": "10.17.27", 32 | "@types/uuid": "^8.3.3", 33 | "aws-cdk": "2.1.0", 34 | "jest": "^26.4.2", 35 | "ts-jest": "^26.2.0", 36 | "ts-node": "^9.0.0", 37 | "typescript": "~3.9.7" 38 | }, 39 | "dependencies": { 40 | "aws-cdk-lib": "2.1.0", 41 | "aws-sdk": "^2.1048.0", 42 | "constructs": "^10.0.0", 43 | "data-api-client": "^1.2.0", 44 | "source-map-support": "^0.5.16", 45 | "uuid": "^8.3.2" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /aws-blog/postman/serverless-caching-aws-blogs.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "a13c52a8-0feb-4e13-83f5-6f299497bba6", 4 | "name": "serverless-caching-aws-blogs", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" 6 | }, 7 | "item": [ 8 | { 9 | "name": "GET Blogs", 10 | "request": { 11 | "method": "GET", 12 | "header": [], 13 | "url": { 14 | "raw": "https://{{api}}/prod/blogs", 15 | "protocol": "https", 16 | "host": ["{{api}}"], 17 | "path": ["prod", "blogs"] 18 | } 19 | }, 20 | "response": [] 21 | }, 22 | { 23 | "name": "GET Blogs (In memory Lambda cache)", 24 | "request": { 25 | "method": "GET", 26 | "header": [], 27 | "url": { 28 | "raw": "https://{{api}}/prod/blogs-cached-in-memory", 29 | "protocol": "https", 30 | "host": ["{{api}}"], 31 | "path": ["prod", "blogs-cached-in-memory"] 32 | } 33 | }, 34 | "response": [] 35 | }, 36 | { 37 | "name": "GET Logos (In memory Lambda tmp cache)", 38 | "request": { 39 | "method": "GET", 40 | "header": [], 41 | "url": { 42 | "raw": "https://{{api}}/prod/logos-cached-in-tmp", 43 | "protocol": "https", 44 | "host": ["{{api}}"], 45 | "path": ["prod", "logos-cached-in-tmp"] 46 | } 47 | }, 48 | "response": [] 49 | }, 50 | { 51 | "name": "Get Blog by ID", 52 | "request": { 53 | "method": "GET", 54 | "header": [], 55 | "url": { 56 | "raw": "https://{{api}}/prod/blogs/1", 57 | "protocol": "https", 58 | "host": ["{{api}}"], 59 | "path": ["prod", "blogs", "1"] 60 | } 61 | }, 62 | "response": [] 63 | } 64 | ], 65 | "variable": [ 66 | { 67 | "key": "api", 68 | "value": "your-api.execute-api.eu-west-1.amazonaws.com" 69 | } 70 | ] 71 | } 72 | -------------------------------------------------------------------------------- /aws-blog/src/blogs/create-blogs-table/create-blogs-table.ts: -------------------------------------------------------------------------------- 1 | import * as AWS from "aws-sdk"; 2 | 3 | import { 4 | CdkCustomResourceHandler, 5 | CdkCustomResourceResponse, 6 | } from "aws-lambda"; 7 | 8 | import { v4 as uuid } from "uuid"; 9 | 10 | const rdsDataService = new AWS.RDSDataService(); 11 | 12 | export const createBlogsTableHandler: CdkCustomResourceHandler = 13 | async (): Promise => { 14 | try { 15 | const correlationId = uuid(); 16 | const method = "create-blogs-table.handler"; 17 | const prefix = `${correlationId} - ${method}`; 18 | 19 | console.log(`${prefix} - started`); 20 | 21 | const secretArn = process.env.SECRET_ARN as string; 22 | const resourceArn = process.env.CLUSTER_ARN as string; 23 | 24 | // create the table if it does not already exist 25 | const createTableSql = `CREATE TABLE IF NOT EXISTS Blogs ( 26 | blogID int, 27 | blogTitle varchar(255), 28 | blogBody varchar(255), 29 | blogDate Date 30 | );`; 31 | 32 | // add some ficticious blog records 33 | const createRecordsSql = ` 34 | INSERT INTO blogsdb.Blogs (blogID, blogTitle, blogBody, blogDate) 35 | VALUES (1, 'API Gateway 101', 'This is a dummy post on API Gateway', '2021-01-01'), 36 | (2, 'Getting started with Lambda', 'This is a dummy post on Lambda', '2021-02-01'), 37 | (3, 'DynamoDB in action', 'This is a dummy post on DynamoDB', '2021-03-01'); 38 | `; 39 | 40 | const sqlParams: AWS.RDSDataService.ExecuteStatementRequest = { 41 | secretArn, 42 | resourceArn, 43 | sql: "", 44 | continueAfterTimeout: true, 45 | database: process.env.DB, 46 | includeResultMetadata: true, 47 | }; 48 | 49 | // add the create table sql 50 | sqlParams["sql"] = createTableSql; 51 | 52 | console.log(`${prefix} - creating table`); 53 | 54 | await rdsDataService.executeStatement(sqlParams).promise(); 55 | 56 | console.log(`${prefix} - creating records`); 57 | 58 | // add the creation of the blog records 59 | sqlParams["sql"] = createRecordsSql; 60 | 61 | const { 62 | numberOfRecordsUpdated, 63 | }: AWS.RDSDataService.Types.ExecuteStatementResponse = await rdsDataService 64 | .executeStatement(sqlParams) 65 | .promise(); 66 | 67 | console.log( 68 | `${prefix} - successfully created ${numberOfRecordsUpdated} records` 69 | ); 70 | 71 | return { 72 | success: true, 73 | }; 74 | } catch (error) { 75 | console.error(error); 76 | throw error; 77 | } 78 | }; 79 | -------------------------------------------------------------------------------- /aws-blog/src/blogs/get-blog/get-blog.ts: -------------------------------------------------------------------------------- 1 | import { 2 | APIGatewayEvent, 3 | APIGatewayProxyHandler, 4 | APIGatewayProxyResult, 5 | } from "aws-lambda"; 6 | 7 | import { v4 as uuid } from "uuid"; 8 | 9 | const data = require("data-api-client")({ 10 | secretArn: process.env.SECRET_ARN as string, 11 | resourceArn: process.env.CLUSTER_ARN as string, 12 | database: process.env.DB, 13 | continueAfterTimeout: true, 14 | includeResultMetadata: false, 15 | }); 16 | 17 | export const getBlogHandler: APIGatewayProxyHandler = async ({ 18 | pathParameters, 19 | }: APIGatewayEvent): Promise => { 20 | try { 21 | const correlationId = uuid(); 22 | const method = "get-blog.handler"; 23 | const prefix = `${correlationId} - ${method}`; 24 | 25 | console.log(`${prefix} - started`); 26 | 27 | if (!pathParameters) { 28 | throw new Error("Blog Id not defined"); 29 | } 30 | 31 | const blogId = pathParameters["id"]; 32 | 33 | console.log(`${prefix} - Blog Id: ${blogId}`); 34 | 35 | // get the correct record back 36 | const { records }: { records: BlogResponse[] } = await data.query( 37 | `select * from blogsdb.Blogs where blogID = ${blogId};` 38 | ); 39 | 40 | if (!records.length) { 41 | throw new Error("blog not found"); 42 | } 43 | 44 | const response: BlogResponse = records[0]; 45 | response.responseDateTime = new Date().toUTCString(); // we add this simply to see the caching working 46 | 47 | return { 48 | body: JSON.stringify(response), 49 | statusCode: 200, 50 | }; 51 | } catch (error) { 52 | return { 53 | body: "An error has occurred", 54 | statusCode: 500, 55 | }; 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /aws-blog/src/blogs/list-blogs-cache-in-memory/list-blogs-cache-in-memory.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyHandler, APIGatewayProxyResult } from "aws-lambda"; 2 | 3 | import { v4 as uuid } from "uuid"; 4 | 5 | const data = require("data-api-client")({ 6 | secretArn: process.env.SECRET_ARN as string, 7 | resourceArn: process.env.CLUSTER_ARN as string, 8 | database: process.env.DB, 9 | continueAfterTimeout: true, 10 | includeResultMetadata: false, 11 | }); 12 | 13 | // cachedBlogs is outside of the handler and will persist between invocations 14 | // Note: the related api gateway endpoint does not have caching enabled 15 | 16 | let cachedBlogs: BlogsResponse; 17 | 18 | export const listBlogsCachedInMemoryHandler: APIGatewayProxyHandler = 19 | async (): Promise => { 20 | try { 21 | const correlationId = uuid(); 22 | const method = "list-blogs-cache-in-memory.handler"; 23 | const prefix = `${correlationId} - ${method}`; 24 | 25 | console.log(`${prefix} - started`); 26 | 27 | // if no blogs have been cached previously then we need to fetch from the db 28 | if (!cachedBlogs?.items?.length) { 29 | console.log( 30 | `${prefix} - no blogs cached in memory.. fetching blogs from db` 31 | ); 32 | 33 | const { records }: { records: BlogResponse[] } = await data.query( 34 | "select * from Blogs;" 35 | ); 36 | 37 | // populate our in memory cache with the retrieved blogs 38 | cachedBlogs = { 39 | items: [...records], 40 | responseDateTime: new Date().toUTCString(), // we add this simply to see the caching working 41 | }; 42 | } else { 43 | console.log( 44 | `${prefix} - ${cachedBlogs.items.length} blogs cached in memory.. no need to go to db!` 45 | ); 46 | } 47 | 48 | console.log(`${prefix} - successful: ${JSON.stringify(cachedBlogs)}`); 49 | 50 | return { 51 | body: JSON.stringify(cachedBlogs), 52 | statusCode: 200, 53 | }; 54 | } catch (error) { 55 | return { 56 | body: "An error has occurred", 57 | statusCode: 500, 58 | }; 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /aws-blog/src/blogs/list-blogs-cache-in-tmp/list-blogs-cache-in-tmp.ts: -------------------------------------------------------------------------------- 1 | import * as AWS from "aws-sdk"; 2 | 3 | import { APIGatewayProxyHandler, APIGatewayProxyResult } from "aws-lambda"; 4 | 5 | import { v4 as uuid } from "uuid"; 6 | 7 | const fs = require("fs").promises; 8 | const s3 = new AWS.S3(); 9 | 10 | // a function to write the files to tmp on the lambda 11 | async function writeFilesToTemp(files: CompanyLogos): Promise { 12 | console.log(`writing cached files to /tmp`); 13 | 14 | const promises = files.map((file: CompanyLogo) => { 15 | return fs.writeFile(`/tmp/${file.key}`, file.logo); 16 | }); 17 | 18 | await Promise.all(promises); 19 | } 20 | 21 | // a function to read the cached files from tmp 22 | async function readFilesFromTemp(): Promise { 23 | const filesList: string[] = await fs.readdir("/tmp/"); 24 | 25 | return await Promise.all( 26 | filesList.map(async (fileName: string) => { 27 | const file: Buffer = await fs.readFile(`/tmp/${fileName}`); 28 | return { 29 | key: fileName, 30 | logo: Buffer.from(file).toString(), 31 | }; 32 | }) 33 | ); 34 | } 35 | 36 | // a function to pull the files from an s3 bucket before caching them locally 37 | async function readFilesFromS3Bucket() { 38 | const downloadedFiles: CompanyLogos = []; 39 | 40 | // list the objects in the s3 bucket 41 | const { Contents: contents = [] }: AWS.S3.ListObjectsV2Output = await s3 42 | .listObjectsV2({ Bucket: bucketName }) 43 | .promise(); 44 | 45 | // get each of the objects from the list 46 | for (const file of contents) { 47 | const object: AWS.S3.GetObjectOutput = await s3 48 | .getObject({ Key: file.Key as string, Bucket: bucketName }) 49 | .promise(); 50 | 51 | downloadedFiles.push({ 52 | key: file.Key as string, 53 | logo: object.Body?.toString("base64") as string, 54 | }); 55 | } 56 | 57 | return downloadedFiles; 58 | } 59 | 60 | const bucketName = process.env.BUCKET as string; 61 | 62 | // set this defaulted to false, and set to true when files are cached to tmp 63 | let filesCached = false; 64 | 65 | // Note: the related api gateway endpoint does not have caching enabled 66 | export const listLogosCachedInTmpHandler: APIGatewayProxyHandler = 67 | async (): Promise => { 68 | try { 69 | const correlationId = uuid(); 70 | const method = "list-company-logos.handler"; 71 | const prefix = `${correlationId} - ${method}`; 72 | 73 | console.log(`${prefix} - started`); 74 | 75 | if (filesCached) { 76 | console.log(`${prefix} files are cached - read from tmp on Lambda`); 77 | 78 | const companyLogos: CompanyLogos = await readFilesFromTemp(); 79 | 80 | return { 81 | body: JSON.stringify(companyLogos), 82 | statusCode: 200, 83 | }; 84 | } else { 85 | console.log( 86 | `${prefix} files are not cached - read from s3 bucket and cache in tmp` 87 | ); 88 | const companyLogos: CompanyLogos = await readFilesFromS3Bucket(); 89 | await writeFilesToTemp(companyLogos); 90 | 91 | filesCached = true; // set cached to true 92 | 93 | return { 94 | body: JSON.stringify(companyLogos), 95 | statusCode: 200, 96 | }; 97 | } 98 | } catch (error) { 99 | return { 100 | body: "An error has occurred", 101 | statusCode: 500, 102 | }; 103 | } 104 | }; 105 | -------------------------------------------------------------------------------- /aws-blog/src/blogs/list-blogs/list-blogs.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyHandler, APIGatewayProxyResult } from "aws-lambda"; 2 | 3 | import { v4 as uuid } from "uuid"; 4 | 5 | const data = require("data-api-client")({ 6 | secretArn: process.env.SECRET_ARN as string, 7 | resourceArn: process.env.CLUSTER_ARN as string, 8 | database: process.env.DB, 9 | continueAfterTimeout: true, 10 | includeResultMetadata: false, 11 | }); 12 | 13 | export const listBlogsHandler: APIGatewayProxyHandler = 14 | async (): Promise => { 15 | try { 16 | const correlationId = uuid(); 17 | const method = "list-blogs.handler"; 18 | const prefix = `${correlationId} - ${method}`; 19 | 20 | console.log(`${prefix} - started`); 21 | 22 | const { records }: { records: BlogResponse[] } = await data.query( 23 | "select * from Blogs;" 24 | ); 25 | 26 | const response: BlogsResponse = { 27 | items: records, 28 | responseDateTime: new Date().toUTCString(), // we add this simply to see the caching working 29 | }; 30 | 31 | console.log(`${prefix} - successful: ${JSON.stringify(response)}`); 32 | 33 | return { 34 | body: JSON.stringify(response), 35 | statusCode: 200, 36 | }; 37 | } catch (error) { 38 | return { 39 | body: "An error has occurred", 40 | statusCode: 500, 41 | }; 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /aws-blog/src/blogs/types.ts: -------------------------------------------------------------------------------- 1 | type BlogsResponse = { 2 | items: BlogResponse[]; 3 | responseDateTime: string; 4 | }; 5 | 6 | type BlogResponse = { 7 | blogID: string; 8 | blogTitle: string; 9 | blogBody: string; 10 | blogDate: string; 11 | responseDateTime: string; 12 | }; 13 | 14 | type CompanyLogo = { 15 | key: string; 16 | logo: string; 17 | }; 18 | 19 | type CompanyLogos = CompanyLogo[]; 20 | -------------------------------------------------------------------------------- /aws-blog/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "commonjs", 5 | "lib": ["DOM", "es2018"], // DOM added as fix for axios issue https://github.com/axios/axios/issues/4124 6 | "declaration": true, 7 | "strict": true, 8 | "noImplicitAny": true, 9 | "strictNullChecks": true, 10 | "noImplicitThis": true, 11 | "alwaysStrict": true, 12 | "noUnusedLocals": false, 13 | "noUnusedParameters": false, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": false, 16 | "inlineSourceMap": true, 17 | "inlineSources": true, 18 | "experimentalDecorators": true, 19 | "strictPropertyInitialization": false, 20 | "typeRoots": ["./node_modules/@types"] 21 | }, 22 | "exclude": ["node_modules", "cdk.out"] 23 | } 24 | -------------------------------------------------------------------------------- /docs/images/header-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leegilmorecode/serverless-caching/886a16fadfaa9b85d7f5ec4960657992c6dc6941/docs/images/header-2.png -------------------------------------------------------------------------------- /docs/images/header-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leegilmorecode/serverless-caching/886a16fadfaa9b85d7f5ec4960657992c6dc6941/docs/images/header-3.png -------------------------------------------------------------------------------- /docs/images/header-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leegilmorecode/serverless-caching/886a16fadfaa9b85d7f5ec4960657992c6dc6941/docs/images/header-4.png -------------------------------------------------------------------------------- /docs/images/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leegilmorecode/serverless-caching/886a16fadfaa9b85d7f5ec4960657992c6dc6941/docs/images/header.png -------------------------------------------------------------------------------- /serverless-blog/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | 10 | # apple 11 | .DS_store 12 | 13 | cdk-outputs.json 14 | -------------------------------------------------------------------------------- /serverless-blog/.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /serverless-blog/bin/serverless-blog.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import "source-map-support/register"; 4 | 5 | import * as cdk from "@aws-cdk/core"; 6 | 7 | import { ServerlessBlogStack } from "../lib/serverless-blog-stack"; 8 | 9 | const app = new cdk.App(); 10 | new ServerlessBlogStack(app, "ServerlessBlogStack", {}); 11 | -------------------------------------------------------------------------------- /serverless-blog/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/serverless-blog.ts", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "**/*.d.ts", 11 | "**/*.js", 12 | "tsconfig.json", 13 | "package*.json", 14 | "yarn.lock", 15 | "node_modules", 16 | "test" 17 | ] 18 | }, 19 | "context": { 20 | "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true, 21 | "@aws-cdk/core:stackRelativeExports": true, 22 | "@aws-cdk/aws-rds:lowercaseDbIdentifier": true, 23 | "@aws-cdk/aws-lambda:recognizeVersionProps": true, 24 | "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /serverless-blog/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | roots: ['/test'], 4 | testMatch: ['**/*.test.ts'], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /serverless-blog/lib/serverless-blog-stack.ts: -------------------------------------------------------------------------------- 1 | import * as appsync from "@aws-cdk/aws-appsync"; 2 | import * as cdk from "@aws-cdk/core"; 3 | import * as customResources from "@aws-cdk/custom-resources"; 4 | import * as dynamodb from "@aws-cdk/aws-dynamodb"; 5 | import * as dynamodbDax from "@aws-cdk/aws-dax"; 6 | import * as ec2 from "@aws-cdk/aws-ec2"; 7 | import * as iam from "@aws-cdk/aws-iam"; 8 | import * as lambda from "@aws-cdk/aws-lambda"; 9 | import * as nodeLambda from "@aws-cdk/aws-lambda-nodejs"; 10 | import * as path from "path"; 11 | 12 | export class ServerlessBlogStack extends cdk.Stack { 13 | constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { 14 | super(scope, id, props); 15 | 16 | const dynamoDBTableName = "BlogTable"; 17 | 18 | // create the vpc for dax to sit in 19 | const vpc: ec2.Vpc = new ec2.Vpc(this, "serverless-blog-vpc", { 20 | cidr: "10.0.0.0/16", 21 | natGateways: 0, 22 | maxAzs: 2, 23 | subnetConfiguration: [ 24 | { 25 | cidrMask: 24, 26 | name: "private-subnet-1", 27 | subnetType: ec2.SubnetType.PRIVATE_ISOLATED, 28 | }, 29 | ], 30 | }); 31 | 32 | // create the appsync api 33 | const api = new appsync.GraphqlApi(this, "Api", { 34 | name: "serverless-blog-api", 35 | schema: appsync.Schema.fromAsset( 36 | path.join(__dirname, "../src/schema.graphql") 37 | ), 38 | authorizationConfig: { 39 | defaultAuthorization: { 40 | authorizationType: appsync.AuthorizationType.API_KEY, // we will use api key for the demo 41 | }, 42 | }, 43 | xrayEnabled: true, 44 | logConfig: { 45 | excludeVerboseContent: false, 46 | fieldLogLevel: appsync.FieldLogLevel.NONE, 47 | }, 48 | }); 49 | 50 | // create the dynamodb table 51 | const blogTable = new dynamodb.Table(this, dynamoDBTableName, { 52 | billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, 53 | encryption: dynamodb.TableEncryption.AWS_MANAGED, 54 | pointInTimeRecovery: false, 55 | tableName: dynamoDBTableName, 56 | contributorInsightsEnabled: true, 57 | removalPolicy: cdk.RemovalPolicy.DESTROY, 58 | partitionKey: { 59 | name: "id", 60 | type: dynamodb.AttributeType.STRING, 61 | }, 62 | }); 63 | 64 | // create a role for dax 65 | const daxServiceRole: iam.Role = new iam.Role(this, "dax-service-role", { 66 | assumedBy: new iam.ServicePrincipal("dax.amazonaws.com"), 67 | }); 68 | 69 | // create a subnet group for our dax cluster to utilise 70 | const subnetGroup: dynamodbDax.CfnSubnetGroup = 71 | new dynamodbDax.CfnSubnetGroup(this, "dax-subnet-group", { 72 | subnetIds: vpc.selectSubnets({ 73 | subnetType: ec2.SubnetType.PRIVATE_ISOLATED, 74 | }).subnetIds, 75 | subnetGroupName: "dax-subnet-group", 76 | description: "subnet group for our dax cluster", 77 | }); 78 | 79 | // add a security group for the lambdas 80 | const lambdaSecurityGroup: ec2.SecurityGroup = new ec2.SecurityGroup( 81 | this, 82 | "lambda-vpc-sg", 83 | { 84 | vpc, 85 | allowAllOutbound: true, 86 | securityGroupName: "lambda-vpc-sg", 87 | } 88 | ); 89 | 90 | // add a security group for the dax cluster 91 | const daxSecurityGroup: ec2.SecurityGroup = new ec2.SecurityGroup( 92 | this, 93 | "dax-vpc-sg", 94 | { 95 | vpc, 96 | allowAllOutbound: true, 97 | securityGroupName: "dax-vpc-sg", 98 | } 99 | ); 100 | 101 | // allow inbound traffic from the lambda security group on port 8111 102 | daxSecurityGroup.addIngressRule(lambdaSecurityGroup, ec2.Port.tcp(8111)); 103 | 104 | // create the dynamodb dax cluster 105 | const blogsDaxCluster = new dynamodbDax.CfnCluster( 106 | this, 107 | "blogsDaxCluster", 108 | { 109 | iamRoleArn: daxServiceRole.roleArn, 110 | nodeType: "dax.t2.small", 111 | replicationFactor: 2, 112 | securityGroupIds: [daxSecurityGroup.securityGroupId], 113 | subnetGroupName: subnetGroup.subnetGroupName, 114 | availabilityZones: vpc.availabilityZones, 115 | clusterEndpointEncryptionType: "NONE", 116 | clusterName: "blogsDaxCluster", 117 | description: "blogs dax cluster", 118 | preferredMaintenanceWindow: "sun:01:00-sun:09:00", 119 | sseSpecification: { 120 | sseEnabled: false, 121 | }, 122 | } 123 | ); 124 | 125 | // add permissions to the dax policy 126 | daxServiceRole.addToPolicy( 127 | new iam.PolicyStatement({ 128 | effect: iam.Effect.ALLOW, 129 | actions: [ 130 | "dynamodb:DescribeTable", 131 | "dynamodb:PutItem", 132 | "dynamodb:GetItem", 133 | "dynamodb:UpdateItem", 134 | "dynamodb:DeleteItem", 135 | "dynamodb:Query", 136 | "dynamodb:Scan", 137 | "dynamodb:BatchGetItem", 138 | "dynamodb:BatchWriteItem", 139 | "dynamodb:ConditionCheckItem", 140 | ], 141 | resources: [blogTable.tableArn], 142 | }) 143 | ); 144 | 145 | blogsDaxCluster.node.addDependency(vpc); 146 | subnetGroup.node.addDependency(vpc); 147 | blogsDaxCluster.node.addDependency(subnetGroup); 148 | 149 | // get blog endpoint handler - this uses dax for its caching 150 | const getBlogHandler: nodeLambda.NodejsFunction = 151 | new nodeLambda.NodejsFunction(this, "get-blog", { 152 | functionName: "get-blog", 153 | runtime: lambda.Runtime.NODEJS_14_X, 154 | entry: path.join(__dirname, "/../src/blogs/get-blog/get-blog.ts"), 155 | memorySize: 1024, 156 | securityGroups: [lambdaSecurityGroup], 157 | handler: "getBlogHandler", 158 | timeout: cdk.Duration.seconds(30), 159 | vpc, 160 | bundling: { 161 | minify: true, 162 | externalModules: ["aws-sdk"], 163 | }, 164 | environment: { 165 | REGION: cdk.Stack.of(this).region, 166 | TABLE_NAME: blogTable.tableName, 167 | DAX_ENDPOINT: blogsDaxCluster.attrClusterDiscoveryEndpoint, 168 | }, 169 | }); 170 | 171 | // list blogs endpoint handler - this uses dax for its caching 172 | const listBlogsHandler: nodeLambda.NodejsFunction = 173 | new nodeLambda.NodejsFunction(this, "list-blogs", { 174 | functionName: "get-blogs", 175 | runtime: lambda.Runtime.NODEJS_14_X, 176 | entry: path.join(__dirname, "/../src/blogs/list-blogs/list-blogs.ts"), 177 | memorySize: 1024, 178 | securityGroups: [lambdaSecurityGroup], 179 | timeout: cdk.Duration.seconds(30), 180 | handler: "listBlogsHandler", 181 | vpc, 182 | bundling: { 183 | minify: true, 184 | externalModules: ["aws-sdk"], 185 | }, 186 | environment: { 187 | REGION: cdk.Stack.of(this).region, 188 | TABLE_NAME: blogTable.tableName, 189 | DAX_ENDPOINT: blogsDaxCluster.attrClusterDiscoveryEndpoint, 190 | }, 191 | }); 192 | 193 | // create-blogs-table handler - this will populate the table with fake data 194 | const createBlogsTableHandler: nodeLambda.NodejsFunction = 195 | new nodeLambda.NodejsFunction(this, "create-blogs-table", { 196 | functionName: "create-blogs-table", 197 | runtime: lambda.Runtime.NODEJS_14_X, 198 | entry: path.join( 199 | __dirname, 200 | "/../src/blogs/create-blogs-table/create-blogs-table.ts" 201 | ), 202 | memorySize: 1024, 203 | handler: "createBlogsTableHandler", 204 | timeout: cdk.Duration.minutes(15), 205 | bundling: { 206 | minify: true, 207 | externalModules: ["aws-sdk"], 208 | }, 209 | environment: { 210 | REGION: cdk.Stack.of(this).region, 211 | TABLE_NAME: blogTable.tableName, 212 | }, 213 | }); 214 | 215 | // get blog no dax endpoint handler - this does not use dax for its caching 216 | const getBlogNoDaxHandler: nodeLambda.NodejsFunction = 217 | new nodeLambda.NodejsFunction(this, "get-blog-no-dax", { 218 | functionName: "get-blog-no-dax", 219 | runtime: lambda.Runtime.NODEJS_14_X, 220 | entry: path.join( 221 | __dirname, 222 | "/../src/blogs/get-blog-no-dax/get-blog-no-dax.ts" 223 | ), 224 | memorySize: 1024, 225 | securityGroups: [lambdaSecurityGroup], 226 | handler: "getBlogNoDaxHandler", 227 | timeout: cdk.Duration.seconds(30), 228 | bundling: { 229 | minify: true, 230 | externalModules: ["aws-sdk"], 231 | }, 232 | environment: { 233 | REGION: cdk.Stack.of(this).region, 234 | TABLE_NAME: blogTable.tableName, 235 | }, 236 | }); 237 | 238 | // give the create blogs table lambda write access to the database 239 | blogTable.grantWriteData(createBlogsTableHandler); 240 | 241 | // give the lambdas access to the DAX cluster 242 | getBlogHandler.addToRolePolicy( 243 | new iam.PolicyStatement({ 244 | effect: iam.Effect.ALLOW, 245 | actions: ["dax:GetItem"], 246 | resources: [blogsDaxCluster.attrArn], 247 | }) 248 | ); 249 | 250 | listBlogsHandler.addToRolePolicy( 251 | new iam.PolicyStatement({ 252 | effect: iam.Effect.ALLOW, 253 | actions: ["dax:Scan"], 254 | resources: [blogsDaxCluster.attrArn], 255 | }) 256 | ); 257 | 258 | // give the lambda access to dynamodb 259 | getBlogNoDaxHandler.addToRolePolicy( 260 | new iam.PolicyStatement({ 261 | effect: iam.Effect.ALLOW, 262 | actions: ["dynamodb:GetItem"], 263 | resources: [blogTable.tableArn], 264 | }) 265 | ); 266 | 267 | // list blogs lambda data source 268 | const blogsDataSource: appsync.LambdaDataSource = 269 | new appsync.LambdaDataSource(this, "ListBlogsLambdaDataSource", { 270 | api, 271 | lambdaFunction: listBlogsHandler, 272 | description: "List Blogs Lambda Data Source", 273 | name: "ListBlogsLambdaDataSource", 274 | }); 275 | 276 | // get blog lambda data source 277 | const blogDataSource: appsync.LambdaDataSource = 278 | new appsync.LambdaDataSource(this, "GetBlogLambdaDataSource", { 279 | api, 280 | lambdaFunction: getBlogHandler, 281 | description: "Get Blog Lambda Data Source", 282 | name: "GetBlogLambdaDataSource", 283 | }); 284 | 285 | // get blog (no dax) lambda data source 286 | const blogNoDaxDataSource: appsync.LambdaDataSource = 287 | new appsync.LambdaDataSource(this, "GetBlogNoDaxLambdaDataSource", { 288 | api, 289 | lambdaFunction: getBlogNoDaxHandler, 290 | description: "Get Blog (no dax) Lambda Data Source", 291 | name: "GetBlogNoDaxLambdaDataSource", 292 | }); 293 | 294 | // dynamodb blog table (no dax) data source 295 | const blogTableDataSource: appsync.DynamoDbDataSource = 296 | new appsync.DynamoDbDataSource(this, "blogTableDataSource", { 297 | api, 298 | description: "Blog Table (no dax) Data Source", 299 | name: "blogTableDataSource", 300 | table: blogTable, 301 | }); 302 | 303 | // listBlogs resolver going directly to lambda 304 | blogsDataSource.createResolver({ 305 | typeName: "Query", 306 | fieldName: "listBlogs", 307 | }); 308 | 309 | // getBlog by id resolver going directly to lambda 310 | blogDataSource.createResolver({ 311 | typeName: "Query", 312 | fieldName: "getBlog", 313 | }); 314 | 315 | // updateBlog mutation with cache invalidation 316 | blogTableDataSource.createResolver({ 317 | typeName: "Mutation", 318 | fieldName: "updateBlog", 319 | // this is an example of a vtl template generated with the helper methods 320 | requestMappingTemplate: appsync.MappingTemplate.dynamoDbPutItem( 321 | appsync.PrimaryKey.partition("id").is("input.id"), 322 | appsync.Values.projecting("input") 323 | ), 324 | // this is an example of an inline vtl response template (you can also pull in from a file) 325 | responseMappingTemplate: appsync.MappingTemplate.fromString(` 326 | #set($cachingKeys = {}) 327 | $util.qr($cachingKeys.put("context.arguments.id", $context.arguments.input.id)) 328 | $extensions.evictFromApiCache("Query", "getBlogNoDax", $cachingKeys) 329 | 330 | $util.toJson($context.result) 331 | `), 332 | }); 333 | 334 | // getBlog by id (no dax) resolver going directly to lambda 335 | // this also includes a cache of 30 seconds 336 | const getBlogNoDaxResolver: appsync.CfnResolver = new appsync.CfnResolver( 337 | this, 338 | "getBlogNoDaxResolver", 339 | { 340 | apiId: api.apiId, 341 | typeName: "Query", 342 | fieldName: "getBlogNoDax", 343 | cachingConfig: { 344 | ttl: cdk.Duration.seconds(30).toSeconds(), 345 | cachingKeys: ["$context.arguments.id"], // Valid values are entries from the $context.arguments, $context.source, and $context.identity maps 346 | }, 347 | kind: "UNIT", 348 | dataSourceName: blogNoDaxDataSource.name, 349 | } 350 | ); 351 | 352 | getBlogNoDaxResolver.node.addDependency(blogNoDaxDataSource); 353 | 354 | // add caching for the api 355 | new appsync.CfnApiCache(this, "appsync-cache", { 356 | apiCachingBehavior: "PER_RESOLVER_CACHING", 357 | apiId: api.apiId, 358 | ttl: cdk.Duration.seconds(30).toSeconds(), // cache for 30 seconds as default 359 | type: "SMALL", 360 | atRestEncryptionEnabled: true, 361 | transitEncryptionEnabled: true, 362 | }); 363 | 364 | // add a custom sdk call to invoke our create table lambda (this will add the dummy data on deploy) 365 | const lambdaInvokeSdkCall: customResources.AwsSdkCall = { 366 | service: "Lambda", 367 | action: "invoke", 368 | parameters: { 369 | FunctionName: "create-blogs-table", 370 | }, 371 | physicalResourceId: customResources.PhysicalResourceId.of("BlogTable"), 372 | }; 373 | 374 | // custom resource function role for our custom resource 375 | const customResourceFnRole: iam.Role = new iam.Role( 376 | this, 377 | "custom-resource-role", 378 | { 379 | assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"), 380 | } 381 | ); 382 | 383 | // allow the custom role to invoke our create blogs table lambda 384 | customResourceFnRole.addToPolicy( 385 | new iam.PolicyStatement({ 386 | resources: [ 387 | `arn:aws:lambda:${cdk.Stack.of(this).region}:${ 388 | cdk.Stack.of(this).account 389 | }:function:create-blogs-table`, 390 | ], 391 | actions: ["lambda:InvokeFunction"], 392 | }) 393 | ); 394 | 395 | // run a custom resource lambda on deploy to populate the table with dummy data 396 | const customResource: customResources.AwsCustomResource = 397 | new customResources.AwsCustomResource( 398 | this, 399 | "create-blog-table-custom-resource", 400 | { 401 | policy: customResources.AwsCustomResourcePolicy.fromSdkCalls({ 402 | resources: customResources.AwsCustomResourcePolicy.ANY_RESOURCE, 403 | }), 404 | functionName: "invoke-create-table-lambda", 405 | onCreate: lambdaInvokeSdkCall, 406 | onUpdate: lambdaInvokeSdkCall, 407 | timeout: cdk.Duration.minutes(15), 408 | role: customResourceFnRole, 409 | } 410 | ); 411 | 412 | customResource.node.addDependency(createBlogsTableHandler); 413 | 414 | // dynamodb gateway endpoint to allow the lambdas in the private vpcs to call dynamodb 415 | const dynamoDbEndpoint = vpc.addGatewayEndpoint("DynamoDbEndpoint", { 416 | service: ec2.GatewayVpcEndpointAwsService.DYNAMODB, 417 | }); 418 | 419 | // gateway endpoint policy 420 | dynamoDbEndpoint.addToPolicy( 421 | new iam.PolicyStatement({ 422 | principals: [new iam.AnyPrincipal()], 423 | actions: [ 424 | "dynamodb:List*", 425 | "dynamodb:DescribeReservedCapacity*", 426 | "dynamodb:DescribeLimits", 427 | "dynamodb:DescribeTimeToLive", 428 | ], 429 | resources: [blogTable.tableArn], 430 | }) 431 | ); 432 | 433 | dynamoDbEndpoint.addToPolicy( 434 | new iam.PolicyStatement({ 435 | principals: [new iam.AnyPrincipal()], 436 | actions: [ 437 | "dynamodb:BatchGet*", 438 | "dynamodb:DescribeStream", 439 | "dynamodb:DescribeTable", 440 | "dynamodb:Get*", 441 | "dynamodb:Query", 442 | "dynamodb:Scan", 443 | "dynamodb:BatchWrite*", 444 | "dynamodb:CreateTable", 445 | "dynamodb:Delete*", 446 | "dynamodb:Update*", 447 | "dynamodb:PutItem", 448 | ], 449 | resources: [blogTable.tableArn], 450 | }) 451 | ); 452 | 453 | // useful exports 454 | new cdk.CfnOutput(this, "graphqlUrl", { value: api.graphqlUrl }); 455 | new cdk.CfnOutput(this, "apiKey", { value: api.apiKey! }); 456 | new cdk.CfnOutput(this, "apiId", { value: api.apiId }); 457 | new cdk.CfnOutput(this, "daxClusterEndpointUrl", { 458 | value: blogsDaxCluster.attrClusterDiscoveryEndpointUrl, 459 | }); 460 | new cdk.CfnOutput(this, "attrClusterDiscoveryEndpointUrl", { 461 | value: blogsDaxCluster.attrClusterDiscoveryEndpointUrl, 462 | }); 463 | } 464 | } 465 | -------------------------------------------------------------------------------- /serverless-blog/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-blog", 3 | "version": "0.1.0", 4 | "bin": { 5 | "serverless-blog": "bin/serverless-blog.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "cdk": "cdk", 12 | "deploy": "cdk deploy --outputs-file ./cdk-outputs.json", 13 | "remove": "cdk destroy" 14 | }, 15 | "devDependencies": { 16 | "@aws-cdk/aws-appsync": "^1.125.0", 17 | "@aws-cdk/aws-dax": "^1.125.0", 18 | "@aws-cdk/aws-dynamodb": "^1.125.0", 19 | "@aws-cdk/aws-lambda": "^1.125.0", 20 | "@aws-cdk/aws-lambda-nodejs": "^1.125.0", 21 | "@aws-cdk/core": "^1.125.0", 22 | "@aws-cdk/custom-resources": "^1.125.0", 23 | "@types/amazon-dax-client": "^1.2.2", 24 | "@types/aws-lambda": "^8.10.89", 25 | "@types/jest": "^26.0.10", 26 | "@types/node": "10.17.27", 27 | "@types/uuid": "^8.3.4", 28 | "aws-cdk": "2.1.0", 29 | "jest": "^26.4.2", 30 | "ts-jest": "^26.2.0", 31 | "ts-node": "^9.0.0", 32 | "typescript": "~3.9.7" 33 | }, 34 | "dependencies": { 35 | "amazon-dax-client": "^1.2.7", 36 | "aws-cdk-lib": "2.1.0", 37 | "aws-sdk": "^2.1053.0", 38 | "constructs": "^10.0.0", 39 | "source-map-support": "^0.5.16", 40 | "uuid": "^8.3.2" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /serverless-blog/src/blogs/create-blogs-table/create-blogs-table.ts: -------------------------------------------------------------------------------- 1 | import * as AWS from "aws-sdk"; 2 | 3 | import { 4 | CdkCustomResourceHandler, 5 | CdkCustomResourceResponse, 6 | } from "aws-lambda"; 7 | 8 | import { v4 as uuid } from "uuid"; 9 | 10 | const dynamoDb = new AWS.DynamoDB.DocumentClient(); 11 | 12 | export const createBlogsTableHandler: CdkCustomResourceHandler = 13 | async (): Promise => { 14 | try { 15 | const correlationId = uuid(); 16 | const method = "create-blogs-table.handler"; 17 | const prefix = `${correlationId} - ${method}`; 18 | 19 | console.log(`${prefix} - started`); 20 | 21 | const tableName = process.env.TABLE_NAME; 22 | 23 | if (!tableName) throw new Error("table name is not supplied in config"); 24 | 25 | console.log(`${prefix} - creating records`); 26 | 27 | const params: AWS.DynamoDB.DocumentClient.BatchWriteItemInput = { 28 | RequestItems: { 29 | [tableName]: [ 30 | { 31 | PutRequest: { 32 | Item: { 33 | id: "1", 34 | blogTitle: "Lambda News", 35 | blogBody: "Lambda memory increased to 10GB", 36 | blogDate: new Date().toISOString(), 37 | }, 38 | }, 39 | }, 40 | { 41 | PutRequest: { 42 | Item: { 43 | id: "2", 44 | blogTitle: "Serverless Kafka!", 45 | blogBody: "Serverless MSK is now a thing!", 46 | blogDate: new Date().toISOString(), 47 | }, 48 | }, 49 | }, 50 | { 51 | PutRequest: { 52 | Item: { 53 | id: "3", 54 | blogTitle: "DynamoDB Infrequent Access", 55 | blogBody: "this could save you 60% costs", 56 | blogDate: new Date().toISOString(), 57 | }, 58 | }, 59 | }, 60 | ], 61 | }, 62 | }; 63 | 64 | await dynamoDb.batchWrite(params).promise(); 65 | 66 | console.log(`${prefix} - successfully created records`); 67 | 68 | return { 69 | success: true, 70 | }; 71 | } catch (error) { 72 | console.error(error); 73 | throw error; 74 | } 75 | }; 76 | -------------------------------------------------------------------------------- /serverless-blog/src/blogs/get-blog-no-dax/get-blog-no-dax.ts: -------------------------------------------------------------------------------- 1 | import * as AWS from "aws-sdk"; 2 | 3 | import { AppSyncResolverEvent, AppSyncResolverHandler } from "aws-lambda"; 4 | import { Blog, QueryIdArgs } from "../../types"; 5 | 6 | import { v4 as uuid } from "uuid"; 7 | 8 | const dynamoDb: AWS.DynamoDB.DocumentClient = new AWS.DynamoDB.DocumentClient(); 9 | 10 | // Note: This handler resolves directly to dynamodb without caching using dax 11 | export const getBlogNoDaxHandler: AppSyncResolverHandler< 12 | QueryIdArgs, 13 | Blog 14 | > = async ( 15 | event: AppSyncResolverEvent | null> 16 | ): Promise => { 17 | try { 18 | const correlationId = uuid(); 19 | const method = "get-blog-no-dax.handler"; 20 | const prefix = `${correlationId} - ${method}`; 21 | 22 | console.log(`${prefix} - started`); 23 | 24 | console.log( 25 | `${prefix} - event args: ${JSON.stringify(event.arguments.id)}` 26 | ); 27 | console.log(`${prefix} - event request: ${JSON.stringify(event.request)}`); 28 | console.log(`${prefix} - event source: ${JSON.stringify(event.source)}`); 29 | console.log(`${prefix} - event info: ${JSON.stringify(event.info)}`); 30 | 31 | const params: AWS.DynamoDB.DocumentClient.GetItemInput = { 32 | TableName: process.env.TABLE_NAME as string, 33 | ConsistentRead: true, 34 | Key: { 35 | id: event.arguments.id, 36 | }, 37 | }; 38 | 39 | // get the correct record back from dynamodb (note: no dax caching on this lambda) 40 | const { Item: data }: AWS.DynamoDB.DocumentClient.GetItemOutput = 41 | await dynamoDb.get(params).promise(); 42 | 43 | if (!data) throw new Error("item not found"); 44 | 45 | const response: Blog = { 46 | id: data.id, 47 | blogDate: data.blogDate, 48 | blogTitle: data.blogTitle, 49 | blogBody: data.blogBody, 50 | }; 51 | 52 | console.log(`response: ${response}`); 53 | 54 | return response; 55 | } catch (error) { 56 | console.log(`Error: ${error}`); 57 | throw error; 58 | } 59 | }; 60 | -------------------------------------------------------------------------------- /serverless-blog/src/blogs/get-blog/get-blog.ts: -------------------------------------------------------------------------------- 1 | import * as AWS from "aws-sdk"; 2 | 3 | import { AppSyncResolverEvent, AppSyncResolverHandler } from "aws-lambda"; 4 | import { Blog, QueryIdArgs } from "../../types"; 5 | 6 | import { v4 as uuid } from "uuid"; 7 | 8 | const AmazonDaxClient = require("amazon-dax-client"); 9 | 10 | const dax = new AmazonDaxClient({ 11 | endpoints: [process.env.DAX_ENDPOINT], 12 | region: process.env.REGION, 13 | }); 14 | const dynamoDb: AWS.DynamoDB.DocumentClient = new AWS.DynamoDB.DocumentClient({ 15 | service: dax, 16 | }); 17 | 18 | export const getBlogHandler: AppSyncResolverHandler = async ( 19 | event: AppSyncResolverEvent | null> 20 | ): Promise => { 21 | try { 22 | const correlationId = uuid(); 23 | const method = "get-blog.handler"; 24 | const prefix = `${correlationId} - ${method}`; 25 | 26 | console.log(`${prefix} - started`); 27 | 28 | console.log( 29 | `${prefix} - event args: ${JSON.stringify(event.arguments.id)}` 30 | ); 31 | console.log(`${prefix} - event request: ${JSON.stringify(event.request)}`); 32 | console.log(`${prefix} - event source: ${JSON.stringify(event.source)}`); 33 | console.log(`${prefix} - event info: ${JSON.stringify(event.info)}`); 34 | 35 | const params: AWS.DynamoDB.DocumentClient.GetItemInput = { 36 | TableName: process.env.TABLE_NAME as string, 37 | ConsistentRead: false, 38 | Key: { 39 | id: event.arguments.id, 40 | }, 41 | }; 42 | 43 | // get the correct record back from dax or dynamodb 44 | const { Item: data }: AWS.DynamoDB.DocumentClient.GetItemOutput = 45 | await dynamoDb.get(params).promise(); 46 | 47 | if (!data) throw new Error("item not found"); 48 | 49 | const response: Blog = { 50 | id: data.id, 51 | blogDate: data.blogDate, 52 | blogTitle: data.blogTitle, 53 | blogBody: data.blogBody, 54 | }; 55 | 56 | console.log(`response: ${response}`); 57 | 58 | return response; 59 | } catch (error) { 60 | console.log(`Error: ${error}`); 61 | throw error; 62 | } 63 | }; 64 | -------------------------------------------------------------------------------- /serverless-blog/src/blogs/list-blogs/list-blogs.ts: -------------------------------------------------------------------------------- 1 | import * as AWS from "aws-sdk"; 2 | 3 | import { AppSyncResolverEvent, AppSyncResolverHandler } from "aws-lambda"; 4 | import { Blog, NoArgs } from "../../types"; 5 | 6 | import { v4 as uuid } from "uuid"; 7 | 8 | const AmazonDaxClient = require("amazon-dax-client"); 9 | 10 | const dax = new AmazonDaxClient({ 11 | endpoints: [process.env.DAX_ENDPOINT], 12 | region: process.env.REGION, 13 | }); 14 | const dynamoDb = new AWS.DynamoDB.DocumentClient({ service: dax }); 15 | 16 | export const listBlogsHandler: AppSyncResolverHandler = async ( 17 | event: AppSyncResolverEvent | null> 18 | ): Promise => { 19 | try { 20 | const correlationId = uuid(); 21 | const method = "list-blogs.handler"; 22 | const prefix = `${correlationId} - ${method}`; 23 | 24 | console.log(`${prefix} - started`); 25 | 26 | console.log(`${prefix} - event request: ${JSON.stringify(event.request)}`); 27 | console.log(`${prefix} - event source: ${JSON.stringify(event.source)}`); 28 | console.log(`${prefix} - event info: ${JSON.stringify(event.info)}`); 29 | 30 | const params: AWS.DynamoDB.DocumentClient.ScanInput = { 31 | TableName: process.env.TABLE_NAME as string, 32 | ConsistentRead: false, 33 | }; 34 | 35 | // get the correct records back from dax or dynamodb 36 | const { Items: data }: AWS.DynamoDB.DocumentClient.ScanOutput = 37 | await dynamoDb.scan(params).promise(); 38 | 39 | if (!data || !data.length) throw new Error("items not found"); 40 | 41 | const response: Blog[] = data.map((item) => ({ 42 | id: item.id, 43 | blogTitle: item.blogTitle, 44 | blogBody: item.blogBody, 45 | blogDate: item.blogDate, 46 | })); 47 | 48 | console.log(`response: ${response}`); 49 | 50 | return response; 51 | } catch (error) { 52 | console.log(error); 53 | throw error; 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /serverless-blog/src/schema.graphql: -------------------------------------------------------------------------------- 1 | input blogInput { 2 | id: ID! 3 | blogTitle: String! 4 | blogBody: String! 5 | blogDate: AWSDateTime! 6 | } 7 | 8 | type blog { 9 | id: ID! 10 | blogTitle: String! 11 | blogBody: String! 12 | blogDate: AWSDateTime! 13 | } 14 | type Query { 15 | getBlog(id: ID!): blog! 16 | getBlogNoDax(id: ID!): blog! 17 | listBlogs: [blog] 18 | } 19 | type Mutation { 20 | updateBlog(input: blogInput!): blog! 21 | } 22 | -------------------------------------------------------------------------------- /serverless-blog/src/types.ts: -------------------------------------------------------------------------------- 1 | export type Scalars = { 2 | ID: string; 3 | String: string; 4 | Boolean: boolean; 5 | Int: number; 6 | Float: number; 7 | AWSDate: string; 8 | AWSTime: string; 9 | AWSDateTime: string; 10 | AWSTimestamp: number; 11 | AWSEmail: string; 12 | AWSJSON: string; 13 | AWSURL: string; 14 | AWSPhone: string; 15 | AWSIPAddress: string; 16 | }; 17 | 18 | export type QueryIdArgs = { 19 | id: Scalars["ID"]; 20 | }; 21 | export type NoArgs = {}; 22 | 23 | export type Blog = { 24 | __typename?: "Blog"; 25 | id: Scalars["ID"]; 26 | blogTitle: Scalars["String"]; 27 | blogBody: Scalars["String"]; 28 | blogDate: Scalars["AWSDateTime"]; 29 | }; 30 | -------------------------------------------------------------------------------- /serverless-blog/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2018" 7 | ], 8 | "declaration": true, 9 | "strict": true, 10 | "noImplicitAny": true, 11 | "strictNullChecks": true, 12 | "noImplicitThis": true, 13 | "alwaysStrict": true, 14 | "noUnusedLocals": false, 15 | "noUnusedParameters": false, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": false, 18 | "inlineSourceMap": true, 19 | "inlineSources": true, 20 | "experimentalDecorators": true, 21 | "strictPropertyInitialization": false, 22 | "typeRoots": [ 23 | "./node_modules/@types" 24 | ] 25 | }, 26 | "exclude": [ 27 | "node_modules", 28 | "cdk.out" 29 | ] 30 | } 31 | --------------------------------------------------------------------------------