After completing this Guide/Documentation you will:
- have a local development environment where you can develop run and deploy your lambda code
- understand the code structure
- Succesfully run queries and mutations against the Trains and Cars POC Application via GraphQL.
- Node and NPM (NPM is installed when you install Node.js)
brew update && brew install node
node -v
npm -v
- Install AWS SAM CLI
brew tap aws/tap
brew install aws-sam-cli
- Verify the AWS SAM CLI is installed in the floowing location
/usr/local/bin/sam
- Verify the Installation
sam --version
- Output should be
SAM CLI, version xxxx
- Clone Github Project https://github.com/pontus-vision/aws-sam-typescript-lambda-graphql
- Navigate to postgresdb directory e.g. aws-sam-typescript-lambda-graphql/postgresdb
- Deploy a dockerised postgres (useful commands below.)
docker-compose up
docker-compose down
docker volume prune -f
docker image prune -f
docker-compose rm --all
docker-compose build --no-cache
- Create a secrets/POSTGRES_PASSWORD_FILE in aws-sam-typescript-lambda-graphql/postgresdb:
<your pg password>
- Docker exec and enter the password you stored in POSTGRES_PASSWORD_FILE; this will mount the file under /run/secrets/POSTGRES_PASSWORD_FILE in the container
- Navigate to parent directory of cloned project
- Install required npm modules
npm install
- Navigate to parent directory and run command
npm run local-sam
- Open your browser and Navigate to http://127.0.0.1:4000/graphq
- Other commands you can run are in aws-sam-typescript-lambda-graphql/package.json
"build-schema": "rimraf ./dist/graphql/schema/ && find src/graphql/types -iname \"*.ts\" > ts-files.txt && tsc ./src/graphql/schema/genereator-schema.ts --outDir ./dist/graphql/schema/ --lib esnext && tsc @ts-files.txt --outDir ./dist/graphql/types --lib esnext && rimraf ts-files.txt",
"build-schema:win": "rimraf ./dist/graphql/schema/ && dir /s /b \"src/graphql/types\" | findstr /e .ts > ts-files.txt && tsc ./src/graphql/schema/genereator-schema.ts --outDir ./dist/graphql/schema/ --lib esnext && tsc @ts-files.txt --outDir ./dist/graphql/types --lib esnext && del ts-files.txt",
"generate-ts": "gql-gen",
"pregenerate": "npm run build-schema",
"pregenerate:win": "npm run build-schema:win",
"generate": "npm run generate-ts",
"generate:win": "npm run generate-ts",
"clean": "rimraf dist",
"prebuild": "npm run clean && npm run generate",
"build": "npm run tsc",
"build:win": "npm run tsc:win",
"start": "sls offline start --skipCacheInvalidation",
"dev": "tsc --watch & nodemon dist",
"test": "tsc && mocha dist/**/*.spec.js",
"lint": "eslint src --ext ts",
"tsc": "tsc",
"tsc:win": "tsc",
"sls:offline": "npm run build && npm start",
"sls:offline:win": "npm run build && npm start",
"tslint": "tslint --fix -c tslint.json 'src/**/*.ts'",
"prettier": "prettier --write \"src/**/*.ts\"",
"precommit-tslint": "tslint --fix -c tslint.json",
"precommit-prettier": "prettier --write",
"tslint-check": "tslint-config-prettier-check ./tslint.json",
"local-sam-debug": "sam local start-api --port 4000 --docker-network postgresdb_graphql --debug -d 5010",
"local-sam": "sam local start-api --port 4000 --docker-network postgresdb_graphql",
"deploy": "npm run build && sls deploy"
- From project parent directory, run the app in debug mode e.g.
npm run local-sam-debug
- Open Intellij and Configure NodeJS Debugger against port above 5010
- Select your breakpoints, Submit a Query in the Playground on the Webui and trigger debug
- Jest is already added as a dev dependency in package.json, so running npm install as part of Step 4 will have installed it.
- Look at https://jestjs.io/docs/en/getting-started for more information setting up jest.
- To execute test, run
npm run test
from the command line. - We have added tests for GraphQL queries and Mutations, these can be triggered by executng the command above.
- As part of out test, we Mock interactions with the database via the MockSQLService class.
- We then verify responses and spy on database calls to ensure correct calls are made and with the correct parameters.
Technologies
- AWS SAM - https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html
- GraphQL - https://graphql.org/learn/ and https://www.npmjs.com/package/graphql-cli
- TypeScript - https://www.typescriptlang.org/
- PostgresQL Database with JsonB: https://www.postgresql.org/docs/9.4/datatype-json.html
- Nodejs - https://nodejs.org/en/docs/guides/
βββ LICENSE -- License file
βββ README.md -- This file
βββ bzt -- Blazemeter Taurus end to end tests
βΒ Β βββ README.md -- This file
βΒ Β βββ docker_run.sh -- this file has the configuration to build docker and run the taurus test.
βΒ Β βββ 00_mutation.yml -- this file contains taurus post request script.
βΒ Β βββ 01_query.yml -- this file contains taurus get request script
βΒ Β βββ addcar.json -- file contains payload request for mutation.
βΒ Β βββ test1.json -- file contains payload request for query.
βΒ Β βββ 90-artifacts-dir.json -- file contains setting for artifacts-dir directory.
βΒ Β βββ 99-zinstallID.json -- contains the docker installing id
βΒ Β βββ 90-no-console.json -- file contains enabling the console log settings.
βββ codegen.yml -- this file has the configuration to create the auto-generated types.d.ts file.
βββ jest.config.js -- configuration file that defines the behaviour of the Jest Unit Tests.
βββ lambda.js -- entry point for the AWS Lambda
βββ package-lock.json
βββ package.json -- this has all the NPM dependencies and utility scripts for this project.
βββ postgresdb -- All the files in this directory enable users to create a dockerised PostgreSQL DB
βΒ Β βββ docker
βΒ Β βΒ Β βββ Dockerfile -- Docker file that pulls the PostGreSQL
βΒ Β βΒ Β βββ init.sql -- initial table creation DDL (this is automatically called from the docker image code
βΒ Β βββ docker-compose.yml
βΒ Β βββ secrets -- database password; this is not checked into this project, as it contains sensitive information; see step 3 above
βΒ Β βββ POSTGRES_PASSWORD_FILE
βββ sam-template.yml -- AWS SAM template file.
βββ serverless.yml -- Alternative template for the Serverless framework
βββ src
βΒ Β βββ aws-wrapper.ts -- instantiates the apollo server object used by the lambda entry point
βΒ Β βββ context.ts -- class that is passed to the resolvers; this is initialized with injectable services (currently SQLService)
βΒ Β βββ core
βΒ Β βΒ Β βββ config
βΒ Β βΒ Β βΒ Β βββ AbstractSetting.ts -- exposes the configuration interfaces
βΒ Β βΒ Β βΒ Β βββ IConfig.ts -- interfaces for configuration parameters used in other classes.
βΒ Β βΒ Β βΒ Β βββ Setting.ts -- creates the concrete class implementation of the AbstractSetting. Uses IConfig as a class attribute.
βΒ Β βΒ Β βββ constants
βΒ Β βΒ Β βΒ Β βββ Queries.ts -- class that has a list of all the query strings.
βΒ Β βΒ Β βββ injector.ts -- class that has the list of all the Injectable classes. You MUST define all injectable classes here.
βΒ Β βΒ Β βββ logger -- logging implementation.
βΒ Β βΒ Β βββ AbstractLogger.ts
βΒ Β βΒ Β βββ Logger.ts
βΒ Β βββ graphql
βΒ Β βΒ Β βββ resolvers -- This directory has the code that maps the GraphQL request
βΒ Β βΒ Β βΒ Β βββ car.ts -- this class maps the GraphQL queries and mutations into SQL statements for cars.
βΒ Β βΒ Β βΒ Β βββ train.ts -- this class maps the GraphQL queries and mutations into SQL statements for trains.
βΒ Β βΒ Β βββ schema
βΒ Β βΒ Β βΒ Β βββ genereator-schema.ts -- used by (npm run build-schema); merges and exports all the types defined in the types folder
βΒ Β βΒ Β βΒ Β βββ schema.ts -- merges and exports the schemas and resolvers.
βΒ Β βΒ Β βββ types -- this folder has a list of all the graphql schemas. These are concatenated to form the whole GraphQL
βΒ Β βΒ Β βββ car.ts
βΒ Β βΒ Β βββ train.ts
βΒ Β βββ interfaces
βΒ Β βΒ Β βββ IAppContext.ts -- defines the interface for the Context otbject
βΒ Β βΒ Β βββ types.d.ts -- auto-generated file that has the transpiled GQL schema; DO NOT change this, as it's built with npm.
βΒ Β βββ server.ts -- This file defines the signature of the ApolloServer instance in typescript
βΒ Β βββ services
βΒ Β βββ sql
βΒ Β βββ SQLService.ts -- service that provides access to the DB (NOTE: must add these in the core/injector.ts)
βββ template.yml -> sam-template.yml
βββ test -- Unit tests for the services and resolvers.
βΒ Β βββ car.test.ts
βΒ Β βββ services
βΒ Β βββ sql
βΒ Β βββ MockSQLService.ts
βββ tsconfig.json -- config file for typescript
βββ tslint.json -- on git commit, this enforces that the code conforms to pure typescript syntax.
- Jest is already added as a dev dependency in package.json, Look at https://jestjs.io/docs/en/getting-started for more information setting up jest.
- To execute test, run "npm run test" from the command line.
- GraphQL Queries and Mutations and mutation tests have been amended to validate encryption of items being stored to the database and decryption of items leaving the database before they are presented to the user.
- Unit Test have been added for the Cryptography Service for generating cipher angorithm, encryption and decryption using the nodejs primary crypto module.
Blazemeter Taurus is a test harness that enables functional and non-functional tests to be run; the samples included here only send one mutation, and wait for one response back. When running this locally, blazemeter will run in a docker container, with a network set to 'host', so it can contact the SAM framework lambda.
To run the tests, run the following:
cd bzt
./docker_run.sh
Here is a sample result:
Successfully tagged carsbzt:latest
09:58:13 INFO: Taurus CLI Tool v1.13.8
09:58:13 INFO: Starting with configs: ['/bzt-config/00_mutation.yml']
09:58:13 INFO: Configuring...
09:58:14 INFO: Artifacts dir: /bzt/bzt_artifacts
09:58:14 INFO: Preparing...
09:58:23 INFO: Starting...
09:58:23 INFO: Waiting for results...
09:58:51 WARNING: Please wait for graceful shutdown...
09:58:51 INFO: Shutting down...
09:58:51 INFO: Post-processing...
09:58:51 INFO: Test duration: 0:00:28
09:58:51 INFO: Samples count: 1, 0.00% failures
09:58:51 INFO: Average times: total 21.402, latency 21.402, connect 0.063
09:58:51 INFO: Percentiles:
βββββββββββββββββ¬ββββββββββββββββ
β Percentile, % β Resp. Time, s β
βββββββββββββββββΌββββββββββββββββ€
β 0.0 β 21.392 β
β 50.0 β 21.392 β
β 90.0 β 21.392 β
β 95.0 β 21.392 β
β 99.0 β 21.392 β
β 99.9 β 21.392 β
β 100.0 β 21.392 β
βββββββββββββββββ΄ββββββββββββββββ
09:58:51 INFO: Request label stats:
ββββββββββ¬βββββββββ¬ββββββββββ¬βββββββββ¬ββββββββ
β label β status β succ β avg_rt β error β
ββββββββββΌβββββββββΌββββββββββΌβββββββββΌββββββββ€
β addCar β OK β 100.00% β 21.402 β β
ββββββββββ΄βββββββββ΄ββββββββββ΄βββββββββ΄ββββββββ
09:58:51 INFO: Test duration: 0:00:28
09:58:51 INFO: Samples count: 1, 0.00% failures
09:58:51 INFO: Average times: total 21.402, latency 21.402, connect 0.063
09:58:51 INFO: Percentiles:
βββββββββββββββββ¬ββββββββββββββββ
β Percentile, % β Resp. Time, s β
βββββββββββββββββΌββββββββββββββββ€
β 0.0 β 21.392 β
β 50.0 β 21.392 β
β 90.0 β 21.392 β
β 95.0 β 21.392 β
β 99.0 β 21.392 β
β 99.9 β 21.392 β
β 100.0 β 21.392 β
βββββββββββββββββ΄ββββββββββββββββ
09:58:51 INFO: Request label stats:
ββββββββββ¬βββββββββ¬ββββββββββ¬βββββββββ¬ββββββββ
β label β status β succ β avg_rt β error β
ββββββββββΌβββββββββΌββββββββββΌβββββββββΌββββββββ€
β addCar β OK β 100.00% β 21.402 β β
ββββββββββ΄βββββββββ΄ββββββββββ΄βββββββββ΄ββββββββ
09:58:51 INFO: Dumping final status as CSV: /tmp/test-res.csv
09:58:51 INFO: Artifacts dir: /bzt/bzt_artifacts
09:58:51 INFO: Done performing with code: 0
09:58:51 INFO: Taurus CLI Tool v1.13.8
09:58:51 INFO: Starting with configs: ['/bzt-config/01_query.yml']
09:58:51 INFO: Configuring...
09:58:51 INFO: Artifacts dir: /bzt/bzt_artifacts
09:58:52 INFO: Preparing...
09:59:01 INFO: Starting...
09:59:01 INFO: Waiting for results...
09:59:54 WARNING: Please wait for graceful shutdown...
09:59:54 INFO: Shutting down...
09:59:54 INFO: Post-processing...
09:59:54 INFO: Test duration: 0:00:53
09:59:54 INFO: Samples count: 1, 0.00% failures
09:59:54 INFO: Average times: total 19.076, latency 19.076, connect 0.033
09:59:54 INFO: Percentiles:
βββββββββββββββββ¬ββββββββββββββββ
β Percentile, % β Resp. Time, s β
βββββββββββββββββΌββββββββββββββββ€
β 0.0 β 19.072 β
β 50.0 β 19.072 β
β 90.0 β 19.072 β
β 95.0 β 19.072 β
β 99.0 β 19.072 β
β 99.9 β 19.072 β
β 100.0 β 19.072 β
βββββββββββββββββ΄ββββββββββββββββ
09:59:54 INFO: Request label stats:
ββββββββββ¬βββββββββ¬ββββββββββ¬βββββββββ¬ββββββββ
β label β status β succ β avg_rt β error β
ββββββββββΌβββββββββΌββββββββββΌβββββββββΌββββββββ€
β getcar β OK β 100.00% β 19.076 β β
ββββββββββ΄βββββββββ΄ββββββββββ΄βββββββββ΄ββββββββ
09:59:54 INFO: Test duration: 0:00:53
09:59:54 INFO: Samples count: 1, 0.00% failures
09:59:54 INFO: Average times: total 19.076, latency 19.076, connect 0.033
09:59:54 INFO: Percentiles:
βββββββββββββββββ¬ββββββββββββββββ
β Percentile, % β Resp. Time, s β
βββββββββββββββββΌββββββββββββββββ€
β 0.0 β 19.072 β
β 50.0 β 19.072 β
β 90.0 β 19.072 β
β 95.0 β 19.072 β
β 99.0 β 19.072 β
β 99.9 β 19.072 β
β 100.0 β 19.072 β
βββββββββββββββββ΄ββββββββββββββββ
09:59:54 INFO: Request label stats:
ββββββββββ¬βββββββββ¬ββββββββββ¬βββββββββ¬ββββββββ
β label β status β succ β avg_rt β error β
ββββββββββΌβββββββββΌββββββββββΌβββββββββΌββββββββ€
β getcar β OK β 100.00% β 19.076 β β
ββββββββββ΄βββββββββ΄ββββββββββ΄βββββββββ΄ββββββββ
After completing this section you will:
- understand how we encrypt/decrypt queries and mutations calls from graphql via lambda
- be able to add encryption functionality to specific fields and extend the cryptography functionality of the system.
- Schema Directives. https://www.apollographql.com/docs/graphql-tools/schema-directives/
- NodeJS Crypto Module https://nodejs.org/api/crypto.html
- Make specific field candidates for encryption/decryption by annotating them with @sensitive (Annotation driven approach achieved through the use of Schema directives.)
- Call makeExecutableSchema with the @sensitive directive and its handlers (You can add multiple directive and handlers.)
- In the schema directive handler, override the visitFieldDefinition and capture annotated fields
- In the resolver, for queries, encrypt the annotated fields on the argument passed, retrieve the data and decrypt the values before passing back to graphql
- In the resolver, for mutations, encrypt the annotated fields and store the encrypted form into the database.
- An Injectable Cryptography service is used to perform encryption and decryption.
directive @sensitive on FIELD_DEFINITION
type Car {
_id: String!
name : String @sensitive
}
schema = makeExecutableSchema({
typeDefs: allTypes,
schemaDirectives: {
sensitive: CryptographyDirective
},
resolvers: allResolvers
});
export class CryptographyDirective extends SchemaDirectiveVisitor {
public static fields: any = {};
public visitFieldDefinition(
field: GraphQLField<any, any>,
details: {
objectType: GraphQLObjectType | GraphQLInterfaceType;
}
): GraphQLField<any, any> | void | null {
const { resolve = defaultFieldResolver } = field;
CryptographyDirective.fields[field.name] = true;
}
}
console.log(`Quering with args ${JSON.stringify(args)}`);
context.cryptographyService.recursiveCrypto(args, Cryptography.ENCRYPT);
return context.sqlService
.runQuery(Queries.SEARCH_CAR, [JSON.stringify(args)])
.then(res => {
console.log(`Database response is: ${JSON.stringify(res)}`);
const result = context.cryptographyService.recursiveCrypto(
res.rows.map(row => row.search),
Cryptography.DECRYPT
);
console.log(`Result is: ${JSON.stringify(result)}`);
return result;
})
.catch(e => console.error(e.stack));
const sqlService: SQLService = context.sqlService;
context.cryptographyService.recursiveCrypto(args, Cryptography.ENCRYPT);
const id = args.car._id;
const insert = JSON.stringify(args.car);
console.log('Mutating with id "%s" and insert "%s"', id, insert);
return sqlService
.runQuery(Queries.MUTATE_CAR, [id, insert, id, insert])
.then(res => {
const response = JSON.parse('{"status": " 200 "}');
return response;
})
.catch(e => console.error(e.stack));
Encryption/Decryption Ciphers are created using the aes-256-cbc. This stack overflow entry explains it well. https://stackoverflow.com/questions/33121619/is-there-any-difference-between-aes-128-cbc-and-aes-128-encryption
To Encrypt
- Within the NodeJS Crypto module, we generate the cipher with crypto.createCipheriv(algorithm, key, iv[, options]). For more information, see https://nodejs.org/api/crypto.html#crypto_crypto_createcipheriv_algorithm_key_iv_options
- Then call cipher.update() passing it the data we are encrypting, for more information, see https://nodejs.org/api/crypto.html#crypto_cipher_update_data_inputencoding_outputencoding
To Decrypt
- Within the NodeJS Crypto module, we generate the decipher with crypto.createDecipheriv(algorithm, key, iv[, options]). For more information, see https://nodejs.org/api/crypto.html#crypto_crypto_createdecipheriv_algorithm_key_iv_options
- Then call decipher.update(), passing it the data we are decrypting, for more information, see https://nodejs.org/api/crypto.html#crypto_decipher_update_data_inputencoding_outputencoding