Skip to content

Commit

Permalink
apollo-server-lambda: outsource Lambda expertise
Browse files Browse the repository at this point in the history
Before this change, apollo-server-lambda contained a lot of code that tried to
understand the formats of Lambda event (request) and result (response)
formats. It tried to work with APIGatewayProxy messages of payloadFormatVersion
1.0 and 2.0 as well as ALB.

But understanding the intricacies of the various Lambda message formats isn't
really what the Apollo Server project is about. A pretty large fraction of all
maintenance on Apollo Server in 2021 has gone into tweaking details of the
Lambda event parsing and making the Lambda handler support all AS features
without the help of a library supporting composable middleware.

This PR (targeted for AS3) throws away the laboriously constructed bespoke
Lambda parsing and faux-middleware implementation and replaces it with two
packages that solve these problems for us: `@vendia/serverless-express` which
understands a variety of Lambda input and output formats and converts them to
Express format, and `express`, the most popular Node library for defining HTTP
server behavior. (Note that `@vendia/serverless-express` is not related to the
`serverless` CLI/framework.)

Now `apollo-server-lambda` is just a convenience wrapper around
`apollo-server-express`. It has to deal with the difference in startup logic
between serverless and non-serverless environments, but it doesn't have to
reimplement all of the Lambda and HTTP logic from scratch.

As an added advantage, you can now optionally provide your own express app to
`apollo-server-lambda`. Previously, `apollo-server-lambda` gave no real way to
customize any of its behavior past the particular options we define, because
Lambda doesn't have a built-in middleware framework. Since we are removing some
built-in features like `graphql-upload` integration in AS3, it's important that
we continue a way to add custom behavior to your Lambda server. Letting you
define that custom behavior with a standard Express app seems reasonable.

We recognize that Lambda users generally care strongly about bundle size, so
adding two new dependencies may seem problematic. That said, we don't currently
have a principled way of evaluating Lambda bundle sizes when we make choices in
this project, and compared to other dependencies of Apollo Server, these new
dependencies are not very large. For now, the improvement in maintainability and
flexibility seems worth the bundle size increase. If `apollo-server-lambda`
users want to help out with a new project of focusing on Lambda bundle size
optimization, we can work together to define benchmarks based on realistic
build/bundler conditions, and find other ways to reduce bundle size (eg, there
is a fair amount of low hanging fruit inside `apollo-reporting-protobuf`).

Fix inconsistency in the content-type returned from health checks (the old
Lambda used `application/json` instead of `application/health+json` like most
other integrations).

This new version passes all of the existing integration tests (plus the
`testApolloServer` suite from
`apollo-server-integration-testsuite/src/ApolloServer.ts` which wasn't being run
previously!) essentially out of the box. (I fleshed out the "mock server"
implementation which converts from Node http requests to Lambda events a bit
more, and changed, but no "core" code or test changes were needed other than
fixing the health check `content-type`.)

Fixes #5078. Fixes #4951 (because that API is just "Express").
  • Loading branch information
glasser committed May 21, 2021
1 parent aee0ec4 commit cef6147
Show file tree
Hide file tree
Showing 11 changed files with 214 additions and 418 deletions.
1 change: 1 addition & 0 deletions docs/source/deployment/lambda.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ The following must be done before following this guide:

---

FIXME see what needs to be improved
## Setting up your project

