diff --git a/README.md b/README.md index 0e5ac0b..09b396d 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@ To use these services, in your ISLE `docker-compose.yml` you can point to the Cl alpaca-prod: &alpaca-prod <<: [*prod, *alpaca] environment: - ALPACA_DERIVATIVE_FITS_URL: https://CR_URL - ALPACA_DERIVATIVE_HOMARUS_URL: https://CR_URL - ALPACA_DERIVATIVE_HOUDINI_URL: https://CR_URL - ALPACA_DERIVATIVE_OCR_URL: https://CR_URL + ALPACA_DERIVATIVE_FITS_URL: https://microservice.libops.site/crayfits + ALPACA_DERIVATIVE_HOMARUS_URL: https://microservice.libops.site/homarus + ALPACA_DERIVATIVE_HOUDINI_URL: https://microservice.libops.site/houdini + ALPACA_DERIVATIVE_OCR_URL: https://microservice.libops.site/hypercube ``` diff --git a/main.tf b/main.tf index 3dfb537..4e931ab 100644 --- a/main.tf +++ b/main.tf @@ -148,3 +148,15 @@ EOT docker = docker.local } } + +module "lb" { + source = "./modules/lb" + + project = var.project + backends = { + "homarus" = module.homarus.backend, + "houdini" = module.houdini.backend, + "hypercube" = module.hypercube.backend, + "crayfits" = module.crayfits.backend + } +} diff --git a/modules/cloudrun/main.tf b/modules/cloudrun/main.tf index 965f3c3..50bb8c8 100644 --- a/modules/cloudrun/main.tf +++ b/modules/cloudrun/main.tf @@ -114,3 +114,38 @@ resource "google_cloud_run_service_iam_member" "invoker" { role = "roles/run.invoker" member = "allUsers" } + +# create a serverless NEG for this set of regional services +resource "google_compute_region_network_endpoint_group" "neg" { + count = length(var.regions) + + name = "libops-neg-${google_cloud_run_service.cloudrun[count.index].name}" + network_endpoint_type = "SERVERLESS" + region = google_cloud_run_service.cloudrun[count.index].location + project = var.project + + cloud_run { + service = google_cloud_run_service.cloudrun[count.index].name + } +} + +resource "google_compute_backend_service" "backend" { + project = var.project + name = "libops-backend-${var.name}" + + load_balancing_scheme = "EXTERNAL_MANAGED" + protocol = "HTTPS" + + dynamic "backend" { + for_each = google_compute_region_network_endpoint_group.neg + + content { + group = backend.value.id + } + } + + log_config { + enable = true + sample_rate = 1.0 + } +} diff --git a/modules/cloudrun/outputs.tf b/modules/cloudrun/outputs.tf new file mode 100644 index 0000000..4c58f39 --- /dev/null +++ b/modules/cloudrun/outputs.tf @@ -0,0 +1,3 @@ +output "backend" { + value = google_compute_backend_service.backend.id +} diff --git a/modules/lb/dashboard.json b/modules/lb/dashboard.json new file mode 100644 index 0000000..fba6a6c --- /dev/null +++ b/modules/lb/dashboard.json @@ -0,0 +1,306 @@ +{ + "displayName": "Cloud CDN", + "mosaicLayout": { + "columns": 12, + "tiles": [ + { + "height": 4, + "widget": { + "title": "Request Count by Continent", + "xyChart": { + "chartOptions": { + "mode": "COLOR" + }, + "dataSets": [ + { + "plotType": "STACKED_AREA", + "timeSeriesQuery": { + "timeSeriesQueryLanguage": "fetch https_lb_rule\n| metric 'loadbalancing.googleapis.com/https/request_count'\n| align rate(1m)\n| every 1m\n| group_by [metric.proxy_continent],\n [row_count: row_count()]" + } + } + ], + "timeshiftDuration": "0s", + "yAxis": { + "scale": "LINEAR" + } + } + }, + "width": 6, + "yPos": 8 + }, + { + "height": 4, + "widget": { + "title": "Cache Hit Response Bytes by Continent", + "xyChart": { + "chartOptions": { + "mode": "COLOR" + }, + "dataSets": [ + { + "plotType": "STACKED_BAR", + "timeSeriesQuery": { + "timeSeriesQueryLanguage": "fetch https_lb_rule\n| metric 'loadbalancing.googleapis.com/https/response_bytes_count'\n| filter (metric.cache_result = 'HIT')\n| align rate(1m)\n| every 1m\n| group_by [metric.proxy_continent],\n [value_response_bytes_count_aggregate:\n aggregate(value.response_bytes_count)]" + } + } + ], + "timeshiftDuration": "0s", + "yAxis": { + "scale": "LINEAR" + } + } + }, + "width": 6, + "xPos": 6, + "yPos": 4 + }, + { + "height": 4, + "widget": { + "title": "Cache Status by Count", + "xyChart": { + "chartOptions": { + "mode": "COLOR" + }, + "dataSets": [ + { + "plotType": "LINE", + "timeSeriesQuery": { + "timeSeriesQueryLanguage": "fetch https_lb_rule\n| metric 'loadbalancing.googleapis.com/https/request_count'\n| align rate(1m)\n| every 1m\n| group_by [metric.cache_result],\n [row_count: row_count()]\n" + } + } + ], + "timeshiftDuration": "0s", + "yAxis": { + "scale": "LINEAR" + } + } + }, + "width": 6, + "yPos": 4 + }, + { + "height": 4, + "widget": { + "title": "Response Egress by Cache Status", + "xyChart": { + "chartOptions": { + "mode": "COLOR" + }, + "dataSets": [ + { + "plotType": "STACKED_AREA", + "timeSeriesQuery": { + "timeSeriesQueryLanguage": "fetch https_lb_rule\n| metric 'loadbalancing.googleapis.com/https/response_bytes_count'\n| align rate(1m)\n| every 1m\n| group_by [metric.cache_result],\n [value_response_bytes_count_aggregate:\n aggregate(value.response_bytes_count)]" + } + } + ], + "timeshiftDuration": "0s", + "yAxis": { + "scale": "LINEAR" + } + } + }, + "width": 6, + "xPos": 6 + }, + { + "height": 4, + "widget": { + "title": "Latency by Continent 95%", + "xyChart": { + "chartOptions": { + "mode": "COLOR" + }, + "dataSets": [ + { + "plotType": "LINE", + "timeSeriesQuery": { + "timeSeriesQueryLanguage": "fetch https_lb_rule\n| metric 'loadbalancing.googleapis.com/https/frontend_tcp_rtt'\n| group_by 1m,\n [value_frontend_tcp_rtt_aggregate: aggregate(value.frontend_tcp_rtt)]\n| every 1m\n| group_by [metric.proxy_continent],\n [value_frontend_tcp_rtt_aggregate_percentile:\n percentile(value_frontend_tcp_rtt_aggregate, 95)]" + } + } + ], + "timeshiftDuration": "0s", + "yAxis": { + "scale": "LINEAR" + } + } + }, + "width": 6, + "yPos": 12 + }, + { + "height": 4, + "widget": { + "title": "Client Response Code", + "xyChart": { + "chartOptions": { + "mode": "COLOR" + }, + "dataSets": [ + { + "plotType": "STACKED_AREA", + "timeSeriesQuery": { + "timeSeriesQueryLanguage": "fetch https_lb_rule\n| metric 'loadbalancing.googleapis.com/https/request_count'\n| group_by 1h, [row_count: row_count()]\n| every 1h\n| group_by [metric.response_code_class],\n [row_count_aggregate: aggregate(row_count)]" + } + } + ], + "timeshiftDuration": "0s", + "yAxis": { + "scale": "LINEAR" + } + } + }, + "width": 6, + "xPos": 6, + "yPos": 8 + }, + { + "height": 4, + "widget": { + "title": "Non 2xx Error Codes", + "xyChart": { + "chartOptions": { + "mode": "COLOR" + }, + "dataSets": [ + { + "plotType": "STACKED_AREA", + "timeSeriesQuery": { + "timeSeriesQueryLanguage": "fetch https_lb_rule\n| metric 'loadbalancing.googleapis.com/https/request_count'\n| filter (metric.response_code_class != 200)\n| group_by 1h, [row_count: row_count()]\n| every 1h\n| group_by [metric.response_code_class],\n [row_count_aggregate: aggregate(row_count)]" + } + } + ], + "timeshiftDuration": "0s", + "yAxis": { + "scale": "LINEAR" + } + } + }, + "width": 6, + "xPos": 6, + "yPos": 12 + }, + { + "height": 2, + "widget": { + "scorecard": { + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_SUM", + "perSeriesAligner": "ALIGN_SUM" + }, + "filter": "metric.type=\"loadbalancing.googleapis.com/https/request_count\" resource.type=\"https_lb_rule\"" + } + } + }, + "title": "Total Requests" + }, + "width": 3 + }, + { + "height": 2, + "widget": { + "scorecard": { + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_SUM", + "perSeriesAligner": "ALIGN_SUM" + }, + "filter": "metric.type=\"loadbalancing.googleapis.com/https/response_bytes_count\" resource.type=\"https_lb_rule\"" + } + } + }, + "title": "Total Egress" + }, + "width": 3, + "xPos": 3 + }, + { + "height": 2, + "widget": { + "scorecard": { + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_SUM", + "perSeriesAligner": "ALIGN_SUM" + }, + "filter": "metric.type=\"loadbalancing.googleapis.com/https/response_bytes_count\" resource.type=\"https_lb_rule\" metric.label.\"cache_result\"=\"HIT\"" + } + } + }, + "title": "Cache Hit Egress" + }, + "width": 3, + "yPos": 2 + }, + { + "height": 2, + "widget": { + "scorecard": { + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_SUM", + "perSeriesAligner": "ALIGN_SUM" + }, + "filter": "metric.type=\"loadbalancing.googleapis.com/https/response_bytes_count\" resource.type=\"https_lb_rule\" metric.label.\"cache_result\"=\"MISS\"" + } + } + }, + "title": "Cache Miss Egress" + }, + "width": 3, + "xPos": 3, + "yPos": 2 + }, + { + "height": 4, + "widget": { + "title": "Request Count by Country", + "xyChart": { + "chartOptions": { + "mode": "COLOR" + }, + "dataSets": [ + { + "minAlignmentPeriod": "60s", + "plotType": "STACKED_AREA", + "timeSeriesQuery": { + "timeSeriesFilter": { + "aggregation": { + "alignmentPeriod": "60s", + "perSeriesAligner": "ALIGN_RATE" + }, + "filter": "metric.type=\"loadbalancing.googleapis.com/https/request_count\" resource.type=\"https_lb_rule\"", + "secondaryAggregation": { + "alignmentPeriod": "60s", + "crossSeriesReducer": "REDUCE_SUM", + "groupByFields": [ + "metric.label.\"client_country\"" + ], + "perSeriesAligner": "ALIGN_SUM" + } + } + } + } + ], + "timeshiftDuration": "0s", + "yAxis": { + "scale": "LINEAR" + } + } + }, + "width": 6, + "yPos": 16 + } + ] + } +} \ No newline at end of file diff --git a/modules/lb/main.tf b/modules/lb/main.tf new file mode 100644 index 0000000..936e3d2 --- /dev/null +++ b/modules/lb/main.tf @@ -0,0 +1,101 @@ +locals { + metrics_json = file("${path.module}/dashboard.json") + md5 = md5(local.metrics_json) +} + +data "google_compute_global_address" "default" { + project = var.project + name = "microservices-ipv4" +} + +data "google_compute_global_address" "default-v6" { + project = var.project + name = "microservices-ipv6" +} + +resource "google_compute_global_forwarding_rule" "https" { + project = var.project + name = "microservices-https" + target = google_compute_target_https_proxy.default.self_link + ip_address = data.google_compute_global_address.default.address + port_range = "443" + load_balancing_scheme = "EXTERNAL_MANAGED" +} + +resource "google_compute_global_forwarding_rule" "https-v6" { + project = var.project + name = "microservices-https-v6" + target = google_compute_target_https_proxy.default.self_link + ip_address = data.google_compute_global_address.default-v6.address + port_range = "443" + load_balancing_scheme = "EXTERNAL_MANAGED" +} + +resource "google_compute_target_https_proxy" "default" { + project = var.project + name = "microservices-https-proxy" + url_map = google_compute_url_map.default.self_link + + ssl_certificates = [ + google_compute_managed_ssl_certificate.default.id, + ] +} + +resource "google_compute_managed_ssl_certificate" "default" { + name = "microservices-tls" + managed { + domains = [ + "microservices.libops.site" + ] + } + project = var.project +} + +resource "google_compute_url_map" "default" { + name = "microservices-url-map" + project = var.project + + host_rule { + hosts = ["*"] + path_matcher = "allpaths" + } + + default_url_redirect { + https_redirect = true + strip_query = true + host_redirect = "github.com" + path_redirect = "LibOps/islandora-microservices" + } + + path_matcher { + name = "allpaths" + default_service = var.backends.houdini + dynamic "path_rule" { + for_each = var.backends + content { + paths = ["/${path_rule.key}"] + service = path_rule.value + } + } + } +} + +# add a dashboard +# only updating it if we update our JSON +# since terraform and google's dashboard exports don't play nice +resource "null_resource" "metrics-json" { + triggers = { + md5 = local.md5 + } +} +resource "google_monitoring_dashboard" "dashboard" { + project = var.project + dashboard_json = local.metrics_json + + lifecycle { + ignore_changes = [ + dashboard_json + ] + replace_triggered_by = [null_resource.metrics-json.id] + } +} diff --git a/modules/lb/variables.tf b/modules/lb/variables.tf new file mode 100644 index 0000000..0acf348 --- /dev/null +++ b/modules/lb/variables.tf @@ -0,0 +1,9 @@ +variable "backends" { + type = map(string) + description = "The Cloud Run Serverless NEG backends" +} + +variable "project" { + type = string + description = "The GCP project to use" +}