GCP Load Balancers are often built using the web console or CLI, which makes backup and restoration difficult. By using OpenTofu or Terraform, you can implement Infrastructure as Code (IaC) even for existing resources.
By preparing an import.tf like the following and running tofu plan -generate-config-out=export.tf, you can export the current configuration. Since tofu plan queries GCP, you must be authenticated with a user who has appropriate access rights to GCP objects via gcloud auth application-default login.
# gcloud compute forwarding-rules list --global --uri
import {
id = "projects/<project-id>/global/forwardingRules/<object-id>"
to = google_compute_global_forwarding_rule.<unique-name>
}
# gcloud compute target-https-proxies list --global --uri
import {
id = "projects/<project-id>/global/targetHttpsProxies/<object-id>"
to = google_compute_target_https_proxy.<unique-name>
}
# gcloud compute target-http-proxies list --global --uri
import {
id = "projects/<project-id>/global/targetHttpProxies/<object-id>"
to = google_compute_target_http_proxy.<unique-name>
}
# gcloud compute url-maps list --uri
import {
id = "projects/<project-id>/global/urlMaps/<object-id>"
to = google_compute_url_map.<unique-name>
}
# gcloud compute backend-buckets list --uri
import {
id = "projects/<project-id>/global/backendBuckets/<object-id>"
to = google_compute_global_backend_bucket.<unique-name>
}
# gcloud compute backend-services list --uri
import {
id = "projects/<project-id>/global/backendServices/<object-id>"
to = google_compute_backend_service.<unique-name>
}
# gcloud compute health-checks list --uri
import {
id = "projects/<project-id>/global/healthChecks/<object-id>"
to = google_compute_health_check.<unique-name>
}
# gcloud compute firewall-rules list --uri
import {
id = "projects/<project-id>/global/firewalls/<object-id>"
to = google_compute_firewall.<unique-name>
}
# gcloud compute ssl-certificates list --global --uri
# NOTE: TLS certificates can be used for configuration changes if managed. Unmanaged ones are hard to manage declaratively.
import {
id = "projects/<project-id>/global/sslCertificates/<object-id>"
to = google_compute_ssl_certificate.<unique-name>
}
# gcloud compute addresses list --global --uri
# NOTE: IP addresses are hard to manage declaratively.
import {
id = "projects/<project-id>/global/addresses/<object-id>"
to = google_compute_global_address.<unique-name>
}
For the id, specify the URL of the target resource obtained with gcloud compute <gcp-object-type> list. For to, specify the type corresponding to the GCP object as the prefix, and provide a <unique-name> for identification within OpenTofu.
The exported configuration file will likely not be optimal, so refactoring is necessary. You will likely want to remove many attributes that tofu specifies by default. Since the meaning of each field is not always self-evident even if concise, it’s important to verify them in the reference documentation. Additionally, using for_each can help consolidate blocks where only a few parameters differ, maintaining code brevity.
The workflow consists of checking the definitions with tofu plan and applying changes with tofu apply.
Management Points
As shown in the previous examples, Load Balancers have many associated objects, which can make the configuration difficult to understand later. Bundling them by purpose in OpenTofu HCL makes management easier. Especially when building multiple clusters within a project, native tools like the Cloud Console list GCP objects by type, which can obscure dependency relationships.
Firewalls
Among Load Balancer related objects, firewall rule settings can be the most difficult to understand. There are prerequisites described in Firewall rules for load balancing. Without firewall settings, health checks will fail first. A point often not explicitly mentioned in the documentation is that actual traffic also follows these conditions. If you only permit the health check target port and don’t configure settings for actual traffic on a different port, you may experience a confusing situation where the console shows everything is up, but requests time out. Since firewalls are global objects, it’s almost impossible to track which cluster depends on which rule without codifying them.
Adding depends_on Blocks
depends_on blocks are useful for structuring code. These are not recognized automatically and must be added manually. You define them using the HCL names:
resource "google_compute_target_https_proxy" "proxy_set" {
depends_on = [
google_compute_url_map.url_map,
google_compute_firewall.allow_hc_v6,
google_compute_firewall.allow_hc_v4,
google_compute_firewall.allow_glb_v6,
google_compute_firewall.allow_glb_v4,
]
}
As in this example, there are constraints such as not being able to mix IPv6 and IPv4 or HTTPS and HTTP within a single object. As the Load Balancer configuration grows, defining dependencies allows tofu plan to detect inconsistencies before applying changes. Furthermore, there are actual dependencies between GCP objects; requesting a build in a flat manner can result in errors due to missing prerequisite objects. depends_on ensures that execution waits according to the definitions, leading to stable behavior during clean builds.
Obtaining IP Addresses
One strategy is to not manage GCP IP addresses in HCL. While they can be created with tofu, IP addresses are not resources where you can “gain” a specific desired address, so the effect of declaration is weak. If you simply obtain and lock an IP address from the Cloud Console, you can just specify it in google_compute_global_forwarding_rule.ip_address. If you want to obtain an ephemeral IP, specify IPV4|IPV6 in google_compute_global_forwarding_rule.ip_version. After startup, lock it as a static IP in the Cloud Console and update the HCL accordingly. SSL certificates are handled similarly; if you are registering and updating them on GCP via an external process, you can simply reference those objects.
Cleanup after Export
Files generated by tofu plan -generate-config-out contain many fields automatically assigned by GCP and read-only attributes that do not need management in HCL. Leaving these can make the code hard to read and may cause unnecessary drifts during future provider updates.
Specifically, you should review or remove the following fields:
id,self_link: Attributes fixed after resource creation.creation_timestamp: The creation date and time.project: If configured at the provider level, it doesn’t need to be specified for each resource.fingerprint: Values for preventing update conflicts, often found inbackend_service. Not needed during initial import.
Consolidating common values such as project IDs and regions into locals or variables makes it easier to deploy to multiple environments.
Verification and Achieving Zero-drift
After completing the tofu apply with the import blocks, immediately run a standard tofu plan to verify that it shows “No changes”.
OpenTofu compares the actual GCP configuration with the terraform.tfstate (state file), which records the state of managed resources. Since the state file plays a critical role as the “source of truth” in IaC, establishing a zero-diff state immediately after import is a fundamental prerequisite for stable long-term operation.
If there are differences, the generated HCL definitions slightly mismatch the actual configuration on GCP.
Be particularly careful if a resource recreation (Replace) occurs, such as “Plan: 1 to add, 0 to change, 1 to destroy”. Recreating a Load Balancer can lead to IP address changes or service downtime. You must carefully examine the differences and adjust the HCL to match the actual configuration.
Standard Workflow Preparation
Once IaC implementation is complete, setting up a safe operational environment for the entire team is essential. The codebase should be tracked using version control systems like Git.
.tfstate: The state file (.tfstate) is the only source of truth defining the current state of your system. Even if you have the HCL, losing this file means OpenTofu can no longer recognize your existing resources. This creates a severe risk where an accidentalapplymight attempt to recreate or overwrite resources that are already in production, leading to potential collisions or unintended destruction, and requiring a dangerous, labor-intensive re-import process to recover..terraform.lock.hcl: Pin thegoogleprovider version in arequired_providersblock and include.terraform.lock.hclin the repository to prevent unexpected behavior from provider specification changes.
Additionally, automatically running tofu plan during pull requests via CI/CD ensures that the impact of changes is visualized beforehand and prevents unintended infrastructure changes from being mixed into production.
Integration with Google Kubernetes Engine
This method assumes a configuration connecting from a backendService to a Service with Standalone NEG. It can connect to standard Kubernetes Services and Pods, but adding annotations to the Service and setting up to accept GCP health checks is necessary.
This approach foregoes the adoption of Gateway API. Even if you could build using Gateway API, it is possible to export the full set of GCP objects with OpenTofu. However, in reality, there are many cases where Gateway API cannot fully support requirements—for example, it has long lacked support for housing a backendBucket in the same Load Balancer. Without Gateway API, this network access layer won’t be codified, necessitating the use of HCL.
Dual-stack NEGs
Network Endpoint Groups are added via annotations, but the dual-stack setting is determined by the Service spec. If the default is IPv4 only, the following setting allows the NEG to also accept IPv6:
apiVersion: v1
kind: Service
metadata:
annotations:
cloud.google.com/neg: '{"exposed_ports": {"443": {"name": "web-neg"}}}'
spec:
type: ClusterIP
ipFamilyPolicy: PreferDualStack
ipFamilies:
- IPv6
- IPv4
In this case, the application within the Pod must obviously listen not only on 0.0.0.0 but also on [::]. Also, the order of ipFamilies is significant.