|
|
|
@ -0,0 +1,237 @@
|
|
|
|
|
---
|
|
|
|
|
title: "AWS API Gateway Terraform"
|
|
|
|
|
date: 2022-12-01T15:30:00-03:00
|
|
|
|
|
draft: false
|
|
|
|
|
summary: "How to create API Gateway endpoints with Terraform."
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
Right when we first started to use Terraform, we had a lot of problems with the API Gateway. We had to create a lot of resources, manage 1,000s of lines of `.tf` files, and it was a lot of work that required more attention and time, things that are critical in software development as we all know.
|
|
|
|
|
|
|
|
|
|
So we decided to create a module to help us with this. Big thanks to [Stephano](https://www.linkedin.com/in/stephano-macedo/) who helped me with this!
|
|
|
|
|
|
|
|
|
|
## Before
|
|
|
|
|
Basically, when we are developing a new API, we need to create a lot of resources in the API Gateway. We need to create a new resource, a new method and a new integration, and therefore connecting all of them using their respectives IDs.
|
|
|
|
|
|
|
|
|
|
Let's suppose a endpoint called `/users/all`. This is a snippet of the code we had before:
|
|
|
|
|
|
|
|
|
|
#### Resource
|
|
|
|
|
```terraform
|
|
|
|
|
resource "aws_api_gateway_resource" "api_users_all" {
|
|
|
|
|
rest_api_id = aws_api_gateway_rest_api.api.id
|
|
|
|
|
parent_id = aws_api_gateway_resource.api_users.id
|
|
|
|
|
path_part = "all"
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
#### Method
|
|
|
|
|
```terraform
|
|
|
|
|
resource "aws_api_gateway_method" "api_users_all" {
|
|
|
|
|
rest_api_id = aws_api_gateway_rest_api.api.id
|
|
|
|
|
resource_id = aws_api_gateway_resource.users_all.id
|
|
|
|
|
http_method = "GET"
|
|
|
|
|
|
|
|
|
|
request_parameters = {
|
|
|
|
|
"method.request.header.Authorization" = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
#### Integration
|
|
|
|
|
```terraform
|
|
|
|
|
resource "aws_api_gateway_integration" "api_users_all" {
|
|
|
|
|
rest_api_id = aws_api_gateway_rest_api.api.id
|
|
|
|
|
resource_id = aws_api_gateway_resource.api_users_all.id
|
|
|
|
|
http_method = aws_api_gateway_method.api_users_all.http_method
|
|
|
|
|
type = "HTTP_PROXY"
|
|
|
|
|
integration_http_method = "GET"
|
|
|
|
|
uri = "https://api.example.com/users/all"
|
|
|
|
|
|
|
|
|
|
request_parameters = {
|
|
|
|
|
"integration.request.header.Authorization" = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Obviously there is more code to that, but this is the main part of it and that's what we will be using in the module.
|
|
|
|
|
|
|
|
|
|
## After (Creating a module)
|
|
|
|
|
|
|
|
|
|
Now, we can create a module to help us with this. We can start by summarizing what we had into a separate folder. We will call this folder `terraform/modules/api`, inside it there will be a couple of files:
|
|
|
|
|
|
|
|
|
|
### variables.tf
|
|
|
|
|
Here we will define the variables that we will use in the module, what will come from the outside.
|
|
|
|
|
```terraform
|
|
|
|
|
# This is the parent resource ID in case we have something like /users/all/prune
|
|
|
|
|
variable "parent_id" {
|
|
|
|
|
description = "Resource Parent ID"
|
|
|
|
|
type = string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# This is the last part of the path, we can infer it from the endpoint URI
|
|
|
|
|
variable "path_part" {
|
|
|
|
|
description = "Path Part"
|
|
|
|
|
type = string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Here we will put all the HTTP methods that the endpoint will accept
|
|
|
|
|
variable "http_methods" {
|
|
|
|
|
description = "HTTP Methods"
|
|
|
|
|
type = list(string)
|
|
|
|
|
default = []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# The complete endpoint URI
|
|
|
|
|
variable "uri" {
|
|
|
|
|
description = "URI"
|
|
|
|
|
type = string
|
|
|
|
|
default = ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# The API Gateway ID
|
|
|
|
|
variable "gateway_id" {
|
|
|
|
|
description = "API Gateway ID"
|
|
|
|
|
type = string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# If we have a URI that won't accept any HTTP method, we set this to true
|
|
|
|
|
variable "only_resource" {
|
|
|
|
|
description = "Only create the resource"
|
|
|
|
|
type = bool
|
|
|
|
|
default = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Authorization as an example so that we can pass the headers
|
|
|
|
|
variable "authorization" {
|
|
|
|
|
description = "Required authorization"
|
|
|
|
|
type = bool
|
|
|
|
|
default = false
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### outputs.tf
|
|
|
|
|
This file is needed for at least one important variable, which is the `resource_id`. That's needed if we have some endpoint like `/users/all/prune`.
|
|
|
|
|
```terraform
|
|
|
|
|
output "resource_id" {
|
|
|
|
|
value = local.resource_id
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### locals.tf
|
|
|
|
|
As we referenced the `resource_id` in the `outputs.tf`, we need to define it in the `locals.tf`.
|
|
|
|
|
```terraform
|
|
|
|
|
locals {
|
|
|
|
|
// this join is because we can't do aws_api_gateway_resource.api_resource.id
|
|
|
|
|
resource_id = join("", aws_api_gateway_resource.api_resource[*].id)
|
|
|
|
|
|
|
|
|
|
// if starts with '{' and ends with '}' then it's a path parameter
|
|
|
|
|
// take all the middle characters
|
|
|
|
|
// if it's empty then it's a normal path
|
|
|
|
|
path_variable = length(regexall("{.*}", var.path_part)) > 0 ? substr(var.path_part, 1, length(var.path_part) - 2) : ""
|
|
|
|
|
|
|
|
|
|
// in case we need Authorization
|
|
|
|
|
integration_request_parameters = var.authorization ? {
|
|
|
|
|
"integration.request.header.Authorization" = "method.request.header.Authorization"
|
|
|
|
|
} : {}
|
|
|
|
|
|
|
|
|
|
method_request_parameters = {
|
|
|
|
|
"method.request.header.Authorization" = var.authorization
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### gateway.resources.tf
|
|
|
|
|
Here is where the fun begins, hehe. Thank God it's pretty straightforward. All of the variables are coming from the `variables.tf` file.
|
|
|
|
|
```terraform
|
|
|
|
|
resource "aws_api_gateway_resource" "api_resource" {
|
|
|
|
|
rest_api_id = var.gateway_id
|
|
|
|
|
parent_id = var.parent_id
|
|
|
|
|
path_part = var.path_part
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### gateway.methods.tf
|
|
|
|
|
Since we need one `aws_api_gateway_method` for each HTTP Method, we use the `count` to iterate over the list of HTTP Methods and create one api_gateway_method for each http method we defined in the `var.http_methods` list.
|
|
|
|
|
```terraform
|
|
|
|
|
resource "aws_api_gateway_method" "api_method" {
|
|
|
|
|
count = var.only_resource ? 0 : length(var.http_methods)
|
|
|
|
|
rest_api_id = var.gateway_id
|
|
|
|
|
resource_id = local.resource_id
|
|
|
|
|
|
|
|
|
|
http_method = var.http_methods[count.index]
|
|
|
|
|
authorization = var.authorization ? "CUSTOM" : "NONE"
|
|
|
|
|
|
|
|
|
|
// Got a path variable? No problem! We deal with that too right here
|
|
|
|
|
request_parameters = merge(local.method_request_parameters, local.path_variable != "" ? {
|
|
|
|
|
"method.request.path.${local.path_variable}" = local.path_variable != ""
|
|
|
|
|
} : {})
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### gateway.integrations.tf
|
|
|
|
|
```terraform
|
|
|
|
|
resource "aws_api_gateway_integration" "api_integration" {
|
|
|
|
|
count = var.only_resource ? 0 : length(var.http_methods)
|
|
|
|
|
rest_api_id = var.gateway_id
|
|
|
|
|
resource_id = local.resource_id
|
|
|
|
|
http_method = aws_api_gateway_method.api_method[count.index].http_method
|
|
|
|
|
|
|
|
|
|
integration_http_method = var.http_methods[count.index]
|
|
|
|
|
uri = var.uri
|
|
|
|
|
|
|
|
|
|
// Aahh I see your path variable, let's do some magic here
|
|
|
|
|
request_parameters = merge(local.integration_request_parameters, local.path_variable != "" ? {
|
|
|
|
|
"integration.request.path.${local.path_variable}" = "method.request.path.${local.path_variable}"
|
|
|
|
|
} : {})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Using the module
|
|
|
|
|
|
|
|
|
|
Now that we have the module, we can use it in our `main.tf` file. We will use the same example as before, but now we will use the module and we will create some other endpoints as example as well.
|
|
|
|
|
```terraform
|
|
|
|
|
# this is our main API endpoint, we don't want to receive any request here, so we will only create the resource
|
|
|
|
|
# /users (only resource)
|
|
|
|
|
module "api_users" {
|
|
|
|
|
source = "./api"
|
|
|
|
|
|
|
|
|
|
gateway_id = gateway.outputs.gateway.gateway_config.gateway_id
|
|
|
|
|
parent_id = gateway.outputs.gateway.gateway_config.root_endpoints.api_root
|
|
|
|
|
path_part = "users"
|
|
|
|
|
only_resource = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# /users/all (get)
|
|
|
|
|
module "api_users_all" {
|
|
|
|
|
source = "./api"
|
|
|
|
|
|
|
|
|
|
gateway_id = gateway.outputs.gateway.gateway_config.gateway_id
|
|
|
|
|
parent_id = module.api_users.resource_id
|
|
|
|
|
path_part = "all"
|
|
|
|
|
http_methods = ["GET"]
|
|
|
|
|
uri = "http://api.example.com/users/all"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# /users/all/{userid} (get, post, put, delete)
|
|
|
|
|
module "api_users_all" {
|
|
|
|
|
source = "./api"
|
|
|
|
|
|
|
|
|
|
gateway_id = gateway.outputs.gateway.gateway_config.gateway_id
|
|
|
|
|
parent_id = module.api_users_all.resource_id
|
|
|
|
|
path_part = "{userid}"
|
|
|
|
|
http_methods = ["GET", "POST", "PUT", "DELETE"]
|
|
|
|
|
uri = "http://api.example.com/users/all/{userid}"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# and so on...
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Conclusion
|
|
|
|
|
For one endpoint, we went from having to manage 15 lines splitted in 3 files to just 5 lines inside of one file. If you have to manage hundreds of endpoints, that will be a great help.
|
|
|
|
|
|
|
|
|
|
## WWW-Authenticate header
|
|
|
|
|
We can also add the `WWW-Authenticate` header to the request for example. We tried to do that by adding it to the files properly, but it didn't work. The reason was that the API Gateway was not passing the `WWW-Authenticate` to our API, and that's because of the name of the header. You may call it `WWW-Authenticate-Header` for example and it will work.
|
|
|
|
|
|
|
|
|
|
## Disclaimer
|
|
|
|
|
This code has not been tested "as is", though it has been tested as part of a bigger project. There is always room for improvements and more possibilities depending on the context, but it's a good start.
|