Share Code Between Services
In these next couple of chapters we’ll look at how to organize all our business logic services (APIs) in the same repo. We’ll start by attempting to answer the following questions:
- Do I have just one or multiple
package.jsonfiles?
- How do I share common code and config between services?
- How do I share common config between the various
serverless.yml?
We are using an extended version of the notes app for this section. You can find the sample repo here. Let’s take a quick look at how the repo is organized.
/ package.json config.js serverless.common.yml libs/ services/ notes-api/ package.json serverless.yml handler.js billing-api/ package.json serverless.yml handler.js notify-job/ serverless.yml handler.js
1. Structuring the package.json
The first question you typically have is about the
package.json. Do I just have one
package.json or do I have one for each service? We recommend having multiple
package.json files.
We use the
package.json at the project root to install the dependencies that will be shared across all the services. For example, the serverless-bundle plugin that we are using to optimally package our Lambda functions is installed at the root level. It doesn’t make sense to install it in each and every service.
On the other hand, dependencies that are specific to a single service are installed in the
package.json for that service. In our example, the
billing-api service uses the
stripe NPM package. So it’s added just to that
package.json. Similarly, the
notes-api service uses the
uuid NPM package, and it’s added just to that
package.json.
This setup implies that when you are deploying your app through a CI; you’ll need to do an
npm install twice. Once in the root level and once in a specific service. Seed does this automatically for you.
You can also use Yarn Workspaces (and Lerna) to manage the dependencies for your monorepo setup. We cover this setup in a separate chapter — Using Lerna and Yarn Workspaces with Serverless.
Usually, you might have to manually pick and choose the modules that need to be packaged with your Lambda function. Simply packaging all the dependencies will increase the code size of your Lambda function and this leads to longer cold start times. However, in our example we are using the
serverless-bundle plugin that internally uses Webpack’s tree shaking algorithm to only package the code that our Lambda function needs.
2. Sharing common code and config
The biggest reason you are using a monorepo setup is because your services need to share some common code, and this is the most convenient way to do so.
Alternatively, you could use a multi-repo approach where all your common code is published as private NPM packages. However, this adds an extra layer of complexity and it doesn’t make sense if you are a small team just wanting to share some common code.
In our example, we want to share some common code. We’ll be placing these in a
libs/ directory. Our services need to make calls to various AWS services using the AWS SDK. And we have the common SDK configuration code in the
libs/aws-sdk.js file.
import aws from "aws-sdk"; import xray from "aws-xray-sdk"; // Do not enable tracing for 'invoke local' const awsWrapped = process.env.IS_LOCAL ? aws : xray.captureAWS(aws); export default awsWrapped;
Our Lambda functions will now import this instead of the standard AWS SDK.
import AWS from '../../libs/aws-sdk';
The great thing about this is that we can easily change any AWS related config and it’ll apply across all of our services. In this case, we are using AWS X-Ray to enable tracing across our entire application. You don’t need to do this but we are going to be talking about this in one of the later chapters. And this is a good example of how to share the same AWS config across all our services.
3. Share common serverless.yml config
We have separate
serverless.yml configs for our services. However, we end up needing to share some config across all of our
serverless.yml files. To do that:
- Place the shared config values in a common yaml file at the root level.
- And reference them in your individual
serverless.ymlfiles.
For example, we want to define the current stage and the resources stage we want to connect to across all of our services. custom definition in their
serverless.yml:
custom: ${file(../../serverless.common.yml):custom}
And we include the lambdaPolicyXRay IAM policy:
iamRoleStatements: - ${file(../../serverless.common.yml):lambdaPolicyXRay}
You can do something similar for any other
serverless.yml config that needs to be shared.
For simplifying our
serverless.yml config within a service, we split it up further. In our
services/notes-api/serverless.yml in our sample repo you’ll notice the following:
resources: # API Gateway Errors - ${file(resources/api-gateway-errors.yml)} # Cognito Identity Pool Policy - ${file(resources/cognito-policy.yml)}
The
api-gateway-errors.yml adds the headers for 4xx and 5xx API errors. While the
cognito-policy.yml adds the IAM policy for allowing our Cognito authenticated users to access the Notes API.
Statement: - Effect: 'Allow' Action: - 'execute-api:Invoke' Resource: !Sub 'arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ApiGatewayRestApi}/*'
Next, let’s look at what happens when multiple API services need to share the same API endpoint.
For help and discussionComments on this chapter