Terraform销毁GCP服务网络连接失败,请求排查原因
问题现象
terraform apply可正常创建所有资源,但执行terraform destroy -auto-approve时,无法删除服务网络连接(Service Networking Connection)或VPC对等连接,报错信息如下:
Unable to remove Service Networking Connection, err: Error waiting for Delete Service Networking Connection: Error code 9, message: Failed to delete connection; Producer services (e.g. CloudSQL, Cloud Memstore, etc.) are still using this connection.
- 手动删除VPC对等连接并释放私有IP后,销毁操作可正常完成。
- 销毁操作通过GitHub Actions执行,命令为:
- name: Terraform destroy working-directory: ${{ env.DIR }} run: terraform destroy -auto-approve
当前模块配置
VPC模块代码
# Creating a custom VPC resource "google_compute_network" "custom-vpc-network" { name = var.vpc_name project = var.project_id auto_create_subnetworks = false mtu = 1460 routing_mode = var.route_mode description = "Custom VPC for an ecommerce architecture" } # Creating a custom subnet resource "google_compute_subnetwork" "front-end-subnet" { ip_cidr_range = var.ip_address_range_frontend region = var.project_region name = var.subnet_name_frontend network = google_compute_network.custom-vpc-network.name description = "subnet for frontend" private_ip_google_access = true stack_type = var.stack_type log_config { aggregation_interval = var.aggregate_interval flow_sampling = 0.5 metadata = var.include_all_metadata } } resource "google_compute_subnetwork" "back-end-subnet" { ip_cidr_range = var.ip_address_range_backend region = var.project_region name = var.subnet_name_backend network = google_compute_network.custom-vpc-network.name description = "subnet for backend" private_ip_google_access = true stack_type = var.stack_type log_config { aggregation_interval = var.aggregate_interval flow_sampling = 0.5 metadata = var.include_all_metadata } } resource "google_compute_subnetwork" "database-subnet" { ip_cidr_range = var.ip_address_range_database region = var.project_region name = var.subnet_name_database network = google_compute_network.custom-vpc-network.name description = "subnet for database" private_ip_google_access = true stack_type = var.stack_type log_config { aggregation_interval = var.aggregate_interval flow_sampling = 0.5 metadata = var.include_all_metadata } }
数据库模块代码
resource "google_compute_global_address" "private-ip-address" { name = var.private_ip_address address_type = var.private_ip_type purpose = var.private_ip_purpose network = var.network_id prefix_length = 16 depends_on = [ var.network_id ] } resource "google_service_networking_connection" "private-vpc-connection" { network = var.network_id service = var.service reserved_peering_ranges = [google_compute_global_address.private-ip-address.name] lifecycle { create_before_destroy = true } } resource "google_sql_database_instance" "sql-instance" { name = var.sql_instance region = var.project_region database_version = var.database_version depends_on = [ google_service_networking_connection.private-vpc-connection ] settings { tier = var.machine_type availability_type = var.availability_type_regional edition = var.edition user_labels = { environment = var.environment } ip_configuration { ipv4_enabled = true private_network = var.network_id enable_private_path_for_google_cloud_services = true } backup_configuration { enabled = true start_time = var.backup-time point_in_time_recovery_enabled = true } maintenance_window { day = 7 hour = 20 update_track = var.update_track } } deletion_protection = false lifecycle { create_before_destroy = true } } resource "google_sql_database_instance" "sql-instance-read-replica" { name = "${var.sql_instance}-replica" master_instance_name = google_sql_database_instance.sql-instance.name region = var.project_region database_version = var.database_version settings { tier = var.machine_type availability_type = var.availability_type_zonal edition = var.edition user_labels = { environment = var.environment } ip_configuration { ipv4_enabled = true private_network = var.network_id enable_private_path_for_google_cloud_services = true } } deletion_protection = false lifecycle { create_before_destroy = true } } resource "google_sql_database" "postgresql-database" { name = var.postgresql-database instance = google_sql_database_instance.sql-instance.name } resource "google_sql_user" "postgresql-user" { name = var.db_username_output instance = google_sql_database_instance.sql-instance.name password = var.db_password_output }
问题根源与解决方案
问题根源
当前配置中,google_service_networking_connection没有明确依赖Cloud SQL实例的销毁流程,导致Terraform尝试先删除服务网络连接,而此时Cloud SQL主实例和只读副本仍在占用该连接,触发报错。同时,SQL实例上配置的create_before_destroy会干扰销毁顺序,进一步加剧问题。
解决方案
调整服务网络连接的依赖与生命周期
移除create_before_destroy配置,并添加依赖确保服务网络连接在所有SQL实例销毁后再删除:resource "google_service_networking_connection" "private-vpc-connection" { network = var.network_id service = var.service reserved_peering_ranges = [google_compute_global_address.private-ip-address.name] # 依赖所有SQL实例,确保销毁顺序 depends_on = [ google_sql_database_instance.sql-instance, google_sql_database_instance.sql-instance-read-replica ] }移除SQL实例不必要的
create_before_destroy
销毁场景下,create_before_destroy会导致先创建新实例再删除旧实例,无实际意义且干扰顺序,直接移除SQL实例的该配置:resource "google_sql_database_instance" "sql-instance" { # ... 其他配置 ... # 移除以下生命周期配置 # lifecycle { # create_before_destroy = true # } }对只读副本实例执行同样操作。
调整私有IP地址的销毁顺序
让私有IP地址在服务网络连接销毁后再释放,避免提前释放导致的异常:resource "google_compute_global_address" "private-ip-address" { name = var.private_ip_address address_type = var.private_ip_type purpose = var.private_ip_purpose network = var.network_id prefix_length = 16 depends_on = [ var.network_id, google_service_networking_connection.private-vpc-connection ] lifecycle { destroy_after_create = true } }验证销毁顺序
执行terraform plan -destroy查看资源销毁顺序,确认顺序为:SQL实例 → 服务网络连接 → 私有IP地址 → VPC资源。
循环依赖处理方案
如果调整配置后出现循环依赖,可通过null_resource间接控制顺序:
resource "null_resource" "destroy_sql_first" { depends_on = [ google_sql_database_instance.sql-instance, google_sql_database_instance.sql-instance-read-replica ] } resource "google_service_networking_connection" "private-vpc-connection" { # ... 其他配置 ... depends_on = [null_resource.destroy_sql_first] }
内容的提问来源于stack exchange,提问作者Amith Sai

