HTTP Direct with Datomic & Terraform

23 December 2019 · 3 minute read

As a follow up to my “Datomic with Terraform” post and talk, I’d like to discuss how to use Datomic’s HTTP Direct feature with API Gateway. While the Cognitect tutorial can be helpful initially (I mean, hey, that’s where I started), there are a few things that weren’t explained as clearly as I would have liked. And, because the tutorial relies entirely on the AWS Console, I had to reverse-engineer some of the Console magic in order to figure out how to wire everything up using Terraform.

You can view the example code on GitHub. I’ve structured the project like my datomic-terraform-example, so if you’ve had a look at it, this should be familiar.

Unlike when using Lambda proxies, this is more like writing a traditional web backend. You’ll need to handle routing in your ion (rather than leaving it to API Gateway). This does mean you will lose some of the advantages of API Gateway, however, the improvement in latency is almost always worth it. I chose bidi for routing because I like that it just uses data for syntax.

(def ^:private routes
  {:get ["/"
         [["resource" resource/handler]
          [true not-found/handler]]]})

(defn handler
  [req]
  (let [{:keys [request-method uri]} req
        route (bidi/match-route (get routes request-method)
                                (cs/replace uri #"/datomic" ""))
        handle (:handler route)]
    (handle req)))

Note that I’ve worked up my own quick-and-dirty handling for routing based off request method—YMMV. Note also that I’m stripping out the /datomic prefix from the incoming URI. The /datomic prefix seems to be required on all incoming proxy requests, though the Cognitect documentation does not specifically document that requirement. There may be a way to bake that into the API Gateway integration, but I haven’t explored that possibility just yet.

(defn response
  [status body]
  (let [allow-origin (:allow-origin (ion/get-env))]
    {:status  status
     :headers {"content-type"                     "application/transit+json"
               "access-control-allow-origin"      allow-origin
               "access-control-allow-credentials" "true"}
     :body    body}))

Because apigw/ionize sorts out a lot, one thing that caught me off guard when transitioning from the Lambda proxy is that Datomic is rather specific with its response spec. Header keys need to be strings. I’m defaulting to lower-case because it seems most implementations run that way (the standard being case-insensitive).

Creating a VPC Link with Terraform is trivial:

resource "aws_api_gateway_vpc_link" "query_group" {
  name        = local.stack_name
  target_arns = [aws_cloudformation_stack.query_group.outputs["LoadBalancer"]]
}

It took me a bit of experimentation to figure out how to properly proxy the path to the Datomic ion (thanks mostly to my lack of understanding of how API Gateway resource configuration works). With the API method, you have to first enable the proxy path to pass through to the integration:

resource "aws_api_gateway_method" "proxy" {
  authorization = "NONE"
  http_method   = "ANY"

  request_parameters = {
    "method.request.path.proxy" = true
  }

  resource_id = aws_api_gateway_resource.proxy.id
  rest_api_id = aws_api_gateway_rest_api.site.id
}

Then you have to map that path to something the integration can access:

resource "aws_api_gateway_integration" "proxy" {
  connection_id           = module.query_group.vpc_link_id
  connection_type         = "VPC_LINK"
  http_method             = "ANY"
  integration_http_method = "ANY"

  request_parameters = {
    "integration.request.path.proxy" = "method.request.path.proxy"
  }

  resource_id = aws_api_gateway_resource.proxy.id
  rest_api_id = aws_api_gateway_rest_api.site.id
  type        = "HTTP_PROXY"
  uri         = "${module.query_group.load_balancer_http_direct_endpoint}/{proxy}"
}

That’s pretty much it. The only other minor thing I would call attention to is that per the Datomic ion tutorial, you need to set a binary media type on API Gateway for Datomic integration (no matter whether you’re using the Lambda proxy or HTTP Direct):

resource "aws_api_gateway_rest_api" "site" {
  name               = "Site API${var.suffix == "" ? "" : " ${var.suffix}"}"
  binary_media_types = ["*/*"]
}

In any case, I hope this will be helpful to future adventurers.


If you find my work interesting, sign up for the Statics & Dynamics mailing list so you don’t miss a thing.