How to Use YAML as Data Source in Terraform


I love how DRY and lean YAML can be, since I started to learn Ansible years ago.

Recently I wanted to provision MySQL user privileges right after the database instance provisioned in Google Cloud SQL. I used petoju/mysql Terraform provider to get the job done, it’s a community provider but seemed to be quite popular.

To grant a MySQL user some privileges, the grant statement usually looks like

GRANT SELECT ON database_name.* to 'username'@'%';

Considering I have more than 1 database cluster to provision, I created the following YAML data structure

services:
  app_joe:
    cluster: big1
    grants:
      - db: db_elle
        # joe can only read elle's data
        privileges:
          - SELECT
      - db: db_joe
  app_elle:
    cluster: big1
    grants:
      - db: db_elle
defaults:
  host: '%'
  # full access by default
  privileges:
    - SELECT
    - UPDATE
    - INSERT
    - DELETE

And here’s how I generated a for_each loop in a Terraform HCL file

terraform {
  backend "gcs" {}
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "4.28.0"
    }
    google-beta = {
      source  = "hashicorp/google-beta"
      version = "4.28.0"
    }
    mysql = {
      source  = "petoju/mysql"
      version = "3.0.22"
    }
  }
  required_version = "1.2.8"
}

provider "google" {}

provider "google-beta" {}

provider "mysql" {
  # lines here only work for 1 db cluster.
  # can use terragrunt to make dynamic providers
  endpoint = "127.0.0.1:13306"
  username = "root"
  password = "password"
}

locals {
  cluster_name = "big1"
  service_account_maniffest = yamldecode(file("data.yaml"))
  # a flatterned list of objects containing grant info
  service_account_grants = distinct(flatten([
    for k, v in local.service_account_maniffest.services : [
      for g in v.grants : {
        service    = k
        db         = g.db
        privileges = lookup(g, "privileges", local.service_account_maniffest.defaults.privileges)
      }
    ] if v.cluster == local.cluster_name
  ]))
}

# Authorization via MySQL grants
resource "mysql_grant" "iam_sql_service" {
  # for_each only works with set or map, so let's make a map
  for_each = {
    for grant in local.service_account_grants : "${grant.service}-${grant.db}" => grant
  }
  user       = each.value.service
  host       = local.service_account_maniffest.defaults.host
  database   = each.value.db
  privileges = each.value.privileges
}

All done 🙂