AWS API Gateway with Terraform
Or, how a backend developer coded some infrastructure with zero DevOps knowledge
Ever wanted to create something in AWS? Feel over amazed with the AWS interface? Do you cry a little bit inside every time you look at the AWS dashboard? Then stay tuned for something that can ease your life, more than sliced bread did 🍞.
In recent times I had the opportunity of diving (and might I had, it was head first in some shallow waters) into the world of Terraform. The objective was, in a very first step, to create a proxy gateway in front of our API. At first this will simply be a “passthrough” proxy were everything that comes into to the API Gateway will be passed to our API and sent back.
I can only imagine your face right now… “But that’s the same as having nothing at all..”
You’re correct but let me continue by saying that this will just be the first step to something greater.
Most of the projects start as a monolith and then, in time, comes the part where we start splitting it in the “microservices” we all love. With this change in architecture there will be a part of your code that will start to look like a router, where it receives some request from the outside world and proceeds to redirect it to whoever is responsible for answering it. The AWS API Gateway is here so, we can lift those concerns from your service. We can even add more cross-cutting concerns to it (like authentication or rate limiting). Having this approach will even help with the latency of your response as it will skip the step of interpreting code to know were it should go, because it’s doing it directly in the AWS API Gateway.
Enough chit-chat and let’s get some coding done, starting with the creation of the api
entry:
resource “aws_api_gateway_rest_api” “api” {
name = “api-gateway”
description = “Proxy to handle requests to our API”
}
In here we are creating the REST API resource to where all the requests are going to hit. Next we will start to configure what we want this API to do:
resource "aws_api_gateway_resource" "resource" {
rest_api_id = "${aws_api_gateway_rest_api.api.id}"
parent_id = "${aws_api_gateway_rest_api.api.root_resource_id}"
path_part = "{proxy+}"
}resource "aws_api_gateway_method" "method" {
rest_api_id = "${aws_api_gateway_rest_api.api.id}"
resource_id = "${aws_api_gateway_resource.resource.id}"
http_method = "ANY"
authorization = "NONE" request_parameters = {
"method.request.path.proxy" = true
}
}resource "aws_api_gateway_integration" "integration" {
rest_api_id = "${aws_api_gateway_rest_api.api.id}"
resource_id = "${aws_api_gateway_resource.resource.id}"
http_method = "${aws_api_gateway_method.method.http_method}" integration_http_method = "ANY"
type = "HTTP_PROXY"
uri = "http://your.domain.com/{proxy}"
request_parameters = {
"integration.request.path.proxy" = "method.request.path.proxy"
}}
Let’s do this in small steps so it’s clearer what is happening here.
Resource
This is where we’ll configure on what endpoint are we listening for requests. The path_part
argument will contain a string that represents the endpoint path, as our case is a simple proxy, AWS provides a special handler to listen all the requests, the "{proxy+}"
. This handler can also be applied to a more specific path, i.e "users/{proxy+}"
where it will listen to anything starting with users
( i.e users/1/posts
, users/3/notes
, etc). The other values presented in there are related to where will this resource be applied, the rest_api_id
will have the id
of what API we are mounting this resource and the parent_id
has the id
of the parent on where are mounting this. This last one can be mounted directly on the root api (as we have) or mounted in another resource making it a child resource that will be composed by both parent and child. So if you have the path_part
defined as "active"
in the child resource and it’s mounted in the users
resource, the child resource will listen to a users/active
endpoint.
Method
In the method
resource is were we build the specification of the endpoint we are listening. The http_method
argument will have the string with what HTTP method we’re interested, the "ANY"
value is, again, a special handler where we’ll accept any HTTP method that comes our way. In the case we have in hands we won’t need any authorization done in our AWS API Gateway, and that’s why the value in authorization
is "NONE"
. The request_parameters
argument, will state that we will have something in a proxy
handler required for this method and that will passed to the integration resource (described next).This resource also has some arguments related to where it should be mounted, namely the rest_api_id
that is the same as the one described in the Resource resource and the resource_id
that represents to what resource it should be related to.
Integration
The integration
resource is related to how are we going to react to the request that we just received, it could go from passing the request to a backend, run some lambda function or even doing nothing with it. Besides the arguments previously referenced ( rest_api_id
and resource_id
), we have the http_method
argument that will be the same as the method
resource (that’s why we link both of them with the "${aws_api_gateway_method.method.http_method}"
), the integration_http_method
argument represents the HTTP method that will be done from the integration to our backend (again the "ANY"
value is a special one) and we have the type
argument where we configure what type of integration this is. As for the uri
argument it will contain the endpoint to where we are proxying to, and the request_paramenters
argument will map what we need from the method
resource to our request to the backend, in our case we are are replacing the {proxy}
handler present in the uri
argument with the path that comes after the domain of our API Gateway.
The way that all of this can be grouped together is that a method
and a integration
are wrapped around a resource
, the method
will be responsible on how can the API Gateway be reached and the integration
will be responsible in interacting with the backend. Kinda like this:
So with all this we’ll be able to apply this Terraform file and (hopefully) have our first AWS API Gateway all working!! 🎉 If we go to https://api-gateway.execute-api.{region}.amazonaws.com
(the {region}
is the region where you’ll deploy the API, i.e eu-west-1
) we’ll be communicating with our API Gateway. But there’s something still bugging you, right? Do we really want to tell the our costumers to make requests to that URI? They’ll probably think it’s a scam..
Rest at ease because AWS have it covered, and so does Terraform. To accomplish that we need to add two resources to our file:
resource "aws_api_gateway_domain_name" "domain" { domain_name = "totallynotascam.com" certificate_name = "example-api" certificate_body = "${file("${path.module}/example.com/example.crt")}" certificate_chain = "${file("${path.module}/example.com/ca.crt")}" certificate_private_key = "${file("${path.module}/example.com/example.key")}"
}resource "aws_api_gateway_base_path_mapping" "base_path_mapping" {
api_id = "${aws_api_gateway_rest_api.api.id}"
domain_name = "${aws_api_gateway_domain_name.domain.domain_name}"
}
Domain
Here is where we’ll create the configuration to the domain that we have. The domain_name
attribute will have the string with the name that we want for our domain, the other attributes are all optional and just an example on how you can have the certificates to that domain. As stated in the Terraform documentation about this resource this is not the way of doing it, because it will store that information in clear text.
Base_path_mapping
There isn’t much to say about this resource, it’s purpose it’s only to map the domain
configuration to our API. So the api_id
will have the in for our API Gateway and the domain_name
links to the domain
domain name that we had chosen previously.
Having this all in place we’ll be able to reach https://totallynotascam.com
and send all the money that the Nigerian prince is always asking 😄.
As usual if you find this useful smash that 👏 button, and if you have anything to say just leave a comment or drop it like it’s hot @RicBrazao!