Sunday, February 14, 2016

API Gateway as an HTTP Service Proxy: Lessons Learned

At work, we’re finishing the implementation of our first API using the Amazon API Gateway for decoupling the API key management, logging, and throttling from the actual backend service. This also marks our first OAuth 2.0 Resource Server, and makes heavier use of Swagger 2.0 for the entire pipeline.  With all these “firsts,” I’d like to share a few notes on our setup.

First off, Swagger.  Because Amazon offers the Swagger importer, we decided that our Swagger specification should be the definitive resource for our API.  Anything we want to do, must be representable in Swagger, and we must be able to import the resulting file into API Gateway and have no manual configuration afterward.

It’s important to note that Swagger is allowed to be specified in YAML, but basically everything directly works with JSON under the hood.  If you’re using PHP to implement the API, JSON decoding is built into PHP, and YAML isn’t. That really ends up being the nail in the coffin for YAML: we can load a JSON spec with a line of code and no dependencies, and nothing else has to waste time translating the format every time we want to work with it.

We actually started out with YAML and I converted it to JSON later.  Then I had to fix a number of type issues, turning numbers back to numbers and so forth.  YAML seems to default everything to strings.

The Swagger importer for API Gateway is written in Java using Maven for the build.  No problems here, just some yak shaving to get Java and disable the browser plugin.  The documentation was surprisingly accurate.

The only catch is that, whenever the importer has made changes to the API, they are not enabled on executions until the API is deployed again.  As I’ve thrashed around with getting things working on my first time, this has led to a lot of pointless and broken deployments that can’t be deleted.  It offends my sense of aesthetics, but I totally understand where API Gateway is coming from.  If the new version is broken and the old one wasn’t, it wants to be able to let me go back.  In general, though, it seems like it would be improved by a “this deployment is broken” flag that would hide it by default, or let broken deployments self-delete after a few weeks.

When using the importer, it turns out that everything must be specified in the imported Swagger. I got stuck for quite some time trying to resolve an issue with an “invalid integration;” it turned out, since I didn’t specify that the OAuth token should be a parameter in the Authorization header, API Gateway was not making the Authorization header available to be copied between the requests.  (This resulted in an “invalid integration method.request.header.Authorization” error, even though that is the exact syntax required in the end.)

Essentially, our Swagger ended up with a lot of redundancy, because API Gateway wants to be a flexible mapping between an arbitrary API supposedly designed within API Gateway, and any other HTTP(S) API service that may have no relation to the API Gateway structure. When simply wishing to mirror an API in the Gateway, it ends up with a lot of tedious “connect the red wire to the red wire” style instructions.

What I’m calling ‘instructions’ are the extensions that API Gateway adds to the Swagger schema.  Once the exposed parts of the API are put into the “public” section of the Swagger file, they can be referenced from the “private” section—the API Gateway extensions—to be connected to the backend.

I haven’t quite figured out how the return code mapping works, yet.  Errors from my service implementation typically result in API Gateway converting to status 500 on the way through.  (Although it does deliver the Error object untouched, it’s going to be a problem with my clients, if the HTTP 409 Conflict generated in response to “create <some item that exists>” requests gets modified.) I think this is just another problem with not defining enough in the Swagger responses.  Once again, API Gateway only maps things that are defined on the Swagger level.  (Updated: that was the problem.  It has to be defined as a response code in Swagger for API Gateway to return that response.)

A major benefit to working from “Swagger as authoritative” has been simplifying input validation code. Instead of tediously duplicating the conditions from the schema, I got justinrainbow’s validator to check inputs directly from that schema. It’s a pretty straightforward process: load the schema, expand the $ref keys, and then validate the incoming object against its definition.  For instance, $validator->check($user, $schema->definitions->user); in a request dealing with a user model.

On the other hand, this limits validation to being done by what Swagger can express.  While it borrows a lot from the JSON Schema specification, it is also a subset of JSON Schema. Notably, there is no support for oneOf to properly express, “you must put your Bearer token either in the Authorization header or in the query string.”

Finally, this is our first API Gateway project, because some legacy APIs failed to get off the ground.  They took input as “form data,” that is, application/x-www-form-urlencoded type, which API Gateway does not natively support.  We tried someone’s mapping code found on the Internet to convert the format, but that turned into one big JSON injection issue, as the mapping code didn’t escape things properly.  (I wasn’t on this exploratory project, so I’m a little fuzzy on the detail there.  I just know our current JSON-all-the-way API will be our first to make it to production on API Gateway.)

Let’s wrap up with an Ars Technica style summary…

The Good:

  • The API importer works well.
  • The gateway has a lot of flexibility to change request and response formats.
  • Establishing the Swagger specification as authoritative makes input validation effortless and always up-to-date.

The Bad:

  • Having a flexible mapping requires everything to be mapped.
  • YAML as an option for Swagger is practically trolling API developers.
  • Automatic validation is limited to what Swagger can express, which is a subset of JSON Schema.

The Ugly:

  • Importer error messages are cryptic at best, and sometimes misleading as well.
  • API Gateway doesn’t support “form data” natively.  (I didn’t mention it above; it’s probably a separate topic.) It’s best used for new APIs that both produce and consume JSON, although the documentation hints that XML is also an option. Update: It can pass through, but not process nor validate, this data. It’s adequate.

No comments: