Terraform module for provisioning of GCP LB on top of precreated named NEGs, Cloud Run services and GCS buckets passed as parameter to this module.
data "cloudflare_zones" "ackee_cz" {
filter {
name = "ackee.cz"
}
}
resource "google_storage_bucket" "test" {
name = "test-randompostfix-98582341"
location = var.region
storage_class = "STANDARD"
uniform_bucket_level_access = true
website {
main_page_suffix = "index.html"
}
}
module "api_unicorn" {
source = "git::ssh://[email protected]/Infra/tf-module/terraform-gcp-lb.git?ref=master"
name = "main-${var.project}-${var.namespace}"
project = var.project
region = var.region
self_signed_tls = true
services = [
{
type = "neg"
name = "ackee-api-unicorn"
zone = var.zone
},
{
type = "bucket"
bucket_name = "${google_storage_bucket.test.name}"
},
{
type = "cloudrun"
service_name = cloud-run-service
}
]
url_map = {
matcher1 = {
hostnames = ["api-unicorn.ackee.cz", "api-unicorn2.ackee.cz"]
path_rules = [
{
paths = ["/api/v1/*"]
service = {
type = "neg"
name = "ackee-api-unicorn"
zone = var.zone
}
},
]
}
matcher2 = {
hostnames = ["api-unicorn.ackee.cz", "api-unicorn2.ackee.cz"]
path_rules = [
{
paths = ["/*"]
service = "${google_storage_bucket.test.name}"
},
]
}
matcher3 = {
hostnames = ["cloud-run-service.ackee.cz"]
path_rules = [
{
paths = ["/*"]
service = "cloud-run-service"
},
]
}
}
}
resource "cloudflare_record" "api" {
zone_id = data.cloudflare_zones.ackee_cz.zones[0].id
name = "api-unicorn"
value = module.api_unicorn.ip_address
type = "A"
ttl = 1
proxied = true
}
If NEG named ackee-api-unicorn
exists and CF is set to "SSL:Full" you should have working app now on https://api-unicorn.ackee.cz and https://api-unicorn2.ackee.cz
data "cloudflare_zones" "ackee_cz" {
filter {
name = "ackee.cz"
}
}
module "api_unicorn" {
source = "git::ssh://[email protected]/Infra/tf-module/terraform-gcp-lb.git?ref=master"
name = "main-${var.project}-${var.namespace}"
project = var.project
region = var.region
google_managed_tls = true
services = [
{
type = "neg"
name = "ackee-api-unicorn"
zone = var.zone
},
]
url_map = {
matcher1 = {
hostnames = ["api-unicorn.ackee.cz", "api-unicorn2.ackee.cz"]
path_rules = [
{
paths = ["/api/v1/*"]
service = "ackee-api-unicorn"
},
]
}
}
}
resource "cloudflare_record" "api" {
zone_id = data.cloudflare_zones.ackee_cz.zones[0].id
name = "api-unicorn"
value = module.api_unicorn.ip_address
type = "A"
ttl = 1
proxied = false
}
If NEG named ackee-api-unicorn
exists you should have working app now on https://api-unicorn.ackee.cz
Beware: If you use more then one hostname with Google-managed certificate, only one certificate, with first hostname in list, will be created.
HTTPS Load-balancer with preexisting NEG, Google-managed certificate and Cloudflare DNS record creation:
data "google_compute_network_endpoint_group" "old_neg" {
name = "k8s1-aab5af95-production-ackee-unicorn-80-bafd3c69"
zone = "europe-west3-c"
count = 1
}
data "cloudflare_zones" "ackee_cz" {
filter {
name = "ackee.cz"
}
}
module "api_unicorn" {
source = "git::ssh://[email protected]/Infra/tf-module/terraform-gcp-lb.git?ref=master"
name = "main-${var.project}-${var.namespace}"
project = var.project
region = var.region
google_managed_tls = true
services = [
{
type = "neg"
name = "ackee-api-unicorn"
zone = var.zone
additional_negs = [data.google_compute_network_endpoint_group.old_neg]
http_backend_protocol = "HTTP"
},
]
url_map = {
matcher1 = {
hostnames = ["api-unicorn.ackee.cz""]
path_rules = [
{
paths = ["/*"]
service = "ackee-api-unicorn"
},
]
}
}
}
resource "cloudflare_record" "api" {
zone_id = data.cloudflare_zones.ackee_cz.zones[0].id
name = "api-unicorn"
value = module.api_unicorn.ip_address
type = "A"
ttl = 1
proxied = false
}
If we pass data.google_compute_network_endpoint_group
resource as value for additional_negs
parameter, then our new load-balancer
gets created from new named NEG's auto discovered by name in neg_name
parameter and from NEG's from additional_negs
parameter -
this should be used when migrating from old setup, so we balance to both new and old application.
module "api_unicorn" {
source = "git::ssh://[email protected]/Infra/tf-module/terraform-gcp-lb.git?ref=master"
name = "main-${var.project}-${var.namespace}"
project = var.project
region = var.region
services = [
{
type = "neg"
name = "ackee-api-unicorn"
zone = var.zone
additional_negs = [data.google_compute_network_endpoint_group.old_neg]
http_backend_protocol = "HTTP"
},
]
url_map = {
matcher1 = {
hostnames = ["api-unicorn.ackee.cz""]
path_rules = [
{
paths = ["/*"]
service = "ackee-api-unicorn"
},
]
}
}
certificate = file("${path.root}/tls/certificate_chain.crt")
private_key = file("${path.root}/tls/private.key")
}
It is recommended to use some secure storage (eg. Vault) and pass value from here, rather then saving plaintext private key into git repo
Error: Error creating SslCertificate: googleapi: Error 409: The resource ... already exists, alreadyExists
This might show once you are adding new hostname to the load balancer and SSL certificate web_lb_cert
needs to add the hostname into dns_names
. Terraform is trying to update the certificate in-place or creates a certificate with the same name. For that, you might want to do these few steps manually:
Get certificates from the state file:
CERT=`mktemp`
CERT_KEY=`mktemp`
terraform show -json | jq -r --arg MODULE "$MODULE" '.values.root_module.child_modules[] | select (.address=="module.lb_72541") | .resources[] | select(.address=="module.lb_72541.google_compute_ssl_certificate.gcs_certs[0]") | .values.private_key' > $CERT_KEY
terraform show -json | jq -r --arg MODULE "$MODULE" '.values.root_module.child_modules[] | select (.address=="module.lb_72541") | .resources[] | select(.address=="module.lb_72541.google_compute_ssl_certificate.gcs_certs[0]") | .values.certificate' > $CERT
where module.lb_72541
is the name of the module used in your Terraform.
Create new temporary certificate:
gcloud compute ssl-certificates create tmp --certificate=$CERT --private-key=$CERT_KEY
gcloud compute target-https-proxies update "NAME_OF_PROXY" --ssl-certificates "tmp"
The name of the https proxy can be found in the state file:
terraform state show 'module.lb_72541.google_compute_target_https_proxy.self_signed[0]'
Remove the old certificate:
Get the name from the error output. Let's say you have this error:
Error: Error creating SslCertificate: googleapi: Error 409: The resource 'projects/awesome-project/global/sslCertificates/main-awesome-project-development-72541-cert-self-signed' already exists, alreadyExists
Then command will look like this:
gcloud compute ssl-certificates delete main-awesome-project-development-72541-cert-self-signed
Run terraform apply
to create a certificate from Terraform and once done delete the temporary certificate:
gcloud compute ssl-certificates delete tmp
BEWARE: Network Endpoint Groups REFERENCED BY THIS MODULE MUST EXIST BEFORE YOU USE THIS MODULE, OTHERWISE IT WILL FAIL WITH ERROR SIMILIAR TO:
Error: Required attribute is not set
on ../load-balancer.tf line 68, in resource "google_compute_backend_service" "cn_lb":
68: resource "google_compute_backend_service" "cn_lb" {
Also note, that purging app (typically with helm delete
) does not automatically cleanup existing NEGs
Because of chicken-egg mentioned in previous section, it is also not so easy to use provider example code. Running just terraform apply
will fail
on similiar error. Workaround is to create testing NEG first and load balancer above it in next step.
terraform apply -target=google_compute_network_endpoint_group.neg_one -target=google_compute_network_endpoint_group.neg_two
terraform apply
Install pre-commit hooks by running following commands:
brew install pre-commit
pre-commit install
Name | Version |
---|---|
terraform | >= 1.0 |
Name | Version |
---|---|
5.8.0 | |
google-beta | 5.8.0 |
random | 3.6.0 |
tls | 4.0.5 |
No modules.
Name | Description | Type | Default | Required |
---|---|---|---|---|
allow_non_tls_frontend | If true, enables port 80 frontend - creates non-TLS (http://) variant of LB | string |
false |
no |
backend_bucket_location | GCS location(https://cloud.google.com/storage/docs/locations) of bucket where invalid requests are routed. | string |
"EUROPE-WEST3" |
no |
certificate | The certificate in PEM format. The certificate chain must be no greater than 5 certs long. The chain must include at least one intermediate cert. Note: This property is sensitive and will not be displayed in the plan. | string |
null |
no |
check_interval_sec | How often (in seconds) to send a health check. The default value is 5 seconds. | number |
5 |
no |
create_logging_sink_bucket | If true, creates bucket and set up logging sink | bool |
false |
no |
custom_health_check_ports | Custom ports for GCE health checks, not needed unless your services are not in 30000-32767 or 3000, 5000 | list(string) |
[] |
no |
custom_target_http_proxy_name | Custom name for HTTP proxy name used instead of non-tls-proxy- | string |
"" |
no |
custom_url_map_name | Custom name for URL map name used instead of lb-var.name | string |
"" |
no |
default_iap_setup | In case you use the same IAP setup for all backends | object({ |
null |
no |
default_network_name | Default firewall network name, used to place a default fw allowing google's default health checks. Leave blank if you use GKE ingress-provisioned LB (now deprecated) | string |
"default" |
no |
dont_use_dns_names_in_certificate | Due to backward compatibility, TLS setup can omit setup of dns_names in self signed certificate | bool |
false |
no |
google_managed_tls | If true, creates Google-managed TLS cert | bool |
false |
no |
health_check_request_path | Health checked path (URN) | string |
"/healthz" |
no |
healthy_threshold | A so-far unhealthy instance will be marked healthy after this many consecutive successes. The default value is 2. | number |
2 |
no |
http_backend_protocol | HTTP backend protocol, one of: HTTP/HTTP2 | string |
"HTTP" |
no |
http_backend_timeout | Time of http request timeout (in seconds) | string |
"30" |
no |
iap_setup | Service setup for IAP, overwrites default_iap_setup if used | map(object({ |
{} |
no |
keys_alg | Algorithm used for private keys | string |
"RSA" |
no |
keys_valid_period | Validation period of the self signed key | number |
29200 |
no |
log_config_sample_rate | The value of the field must be in [0, 1]. This configures the sampling rate of requests to the load balancer where 1.0 means all logged requests are reported and 0.0 means no logged requests are reported. The default value is 1.0. | string |
"1.0" |
no |
logging_sink_bucket_retency | Number of days after which log files are deleted from bucket | number |
730 |
no |
managed_certificate_name | Name of Google-managed certificate. Useful when migrating from Ingress-provisioned load balancer | string |
null |
no |
mask_metrics_endpoint | If set, requests /metrics will be sent to default backend | bool |
false |
no |
name | Instance name | string |
"default_value" |
no |
non_tls_global_forwarding_rule_name | Global non tls forwarding rule name, if set, changes name of non-tls forwarding rule | string |
"" |
no |
private_key | The write-only private key in PEM format. Note: This property is sensitive and will not be displayed in the plan. | string |
null |
no |
project | Project ID | string |
n/a | yes |
random_suffix_size | Size of random suffix | number |
8 |
no |
region | GCP region where we will look for NEGs | string |
n/a | yes |
self_signed_tls | If true, creates self-signed TLS cert | bool |
false |
no |
services | List of services: cloudrun, neg, bucket, ... to be used in the map | list(object({ |
n/a | yes |
timeout_sec | How long (in seconds) to wait before claiming failure. The default value is 5 seconds. It is invalid for timeout_sec to have greater value than check_interval_sec. | number |
5 |
no |
unhealthy_threshold | A so-far healthy instance will be marked unhealthy after this many consecutive failures. The default value is 2. | number |
2 |
no |
url_map | Url map setup | map(object({ |
n/a | yes |
use_random_suffix_for_network_endpoint_group | If true, uses random suffix for NEG name | bool |
true |
no |
zone | GCP zone where we will look for NEGs - optional parameter, if not set, the we will automatically search in all zones in region | string |
null |
no |
Name | Description |
---|---|
ip_address | IP address |