diff --git a/src/content/guides/sls-prisma/index.mdx b/src/content/guides/sls-prisma/index.mdx new file mode 100644 index 0000000..2b6cd4e --- /dev/null +++ b/src/content/guides/sls-prisma/index.mdx @@ -0,0 +1,435 @@ +There are a few challanges when we want to combine technologies like AWS Lambda, Prisma, Apollo GraphQL and Serverless. That is a great stack, effective, easy to set up, suitable for starting out things, but it has some things to look out for. + +First, let's create a simple repo and build our way slowly to our desired goal and techs. + +# Creating the repo + +To deploy our application we will rely heavily on the Serverless framework with Webpack. That means we are not going to upload to AWS the raw code. We will transpile it and also bundle it. + +Having said that let's have a look at the `package.json` and see what the dependencies we use. + +```json +{ + "name": "prisma_aws_sls_apollo", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "dependencies": { + "@prisma/client": "^2.1.3", + "apollo-server-lambda": "^2.15.0", + "graphql": "14.5.0", + "serverless-webpack": "^5.3.2" + }, + "devDependencies": { + "@babel/core": "^7.10.3", + "@babel/preset-env": "^7.10.3", + "@babel/preset-typescript": "^7.10.1", + "babel-loader": "^8.1.0", + "copy-webpack-plugin": "^6.0.2", + "webpack": "^4.43.0", + } +} +``` + +`apollo-server-lambda` is a fully fledged apollo server plus it also creates a callback style handler for us that can be deployed on AWS. Tha backbone of our project. + +`@prisma/client` is our way to communicate to our db instance. You can learn more about the Prisma project [here](https://www.prisma.io/). Prisma is library that provides typesafe and convienent way to interact with our db. + +`graphql` is a peerdepency for apollo, the runtime for graphql. + +`serverless-webpack` is a nice abstraction that does the heavy lifting of bundling. + +We have Babel dependencies. They will be used by Webpack. + +`@risma/cli` is to generate our client, do the migrations and things like that. + +Some Webpack related deps are also added. More on that later as we progress with our app. + +## The handler + +Let's just create a handler file without any Graphql or Prisma. My intention is to create a working sls worklfow and expand upon that. + +```js +exports.handler = async () => { + return { + statusCode: 200, + body: JSON.stringify({ hello: "world" }), + }; +}; +``` + +## Serverless.yml + +This `yml` is the core of our workflow. Let's got through what is happening here. + +```yml +service: prisma + +provider: + name: aws + region: eu-west-1 + stage: dev + memorySize: 256 + runtime: nodejs12.x + role: LambdaRole + +resources: + Resources: + LambdaRole: ${file(./resource/LambdaRole.yml)} + +functions: + graphql: + handler: src/graphql.handler + events: + - http: + path: /graphql + method: post + cors: true + - http: + path: /graphql + method: get + cors: true +``` + +We provide some basic data, like host provider, stage and memory size. The only bit I attached is the `LambdaRole`. It is coming from an external `yml`. + +In the functions section there is only one function: the graphql that has two methods: get and post. + +If everything is set up, issuing sls deploy takes care of everything and soon you can have the code on AWS. + +After you can run curl or visit the browser to see if everything went well. + +## LambdaRole.yml + +```yml +Type: AWS::IAM::Role +Properties: + AssumeRolePolicyDocument: + Statement: + - Effect: Allow + Action: sts:AssumeRole + Principal: + Service: lambda.amazonaws.com + Version: "2012-10-17" + Policies: + - PolicyName: log + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - logs:CreateLogStream + - logs:CreateLogGroup + - logs:PutLogEvents + Resource: arn:aws:logs:*:*:* +``` + +That role provides permission for our Lambda so it can write to the CloudWatch logs. + +## Add webpack + +We use Webpack for two thing. First to embed babel, so we can write ES6+ code. Second, to bundle our app, so we can get rid of (most) node dependencies. + +Let's move to ES6 and refactor our handler. + +```js +export const handler = async () => { + return { + statusCode: 200, + body: JSON.stringify({ hello: "world" }), + }; +}; +``` + +Since AWS can't run this syntax we need to use babel to transpile it. We can create a webpack config (`webpack.config.js`) in our project and put babel in. Among other things. So let's dive into how to integrate Serverless and webpack. + +### Serverless-webpack and webpack config + +We use Serverless and the serverless-webpack plugin. It helps to integrate with webpack. Let's see how. + +```js +const slsw = require("serverless-webpack"); + +module.exports = { + entry: slsw.lib.entries, + target: "node", + mode: "production", + resolve: { + extensions: [".web.js", ".mjs", ".js", ".json", ".ts"], + }, + module: { + rules: [ + { + test: /\.(mjs|js|ts)/, + exclude: /node_modules/, + use: { + loader: "babel-loader", + options: { + presets: [ + "@babel/preset-typescript", + [ + "@babel/preset-env", + { + targets: { + node: true, + }, + useBuiltIns: "usage", + corejs: { + version: 3.6, + proposals: true, + }, + }, + ], + ], + }, + }, + }, + ], + }, +}; +``` + +This is a fairly simple webpack config. The line that can be unfamiliar is the `entry: slsw.lib.entries`. Normally we define the entry point to our bundle manually, like `index.ts`, or with `path.resolve`. One of the advantages of using `serverless-webpack` is that it can keep track of all of our lambda entry points so we don't have to. All we need to do is use `slsw.lib.entries` as the entry point. + +The target, mode and resolve parts are self explanatory, they are part of most webpack config. In module, we define our babel config. It does take care of typescript and with preset, all the modern javascript. + +So how does Serverless knows about this config file? + +### Serverless.yml and webpack + +The `serverless.yml` needs some modifcation to pick up these changes and fully integrate with webpack. Serverless-webpack plugin just does that. Serverless-webpack is also responsible for bundling. So when we will use `sls deploy` serverless won't just upload our file to AWS but runs webpack before that. What needs to be changed in our `yml` file? + +```yml +service: prisma + +provider: + name: aws + region: eu-west-1 + stage: dev + memorySize: 256 + runtime: nodejs12.x + role: LambdaRole + +custom: + webpack: + webpackConfig: "webpack.config.js" + +plugins: + - serverless-webpack + +resources: + Resources: + LambdaRole: ${file(./resource/LambdaRole.yml)} + +functions: + graphql: + handler: src/graphql.handler + events: + - http: + path: /graphql + method: post + cors: true + - http: + path: /graphql + method: get + cors: true +``` + +We specify the serverless-webpack as a plugin. That is how Serverless knows to use Serverless-webpack. We also tell where the config file is. + +After we are set we can deploy with `sls deploy --function graphql`. + +If everything has worked we should still be able to invoke the functions but know webpack took care of the bundle process + +## Apollo + +After the webpack setup is done we can move to Apollo. Apollo is pretty easy to set up. I almost copied all the code it from their docs. Just some basic typedef, basic resolvers and a handler. + +```js +import { ApolloServer, gql } from "apollo-server-lambda"; + +const typeDefs = gql` + type Query { + hello: String + } +`; + +const resolvers = { + Query: { + hello: () => "Hello, World!", + }, +}; + +const server = new ApolloServer({ + typeDefs, + resolvers, + introspection: true, + playground: { + endpoint: "/dev/graphql", + }, + context: ({ event, context }) => ({ + headers: event.headers, + functionName: context.functionName, + event, + context, + }), +}); + +export const handler = server.createHandler(); +``` + +The tricky part is the playground. On a local environment we could reach the playground on `/graphql`. On AWS we have stages in API Gateway. So by default every function is prefixed with the stage. That means our graphql url endpoint changes to `/dev/graphql`. + +It is still working, visiting the endpoint that the Serverless framework creates will load the graphql playground. We can move forward to the last piece of our tech stack. + +## Prisma + +Adding Prisma as a library to communicate to our db will make our setup more complicated. Why? It is because of the nature of Prisma. + +Let's see the following code. + +```ts +import { ApolloServer, gql } from "apollo-server-lambda"; + +import { PrismaClient } from "@prisma/client"; + +const database = new PrismaClient(); + +const typeDefs = gql` + type Query { + hello: String + } +`; + +const resolvers = { + Query: { + hello: async () => { + const result = await database.executeRaw("SELECT * FROM user;"); + + return JSON.stringify(result); + }, + }, +}; +``` + +We import `PrismaClient` from `@prisma/client` and then we can then instantiate a new instance. That instance will have all the methods to interact with the database. The only problem is that `PrismaClient` is generated **on the fly**. It is in `node_modules`, but not fetched from npm. Prisma generates it based on our database schema when we call `prisma generate`. It will write it to `node_modules`. It is a very clever way to customize code whilst hidyng the implementation details. + +It would not be a problem under normal conditions. We run `prisma generate` locally bundle things, and upload. We have the prisma client generated. Problem is, serverless webpack does not use the `node_modules` we have **locally**. It creates **its own** working space. That means we won't have the prisma client when we use webpack. + +Fortunately, there is a way to solve this. + +With serverless webpack we can run scripts before uploading our bundle to AWS. + +```yml +custom: + webpack: + webpackConfig: "webpack.config.js" + packagerOptions: + scripts: + - prisma generate +``` + +With that little bit of modification serverless webpack will create its own generated prisma client in its own place. Before running that command we need a prisma database schema however. + +```js +datasource db { + provider = "postgres" + url = env("DATABASE_URL") +} + +generator client { + provider = "prisma-client-js" + binaryTargets = ["rhel-openssl-1.0.x"] +} + +model User { + id Int @default(autoincrement()) @id +} +``` + +The prisma.shema file is where we can specify settings so prisma knows what client it should generate. + +Here we set up datasource to be a `postgres` instance and ask Prisma to use the `DATABASE_URL` entry in our `.env` file as an address to our db. We will not create a db on AWS so this code will not work at all. This is just not the aim of this article. But we need to provide a valid url in our `.env`, otherwise the `prisma generate` command will fail. + +The next thing is in the binary targets. Under the hood Prisma is using rust binary to communicate with the db. So the typescript layer is just the top layer that will communicate with rust and rust will communicate with the db. + +But different operating systems might need different rust binaries. Because of that we need to specify the type of binary that will work on the linux based VMs that are used to run Lambda functions. That binary is called `rhel-openssl-1.0.x`. + +The last is to have at least one db model. We have a model called User that has only an id. + +We are set, everything should work right? + +## Copy the .env and db shema to the local playground of webpack + +No. The `prisma generate` command will be run in webpack's **own temporary** file structure. But in there there is no `.env` file or `schema.prisma`. Without these files we can't generate our presonalised client. + +Fortunately, webpack has a solution for that. We can copy file to our target directory with the help of a plugin called `CopyWebpackPlugin`. + +```js +plugins: [ + new CopyWebpackPlugin({ + patterns: [ + path.resolve(__dirname, "./prisma/schema.prisma"), + path.resolve(__dirname, "./prisma/.env"), + ], + }), +], +``` + +Adding the plugins section to our `webpack.config.js` and describing the locations of the two files will copy these files. That means the `prisma generate` can run successfully. + +## Modifying the bundle + +There is another issues. Remember webpack bundles our files together. That means it creates one big file with all our source and all our dependencies. But we need to provide `@prisma/client` because that is where our generated client and the rust binary lives. Without uploading the rust binary our Lambda function will fail when it needs to communicate with databases. + +How to solve that problem? We need to modify our `serverless.yml` and our webpack config. Let's see how. + +```yml +custom: + webpack: + webpackConfig: "webpack.config.js" + includeModules: + forceInclude: + - "@prisma/client" + packagerOptions: + scripts: + - prisma generate +``` + +The `includeModules` object in the `yml` can take a `forceInclude` array. There we can specify all the node_modules we ant to **include** in our bundle no matter what. The other half of the picture is in the webpack config. What we **include** in the `yml` file must be **excluded** in the webpack config. + +In the webpack cinfig. + +```js +externals: ["@prisma/client"] +``` + +The `externals` field in the `webpack.config.js` just does that. We tell webpack **not** to include it in the bundle. It will be provided as a node dependency. + +That is it. We have a generated client with the rust binary uploaded to AWS. All written using modern JS or rather TS. Altough there is one more thing where we can do better. + +## optimisign for bundle size + +Our bundle size is quite big. In AWS we have a limit of 250 mb when we upload our function through s3 and a 50mb limit when we do directly. We did not exceed that limit, yet it is generally a good thing to keep our Lambdas as tiny as possible. + +One of the big thing in our bundle is `@prisma/cli`. We don't need that in our Lambda.`@prisma/cli` deals with compile / build time matters whereas our Lambda should only be concerned about runtime dependencies. We can leave that out. The way to do that: + +```yml +custom: + webpack: + webpackConfig: "webpack.config.js" + includeModules: + forceInclude: + - "@prisma/client" + forceExclude: + - "@prisma/cli" + packagerOptions: + scripts: + - prisma generate +``` + +Just as we used forceInclude to make sure something will be uploaded to AWS we can use forceExclude to make sure that something will be left out. + +# sum + +We created a setup with AWS, Serverless, Prisma and Apollo. The most important bit was Prisma. Setting Prisma up, not with just GraphQL, but in any Lambda can be somewhat challenging. But with some good webpack config we can make it realtively easy. \ No newline at end of file diff --git a/src/data/sidebar/guides-links.yaml b/src/data/sidebar/guides-links.yaml index 838f7f4..2259c44 100644 --- a/src/data/sidebar/guides-links.yaml +++ b/src/data/sidebar/guides-links.yaml @@ -11,3 +11,5 @@ link: /guides/cors - title: CORS with AWS Lambda link: /guides/cors-aws-lambda + - title: Create Lambda with Prisma and Apollo + link: /guides/sls-prisma