From a0920e2e59dc66886cbfbfc8aa3dce000f8c4c7c Mon Sep 17 00:00:00 2001 From: Benjamin Clark Date: Tue, 17 Sep 2024 19:35:31 +0100 Subject: [PATCH] Initial module setup --- .terraform-version | 1 + README.md | 64 +++++++++++++++++++++++- api_gateway_api_key.tf | 17 +++++++ api_gateway_deployment.tf | 16 ++++++ api_gateway_stage.tf | 8 +++ api_gateway_usage_plan.tf | 27 ++++++++++ api_gateway_usage_plan_key.tf | 12 +++++ lambda_permission.tf | 42 ++++++++++++++++ main.tf | 9 ++++ rest_api.tf | 21 ++++++++ variables.tf | 92 +++++++++++++++++++++++++++++++++++ 11 files changed, 308 insertions(+), 1 deletion(-) create mode 100644 .terraform-version create mode 100644 api_gateway_api_key.tf create mode 100644 api_gateway_deployment.tf create mode 100644 api_gateway_stage.tf create mode 100644 api_gateway_usage_plan.tf create mode 100644 api_gateway_usage_plan_key.tf create mode 100644 lambda_permission.tf create mode 100644 main.tf create mode 100644 rest_api.tf create mode 100644 variables.tf diff --git a/.terraform-version b/.terraform-version new file mode 100644 index 0000000..8e03717 --- /dev/null +++ b/.terraform-version @@ -0,0 +1 @@ +1.5.1 \ No newline at end of file diff --git a/README.md b/README.md index c87a030..5162392 100644 --- a/README.md +++ b/README.md @@ -42,11 +42,73 @@ The below documentation is intended to assist users in utilising the module, the the module itself, and the [examples](#examples) section which has examples of how to utilise the module. +## Requirements +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | ~> 1.5.0 | +| [aws](#requirement\_aws) | >= 5.61.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | 5.67.0 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [aws_api_gateway_api_key.api_keys](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/api_gateway_api_key) | resource | +| [aws_api_gateway_deployment.deployments](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/api_gateway_deployment) | resource | +| [aws_api_gateway_rest_api.api_gateway](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/api_gateway_rest_api) | resource | +| [aws_api_gateway_stage.stages](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/api_gateway_stage) | resource | +| [aws_api_gateway_usage_plan.usage_plans](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/api_gateway_usage_plan) | resource | +| [aws_api_gateway_usage_plan_key.keys](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/api_gateway_usage_plan_key) | resource | +| [aws_lambda_permission.allow_execution](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_permission) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [application\_name](#input\_application\_name) | Name of the application utilising resource. | `string` | n/a | yes | +| [environment](#input\_environment) | Which environment this is being instantiated in. | `string` | n/a | yes | +| [raw\_api\_gateway\_rest\_apis](#input\_raw\_api\_gateway\_rest\_apis) | Data structure
---------------
A list of dictionaries, where each dictionary has the following attributes:

REQUIRED
---------
- suffix : Suffix to use when creating the RESTAPI Gateway
- open\_api\_file\_path : Path to OpenAPI definition file
- description : A human-friendly description of the API
- tags : A dictionary of tags, required to tag Stage to ensure its protected by WAF.


OPTIONAL
---------
- template\_input : A dictionary of variable input for the OpenAPI definition file (leave blank if no template required)
- allowed\_lambdas : A list of strings, where each string is the function\_name of a lambda to allow access to.
- quota\_limit : Maximum number of requests that can be made in a given time period, defaults to 10.
- quota\_offset : Number of requests subtracted from the given limit in the initial time period, defaults to 0.
- quota\_period : Time period in which the limit applies. Valid values are "DAY", "WEEK" or "MONTH". Defaults to "DAY"
- burst\_limit : The API request burst limit, the maximum rate limit over a time ranging from one to a few seconds, depending upon whether the underlying token bucket is at its full capacity. Defaults to 5.
- rate\_limit : The API request steady-state rate limit, defaults to 10.
- api\_keys : List of strings, where each string is name of an API key to create for the API, defaults to empty list. |
list(
object({
suffix = string,
description = string,
tags = map(string),
open_api_file_path = string,
template_input = optional(map(string), {}),
quota_limit = optional(number, 10),
quota_offset = optional(number, 0),
quota_period = optional(string, "DAY"),
burst_limit = optional(number, 5),
rate_limit = optional(number, 10)
allowed_lambdas = optional(list(string), [])
api_keys = optional(list(string), [])
})
)
| n/a | yes | + +## Outputs + +No outputs. ## Data structure - +``` +Data structure +--------------- +A list of dictionaries, where each dictionary has the following attributes: + +REQUIRED +--------- +- suffix : Suffix to use when creating the RESTAPI Gateway +- open_api_file_path : Path to OpenAPI definition file +- description : A human-friendly description of the API +- tags : A dictionary of tags, required to tag Stage to ensure its protected by WAF. + + +OPTIONAL +--------- +- template_input : A dictionary of variable input for the OpenAPI definition file (leave blank if no template required) +- allowed_lambdas : A list of strings, where each string is the function_name of a lambda to allow access to. +- quota_limit : Maximum number of requests that can be made in a given time period, defaults to 10. +- quota_offset : Number of requests subtracted from the given limit in the initial time period, defaults to 0. +- quota_period : Time period in which the limit applies. Valid values are "DAY", "WEEK" or "MONTH". Defaults to "DAY" +- burst_limit : The API request burst limit, the maximum rate limit over a time ranging from one to a few seconds, depending upon whether the underlying token bucket is at its full capacity. Defaults to 5. +- rate_limit : The API request steady-state rate limit, defaults to 10. +- api_keys : List of strings, where each string is name of an API key to create for the API, defaults to empty list. +``` ## Examples See `examples` folder for an example setup. diff --git a/api_gateway_api_key.tf b/api_gateway_api_key.tf new file mode 100644 index 0000000..2adb3cc --- /dev/null +++ b/api_gateway_api_key.tf @@ -0,0 +1,17 @@ +locals { + actual_api_keys = flatten([ + for api in var.raw_api_gateway_rest_apis : [ + for key in api.api_keys : { + api = api.suffix + name = format("%s/%s", api.suffix, key) + } + ] + ]) +} + +resource "aws_api_gateway_api_key" "api_keys" { + for_each = { for key in local.actual_api_keys : key.name => key } + + name = each.value["name"] + description = format("Automatically generated key for %s API Gateway - managed by Terraform", each.value["api"]) +} \ No newline at end of file diff --git a/api_gateway_deployment.tf b/api_gateway_deployment.tf new file mode 100644 index 0000000..b00bd12 --- /dev/null +++ b/api_gateway_deployment.tf @@ -0,0 +1,16 @@ +resource "aws_api_gateway_deployment" "deployments" { + for_each = { for api in local.actual_raw_api_gateway_rest_apis : api.suffix => api } + + rest_api_id = aws_api_gateway_rest_api.api_gateway[each.value["suffix"]].id + triggers = { + redeployment = sha1(jsonencode(aws_api_gateway_rest_api.api_gateway[each.value["suffix"]].body)) + } + + lifecycle { + create_before_destroy = true + } + + depends_on = [ + aws_api_gateway_rest_api.api_gateway + ] +} \ No newline at end of file diff --git a/api_gateway_stage.tf b/api_gateway_stage.tf new file mode 100644 index 0000000..0eb5b36 --- /dev/null +++ b/api_gateway_stage.tf @@ -0,0 +1,8 @@ +resource "aws_api_gateway_stage" "stages" { + for_each = { for api in local.actual_raw_api_gateway_rest_apis : api.suffix => api } + + deployment_id = aws_api_gateway_deployment.deployments[each.value["suffix"]].id + rest_api_id = aws_api_gateway_rest_api.api_gateway[each.value["suffix"]].id + stage_name = var.environment + tags = each.value["tags"] +} \ No newline at end of file diff --git a/api_gateway_usage_plan.tf b/api_gateway_usage_plan.tf new file mode 100644 index 0000000..36197b8 --- /dev/null +++ b/api_gateway_usage_plan.tf @@ -0,0 +1,27 @@ +resource "aws_api_gateway_usage_plan" "usage_plans" { + for_each = { for api in local.actual_raw_api_gateway_rest_apis : api.suffix => api } + + name = format("%s-usage-plan", each.value["suffix"]) + description = format("API Gateway usage plan for %s - managed by Terraform", each.value["suffix"]) + + api_stages { + api_id = aws_api_gateway_rest_api.api_gateway[each.value["suffix"]].id + stage = aws_api_gateway_stage.stages[each.value["suffix"]].stage_name + } + + quota_settings { + limit = try(each.value["quota_limit"], 10) + offset = try(each.value["quota_offset"], 0) + period = try(each.value["quota_period"], "DAY") + } + + throttle_settings { + burst_limit = try(each.value["burst_limit"], 5) + rate_limit = try(each.value["rate_limit"], 10) + } + + depends_on = [ + aws_api_gateway_rest_api.api_gateway, + aws_api_gateway_stage.stages + ] +} \ No newline at end of file diff --git a/api_gateway_usage_plan_key.tf b/api_gateway_usage_plan_key.tf new file mode 100644 index 0000000..4d21843 --- /dev/null +++ b/api_gateway_usage_plan_key.tf @@ -0,0 +1,12 @@ +resource "aws_api_gateway_usage_plan_key" "keys" { + for_each = { for key in local.actual_api_keys : key.name => key } + + key_id = aws_api_gateway_api_key.api_keys[each.value["name"]].id + key_type = "API_KEY" + usage_plan_id = aws_api_gateway_usage_plan.usage_plans[each.value["api"]].id + + depends_on = [ + aws_api_gateway_usage_plan.usage_plans, + aws_api_gateway_api_key.api_keys + ] +} \ No newline at end of file diff --git a/lambda_permission.tf b/lambda_permission.tf new file mode 100644 index 0000000..cdf8e67 --- /dev/null +++ b/lambda_permission.tf @@ -0,0 +1,42 @@ +locals { + actual_lambda_permissions = flatten([ + for api in local.actual_raw_api_gateway_rest_apis : [ + for lambda in api.allowed_lambdas : [ + { + key : format("%s/%s/stage", api.suffix, lambda) + function_name : lambda + source_arn : format("%s/*", aws_api_gateway_stage.stages[api.suffix].arn) + statement_id = format("AllowExecutionFrom%sAPIGatewayStage", replace(api.suffix, "-", "")) + }, + { + key : format("%s/%s/api", api.suffix, lambda) + function_name : lambda + source_arn : format("%s/*", aws_api_gateway_rest_api.api_gateway[api.suffix].arn) + statement_id = format("AllowExecutionFrom%sAPIGateway", replace(api.suffix, "-", "")) + }, + { + key : format("%s/%s/deployment", api.suffix, lambda) + function_name : lambda + source_arn : format("%s*", aws_api_gateway_deployment.deployments[api.suffix].execution_arn) + statement_id = format("AllowExecutionFrom%sAPIGatewayDeployment", replace(api.suffix, "-", "")) + } + ] + ] + ]) +} + +resource "aws_lambda_permission" "allow_execution" { + for_each = { for permission in local.actual_lambda_permissions : permission.key => permission } + + depends_on = [ + aws_api_gateway_stage.stages, + aws_api_gateway_rest_api.api_gateway, + aws_api_gateway_deployment.deployments + ] + + statement_id = each.value["statement_id"] + action = "lambda:InvokeFunction" + function_name = each.value["function_name"] + principal = "apigateway.amazonaws.com" + source_arn = each.value["source_arn"] +} \ No newline at end of file diff --git a/main.tf b/main.tf new file mode 100644 index 0000000..f022823 --- /dev/null +++ b/main.tf @@ -0,0 +1,9 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.61.0" + } + } + required_version = "~> 1.5.0" +} \ No newline at end of file diff --git a/rest_api.tf b/rest_api.tf new file mode 100644 index 0000000..aec92d2 --- /dev/null +++ b/rest_api.tf @@ -0,0 +1,21 @@ +locals { + actual_raw_api_gateway_rest_apis = flatten([ + for api in var.raw_api_gateway_rest_apis : merge(api, { + open_api_definition = templatefile(api.open_api_file_path, try(api.template_input, {})) + }) + ]) +} + +resource "aws_api_gateway_rest_api" "api_gateway" { + + for_each = { for api in local.actual_raw_api_gateway_rest_apis : api.suffix => api } + + name = format(lower("aws-${var.environment}-${var.application_name}-%s"), each.value["suffix"]) + description = format("%s- managed by Terraform", each.value["description"]) + body = each.value["open_api_definition"] + tags = each.value["tags"] + + lifecycle { + create_before_destroy = true + } +} \ No newline at end of file diff --git a/variables.tf b/variables.tf new file mode 100644 index 0000000..b4a8950 --- /dev/null +++ b/variables.tf @@ -0,0 +1,92 @@ +variable "environment" { + description = "Which environment this is being instantiated in." + type = string + validation { + condition = contains(["dev", "test", "prod"], var.environment) + error_message = "Must be either dev, test or prod" + } +} + +variable "application_name" { + description = "Name of the application utilising resource." + type = string +} + +variable "raw_api_gateway_rest_apis" { + description = <= 0) + ]) + error_message = "quota_limit must be greater than or equal to 0" + } + + validation { + condition = alltrue([ + for api in var.raw_api_gateway_rest_apis : (api.quota_offset >= 0) + ]) + error_message = "quota_offset must be greater than or equal to 0" + } + + validation { + condition = alltrue([ + for api in var.raw_api_gateway_rest_apis : contains(["DAY", "WEEK", "MONTH"], api.quota_period) + ]) + error_message = "quota_period must be one of the following: DAY, WEEK, MONTH" + } + + validation { + condition = alltrue([ + for api in var.raw_api_gateway_rest_apis : (api.burst_limit >= 0) + ]) + error_message = "burst_limit must be greater than or equal to 0" + } + + validation { + condition = alltrue([ + for api in var.raw_api_gateway_rest_apis : (api.rate_limit >= 0) + ]) + error_message = "rate_limit must be greater than or equal to 0" + } +} \ No newline at end of file