Hedronite · Ops Lesson · 01-Earth-DevOps / Terraform · Mon 2026-08-24

Terraform GCP Cloud SQL Settings — the nest the provider still has to answer

You can type the nest. Only the plugin can answer it.

Lesson Class: Ops (DevOps + Terraform + GCP Cloud SQL)
Cloud Referent: GCP Cloud SQL — google_sql_database_instance.settings / ip_configuration / database_flags. Rebalances after Azure 08-21 and AWS 08-18.
Paired Dev: HCL configuration_aliases + provider schema at the consumer boundary
Paired Cert: Pro-depth Lab 14: providers through modules
Grounding: Brikman Ch.7 leftover modules-with-multiple-providers · gcp-core-services Cloud SQL · Lab 14 referenced
The nest
settings is legal because the google plugin published it. tuning is a validate error.
The stamp
dynamic database_flags is legal because the nest is repeatable. dynamic tuning is not.
The child
The child consumes google and google-beta. The child does not configure them.
You can type the nest. Only the plugin can answer it.

<!-- hal:authoritative:yaml -->

You can type the nest. Only the plugin can answer it.

§I — Frame

Friday this arc flipped an Azure storage account and watched terraform output keep Tuesday's URL. Three days before that, an S3 backend wrote without the claim. 08-15 bound a GKE ServiceAccount with a member no author typed. Today's overlay is GCP on purpose. The last three TF Ops visits sat on Azure, AWS, GCP. Cloud SQL is the rebalance, and it is the relational row the Bootcamp GCP core-services cheatsheet actually names: MySQL, PostgreSQL, SQL Server.

You open a google_sql_database_instance. You write a settings block because every first tutorial does. Inside it you invent tuning { work_mem = "64MB" } because that is how you think about Postgres. terraform validate fails before a plan exists. The error does not say your value is wrong. It says the block does not exist.

Coin it: the nest the provider still has to answer.

This is not 07-28. That lesson isolated two copies of the same provider at the root, two AWS regions, two aliases. Today the isolation that matters is a nested block the google provider published in its schema. Two regions of hashicorp/aws are a configuration choice. A settings.tuning block is a protocol refusal.

This is not 08-15. That lesson isolated a member string Terraform computed. The Cloud SQL instance has computed fields too (private_ip_address, connection_name). Those are answers after apply. The nest is a question the plugin must accept before apply starts.

Brikman, Creating Modules That Can Work with Multiple Providers, is blunt: the child module does not configure the plugin. The child consumes configurations the root passes. The same sentence holds one altitude down. The settings block does not configure Cloud SQL. The settings block consumes a nest the plugin declared.

§II — Foundations: four facts about the nest

Fact one. The resource type is a prefix plus a name the plugin registered.

google_sql_database_instance is not a Terraform keyword. It is a type the hashicorp/google plugin registered when Core asked for its schema. Brikman, How Do You Install Providers, states the prefix rule: every resource and data source from a provider carries that provider's prefix. google_sql_database_instance, google_sql_database, google_sql_user, google_sql_ssl_cert. Change the source address and the prefix family changes with it. hashicorp/google-beta is a different plugin with a different binary, a different schema document, and the same google_ prefix on many types. Two source addresses. One prefix. That collision is why a module that needs a beta-only nest must receive google-beta as its own provider, not as a wish.

The Bootcamp cheatsheet lists Cloud SQL as a relational service. The cheatsheet does not list the Terraform type. The type is the plugin's claim, not GCP's product page.

**Fact two. settings is required because the schema said so.**

A google_sql_database_instance without settings { tier = "..." } does not validate. tier is not a taste. It is the machine shape Cloud SQL will bill. The plugin marked the nest required. Terraform Core did not invent that rule. Core asked the plugin, over the same gRPC protocol 07-28 named, "what blocks exist on this type, and which of them must be present?" The plugin answered with a schema. Core enforces the answer.

Inside settings the plugin declared more nests: backup_configuration, ip_configuration, insights_config, maintenance_window, location_preference, database_flags. Each nest has attributes the plugin named. ip_configuration has ipv4_enabled, private_network, allocated_ip_range, and a repeated authorized_networks nest with name and value. database_flags is a repeated nest with name and value. You may omit an optional nest. You may not rename one.

Fact three. A repeated nest is not a resource loop.

database_flags looks like a list you would for_each. It is not. A for_each on the resource would create more instances. A dynamic "database_flags" block stamps more nests inside one instance. 07-25 taught the syntax of dynamic. Today's fact is the permission. dynamic "database_flags" is legal because the plugin declared database_flags as a repeatable nested block. dynamic "tuning" is a validate error even if your HCL is otherwise perfect. Lab 21 drills the syntax on AWS security-group ingress. That lab is a redo if it becomes today's topic. It is a witness if you only steal its distinction: the nest must already exist in the schema.

Fact four. Private IP is a second resource family, still one plugin, still one nest.

A Cloud SQL instance that should not have a public IPv4 needs ip_configuration.ipv4_enabled = false and ip_configuration.private_network set to a VPC self-link. That VPC must already have a google_service_networking_connection on servicenetworking.googleapis.com and a reserved peering range. Those are other types in the same hashicorp/google schema. They are not a second cloud. They are not 07-31's GCS backend prefix. They are the network the nest points at. A plan that sets private_network without the peering connection fails in the API, not in validate. Validate can only enforce the nest. The API enforces the world.

authorized_networks is the other repeated nest people treat as a resource. It lives under ip_configuration. Each entry is a { name, value } pair, a CIDR the instance will accept when ipv4_enabled is true. A google_compute_firewall is a different type on a different API. Opening 0.0.0.0/0 on authorized_networks does not open the VPC. Closing the VPC firewall does not close authorized_networks. Two nests, two plugins-worth of schema if you also manage firewalls, still one cloud. A dynamic "authorized_networks" block is legal for the same reason dynamic "database_flags" is legal: the plugin marked the nest repeatable. A dynamic "ingress" inside ip_configuration is a validate error. The word you remember from security groups is not a nest Cloud SQL published.

§III — Mechanism: the plugin answers, then Cloud SQL does

Terraform Core never knew what a Cloud SQL instance was. 07-28 said this about providers in general. Today we watch one nest travel the path.

  1. terraform init reads required_providers. The source hashicorp/google is a registry address. The lock file pins the binary. Brikman, How Do You Install Providers, describes the implicit-hashicorp fallback when the block is missing. A Cloud SQL module that omits required_providers and hopes provider "google" is enough will still init on a lucky laptop. A second provider named google-beta will not. Two source addresses need two entries. The lock file is the receipt the child cannot write.
  1. terraform validate loads the plugin, asks for schema, and walks every block in your file against that schema. An unknown nest dies here. An unknown attribute inside a known nest dies here. A missing required nest dies here. No GCP credential is required. The plugin is answering as a document, not as a cloud.
  1. terraform plan asks the plugin to read. The plugin calls the Cloud SQL Admin API. Computed attributes (connection_name, private_ip_address, self_link) stay unknown on a create and stay last-apply on an update that does not touch them. 08-21 already taught that last-apply trap on an Azure endpoint. The Cloud SQL connection_name is the same class of string: it does not move when you flip ipv4_enabled.
  1. terraform apply asks the plugin to write. The plugin translates settings.ip_configuration into the instance patch Cloud SQL expects. A nest you never wrote is omitted or defaulted per the plugin's own mapping, not per your taste. availability_type = "REGIONAL" is a settings attribute. It is not a second instance. A replica is google_sql_database_instance with master_instance_name. Two types of HA, two schema answers.

hashicorp/google-beta enters when a nest still lives only in the beta schema. settings.data_cache_config and some Enterprise Plus flags have spent time there. The child that needs that nest must receive the beta plugin. Passing google and hoping the type exists is the 07-28 implicit-provider failure wearing a new shirt. Brikman, Working with Multiple Different Providers, is the leftover section: two plugins, two processes, two schemas. google and google-beta are that case even though they talk to one cloud.

The root configures both. The child declares both in required_providers. The module call passes both through providers = { google = google, google-beta = google-beta }. The child does not grow its own provider "google" block. Brikman, Creating Modules That Can Work with Multiple Providers, is that rule. Lab 14 is the exam shirt for the same rule on two AWS regions. Today's cloud is GCP. The rule does not change.

§IV — Worked Example: one instance, three nests, one illegal line

A production-shaped Cloud SQL Postgres in prod-data that must not listen on the public internet, must keep seven days of backups, and must accept a map of flags the caller owns.

resource "google_sql_database_instance" "primary" {
  name             = var.instance_name
  database_version = "POSTGRES_15"
  region           = var.region
  project          = var.project_id

  deletion_protection = true

  settings {
    tier              = var.tier
    availability_type = "REGIONAL"
    disk_size         = var.disk_size_gb
    disk_autoresize   = true

    backup_configuration {
      enabled                        = true
      start_time                     = "07:00"
      point_in_time_recovery_enabled = true
      transaction_log_retention_days = 7
    }

    ip_configuration {
      ipv4_enabled    = false
      private_network = var.network_id
      ssl_mode        = "ENCRYPTED_ONLY"
    }

    dynamic "database_flags" {
      for_each = var.flags
      content {
        name  = database_flags.key
        value = database_flags.value
      }
    }
  }
}