Setting up a project to work with Lambda isn't that different from a typical NodeJS project.
Expand Down
70 changes: 20 additions & 50 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
"@types/type-is": "1.6.3",
"@types/uuid": "8.3.0",
"@types/ws": "7.4.1",
"@vendia/serverless-express": "4.3.7",
"apollo-link": "1.2.14",
"apollo-link-http": "1.5.17",
"apollo-link-persisted-queries": "0.2.2",
Expand Down
11 changes: 7 additions & 4 deletions packages/apollo-server-core/src/ApolloServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,8 +305,9 @@ export class ApolloServerBase {
// server.start()` before calling it. So we kick off the start
// asynchronously from the constructor, and failures are logged and cause
// later requests to fail (in `ensureStarted`, called by
// `graphQLServerOptions`). There's no way to make "the whole server fail"
// separately from making individual requests fail, but that's not entirely
// `graphQLServerOptions` and sometimes earlier by serverless integrations
// where helpful). There's no way to make "the whole server fail" separately
// from making individual requests fail, but that's not entirely
// unreasonable for a "serverless" model.
if (this.serverlessFramework()) {
this._start().catch((e) => this.logStartupError(e));
Expand Down Expand Up @@ -436,8 +437,10 @@ export class ApolloServerBase {
// verify that) and so the only cases for non-serverless frameworks that this
// should hit are 'started', 'stopping', and 'stopped'. For serverless
// frameworks, this lets the server wait until fully started before serving
// operations.
private async ensureStarted(): Promise<SchemaDerivedData> {
// operations. While it will be called by `graphQLServerOptions`, serverless
// integrations may want to also call it earlier in a request if that is
// helpful.
protected async ensureStarted(): Promise<SchemaDerivedData> {
while (true) {
switch (this.state.phase) {
case 'initialized with gateway':
Expand Down
47 changes: 25 additions & 22 deletions packages/apollo-server-integration-testsuite/src/ApolloServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ export interface StopServerFunc {
export function testApolloServer<AS extends ApolloServerBase>(
createApolloServer: CreateServerFunc<AS>,
stopServer: StopServerFunc,
options: { serverlessFramework?: boolean } = {},
) {
describe('ApolloServer', () => {
afterEach(stopServer);
Expand Down Expand Up @@ -391,31 +392,33 @@ export function testApolloServer<AS extends ApolloServerBase>(
expect(executor).toHaveBeenCalled();
});

it('rejected load promise is thrown by server.start', async () => {
const { gateway, triggers } = makeGatewayMock();
if (!options.serverlessFramework) {
// You don't have to call start on serverless frameworks (or in
// `apollo-server` which does not currently use this test suite).
it('rejected load promise is thrown by server.start', async () => {
const { gateway, triggers } = makeGatewayMock();

const loadError = new Error(
'load error which should be be thrown by start',
);
triggers.rejectLoad(loadError);
const loadError = new Error(
'load error which should be be thrown by start',
);
triggers.rejectLoad(loadError);

expect(
createApolloServer({
gateway,
}),
).rejects.toThrowError(loadError);
});
await expect(
createApolloServer({
gateway,
}),
).rejects.toThrowError(loadError);
});

it('not calling start causes a clear error', async () => {
// Note that this test suite is not used by `apollo-server` or
// serverless frameworks, so this is legit.
expect(
createApolloServer(
{ typeDefs: 'type Query{x: ID}' },
{ suppressStartCall: true },
),
).rejects.toThrow('You must `await server.start()`');
});
it('not calling start causes a clear error', async () => {
await expect(
createApolloServer(
{ typeDefs: 'type Query{x: ID}' },
{ suppressStartCall: true },
),
).rejects.toThrow('You must `await server.start()`');
});
}

it('uses schema over resolvers + typeDefs', async () => {
const typeDefs = gql`
Expand Down
2 changes: 2 additions & 0 deletions packages/apollo-server-lambda/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ This is the AWS Lambda integration of GraphQL Server. Apollo Server is a communi
npm install apollo-server-lambda graphql
```

FIXME see what needs to be improved

## Deploying with AWS Serverless Application Model (SAM)

To deploy the AWS Lambda function we must create a Cloudformation Template and a S3 bucket to store the artifact (zip of source code) and template. We will use the [AWS Command Line Interface](https://aws.amazon.com/cli/).
Expand Down
6 changes: 3 additions & 3 deletions packages/apollo-server-lambda/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@
"node": ">=12.0"
},
"dependencies": {
"@apollographql/graphql-playground-html": "1.6.27",
"@vendia/serverless-express": "^4.3.7",
"@types/aws-lambda": "^8.10.76",
"apollo-server-core": "file:../apollo-server-core",
"apollo-server-env": "file:../apollo-server-env",
"apollo-server-types": "file:../apollo-server-types"
"apollo-server-express": "file:../apollo-server-express",
"express": "^4.17.1"
},
"devDependencies": {
"apollo-server-integration-testsuite": "file:../apollo-server-integration-testsuite"
Expand Down
Loading

0 comments on commit cef6147

Please sign in to comment.