Search code examples
google-cloud-platformterraforminfrastructure

How to assign multiple roles to multiple service accounts in GCP using Terraform?


I have a list of Service Accounts that will be created, and to assign roles to them I'm using a module that Google provides, and my code is as follows:

module "service-accounts" {
  source  = "terraform-google-modules/service-accounts/google"
  version = "4.1.1"

  project_id = var.project
  names = var.sa_list
  project_roles = [
    "${var.project}=>roles/appengine.appAdmin",
    "${var.project}=>roles/artifactregistry.reader",
    "${var.project}=>roles/cloudbuild.builds.builder",
    "${var.project}=>roles/cloudsql.client",
    "${var.project}=>roles/cloudsql.instanceUser"
  ]

  display_name = "Google App Engine SA - Managed by Terraform"
} 

This works just fine. But I don't want to make the roles in project_roles explicit, so I've tried to use the for_each meta-argument"

variables.tf:

variable "rolesList" {
  type =list(string)
  default = ["roles/appengine.appAdmin","roles/artifactregistry.reader", "roles/cloudbuild.builds.builder", "roles/cloudsql.client", "roles/cloudsql.instanceUser"]
}

main.tf:

module "service-accounts" {
  source  = "terraform-google-modules/service-accounts/google"
  version = "4.1.1"

  project_id = var.project
  names = var.sa_list

  for_each = (toset[var.rolesList])
  project_roles = ["${var.project}=>${each.key}"]

  display_name = "Google App Engine SA - Managed by Terraform"
} 

This way won't work because Terraform will create multiple accounts with the same id. How can I remove the hard code from project_roles? Is there a way to store the roles elsewhere and then call them? Or I'll need to assign roles individually?


Solution

  • If you want to give many roles to many services accounts, you can try by using the resources from official Google provider :

    First, put the service accounts you want to create in a config file like a JSON file, it allows to be more flexible in case of modification. You will change the config file, not the Terraform code:

    {
      "servicesAccount": {
        "first-service-account": {
          "account_id": "first-service-account",
          "display_name": "First SA managed by Terraform",
      "roles": [
            "roles/appengine.appAdmin",
            "roles/artifactregistry.reader",
            "roles/cloudbuild.builds.builder",
            "roles/cloudsql.client",
            "roles/cloudsql.instanceUser"
          ]
        },
        "second-service-account": {
          "account_id": "second-service-account",
          "display_name": "Second SA managed by Terraform",
      "roles": [
            "roles/appengine.appAdmin",
            "roles/artifactregistry.reader",
            "roles/cloudbuild.builds.builder",
            "roles/cloudsql.client",
            "roles/cloudsql.instanceUser"
          ]
        }
      }
    }
    

    Then you can parse this JSON file and putting in a local variable :

    locals {
      services_account = jsondecode(file("${path.module}/path/to/your/file.json"))["servicesAccount"]
      sa_flattened = flatten([
        for sa in local.services_account : [
          for role in sa["roles"] : {
            account_id   = sa["account_id"]
            display_name = sa["display_name"]
            role         = role
          }
        ]
      ])
    }
    

    Finally you can create the SA and then give them the associated roles :

    resource "google_service_account" "sa_names" {
      project      = var.project
      for_each     = local.services_account
      account_id   = each.value["account_id"]
      display_name = each.value["display_name"]
    }
    
    resource "google_project_iam_member" "sa_roles" {
      depends_on = [google_service_account.sa_names]
      for_each   = {for idx, sa in local.sa_flattened: "${sa["account_id"]}_${sa["role"]}" => sa}
      project    = var.project
      role       = each.value["role"]
      member     = "serviceAccount:${each.value["account_id"]}@${var.project}.iam.gserviceaccount.com"
    }