Three legal nests. backup_configuration and ip_configuration are single nests the schema named. database_flags is a repeated nest stamped by dynamic because the schema marked it repeatable. var.flags is a map(string). The Dev lesson constructs that map. This lesson consumes it.

Now the illegal line. An author who wants work_mem as a first-class nest writes this inside settings:

tuning {
  work_mem = "64MB"
}

Validate fails. The flag belongs in database_flags { name = "work_mem", value = "64MB" }, or in var.flags = { work_mem = "64MB" } so the dynamic block stamps it. The temptation is to invent a nest that reads like Postgres. The plugin does not speak Postgres. The plugin speaks its schema.

A second illegal line is a provider "google" block inside the Cloud SQL module. It will init. It will also ignore the root's impersonate_service_account, the root's user_project_override, and the root's google-beta pairing. The child has configured a plugin the root cannot see. Brikman's warning on aliases (use them sparingly; each block is a process) applies one step earlier: do not configure a plugin in a child when the child's job is to consume one.

A third failure is silent. The author sets ipv4_enabled = false and forgets private_network. Validate passes. Plan may pass if the plugin treats the attribute as optional. Apply talks to Cloud SQL and the instance comes up with no IP you can reach, or the API refuses the combination. The nest was answered. The world was not. Read the plan's ip_configuration block before you apply. If private_network is empty and ipv4_enabled is false, you have authored an island.

connection_name is exported for Cloud Run, GKE, and the Auth Proxy. It is a computed string of the form project:region:instance. 08-21's lesson applies: a consumer that reads connection_name through terraform_remote_state reads last apply. Flipping ipv4_enabled does not change that string. Flipping settings.tier does not change that string. Renaming the instance does, and a rename of google_sql_database_instance.name is a replace. The nest you answered and the string a consumer stored are different objects.

§V — Connection to Prior Lessons

08-21 taught an output that survived a grant. The blob endpoint stayed pretty. The door moved. Today's connection_name is that class of string on GCP. The door is ip_configuration.ipv4_enabled. The pretty string is the instance identity. Do not export only the pretty string and call the instance private.

08-18 taught a lock you did not hold. Two writers, one state file. Today's writer is a single apply. The refusal happens earlier, at validate, when the nest does not exist. Nobody raced. The plugin said no.

08-15 taught a member Terraform computed. The GSA/KSA binding is a value. Today's database_flags values are also values. The difference is the container. The member sat in an object constructor. The flags sit in a nest the plugin declared. Computed is not the same as permitted.

07-28 taught two copies of one provider at the root. alias = "secondary" and provider = aws.secondary. That lesson closed the Associate-shaped alias. It left the child. Brikman's leftover sections are the child: Creating Modules That Can Work with Multiple Providers, and Working with Multiple Different Providers. Today the child consumes google and, when a nest still lives in beta, google-beta. Two plugins, one cloud, one instance.

07-31 isolated environments on a GCS prefix. A different prefix is a different file. Today's instance lives in a project and a region the provider configuration named. The nest does not isolate environments. The project does. Do not put project inside settings. It is not there.

§VI — Connection to Today's Dev Lesson

The Dev lesson is the leftover HCL fire the runbook named: provider protocol at the consumer boundary. It will write the child required_providers block, the configuration_aliases form Lab 14 exams, and the providers = {} map the root must pass. It will also write the illegal nest and the legal dynamic as HCL, not as Cloud SQL.

This Ops lesson is the nest that protocol answers. If the Dev lesson only teaches aliases, it has repeated 07-28 in a new directory. If this Ops lesson only teaches Cloud SQL product facts, it has skipped the plugin. The hinge is one sentence. The child may stamp a nest the plugin declared. The child may not declare a nest the plugin did not.

var.flags is the object the 08-15 for-expression lesson already taught you to construct. Do not rebuild that lesson. Consume the map. Let dynamic "database_flags" stamp the nest. The constructor and the nest are different altitudes of the same discipline: the author types the input, the machine writes the shape the plugin will accept.

§VII — Closing

settings is legal because the google plugin said so. tuning is not. dynamic "database_flags" is legal because the plugin marked that nest repeatable. dynamic "tuning" is a validate error dressed as metaprogramming. The child does not configure the plugin. The child consumes the schema.

Name it when you see it. The nest the provider still has to answer. Write the nests the schema published. Pass the plugins the root configured. Ask Cloud SQL only after validate has already been answered.

Examine well. The instance name will still be pretty. The door is ip_configuration. The flags are a repeated nest. The plugin is the one who answers first.

Related