API Gateway Domains Across Services
So to summarize so far, we are looking at how to create a Serverless application with multiple services and to link them together using cross-stack references. We’ve created a separate services for DynamoDB and our S3 file uploads bucket.
In this chapter we will look at how to work with API Gateway across multiple services. A challenge that you run into when splitting your APIs into multiple services, is sharing the same domain for them. You might recall that APIs that are created as a part of the Serverless service get their own unique URL that looks something like:
https://z6pv80ao4l.execute-api.us-east-1.amazonaws.com/dev
When you attach a custom domain for your API, it is attached to a specific endpoint like the one above. This means that if you create multiple API services, they will all have unique endpoints.
You can assign different base paths for your custom domains. For example, api.example.com/notes
can point to one service while api.example.com/users
can point to another. But if you try to split your notes
service up, you’ll face the challenge of sharing the custom domain across them.
In this chapter we will look at how to share the API Gateway project across multiple services. For this we will create two separate Serverless services for our APIs. The first is the notes service. This is the same one we’ve used in our note taking app so far. But for this chapter we will simplify the number of endpoints to focus on the cross-stack aspects of it. For the second service, we’ll create a simple users service. This service isn’t a part of our note taking app. We just need it to demonstrate the concepts in this chapter.
Multiple API Services
We are going to be creating a notes and a users service using the following setup.
-
The notes service is going to be our main API service and the users service is going to link to it. This means that the users service will refer to the notes service.
-
The notes service will be under
/notes
dir and the users service will be under the/users
dir.
Notes Service
First let’s look at the notes service. We need to connect it to the DynamoDB service that we previously created. In the example repo, you’ll notice that we have a notes
service in the services/
directory with a serverless.yml
.
service: notes-app-mono-notes
custom:
# Our stage is based on what is passed in when running serverless
# commands. Or fallsback to what we have set in the provider section.
stage: ${opt:stage, self:provider.stage}
provider:
name: aws
runtime: nodejs8.10
stage: dev
region: us-east-1
# These environment variables are made available to our functions
# under process.env.
environment:
tableName:
'Fn::ImportValue': ${self:custom.stage}-NotesTable
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:DescribeTable
- dynamodb:Query
- dynamodb:Scan
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
# Restrict our IAM role permissions to
# the specific table for the stage
Resource:
- 'Fn::ImportValue': ${self:custom.stage}-NotesTableArn
functions:
# Defines an HTTP API endpoint that calls the main function in create.js
# - path: url path is /notes
# - method: POST request
# - cors: enabled CORS (Cross-Origin Resource Sharing) for browser cross
# domain api call
# - authorizer: authenticate using the AWS IAM role
get:
# Defines an HTTP API endpoint that calls the main function in get.js
# - path: url path is /notes/{id}
# - method: GET request
handler: handler.main
events:
- http:
path: notes
method: get
cors: true
authorizer: aws_iam
resources:
Outputs:
ApiGatewayRestApiId:
Value:
Ref: ApiGatewayRestApi
Export:
Name: ${self:custom.stage}-ApiGatewayRestApiId
ApiGatewayRestApiRootResourceId:
Value:
Fn::GetAtt:
- ApiGatewayRestApi
- RootResourceId
Export:
Name: ${self:custom.stage}-ApiGatewayRestApiRootResourceId
Let’s go over some of the details of this service.
-
The Lambda functions in our service need to know which DynamoDB table to connect to. To do this we create an environment variable called
tableName
with the value'Fn::ImportValue': ${self:custom.stage}-NotesTable
. This is the first time we are using the import portion of our cross-stack reference. Back in the chapter where we created the DynamoDB service, we exported${self:custom.stage}-NotesTable
. The value for this cross-stack reference is the name of the table. And we are importing it here using theFn::ImportValue
CloudFormation method. So in our Lambda function,process.env.tableName
should be the generated name of our notes table. -
Next, we need to give our Lambda function permission to talk to this table by adding an IAM policy. The IAM policy needs the ARN of the table. We had exported this value in our DynamoDB service as well. And just as above, we can refer to it by
'Fn::ImportValue': ${self:custom.stage}-NotesTableArn
. -
We are going to export a couple values in this service to be able to share this API Gateway resource in our users service.
-
The first cross-stack reference that needs to be shared is the API Gateway Id that is created as a part of this service. We are going to export it with the name
${self:custom.stage}-ApiGatewayRestApiId
. Again, we want the exports to work across all our environments/stages and so we include the stage name as a part of it. The value of this export is available as a reference in our current stack calledApiGatewayRestApi
. -
Finally, we also need to export the
RootResourceId
. This is a reference to the/
path of this API Gateway project. To this Id we use theFn::GetAtt
CloudFormation function and pass in the currentApiGatewayRestApi
and look up the attributeRootResourceId
. We export this using the name${self:custom.stage}-ApiGatewayRestApiRootResourceId
.
Users Service
In the example repo, open the users
service in the services/
directory.
service: notes-app-mono-users
custom:
# Our stage is based on what is passed in when running serverless
# commands. Or fallsback to what we have set in the provider section.
stage: ${opt:stage, self:provider.stage}
provider:
name: aws
runtime: nodejs8.10
stage: dev
region: us-east-1
apiGateway:
restApiId:
'Fn::ImportValue': ${self:custom.stage}-ApiGatewayRestApiId
restApiRootResourceId:
'Fn::ImportValue': ${self:custom.stage}-ApiGatewayRestApiRootResourceId
# These environment variables are made available to our functions
# under process.env.
environment:
tableName:
'Fn::ImportValue': ${self:custom.stage}-NotesTable
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:DescribeTable
- dynamodb:Query
- dynamodb:Scan
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
# Restrict our IAM role permissions to
# the specific table for the stage
Resource:
- 'Fn::ImportValue': ${self:custom.stage}-NotesTableArn
functions:
# Defines an HTTP API endpoint that calls the main function in create.js
# - path: url path is /users
# - method: POST request
# - cors: enabled CORS (Cross-Origin Resource Sharing) for browser cross
# domain api call
# - authorizer: authenticate using the AWS IAM role
get:
# Defines an HTTP API endpoint that calls the main function in get.js
# - path: url path is /users/{id}
# - method: GET request
handler: handler.main
events:
- http:
path: users
method: get
cors: true
authorizer: aws_iam
Let’s go over this quickly.
-
Just as the notes service we are referencing our DynamoDB table using
'Fn::ImportValue': ${self:custom.stage}-NotesTable
and'Fn::ImportValue': ${self:custom.stage}-NotesTableArn
. -
To share the same API Gateway domain as our notes service, we are adding a
apiGateway:
section to theprovider:
block.-
Here we state that we want to use the
restApiId
of our notes service. We do this by using the cross-stack reference'Fn::ImportValue': ${self:custom.stage}-ApiGatewayRestApiId
that we had exported above. -
We also state that we want all the APIs in our service to be linked under the root path of our notes service. We do this by setting the
restApiRootResourceId
to the cross-stack reference'Fn::ImportValue': ${self:custom.stage}-ApiGatewayRestApiRootResourceId
from above.
-
-
Finally, we don’t need to export anything in this service since we aren’t creating any new resources that need to be referenced.
The key thing to note in this setup is that API Gateway needs to know where to attach the routes that are created in this service. We want the /users
path to be attached to the root of our API Gateway project. Hence the restApiRootResourceId
points to the root resource of our notes service. Of course we don’t have to do it this way. We can organize our service such that the /users
path is created in our main API service and we link to it here.
Next let’s tie our entire stack together and secure it using Cognito User Pool and Identity Pool.
If you liked this post, please subscribe to our newsletter, give us a star on GitHub, and follow us on Twitter.
For help and discussion
Comments on this chapterFor reference, here is the code so far
Mono-repo Backend Source