-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #4 from oaknational/feat/ENG-957-serverless-module
[ENG-957] A module for hosting APIs in Google Cloud
- Loading branch information
Showing
9 changed files
with
462 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ | |
|
||
## Modules | ||
|
||
* [API](modules/gcp_api) | ||
* [Firestore](modules/gcp_firestore) | ||
|
||
## Developing with modules | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
# Google Cloud Firestore | ||
|
||
Deploys one or more Cloud run functions via an API gateway to a domain. | ||
|
||
## Example | ||
|
||
### The Open API YAML file | ||
|
||
In order to use API Gateway you will need a valid Open API YAML file. | ||
|
||
Note. Although this does need to include the paths section of the file schemas are not necessary | ||
in the responses section for the file to be valid. | ||
|
||
```yaml | ||
swagger: "2.0" | ||
info: | ||
title: Example file | ||
description: An example for documentation purposes | ||
version: 1.0.0 | ||
schemes: | ||
- https | ||
produces: | ||
- application/json | ||
paths: | ||
/v1/response: | ||
get: | ||
operationId: getResponse | ||
summary: Responds to a GET request | ||
produces: | ||
- application/json | ||
x-google-backend: | ||
address: ${get_response_url} | ||
responses: | ||
"200": | ||
description: Everyone is happy | ||
/v1/update: | ||
post: | ||
operationId: setValue | ||
summary: Handles a POST request | ||
produces: | ||
- application/json | ||
x-google-backend: | ||
address: ${set_value_url} | ||
responses: | ||
"201": | ||
description: Everyone is happy | ||
|
||
``` | ||
|
||
### The Terraform config | ||
|
||
The above would be stored in a file called `example.yaml`. | ||
|
||
In the same directory as that file should be the Terraform config... | ||
|
||
```hcl | ||
locals { | ||
entrypoints = [ "getResponse", "setValue" ] | ||
} | ||
module "example_api" { | ||
# tflint-ignore: terraform_module_pinned_source | ||
source = "github.com/oaknational/oak-terraform-modules//modules/gcp_api" | ||
name_parts = { | ||
domain = "eg" | ||
app = "example" | ||
} | ||
env = "prod" | ||
cloudflare_account_name = var.cloudflare_account_name | ||
cloudflare_zone_domain = var.cloudflare_zone_domain | ||
sub_domain = "eg" # This would resolve to eg.{cloudflare_zone_domain_name} | ||
function_source_bucket = "example-code-storage-bucket" | ||
# Although you can code each function separately you will find most share common configs so a loop | ||
# with a merge function similar to this may help simplify config management | ||
functions = [for ep in local.entrypoints : merge( | ||
{ entrypoint = ep }, | ||
{ | ||
runtime = "nodejs20" | ||
source_object = "example/api.zip" | ||
service_account_email = "example-api@example-project.iam.gserviceaccount.com" | ||
environment_variables = [ | ||
{ | ||
name = "DATABASE_URL", | ||
value = "db.example.com", | ||
}, | ||
] | ||
}) | ||
] | ||
gateway = { | ||
config_file = "${path.module}/example.yaml" | ||
# The file name of the above yaml, assuming it is stored in the config root dir | ||
config_version = 1 | ||
entrypoint_map = [ | ||
{ | ||
variable = "get_response_url", # This can be found in the example YAML above | ||
entrypoint = "getResponse", # This is from local.entrypoints | ||
}, | ||
{ | ||
variable = "set_value_url", | ||
entrypoint = "setValue", | ||
}, | ||
] | ||
} | ||
} | ||
output "function_uri" { | ||
value = module.hosting.function_uri | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
data "cloudflare_accounts" "this" { | ||
name = var.cloudflare_account_name | ||
} | ||
|
||
data "cloudflare_zone" "this" { | ||
account_id = data.cloudflare_accounts.this.accounts[0].id | ||
name = var.cloudflare_zone_domain | ||
} | ||
|
||
locals { | ||
public_domain_name = join("-", compact([var.sub_domain, var.env == "prod" ? null : var.env])) | ||
} | ||
|
||
resource "cloudflare_record" "cname" { | ||
zone_id = data.cloudflare_zone.this.id | ||
name = local.public_domain_name | ||
type = "CNAME" | ||
value = google_api_gateway_gateway.this.default_hostname | ||
ttl = 1 | ||
proxied = true | ||
} | ||
|
||
resource "cloudflare_page_rule" "this" { | ||
zone_id = data.cloudflare_zone.this.id | ||
target = "${local.public_domain_name}.${data.cloudflare_zone.this.name}/*" | ||
|
||
# Priority will never be this value but by setting it high it won't interfere with the values | ||
# in the cloudflare-page-rules workspace (See that config for a more detailed explanation). | ||
priority = 999 | ||
|
||
# The 999 priority will be reduced to a lower value by Cloudflare | ||
lifecycle { | ||
ignore_changes = [ | ||
priority, | ||
] | ||
} | ||
|
||
actions { | ||
host_header_override = google_api_gateway_gateway.this.default_hostname | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
data "google_project" "current" {} | ||
|
||
locals { | ||
function_records = { for f in var.functions : f.entrypoint => f } | ||
|
||
memory_lookup = [ | ||
"128M", | ||
"256M", | ||
"512M", | ||
"1G", | ||
"2G", | ||
"4G", | ||
"8G", | ||
"16G", | ||
"32G", | ||
] | ||
} | ||
|
||
resource "google_cloudfunctions2_function" "this" { | ||
for_each = local.function_records | ||
|
||
name = "${var.name_parts.domain}-${var.env}-${var.name_parts.region}-${var.name_parts.app}-${lower(each.value.entrypoint)}" | ||
location = var.google_cloud_region | ||
description = "The API endpint for ${var.env} ${join(" ", split("-", var.name_parts.app))}, ${each.key}" | ||
|
||
build_config { | ||
runtime = each.value.runtime | ||
entry_point = each.value.entrypoint | ||
docker_repository = "${data.google_project.current.id}/locations/${var.google_cloud_region}/repositories/gcf-artifacts" | ||
source { | ||
storage_source { | ||
bucket = var.function_source_bucket | ||
object = each.value.source_object | ||
} | ||
} | ||
} | ||
|
||
service_config { | ||
max_instance_count = each.value.max_instance_count | ||
available_memory = local.memory_lookup[each.value.available_memory_pwr] | ||
timeout_seconds = each.value.timeout_seconds | ||
|
||
available_cpu = each.value.available_cpu == 0 ? null : each.value.available_cpu | ||
max_instance_request_concurrency = each.value.max_request_concurrency | ||
|
||
service_account_email = each.value.service_account_email | ||
|
||
environment_variables = { | ||
for e in each.value.environment_variables : e.name => e.value | ||
} | ||
} | ||
} | ||
|
||
data "google_iam_policy" "all_users" { | ||
binding { | ||
role = "roles/run.invoker" | ||
members = [ | ||
"allUsers", | ||
] | ||
} | ||
} | ||
|
||
resource "google_cloud_run_v2_service_iam_policy" "policy" { | ||
for_each = local.function_records | ||
|
||
location = google_cloudfunctions2_function.this[each.key].location | ||
name = google_cloudfunctions2_function.this[each.key].name | ||
policy_data = data.google_iam_policy.all_users.policy_data | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
locals { | ||
gateway_template_map = { | ||
for e in var.gateway.entrypoint_map : e.variable => google_cloudfunctions2_function.this[e.entrypoint].url | ||
} | ||
} | ||
|
||
resource "google_api_gateway_api" "this" { | ||
provider = google-beta | ||
|
||
api_id = "${var.name_parts.domain}-${var.env}-${var.name_parts.region}-${var.name_parts.app}-api" | ||
} | ||
|
||
resource "google_api_gateway_api_config" "this" { | ||
provider = google-beta | ||
|
||
api = google_api_gateway_api.this.api_id | ||
api_config_id = "${var.name_parts.domain}-${var.env}-${var.name_parts.region}-${var.name_parts.app}-api-v${var.gateway.config_version}" | ||
|
||
openapi_documents { | ||
document { | ||
path = "openapi.yaml" | ||
contents = base64encode( | ||
templatefile(var.gateway.config_file, local.gateway_template_map) | ||
) | ||
} | ||
} | ||
|
||
lifecycle { | ||
create_before_destroy = true | ||
} | ||
} | ||
|
||
resource "google_api_gateway_gateway" "this" { | ||
provider = google-beta | ||
|
||
region = var.google_cloud_region | ||
|
||
api_config = google_api_gateway_api_config.this.id | ||
gateway_id = "${var.name_parts.domain}-${var.env}-${var.name_parts.region}-${var.name_parts.app}-api" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
output "function_uri" { | ||
value = "https://${cloudflare_record.cname.hostname}" | ||
} |
Oops, something went wrong.