diff --git a/modules/net-xlb/README.md b/modules/net-xlb/README.md new file mode 100644 index 00000000..726ef7bd --- /dev/null +++ b/modules/net-xlb/README.md @@ -0,0 +1,451 @@ +# External Global (HTTP/S) Load Balancer Module + +The module allows managing External Global HTTP/HTTPS Load Balancers (XGLB), integrating the global forwarding rule, the url-map, the backends (supporting buckets and groups), and optional health checks and SSL certificates (both managed and unmanaged). It's designed to be a simple match for the [`gcs`](../gcs) and the [`compute-vm`](../compute-vm) modules, which can be used to manage GCS buckets, instance templates and instance groups. + +## Examples + +### GCS Bucket Minimal Example + +This is a minimal example, which creates a global HTTP load balancer, pointing the path `/` to an existing GCS bucket called `my_test_bucket`. + +```hcl +module "xlb" { + source = "./modules/net-xlb" + name = "xlb-test" + project_id = var.project_id + + backend_services_config = { + my-bucket-backend = { + bucket_config = { + bucket_name = "my_test_bucket" + options = null + } + group_config = null + enable_cdn = false + cdn_config = null + } + } +} +# tftest:modules=1:resources=4 +``` + +### Group Backend Service Minimal Example + +A very similar coniguration also applies to GCE instance groups: + +```hcl +module "xlb" { + source = "./modules/net-xlb" + name = "xlb-test" + project_id = var.project_id + + backend_services_config = { + my-group-backend = { + bucket_config = null + enable_cdn = false + cdn_config = null + group_config = { + backends = [ + { + group = "my_test_group" + options = null + } + ], + health_checks = [] + log_config = null + options = null + } + } + } +} +# tftest:modules=1:resources=5 +``` + +### Health Checks For Group Backend Services + +Group backend services support health checks. +If no health checks are specified, a default HTTP health check is created and associated to each group backend service. The default health check configuration can be modified through the `health_checks_config_defaults` variable. + +Alternatively, one or more health checks can be either contextually created or attached, if existing. If the id of the health checks specified is equal to one of the keys of the `health_checks_config` variable, the health check is contextually created; otherwise, the health check id is used as is, assuming an health check alredy exists. + +For example, to contextually create a health check and attach it to the backend service: + +```hcl +module "xlb" { + source = "./modules/net-xlb" + name = "xlb-test" + project_id = var.project_id + + backend_services_config = { + my-group-backend = { + bucket_config = null + enable_cdn = false + cdn_config = null + group_config = { + backends = [ + { + group = "my_test_group" + options = null + } + ], + health_checks = ["hc_1"] + log_config = null + options = null + } + } + } + + health_checks_config = { + hc_1 = { + type = "http" + logging = true + options = { + timeout_sec = 40 + } + check = { + port_specification = "USE_SERVING_PORT" + } + } + } +} +# tftest:modules=1:resources=5 +``` + +### Mixing Backends + +Backends can be multiple, group and bucket backends can be mixed and group backends support multiple groups. + +```hcl +module "xlb" { + source = "./modules/net-xlb" + name = "xlb-test" + project_id = var.project_id + + backend_services_config = { + my-group-backend = { + bucket_config = null + enable_cdn = false + cdn_config = null + group_config = { + backends = [ + { + group = "my_test_group" + options = null + } + ], + health_checks = [] + log_config = null + options = null + } + }, + another-group-backend = { + bucket_config = null + enable_cdn = false + cdn_config = null + group_config = { + backends = [ + { + group = "my_other_test_group" + options = null + } + ], + health_checks = [] + log_config = null + options = null + } + }, + a-bucket-backend = { + bucket_config = { + bucket_name = "my_test_bucket" + options = null + } + group_config = null + enable_cdn = false + cdn_config = null + } + } +} +# tftest:modules=1:resources=7 +``` + +### Url-map + +The url-map can be customized with lots of different configurations. This includes leveraging multiple backends in different parts of the configuration. +Given its complexity, it's left to the user passing the right data structure. + +For simplicity, *if no configurations are given* the first backend service defined (in alphabetical order, with priority to bucket backend services, if any) is used as the *default_service*, thus answering to the root (*/*) path. + +Backend services can be specified as needed in the url-map configuration, referencing the id used to declare them in the backend services map. If a corresponding backend service is found, their object id is automatically used; otherwise, it is assumed that the string passed is the id of an already existing backend and it is given to the provider as it was passed. + +In this example, we're using one backend service as the default backend + +```hcl +module "xlb" { + source = "./modules/net-xlb" + name = "xlb-test" + project_id = var.project_id + + url_map_config = { + default_service = "my-group-backend" + default_route_action = null + default_url_redirect = null + tests = null + header_action = null + host_rules = [] + path_matchers = [ + { + name = "my-example-page" + path_rules = [ + { + paths = ["/my-example-page"] + service = "another-group-backend" + } + ] + } + ] + } + + backend_services_config = { + my-group-backend = { + bucket_config = null + enable_cdn = false + cdn_config = null + group_config = { + backends = [ + { + group = "my_test_group" + options = null + } + ], + health_checks = [] + log_config = null + options = null + } + }, + my-example-page = { + bucket_config = null + enable_cdn = false + cdn_config = null + group_config = { + backends = [ + { + group = "my_other_test_group" + options = null + } + ], + health_checks = [] + log_config = null + options = null + } + } + } +} +# tftest:modules=1:resources=6 +``` + +### Reserve a static IP address + +Optionally, a static IP address can be reserved: + +```hcl +module "xlb" { + source = "./modules/net-xlb" + name = "xlb-test" + project_id = var.project_id + + reserve_ip_address = true + + backend_services_config = { + my-group-backend = { + bucket_config = null + enable_cdn = false + cdn_config = null + group_config = { + backends = [ + { + group = "my_test_group" + options = null + } + ], + health_checks = [] + log_config = null + options = null + } + } + } +} +# tftest:modules=1:resources=6 +``` + +### HTTPS And SSL Certificates + +HTTPS is disabled by default but it can be optionally enabled. +The module supports both managed and unmanaged certificates, and they can be either created contextually with other resources or attached, if already existing. + +If no `ssl_certificates_config` variable is specified, a managed certificate for the domain *example.com* is automatically created. + +```hcl +module "xlb" { + source = "./modules/net-xlb" + name = "xlb-test" + project_id = var.project_id + + https = true + + backend_services_config = { + my-group-backend = { + bucket_config = null + enable_cdn = false + cdn_config = null + group_config = { + backends = [ + { + group = "my_test_group" + options = null + } + ], + health_checks = [] + log_config = null + options = null + } + } + } +} +# tftest:modules=1:resources=6 +``` + +Otherwise, SSL certificates can be explicitely defined. In this case, they'll need to be referenced from the `target_proxy_https_config.ssl_certificates` variable. + +If the ids specified in the `target_proxy_https_config` variable are not found in the `ssl_certificates_config` map, they are used as is, assuming the ssl certificates already exist. + +```hcl +module "xlb" { + source = "./modules/net-xlb" + name = "xlb-test" + project_id = var.project_id + + https = true + + ssl_certificates_config = { + my-domain = { + domains = [ + "my-domain.com" + ], + unmanaged_config = null + } + } + + target_proxy_https_config = { + ssl_certificates = [ + "my-domain", + "an-existing-cert" + ] + } + + backend_services_config = { + my-group-backend = { + bucket_config = null + enable_cdn = false + cdn_config = null + group_config = { + backends = [ + { + group = "my_test_group" + options = null + } + ] + health_checks = [] + log_config = null + options = null + } + } + } +} +# tftest:modules=1:resources=6 +``` + +Using unamanged certificates is also possible. Here is an example: + +```hcl +module "xlb" { + source = "./modules/net-xlb" + name = "xlb-test" + project_id = var.project_id + + https = true + + ssl_certificates_config = { + my-domain = { + domains = [ + "my-domain.com" + ], + unmanaged_config = { + tls_private_key = nonsensitive(tls_private_key.self_signed_key.private_key_pem) + tls_self_signed_cert = nonsensitive(tls_self_signed_cert.self_signed_cert.cert_pem) + } + } + } + + target_proxy_https_config = { + ssl_certificates = [ + "my-domain" + ] + } + + backend_services_config = { + my-group-backend = { + bucket_config = null + enable_cdn = false + cdn_config = null + group_config = { + backends = [ + { + group = "my_test_group" + options = null + } + ], + health_checks = [] + log_config = null + options = null + } + } + } +} + +resource "tls_private_key" "self_signed_key" { + algorithm = "RSA" + rsa_bits = 2048 +} + +resource "tls_self_signed_cert" "self_signed_cert" { + key_algorithm = tls_private_key.self_signed_key.algorithm + private_key_pem = tls_private_key.self_signed_key.private_key_pem + validity_period_hours = 12 + early_renewal_hours = 3 + dns_names = ["example.com"] + allowed_uses = [ + "key_encipherment", + "digital_signature", + "server_auth" + ] + subject { + common_name = "example.com" + organization = "My Test Org" + } +} +# tftest:modules=1:resources=6 +``` + +## Components And Files Mapping + +An External Global Load Balancer is made of multiple components, that change depending on the configurations. Sometimes, it may be tricky to understand what they are, and how they relate to each other. Following, we provide a very brief overview to become more familiar with them. + +* The global load balancer ([global_forwarding_rule.tf](global_forwarding_rule.tf)) binds a frontend public Virtual IP (VIP) (ip_address.tf)[ip_address.tf] to an HTTP(S) target proxy ((target_proxy.tf)[target_proxy.tf]). + +* If the target proxy ((target_proxy.tf)[target_proxy.tf]) is HTTPS, it requires one or more -managed or unmanaged- SSL certificates ((ssl_certificates.tf)[ssl_certificates.tf]). + +* Target proxies ((target_proxy.tf)[target_proxy.tf]) leverage url-maps ((url_map.tf)[url_map.tf]): set of L7 rules, which create a mapping between specific hostnames, URIs (and more) to one or more backends services ((backend_services.tf)[backend_services.tf]). + +* Backends services ((backend_services.tf)[backend_services.tf]) can either link to a bucket or -one or multiple- groups, which can be GCE instance groups or NEGs. It is assumed in this module that buckets and groups are previously created through other modules, and passed in as input variables. + +* Backends services ((backend_services.tf)[backend_services.tf]) support one or more health checks ((health_cheks.tf)[health_checks.tf]), used to verify that the backend is indeed healthy, so that traffic can be forwarded to it. Health checks currently supported in this module are HTTP, HTTPS, HTTP2, SSL, TCP. diff --git a/modules/net-xlb/backend_services.tf b/modules/net-xlb/backend_services.tf new file mode 100644 index 00000000..7ac818b9 --- /dev/null +++ b/modules/net-xlb/backend_services.tf @@ -0,0 +1,212 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +locals { + backend_services_bucket = { + for k, v in coalesce(var.backend_services_config, {}) : + k => v if v.bucket_config != null + } + backend_services_group = { + for k, v in coalesce(var.backend_services_config, {}) : + k => v if v.group_config != null + } +} + +resource "google_compute_backend_bucket" "bucket" { + for_each = local.backend_services_bucket + name = "${var.name}-${each.key}" + description = "Terraform managed." + project = var.project_id + bucket_name = try(each.value.bucket_config.bucket_name, null) + custom_response_headers = try(each.value.bucket_config.options.custom_response_headers, null) + enable_cdn = try(each.value.enable_cdn, null) + + dynamic "cdn_policy" { + for_each = try(each.value.cdn_policy, null) == null ? [] : [each.value.cdn_policy] + content { + cache_mode = try(cdn_policy.value.cache_mode, null) + client_ttl = try(cdn_policy.value.client_ttl, null) + default_ttl = try(cdn_policy.value.default_ttl, null) + max_ttl = try(cdn_policy.value.max_ttl, null) + negative_caching = try(cdn_policy.value.negative_caching, null) + serve_while_stale = try(cdn_policy.value.serve_while_stale, null) + signed_url_cache_max_age_sec = try(cdn_policy.value.signed_url_cache_max_age_sec, null) + + dynamic "negative_caching_policy" { + for_each = try(cdn_policy.value.negative_caching_policy, null) == null ? [] : [cdn_policy.value.negative_caching_policy] + iterator = ncp + content { + code = ncp.value.code + ttl = ncp.value.ttl + } + } + } + } +} + +resource "google_compute_backend_service" "group" { + for_each = local.backend_services_group + name = "${var.name}-${each.key}" + project = var.project_id + description = "Terraform managed." + affinity_cookie_ttl_sec = try(each.value.group_config.options.affinity_cookie_ttl_sec, null) + enable_cdn = try(each.value.enable_cdn, null) + custom_request_headers = try(each.value.group_config.options.custom_request_headers, null) + custom_response_headers = try(each.value.group_config.options.custom_response_headers, null) + connection_draining_timeout_sec = try(each.value.group_config.options.connection_draining_timeout_sec, null) + load_balancing_scheme = try(each.value.group_config.options.load_balancing_scheme, null) + locality_lb_policy = try(each.value.group_config.options.locality_lb_policy, null) + port_name = try(each.value.group_config.options.port_name, null) + protocol = try(each.value.group_config.options.protocol, null) + security_policy = try(each.value.group_config.options.security_policy, null) + session_affinity = try(each.value.group_config.options.session_affinity, null) + timeout_sec = try(each.value.group_config.options.timeout_sec, null) + + # If no health checks are defined, use the default one. + # Otherwise, look in the health_checks_config map. + # Otherwise, use the health_check id as is (already existing). + health_checks = ( + try(each.value.group_config.health_checks, null) == null + || length(try(each.value.group_config.health_checks, [])) == 0 + ? try( + [google_compute_health_check.health_check["default"].self_link], + null + ) + : [ + for hc in each.value.group_config.health_checks : + try( + google_compute_health_check.health_check[hc].id, + hc + ) + ] + ) + + dynamic "backend" { + for_each = try(each.value.group_config.backends, []) + content { + balancing_mode = try(backend.value.options.balancing_mode, null) + capacity_scaler = try(backend.value.options.capacity_scaler, null) + group = try(backend.value.group, null) + max_connections = try(backend.value.options.max_connections, null) + max_connections_per_instance = try(backend.value.options.max_connections_per_instance, null) + max_connections_per_endpoint = try(backend.value.options.max_connections_per_endpoint, null) + max_rate = try(backend.value.options.max_rate, null) + max_rate_per_instance = try(backend.value.options.max_rate_per_instance, null) + max_rate_per_endpoint = try(backend.value.options.max_rate_per_endpoint, null) + max_utilization = try(backend.value.options.max_utilization, null) + } + } + + dynamic "circuit_breakers" { + for_each = ( + try(each.value.group_config.options.circuit_breakers, null) == null + ? [] + : [each.value.group_config.options.circuit_breakers] + ) + iterator = cb + content { + max_requests_per_connection = try(cb.value.max_requests_per_connection, null) + max_connections = try(cb.value.max_connections, null) + max_pending_requests = try(cb.value.max_pending_requests, null) + max_requests = try(cb.value.max_requests, null) + max_retries = try(cb.value.max_retries, null) + } + } + + dynamic "consistent_hash" { + for_each = ( + try(each.value.group_config.options.consistent_hash, null) == null + ? [] + : [each.value.group_config.options.consistent_hash] + ) + content { + http_header_name = try(consistent_hash.value.http_header_name, null) + minimum_ring_size = try(consistent_hash.value.minimum_ring_size, null) + + dynamic "http_cookie" { + for_each = try(consistent_hash.value.http_cookie, null) == null ? [] : [consistent_hash.value.http_cookie] + content { + name = try(http_cookie.value.name, null) + path = try(http_cookie.value.path, null) + + dynamic "ttl" { + for_each = try(consistent_hash.value.ttl, null) == null ? [] : [consistent_hash.value.ttl] + content { + seconds = try(ttl.value.seconds, null) # Must be from 0 to 315,576,000,000 inclusive + nanos = try(ttl.value.nanos, null) # Must be from 0 to 999,999,999 inclusive + } + } + } + } + } + } + + dynamic "cdn_policy" { + for_each = ( + try(each.value.cdn_policy, null) == null + ? [] + : [each.value.cdn_policy] + ) + iterator = cdn_policy + content { + signed_url_cache_max_age_sec = try(cdn_policy.value.signed_url_cache_max_age_sec, null) + default_ttl = try(cdn_policy.value.default_ttl, null) + max_ttl = try(cdn_policy.value.max_ttl, null) + client_ttl = try(cdn_policy.value.client_ttl, null) + negative_caching = try(cdn_policy.value.negative_caching, null) + cache_mode = try(cdn_policy.value.cache_mode, null) + serve_while_stale = try(cdn_policy.value.serve_while_stale, null) + + dynamic "negative_caching_policy" { + for_each = ( + try(cdn_policy.value.negative_caching_policy, null) == null + ? [] + : [cdn_policy.value.negative_caching_policy] + ) + iterator = ncp + content { + code = try(ncp.value.code, null) + ttl = try(ncp.value.ttl, null) + } + } + } + } + + dynamic "iap" { + for_each = ( + try(each.value.group_config.options.iap, null) == null + ? [] + : [each.value.group_config.options.iap] + ) + content { + oauth2_client_id = try(iap.value.oauth2_client_id, null) + oauth2_client_secret = try(iap.value.oauth2_client_secret, null) # sensitive + oauth2_client_secret_sha256 = try(iap.value.oauth2_client_secret_sha256, null) # sensitive + } + } + + dynamic "log_config" { + for_each = ( + try(each.value.group_config.log_config, null) == null + ? [] + : [each.value.group_config.log_config] + ) + content { + enable = try(log_config.value.enable, null) + sample_rate = try(log_config.value.sample_rate, null) + } + } +} diff --git a/modules/net-xlb/global_forwarding_rule.tf b/modules/net-xlb/global_forwarding_rule.tf new file mode 100644 index 00000000..bac4cdab --- /dev/null +++ b/modules/net-xlb/global_forwarding_rule.tf @@ -0,0 +1,41 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +locals { + ip_address = ( + var.reserve_ip_address + ? google_compute_global_address.static_ip.0.id + : null + ) + + target = ( + var.https + ? google_compute_target_https_proxy.https.0.id + : google_compute_target_http_proxy.http.0.id + ) +} + +resource "google_compute_global_forwarding_rule" "forwarding_rule" { + provider = google-beta + name = var.name + project = var.project_id + description = "Terraform managed." + ip_protocol = var.global_forwarding_rule_config.ip_protocol + load_balancing_scheme = var.global_forwarding_rule_config.load_balancing_scheme + port_range = var.global_forwarding_rule_config.port_range + target = local.target + ip_address = local.ip_address +} diff --git a/modules/net-xlb/health_checks.tf b/modules/net-xlb/health_checks.tf new file mode 100644 index 00000000..10abb8bb --- /dev/null +++ b/modules/net-xlb/health_checks.tf @@ -0,0 +1,142 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +locals { + # Get group backend services without health checks defined + _backends_without_hcs = [ + for k, v in coalesce(var.backend_services_config, {}) : + v if( + v.group_config != null + && ( + try(v.group_config.health_checks, null) == null + || length(try(v.group_config.health_checks, [])) == 0 + ) + ) + ] + + # If at least one group backend service without + # HC is defined, create also a default HC + health_checks_config = ( + length(local._backends_without_hcs) > 0 + ? merge( + { default = var.health_checks_config_defaults }, + coalesce(var.health_checks_config, {}) + ) + : coalesce(var.health_checks_config, {}) + ) +} + +resource "google_compute_health_check" "health_check" { + for_each = local.health_checks_config + provider = google-beta + name = "${var.name}-${each.key}" + project = var.project_id + description = "Terraform managed." + check_interval_sec = try(each.value.options.check_interval_sec, null) + healthy_threshold = try(each.value.options.healthy_threshold, null) + timeout_sec = try(each.value.options.timeout_sec, null) + unhealthy_threshold = try(each.value.options.unhealthy_threshold, null) + + dynamic "http_health_check" { + for_each = ( + try(each.value.type, null) == "http" || try(each.value.type, null) == null + ? { 1 = 1 } + : {} + ) + content { + host = try(each.value.check.host, null) + port = try(each.value.check.port, null) + port_name = try(each.value.check.port_name, null) + port_specification = try(each.value.check.port_specification, null) + proxy_header = try(each.value.check.proxy_header, null) + request_path = try(each.value.check.request_path, null) + response = try(each.value.check.response, null) + } + } + + dynamic "https_health_check" { + for_each = ( + try(each.value.type, null) == "https" || try(each.value.type, null) == null + ? { 1 = 1 } + : {} + ) + content { + host = try(each.value.check.host, null) + port = try(each.value.check.port, null) + port_name = try(each.value.check.port_name, null) + port_specification = try(each.value.check.port_specification, null) + proxy_header = try(each.value.check.proxy_header, null) + request_path = try(each.value.check.request_path, null) + response = try(each.value.check.response, null) + } + } + + dynamic "tcp_health_check" { + for_each = ( + try(each.value.type, null) == "tcp" || try(each.value.type, null) == null + ? { 1 = 1 } + : {} + ) + content { + port = try(each.value.check.port, null) + port_name = try(each.value.check.port_name, null) + port_specification = try(each.value.check.port_specification, null) + proxy_header = try(each.value.check.proxy_header, null) + request = try(each.value.check.request, null) + response = try(each.value.check.response, null) + } + } + + dynamic "ssl_health_check" { + for_each = ( + try(each.value.type, null) == "ssl" || try(each.value.type, null) == null + ? { 1 = 1 } + : {} + ) + content { + port = try(each.value.check.port, null) + port_name = try(each.value.check.port_name, null) + port_specification = try(each.value.check.port_specification, null) + proxy_header = try(each.value.check.proxy_header, null) + request = try(each.value.check.request, null) + response = try(each.value.check.response, null) + } + } + + dynamic "http2_health_check" { + for_each = ( + try(each.value.type, null) == "http2" || try(each.value.type, null) == null + ? { 1 = 1 } + : {} + ) + content { + host = try(each.value.check.host, null) + port = try(each.value.check.port, null) + port_name = try(each.value.check.port_name, null) + port_specification = try(each.value.check.port_specification, null) + proxy_header = try(each.value.check.proxy_header, null) + request_path = try(each.value.check.request_path, null) + response = try(each.value.check.response, null) + } + } + + dynamic "log_config" { + for_each = try(each.value.logging, false) ? { 0 = 0 } : {} + content { + enable = true + } + } +} diff --git a/modules/net-xlb/ip_address.tf b/modules/net-xlb/ip_address.tf new file mode 100644 index 00000000..bb6750f8 --- /dev/null +++ b/modules/net-xlb/ip_address.tf @@ -0,0 +1,23 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +resource "google_compute_global_address" "static_ip" { + count = var.reserve_ip_address ? 1 : 0 + provider = google-beta + name = var.name + project = var.project_id + description = "Terraform managed." +} diff --git a/modules/net-xlb/outputs.tf b/modules/net-xlb/outputs.tf new file mode 100644 index 00000000..1f4754db --- /dev/null +++ b/modules/net-xlb/outputs.tf @@ -0,0 +1,60 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +output "health_checks" { + description = "Health-check resources." + value = try(google_compute_health_check.health_check, []) +} + +output "backend_services" { + description = "Backend service resources." + value = { + bucket = try(google_compute_backend_bucket.bucket, []) + group = try(google_compute_backend_service.group, []) + } +} + +output "url_map" { + description = "The url-map." + value = google_compute_url_map.url_map +} + +output "ssl_certificates" { + description = "The SSL certificate." + value = try( + google_compute_managed_ssl_certificate.managed, + google_compute_ssl_certificate.unmanaged, + null + ) +} + +output "ip_address" { + description = "The reserved global IP address." + value = try(google_compute_global_address.static_ip, null) +} + +output "target_proxy" { + description = "The target proxy." + value = try( + google_compute_target_http_proxy.http, + google_compute_target_https_proxy.https + ) +} + +output "global_forwarding_rule" { + description = "The global forwarding rule." + value = google_compute_global_forwarding_rule.forwarding_rule +} diff --git a/modules/net-xlb/ssl_certificates.tf b/modules/net-xlb/ssl_certificates.tf new file mode 100644 index 00000000..1f1dc2b8 --- /dev/null +++ b/modules/net-xlb/ssl_certificates.tf @@ -0,0 +1,51 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +locals { + managed = ( + var.https + ? { + for k, v in coalesce(var.ssl_certificates_config, {}) : + k => v if v.unmanaged_config == null + } + : {} + ) + unmanaged = ( + var.https + ? { + for k, v in coalesce(var.ssl_certificates_config, {}) : + k => v if v.unmanaged_config != null + } + : {} + ) +} + +resource "google_compute_managed_ssl_certificate" "managed" { + for_each = local.managed + project = var.project_id + name = "${var.name}-${each.key}" + managed { + domains = try(each.value.domains, null) + } +} + +resource "google_compute_ssl_certificate" "unmanaged" { + for_each = local.unmanaged + project = var.project_id + name = "${var.name}-${each.key}" + certificate = try(each.value.unmanaged_config.tls_self_signed_cert, null) + private_key = try(each.value.unmanaged_config.tls_private_key, null) +} diff --git a/modules/net-xlb/target_proxy.tf b/modules/net-xlb/target_proxy.tf new file mode 100644 index 00000000..dc9f9687 --- /dev/null +++ b/modules/net-xlb/target_proxy.tf @@ -0,0 +1,50 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +locals { + # If no SSL certificates are defined, use the default one. + # Otherwise, look in the ssl_certificates_config map. + # Otherwise, use the SSL certificate id as is (already existing). + ssl_certificates = ( + var.https == true + ? [ + for cert in try(var.target_proxy_https_config.ssl_certificates, ["default"]) : + try( + google_compute_managed_ssl_certificate.managed[cert].id, + google_compute_ssl_certificate.unmanaged[cert].id, + cert + ) + ] + : [] + ) +} + +resource "google_compute_target_http_proxy" "http" { + count = var.https ? 0 : 1 + name = var.name + project = var.project_id + description = "Terraform managed." + url_map = google_compute_url_map.url_map.id +} + +resource "google_compute_target_https_proxy" "https" { + count = var.https ? 1 : 0 + name = var.name + project = var.project_id + description = "Terraform managed." + url_map = google_compute_url_map.url_map.id + ssl_certificates = local.ssl_certificates +} diff --git a/modules/net-xlb/url_map.tf b/modules/net-xlb/url_map.tf new file mode 100644 index 00000000..dd0a6729 --- /dev/null +++ b/modules/net-xlb/url_map.tf @@ -0,0 +1,1180 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +locals { + # Look for a backend services in the config whose id is + # the default_service given in the url-map. + # If not found, use the default_service id as given + # (assuming it's already existing). + # If the variable is null, will be set to null. + _default_service = try( + google_compute_backend_bucket.bucket[var.url_map_config.default_service].id, + google_compute_backend_service.group[var.url_map_config.default_service].id, + var.url_map_config.default_service, + null + ) + + # If no backend services are specified, + # the first backend service defined is associated + default_service = ( + try(local._default_service, null) == null + && try(var.url_map_config.default_route_action.weighted_backend_services, null) == null + && try(var.url_map_config.default_url_redirect, null) == null + ? try( + google_compute_backend_bucket.bucket.0.id, + google_compute_backend_service.group.0.id, + null + ) + : null + ) +} + +resource "google_compute_url_map" "url_map" { + name = var.name + description = "Terraform managed." + project = var.project_id + default_service = local.default_service + + dynamic "header_action" { + for_each = ( + try(var.url_map_config.header_action, null) == null + ? [] + : [var.url_map_config.header_action] + ) + content { + request_headers_to_remove = try(header_action.value.request_headers_to_remove, null) + response_headers_to_remove = try(header_action.value.response_headers_to_remove, null) + + dynamic "request_headers_to_add" { + for_each = ( + try(header_action.value.request_headers_to_add, null) == null + ? [] + : [header_action.value.request_headers_to_add] + ) + content { + header_name = try(request_headers_to_add.value.header_name, null) + header_value = try(request_headers_to_add.value.header_value, null) + replace = try(request_headers_to_add.value.replace, null) + } + } + + dynamic "response_headers_to_add" { + for_each = ( + try(header_action.response_headers_to_add, null) == null + ? [] + : [header_action.response_headers_to_add] + ) + content { + header_name = try(response_headers_to_add.value.header_name, null) + header_value = try(response_headers_to_add.value.header_value, null) + replace = try(response_headers_to_add.value.replace, null) + } + } + } + } + + dynamic "host_rule" { + for_each = ( + try(var.url_map_config.host_rules, null) == null + ? [] + : var.url_map_config.host_rules + ) + content { + description = try(host_rule.value.description, null) + hosts = try(host_rule.value.hosts, null) + path_matcher = try(host_rule.value.path_matcher, null) + } + } + + dynamic "path_matcher" { + for_each = ( + try(var.url_map_config.path_matchers, null) == null + ? [] + : var.url_map_config.path_matchers + ) + content { + name = try(path_matcher.value.name, null) + description = try(path_matcher.value.description, null) + default_service = try( + google_compute_backend_bucket.bucket[var.url_map_config.default_service].id, + google_compute_backend_service.group[var.url_map_config.default_service].id, + path_matcher.value.default_service, + null + ) + + dynamic "header_action" { + for_each = ( + try(path_matcher.value.header_action, null) == null + ? [] + : [path_matcher.value.header_action] + ) + content { + request_headers_to_remove = try(header_action.value.request_headers_to_remove, null) + response_headers_to_remove = try(header_action.value.response_headers_to_remove, null) + + dynamic "request_headers_to_add" { + for_each = ( + try(header_action.value.request_headers_to_add, null) == null + ? [] + : [header_action.value.request_headers_to_add] + ) + content { + header_name = try(request_headers_to_add.value.header_name, null) + header_value = try(request_headers_to_add.value.header_value, null) + replace = try(request_headers_to_add.value.replace, null) + } + } + + dynamic "response_headers_to_add" { + for_each = ( + try(header_action.response_headers_to_add, null) == null + ? [] + : [header_action.response_headers_to_add] + ) + content { + header_name = try(response_headers_to_add.value.header_name, null) + header_value = try(response_headers_to_add.value.header_value, null) + replace = try(response_headers_to_add.value.replace, null) + } + } + } + } + + dynamic "path_rule" { + for_each = ( + try(path_matcher.value.path_rules, null) == null + ? [] + : path_matcher.value.path_rules + ) + content { + paths = try(path_rule.value.paths, null) + service = try( + google_compute_backend_bucket.bucket[path_rule.value.service].id, + google_compute_backend_service.group[path_rule.value.service].id, + path_rule.value.service, + null + ) + + dynamic "route_action" { + for_each = ( + try(path_rule.value.route_action, null) == null + ? [] + : [path_rule.value.route_action] + ) + content { + + dynamic "cors_policy" { + for_each = ( + try(route_action.value.cors_policy, null) == null + ? [] + : [route_action.value.cors_policy] + ) + content { + allow_credentials = try(cors_policy.value.allow_credentials, null) + allow_headers = try(cors_policy.value.allow_headers, null) + allow_methods = try(cors_policy.value.allow_methods, null) + allow_origin_regexes = try(cors_policy.value.allow_origin_regexes, null) + allow_origins = try(cors_policy.value.allow_origins, null) + disabled = try(cors_policy.value.disabled, null) + expose_headers = try(cors_policy.value.expose_headers, null) + max_age = try(cors_policy.value.max_age, null) + } + } + + dynamic "fault_injection_policy" { + for_each = ( + try(route_action.value.fault_injection_policy, null) == null + ? [] + : [route_action.value.fault_injection_policy] + ) + iterator = policy + content { + + dynamic "abort" { + for_each = ( + try(policy.value.abort, null) == null + ? [] + : [policy.value.abort] + ) + content { + http_status = try(abort.value.http_status, null) # Must be between 200 and 599 inclusive + percentage = try(abort.value.percentage, null) # Must be between 0.0 and 100.0 inclusive + } + } + + dynamic "delay" { + for_each = ( + try(policy.value.delay, null) == null + ? [] + : [policy.value.delay] + ) + content { + percentage = try(delay.value.percentage, null) # Must be between 0.0 and 100.0 inclusive + + dynamic "fixed_delay" { + for_each = ( + try(delay.value.fixed_delay, null) == null + ? [] + : [delay.value.fixed_delay] + ) + content { + nanos = try(fixed_delay.value.nanos, null) # Must be from 0 to 999,999,999 inclusive + seconds = try(fixed_delay.value.seconds, null) # Must be from 0 to 315,576,000,000 inclusive + } + } + } + } + } + } + + dynamic "request_mirror_policy" { + for_each = ( + try(route_action.value.request_mirror_policy, null) == null + ? [] + : [route_action.value.request_mirror_policy] + ) + iterator = policy + content { + backend_service = try( + google_compute_backend_bucket.bucket[policy.value.backend_service].id, + google_compute_backend_service.group[policy.value.backend_service].id, + policy.value.backend_service, + null + ) + } + } + + dynamic "retry_policy" { + for_each = ( + try(route_action.value.retry_policy, null) == null + ? [] + : [route_action.value.retry_policy] + ) + iterator = policy + content { + num_retries = try(policy.num_retries, null) # Must be > 0 + # Valid values at https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_url_map#retry_conditions + retry_conditions = try(policy.retry_conditions, null) + + dynamic "per_try_timeout" { + for_each = ( + try(policy.value.per_try_timeout, null) == null + ? [] + : [policy.value.per_try_timeout] + ) + iterator = timeout + content { + nanos = try(timeout.value.nanos, null) # Must be from 0 to 999,999,999 inclusive + seconds = try(timeout.value.seconds, null) # Must be from 0 to 315,576,000,000 inclusive + } + } + } + } + + dynamic "timeout" { + for_each = ( + try(route_action.value.timeout, null) == null + ? [] + : [route_action.value.timeout] + ) + content { + nanos = try(timeout.value.nanos, null) # Must be from 0 to 999,999,999 inclusive + seconds = try(timeout.value.seconds, null) # Must be from 0 to 315,576,000,000 inclusive + } + } + + dynamic "url_rewrite" { + for_each = ( + try(route_action.value.url_rewrite, null) == null + ? [] + : [route_action.value.url_rewrite] + ) + content { + host_rewrite = try(url_rewrite.value.host_rewrite, null) # Must be between 1 and 255 characters + path_prefix_rewrite = try(url_rewrite.value.path_prefix_rewrite, null) # Must be between 1 and 1024 characters + } + } + + dynamic "weighted_backend_services" { + for_each = ( + try(route_action.value.weighted_backend_services, null) == null + ? [] + : route_action.value.weighted_backend_services + ) + iterator = weighted + content { + weight = try(weighted.value.weigth, null) + backend_service = try( + google_compute_backend_bucket.bucket[weighted.value.backend_service].id, + google_compute_backend_service.group[weighted.value.backend_service].id, + policy.value.backend_service, + null + ) + dynamic "header_action" { + for_each = ( + try(path_matcher.value.header_action, null) == null + ? [] + : [path_matcher.value.header_action] + ) + content { + request_headers_to_remove = try(header_action.value.request_headers_to_remove, null) + response_headers_to_remove = try(header_action.value.response_headers_to_remove, null) + + dynamic "request_headers_to_add" { + for_each = ( + try(header_action.value.request_headers_to_add, null) == null + ? [] : + [header_action.value.request_headers_to_add] + ) + content { + header_name = try(request_headers_to_add.value.header_name, null) + header_value = try(request_headers_to_add.value.header_value, null) + replace = try(request_headers_to_add.value.replace, null) + } + } + + dynamic "response_headers_to_add" { + for_each = ( + try(header_action.response_headers_to_add, null) == null + ? [] + : [header_action.response_headers_to_add] + ) + content { + header_name = try(response_headers_to_add.value.header_name, null) + header_value = try(response_headers_to_add.value.header_value, null) + replace = try(response_headers_to_add.value.replace, null) + } + } + } + } + } + } + } + } + + dynamic "url_redirect" { + for_each = ( + try(path_rule.value.url_redirect, null) == null + ? [] + : path_rule.value.url_redirect + ) + content { + host_redirect = try(url_redirect.value.host_redirect, null) # Must be between 1 and 255 characters + https_redirect = try(url_redirect.value.https_redirect, null) + path_redirect = try(url_redirect.value.path_redirect, null) + prefix_redirect = try(url_redirect.value.prefix_redirect, null) # Must be between 1 and 1024 characters + # Valid valus at https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_url_map#redirect_response_code + redirect_response_code = try(url_redirect.value.redirect_response_code, null) + strip_query = try(url_redirect.value.strip_query, null) + } + } + } + } + + dynamic "route_rules" { + for_each = ( + try(path_matcher.value.route_rules, null) == null + ? [] + : path_matcher.value.route_rules + ) + content { + priority = try(route_rules.value.priority, null) + service = try( + google_compute_backend_bucket.bucket[route_rules.value.service].id, + google_compute_backend_service.group[route_rules.value.service].id, + route_rules.value.service, + null + ) + + dynamic "header_action" { + for_each = ( + try(path_matcher.value.header_action, null) == null + ? [] + : [path_matcher.value.header_action] + ) + content { + request_headers_to_remove = try(header_action.value.request_headers_to_remove, null) + response_headers_to_remove = try(header_action.value.response_headers_to_remove, null) + + dynamic "request_headers_to_add" { + for_each = ( + try(header_action.value.request_headers_to_add, null) == null + ? [] + : [header_action.value.request_headers_to_add] + ) + content { + header_name = try(request_headers_to_add.value.header_name, null) + header_value = try(request_headers_to_add.value.header_value, null) + replace = try(request_headers_to_add.value.replace, null) + } + } + + dynamic "response_headers_to_add" { + for_each = ( + try(header_action.response_headers_to_add, null) == null + ? [] + : [header_action.response_headers_to_add] + ) + content { + header_name = try(response_headers_to_add.value.header_name, null) + header_value = try(response_headers_to_add.value.header_value, null) + replace = try(response_headers_to_add.value.replace, null) + } + } + } + } + + dynamic "match_rules" { + for_each = ( + try(path_matcher.value.match_rules, null) == null + ? [] + : path_matcher.value.match_rules + ) + content { + full_path_match = try(match_rules.value.full_path_match, null) # Must be between 1 and 1024 characters + ignore_case = try(match_rules.value.ignore_case, null) + prefix_match = try(match_rules.value.prefix_match, null) + regex_match = try(match_rules.value.regex_match, null) + + dynamic "header_matches" { + for_each = ( + try(match_rules.value.header_matches, null) == null + ? [] + : [match_rules.value.header_matches] + ) + content { + exact_match = try(header_matches.value.exact_match, null) + header_name = try(header_matches.value.header_name, null) + invert_match = try(header_matches.value.invert_match, null) + prefix_match = try(header_matches.value.prefix_match, null) + present_match = try(header_matches.value.present_match, null) + regex_match = try(header_matches.value.regex_match, null) + suffix_match = try(header_matches.value, null) + + dynamic "range_match" { + for_each = try(header_matches.value.range_match, null) == null ? [] : [header_matches.value.range_match] + content { + range_end = try(range_match.value.range_end, null) + range_start = try(range_match.value.range_start, null) + } + } + } + } + + dynamic "metadata_filters" { + for_each = ( + try(match_rules.value.metadata_filters, null) == null + ? [] + : [match_rules.value.metadata_filters] + ) + content { + # Valid values at https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_url_map#filter_match_criteria + filter_match_criteria = try(metadata_filters.value.filter_match_criteria, null) + + dynamic "filter_labels" { + for_each = ( + try(metadata_filters.value.filter_labels, null) == null + ? [] + : metadata_filters.value.filter_labels + ) + content { + name = try(filter_labels.value.name, null) # Must be between 1 and 1024 characters + value = try(filter_labels.value.value, null) # Must be between 1 and 1024 characters + } + } + } + } + + dynamic "query_parameter_matches" { + for_each = ( + try(match_rules.value.query_parameter_matches, null) == null + ? [] + : [match_rules.value.query_parameter_matches] + ) + iterator = query + content { + exact_match = try(query.value.exact_match, null) + name = try(query.value.name, null) + present_match = try(query.value.present_match, null) + regex_match = try(query.value.regex_match, null) + } + } + } + } + + dynamic "route_action" { + for_each = ( + try(route_rules.value.route_action, null) == null + ? [] + : [route_rules.value.route_action] + ) + content { + + dynamic "cors_policy" { + for_each = ( + try(route_action.value.cors_policy, null) == null + ? [] + : [route_action.value.cors_policy] + ) + content { + allow_credentials = try(cors_policy.value.allow_credentials, null) + allow_headers = try(cors_policy.value.allow_headers, null) + allow_methods = try(cors_policy.value.allow_methods, null) + allow_origin_regexes = try(cors_policy.value.allow_origin_regexes, null) + allow_origins = try(cors_policy.value.allow_origins, null) + disabled = try(cors_policy.value.disabled, null) + expose_headers = try(cors_policy.value.expose_headers, null) + max_age = try(cors_policy.value.max_age, null) + } + } + + dynamic "fault_injection_policy" { + for_each = ( + try(route_action.value.fault_injection_policy, null) == null + ? [] + : [route_action.value.fault_injection_policy] + ) + iterator = policy + content { + + dynamic "abort" { + for_each = ( + try(policy.value.abort, null) == null + ? [] + : [policy.value.abort] + ) + content { + http_status = try(abort.value.http_status, null) # Must be between 200 and 599 inclusive + percentage = try(abort.value.percentage, null) # Must be between 0.0 and 100.0 inclusive + } + } + + dynamic "delay" { + for_each = ( + try(policy.value.delay, null) == null + ? [] + : [policy.value.delay] + ) + content { + percentage = try(delay.value.percentage, null) # Must be between 0.0 and 100.0 inclusive + + dynamic "fixed_delay" { + for_each = ( + try(delay.value.fixed_delay, null) == null + ? [] + : [delay.value.fixed_delay] + ) + content { + nanos = try(fixed_delay.value.nanos, null) # Must be from 0 to 999,999,999 inclusive + seconds = try(fixed_delay.value.seconds, null) # Must be from 0 to 315,576,000,000 inclusive + } + } + } + } + } + } + + dynamic "request_mirror_policy" { + for_each = ( + try(route_action.value.request_mirror_policy, null) == null + ? [] + : [route_action.value.request_mirror_policy] + ) + iterator = policy + content { + backend_service = try( + google_compute_backend_bucket.bucket[policy.value.backend_service].id, + google_compute_backend_service.group[policy.value.backend_service].id, + policy.value.backend_service, + null + ) + } + } + + dynamic "retry_policy" { + for_each = ( + try(route_action.value.retry_policy, null) == null + ? [] + : [route_action.value.retry_policy] + ) + iterator = policy + content { + num_retries = try(policy.num_retries, null) # Must be > 0 + # Valid values at https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_url_map#retry_conditions + retry_conditions = try(policy.retry_conditions, null) + + dynamic "per_try_timeout" { + for_each = ( + try(policy.value.per_try_timeout, null) == null + ? [] + : [policy.value.per_try_timeout] + ) + iterator = timeout + content { + nanos = try(timeout.value.nanos, null) # Must be from 0 to 999,999,999 inclusive + seconds = try(timeout.value.seconds, null) # Must be from 0 to 315,576,000,000 inclusive + } + } + } + } + + dynamic "timeout" { + for_each = ( + try(route_action.value.timeout, null) == null + ? [] + : [route_action.value.timeout] + ) + content { + nanos = try(timeout.value.nanos, null) # Must be from 0 to 999,999,999 inclusive + seconds = try(timeout.value.seconds, null) # Must be from 0 to 315,576,000,000 inclusive + } + } + + dynamic "url_rewrite" { + for_each = ( + try(route_action.value.url_rewrite, null) == null + ? [] + : [route_action.value.url_rewrite] + ) + content { + host_rewrite = try(url_rewrite.value.host_rewrite, null) # Must be between 1 and 255 characters + path_prefix_rewrite = try(url_rewrite.value.path_prefix_rewrite, null) # Must be between 1 and 1024 characters + } + } + + dynamic "weighted_backend_services" { + for_each = ( + try(route_action.value.weighted_backend_services, null) == null + ? [] + : [route_action.value.url_rewrite] + ) + iterator = weighted + content { + weight = try(weighted.value.weigth, null) + backend_service = try( + google_compute_backend_bucket.bucket[weighted.value.backend_service].id, + google_compute_backend_service.group[weighted.value.backend_service].id, + weighted.value.backend_service, + null + ) + + dynamic "header_action" { + for_each = ( + try(path_matcher.value.header_action, null) == null + ? [] : + [path_matcher.value.header_action] + ) + content { + request_headers_to_remove = try(header_action.value.request_headers_to_remove, null) + response_headers_to_remove = try(header_action.value.response_headers_to_remove, null) + + dynamic "request_headers_to_add" { + for_each = ( + try(header_action.value.request_headers_to_add, null) == null + ? [] + : [header_action.value.request_headers_to_add] + ) + content { + header_name = try(request_headers_to_add.value.header_name, null) + header_value = try(request_headers_to_add.value.header_value, null) + replace = try(request_headers_to_add.value.replace, null) + } + } + + dynamic "response_headers_to_add" { + for_each = ( + try(header_action.response_headers_to_add, null) == null + ? [] + : [header_action.response_headers_to_add] + ) + content { + header_name = try(response_headers_to_add.value.header_name, null) + header_value = try(response_headers_to_add.value.header_value, null) + replace = try(response_headers_to_add.value.replace, null) + } + } + } + } + } + } + } + } + + dynamic "url_redirect" { + for_each = ( + try(route_rules.value.url_redirect, null) == null + ? [] + : route_rules.value.url_redirect + ) + content { + host_redirect = try(url_redirect.value.host_redirect, null) # Must be between 1 and 255 characters + https_redirect = try(url_redirect.value.https_redirect, null) + path_redirect = try(url_redirect.value.path_redirect, null) + prefix_redirect = try(url_redirect.value.prefix_redirect, null) # Must be between 1 and 1024 characters + # Valid valus at https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_url_map#redirect_response_code + redirect_response_code = try(url_redirect.value.redirect_response_code, null) + strip_query = try(url_redirect.value.strip_query, null) + } + } + } + } + + dynamic "default_url_redirect" { + for_each = ( + try(path_matcher.value.default_url_redirect, null) == null + ? [] + : path_matcher.value.default_url_redirect + ) + content { + host_redirect = try(default_url_redirect.value.host_redirect, null) # Must be between 1 and 255 characters + https_redirect = try(default_url_redirect.value.https_redirect, null) + path_redirect = try(default_url_redirect.value.path_redirect, null) # Must be between 1 and 1024 characters + prefix_redirect = try(default_url_redirect.value.prefix_redirect, null) # Must be between 1 and 1024 characters + # Valid values at https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_url_map#redirect_response_code + redirect_response_code = try(default_url_redirect.value.redirect_response_code, null) + strip_query = try(default_url_redirect.value.strip_query, null) + } + } + + dynamic "default_route_action" { + for_each = ( + try(path_matcher.value.default_route_action, null) == null + ? [] + : path_matcher.value.default_route_action + ) + content { + dynamic "cors_policy" { + for_each = ( + try(default_route_action.value.cors_policy, null) == null + ? [] + : [default_route_action.value.cors_policy] + ) + content { + allow_credentials = try(cors_policy.value.allow_credentials, null) + allow_headers = try(cors_policy.value.allow_headers, null) + allow_methods = try(cors_policy.value.allow_methods, null) + allow_origin_regexes = try(cors_policy.value.allow_origin_regexes, null) + allow_origins = try(cors_policy.value.allow_origins, null) + disabled = try(cors_policy.value.disabled, null) + expose_headers = try(cors_policy.value.expose_headers, null) + max_age = try(cors_policy.value.max_age, null) + } + } + + dynamic "fault_injection_policy" { + for_each = ( + try(default_route_action.value.fault_injection_policy, null) == null + ? [] + : [default_route_action.value.fault_injection_policy] + ) + iterator = policy + content { + + dynamic "abort" { + for_each = ( + try(policy.value.abort, null) == null + ? [] + : [policy.value.abort] + ) + content { + http_status = try(abort.value.http_status, null) # Must be between 200 and 599 inclusive + percentage = try(abort.value.percentage, null) # Must be between 0.0 and 100.0 inclusive + } + } + + dynamic "delay" { + for_each = ( + try(policy.value.delay, null) == null + ? [] + : [policy.value.delay] + ) + content { + percentage = try(delay.value.percentage, null) # Must be between 0.0 and 100.0 inclusive + + dynamic "fixed_delay" { + for_each = ( + try(delay.value.fixed_delay, null) == null + ? [] + : [delay.value.fixed_delay] + ) + content { + nanos = try(fixed_delay.value.nanos, null) # Must be from 0 to 999,999,999 inclusive + seconds = try(fixed_delay.value.seconds, null) # Must be from 0 to 315,576,000,000 inclusive + } + } + } + } + } + } + + dynamic "request_mirror_policy" { + for_each = ( + try(default_route_action.value.request_mirror_policy, null) == null + ? [] + : [default_route_action.value.request_mirror_policy] + ) + iterator = policy + content { + backend_service = try( + google_compute_backend_bucket.bucket[policy.value.backend_service].id, + google_compute_backend_service.group[policy.value.backend_service].id, + policy.value.backend_service, + null + ) + } + } + + dynamic "retry_policy" { + for_each = ( + try(default_route_action.value.retry_policy, null) == null + ? [] + : [default_route_action.value.retry_policy] + ) + iterator = policy + content { + num_retries = try(policy.num_retries, null) # Must be > 0 + # Valid values at https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_url_map#retry_conditions + retry_conditions = try(policy.retry_conditions, null) + + dynamic "per_try_timeout" { + for_each = ( + try(policy.value.per_try_timeout, null) == null + ? [] + : [policy.value.per_try_timeout] + ) + iterator = timeout + content { + nanos = try(timeout.value.nanos, null) # Must be from 0 to 999,999,999 inclusive + seconds = try(timeout.value.seconds, null) # Must be from 0 to 315,576,000,000 inclusive + } + } + } + } + + dynamic "timeout" { + for_each = ( + try(default_route_action.value.timeout, null) == null + ? [] + : [default_route_action.value.timeout] + ) + content { + nanos = try(timeout.value.nanos, null) # Must be from 0 to 999,999,999 inclusive + seconds = try(timeout.value.seconds, null) # Must be from 0 to 315,576,000,000 inclusive + } + } + + dynamic "url_rewrite" { + for_each = ( + try(default_route_action.value.url_rewrite, null) == null + ? [] + : [default_route_action.value.url_rewrite] + ) + content { + host_rewrite = try(url_rewrite.value.host_rewrite, null) # Must be between 1 and 255 characters + path_prefix_rewrite = try(url_rewrite.value.path_prefix_rewrite, null) # Must be between 1 and 1024 characters + } + } + + dynamic "weighted_backend_services" { + for_each = ( + try(default_route_action.value.weighted_backend_services, null) == null + ? [] + : default_route_action.value.weighted_backend_services + ) + iterator = weighted + content { + weight = try(weighted.value.weigth, null) + backend_service = try( + google_compute_backend_bucket.bucket[weighted.value.backend_service].id, + google_compute_backend_service.group[weighted.value.backend_service].id, + weighted.value.backend_service, + null + ) + + dynamic "header_action" { + for_each = ( + try(path_matcher.value.header_action, null) == null + ? [] + : [path_matcher.value.header_action] + ) + content { + request_headers_to_remove = try(header_action.value.request_headers_to_remove, null) + response_headers_to_remove = try(header_action.value.response_headers_to_remove, null) + + dynamic "request_headers_to_add" { + for_each = ( + try(header_action.value.request_headers_to_add, null) == null + ? [] + : [header_action.value.request_headers_to_add] + ) + content { + header_name = try(request_headers_to_add.value.header_name, null) + header_value = try(request_headers_to_add.value.header_value, null) + replace = try(request_headers_to_add.value.replace, null) + } + } + + dynamic "response_headers_to_add" { + for_each = ( + try(header_action.response_headers_to_add, null) == null + ? [] + : [header_action.response_headers_to_add] + ) + content { + header_name = try(response_headers_to_add.value.header_name, null) + header_value = try(response_headers_to_add.value.header_value, null) + replace = try(response_headers_to_add.value.replace, null) + } + } + } + } + } + } + } + } + } + } + + # Up to 100 tests per url_map + dynamic "test" { + for_each = ( + try(var.url_map_config.tests, null) == null + ? [] + : var.url_map_config.tests + ) + content { + description = try(test.value.description, null) + host = try(test.value.host, null) + path = try(test.value.path, null) + service = try( + google_compute_backend_bucket.bucket[test.value.service].id, + google_compute_backend_service.group[test.value.service].id, + test.value.service, + null + ) + } + } + + dynamic "default_url_redirect" { + for_each = ( + try(var.url_map_config.default_url_redirect, null) == null + ? [] + : [var.url_map_config.default_url_redirect] + ) + content { + host_redirect = try(default_url_redirect.value.host_redirect, null) # Must be between 1 and 255 characters + https_redirect = try(default_url_redirect.value.https_redirect, null) + path_redirect = try(default_url_redirect.value.path_redirect, null) # Must be between 1 and 1024 characters + prefix_redirect = try(default_url_redirect.value.prefix_redirect, null) # Must be between 1 and 1024 characters + # Valid values at https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_url_map#redirect_response_code + redirect_response_code = try(default_url_redirect.value.redirect_response_code, null) + strip_query = try(default_url_redirect.value.strip_query, null) + } + } + + dynamic "default_route_action" { + for_each = ( + try(var.url_map_config.default_route_action, null) == null + ? [] + : [var.url_map_config.default_route_action] + ) + content { + dynamic "cors_policy" { + for_each = ( + try(default_route_action.value.cors_policy, null) == null + ? [] + : [default_route_action.value.cors_policy] + ) + content { + allow_credentials = try(cors_policy.value.allow_credentials, null) + allow_headers = try(cors_policy.value.allow_headers, null) + allow_methods = try(cors_policy.value.allow_methods, null) + allow_origin_regexes = try(cors_policy.value.allow_origin_regexes, null) + allow_origins = try(cors_policy.value.allow_origins, null) + disabled = try(cors_policy.value.disabled, null) + expose_headers = try(cors_policy.value.expose_headers, null) + max_age = try(cors_policy.value.max_age, null) + } + } + + dynamic "fault_injection_policy" { + for_each = ( + try(default_route_action.value.fault_injection_policy, null) == null + ? [] + : [default_route_action.value.fault_injection_policy] + ) + iterator = policy + content { + + dynamic "abort" { + for_each = ( + try(policy.value.abort, null) == null + ? [] + : [policy.value.abort] + ) + content { + http_status = try(abort.value.http_status, null) # Must be between 200 and 599 inclusive + percentage = try(abort.value.percentage, null) # Must be between 0.0 and 100.0 inclusive + } + } + + dynamic "delay" { + for_each = ( + try(policy.value.delay, null) == null + ? [] + : [policy.value.delay] + ) + content { + percentage = try(delay.value.percentage, null) # Must be between 0.0 and 100.0 inclusive + + dynamic "fixed_delay" { + for_each = try(delay.value.fixed_delay, null) == null ? [] : [delay.value.fixed_delay] + content { + nanos = try(fixed_delay.value.nanos, null) # Must be from 0 to 999,999,999 inclusive + seconds = try(fixed_delay.value.seconds, null) # Must be from 0 to 315,576,000,000 inclusive + } + } + } + } + } + } + + dynamic "request_mirror_policy" { + for_each = ( + try(default_route_action.value.request_mirror_policy, null) == null + ? [] + : [default_route_action.value.request_mirror_policy] + ) + iterator = policy + content { + backend_service = try( + google_compute_backend_bucket.bucket[policy.value.backend_service].id, + google_compute_backend_service.group[policy.value.backend_service].id, + policy.value.backend_service, + null + ) + } + } + + dynamic "retry_policy" { + for_each = ( + try(default_route_action.value.retry_policy, null) == null + ? [] + : [default_route_action.value.retry_policy] + ) + iterator = policy + content { + num_retries = try(policy.num_retries, null) # Must be > 0 + # Valid values at https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_url_map#retry_conditions + retry_conditions = try(policy.retry_conditions, null) + + dynamic "per_try_timeout" { + for_each = ( + try(policy.value.per_try_timeout, null) == null + ? [] + : [policy.value.per_try_timeout] + ) + iterator = timeout + content { + nanos = try(timeout.value.nanos, null) # Must be from 0 to 999,999,999 inclusive + seconds = try(timeout.value.seconds, null) # Must be from 0 to 315,576,000,000 inclusive + } + } + } + } + + dynamic "timeout" { + for_each = ( + try(default_route_action.value.timeout, null) == null + ? [] + : [default_route_action.value.timeout] + ) + content { + nanos = try(timeout.value.nanos, null) # Must be from 0 to 999,999,999 inclusive + seconds = try(timeout.value.seconds, null) # Must be from 0 to 315,576,000,000 inclusive + } + } + + dynamic "url_rewrite" { + for_each = ( + try(default_route_action.value.url_rewrite, null) == null + ? [] + : [default_route_action.value.url_rewrite] + ) + content { + host_rewrite = try(url_rewrite.value.host_rewrite, null) # Must be between 1 and 255 characters + path_prefix_rewrite = try(url_rewrite.value.path_prefix_rewrite, null) # Must be between 1 and 1024 characters + } + } + + dynamic "weighted_backend_services" { + for_each = ( + try(default_route_action.value.weighted_backend_services, null) == null + ? [] + : default_route_action.value.weighted_backend_services + ) + iterator = weighted + content { + weight = try(weighted.value.weigth, null) + backend_service = try( + google_compute_backend_bucket.bucket[weighted.value.backend_service].id, + google_compute_backend_service.group[weighted.value.backend_service].id, + weighted.value.backend_service, + null + ) + + dynamic "header_action" { + for_each = ( + try(weighted.value.header_action, null) == null + ? [] + : [weighted.value.header_action] + ) + content { + request_headers_to_remove = try(header_action.value.request_headers_to_remove, null) + response_headers_to_remove = try(header_action.value.response_headers_to_remove, null) + + dynamic "request_headers_to_add" { + for_each = ( + try(header_action.value.request_headers_to_add, null) == null + ? [] + : [header_action.value.request_headers_to_add] + ) + content { + header_name = try(request_headers_to_add.value.header_name, null) + header_value = try(request_headers_to_add.value.header_value, null) + replace = try(request_headers_to_add.value.replace, null) + } + } + + dynamic "response_headers_to_add" { + for_each = ( + try(header_action.response_headers_to_add, null) == null + ? [] + : [header_action.response_headers_to_add] + ) + content { + header_name = try(response_headers_to_add.value.header_name, null) + header_value = try(response_headers_to_add.value.header_value, null) + replace = try(response_headers_to_add.value.replace, null) + } + } + } + } + } + } + } + } +} diff --git a/modules/net-xlb/variables.tf b/modules/net-xlb/variables.tf new file mode 100644 index 00000000..1d7050f9 --- /dev/null +++ b/modules/net-xlb/variables.tf @@ -0,0 +1,218 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +variable "name" { + description = "Load balancer name." + type = string +} + +variable "project_id" { + description = "Project id." + type = string +} + +variable "health_checks_config_defaults" { + description = "Auto-created health check default configuration." + type = object({ + type = string # http https tcp ssl http2 + check = map(any) # actual health check block attributes + options = map(number) # interval, thresholds, timeout + logging = bool + }) + default = { + type = "http" + logging = false + options = {} + check = { + port_specification = "USE_SERVING_PORT" + } + } +} + +variable "health_checks_config" { + description = "Custom health checks configuration." + type = map(object({ + type = string # http https tcp ssl http2 + check = map(any) # actual health check block attributes + options = map(number) # interval, thresholds, timeout + logging = bool + })) + default = {} +} + +variable "backend_services_config" { + description = "The backends services configuration." + type = map(object({ + enable_cdn = bool + + cdn_config = object({ + cache_mode = string + client_ttl = number + default_ttl = number + max_ttl = number + negative_caching = bool + negative_caching_policy = map(number) + serve_while_stale = bool + signed_url_cache_max_age_sec = string + }) + + bucket_config = object({ + bucket_name = string + options = object({ + custom_response_headers = list(string) + }) + }) + + group_config = object({ + backends = list(object({ + group = string # IG or NEG FQDN address + options = object({ + balancing_mode = string # Can be UTILIZATION, RATE, CONNECTION + capacity_scaler = number # Valid range is [0.0,1.0] + max_connections = number + max_connections_per_instance = number + max_connections_per_endpoint = number + max_rate = number + max_rate_per_instance = number + max_rate_per_endpoint = number + max_utilization = number + }) + })) + + # Optional health check ids for backend service groups. + # Will lookup for ids in health_chacks_config first, + # then will use the id as is. If no ids are defined + # at all (null, []) health_checks_config_defaults is used + health_checks = list(string) + + log_config = object({ + enable = bool + sample_rate = number # must be in [0, 1] + }) + + options = object({ + affinity_cookie_ttl_sec = number + custom_request_headers = list(string) + custom_response_headers = list(string) + connection_draining_timeout_sec = number + load_balancing_scheme = string # only EXTERNAL (default) makes sense here + locality_lb_policy = string + port_name = string + protocol = string + security_policy = string + session_affinity = string + timeout_sec = number + + circuits_breakers = object({ + max_requests_per_connection = number # Set to 1 to disable keep-alive + max_connections = number # Defaults to 1024 + max_pending_requests = number # Defaults to 1024 + max_requests = number # Defaults to 1024 + max_retries = number # Defaults to 3 + }) + + consistent_hash = object({ + http_header_name = string + minimum_ring_size = string + http_cookie = object({ + name = string + path = string + ttl = object({ + seconds = number + nanos = number + }) + }) + }) + + iap = object({ + oauth2_client_id = string + oauth2_client_secret = string + oauth2_client_secret_sha256 = string + }) + }) + }) + })) + default = {} +} + +variable "url_map_config" { + description = "The url-map configuration." + type = object({ + default_service = string + default_route_action = any + default_url_redirect = map(any) + header_action = any + host_rules = list(any) + path_matchers = list(any) + tests = list(map(string)) + }) + default = null +} + +variable "ssl_certificates_config" { + description = "The SSL certificate configuration." + type = map(object({ + domains = list(string) + # If unmanaged_config is null, the certificate will be managed + unmanaged_config = object({ + tls_private_key = string + tls_self_signed_cert = string + }) + })) + default = { + default = { + domains = ["example.com"], + unmanaged_config = null + } + } +} + +variable "target_proxy_https_config" { + description = "The HTTPS target proxy configuration." + type = object({ + ssl_certificates = list(string) + }) + default = null +} + +variable "global_forwarding_rule_config" { + description = "Global forwarding rule configurations." + type = object({ + ip_protocol = string + ip_version = string + load_balancing_scheme = string + port_range = string + + }) + default = { + load_balancing_scheme = "EXTERNAL" + ip_protocol = "TCP" + ip_version = "IPV4" + port_range = "80" # 80, 8080, 443 + } +} + +variable "https" { + description = "Whether to enable HTTPS." + type = bool + default = false +} + +variable "reserve_ip_address" { + description = "Whether to reserve a static global IP address." + type = bool + default = false +} diff --git a/modules/net-xlb/versions.tf b/modules/net-xlb/versions.tf new file mode 100644 index 00000000..76419788 --- /dev/null +++ b/modules/net-xlb/versions.tf @@ -0,0 +1,27 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +terraform { + required_version = ">= 1.0.0" + required_providers { + google = { + source = "hashicorp/google" + version = ">= 4.0.0" + } + google-beta = { + source = "hashicorp/google-beta" + version = ">= 4.0.0" + } + } +}