petitviolet blog

    Terraform to manage GCP Service Accounts

    2022-06-30

    TerraformGCP

    The Google provider of Terraform has some mechanisms to manage Service Accounts in GCP as followings.

    • google_service_account_iam
      • google_service_account_iam_policy
      • google_service_account_iam_binding
      • google_service_account_iam_member
    • google_project_iam
      • google_project_iam_policy
      • google_project_iam_binding
      • google_project_iam_member

    tl;dr

    Strongly recommend using google_service_account_iam_member and google_project_iam_member to manage GCP service accounts. Not use google_service_account_iam_policy and google_project_iam_policy.

    google_service_account_iam

    This resource is to configure GCP service accounts that perform operations within a resource. Meaning that if a service account doesn't need to interact with other GCP resources, google_service_account_iam is the best choice over google_project_iam. As the document describes, google_service_account_iam_policy and google_service_account_iam_binding are Authoritative, which is possible to delete existing resources that are not managed by terraform. Thus, I recommend using google_service_account_iam_member resource over another two since only google_service_account_iam_member performs additive operation, it's a little bit boring to configure all pairs of roles and members though.

    Indeed, my service account for applying terraform plans was locked out because of wrong usage of google_service_account_iam, then subsequent apply failed due to lack of permission because the service account had been deleted unexpectedly.

    google_project_iam

    As I described above, google_project_iam is to configure GCP service accounts that need to interact with other GCP resources. So, basically we can use google_service_account_iam, but sometimes we have to use it. For example, it requires google_project_iam configurations for giving a permission (roles/cloudsql.client) on a Cloud Run resource to act as a client for a Cloud SQL instance.

    https://cloud.google.com/sql/docs/mysql/roles-and-permissions

    This code snippet shows how google_project_iam_member can be used in configuring the above scenario.

    resource "google_project_iam_member" "cloud_sql_client" {
      project = "${var.gcp_project}"
      role    = "roles/cloudsql.client"
      member  = "serviceAccount:${local.cloud_sql_service_accounts_email}"
      condition {
        expression  = "resource.name == '${google_sql_database_instance.my_database.id}' && resource.type == 'sqladmin.googleapis.com/Instance'"
        title       = "cloudsql.client policy"
        description = "Cloud SQL instance client"
      }
    }
    

    https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/google_project_iam

    This document describes google_project_iam resources and also it mentions that wrong usage of google_project_iam_policy may lock yourself out of your project. It's the reason why I recommend using google_project_iam_member rather than google_project_iam_policy. In addition, there is google_project_iam_binding, but it's also marked as "authoritive", whereas google_project_iam_member is "Non-authoritive". "Authoritive" means that it's possible to delete existing resources by following given configurations. So, even though it takes a time to configure all of role and member mappings, using google_project_iam_member is the safest way, I believe.

    How to configure Service Accounts by Terraform

    Basic usage of google_service_account_iam_member looks like below.

    service_account.tf
    resource "google_service_account" "sa" {
      account_id   = "my-sa"
      display_name = "my ServiceAccount"
    }
    
    resource "google_service_account_iam_member" "sa_run" {
      service_account_id = google_service_account.sa.name
      role               = "roles/run.serviceAgent"
      member             = "serviceAccount:${google_service_account.sa.email}"
    }
    

    First of all, creating a service account is done by google_service_account resource, then giving a role to the created service account.

    As another example, creating a service account for operating GitHub Actions that needs to deploy Cloud Run.

    resource "google_service_account" "github_actions" {
      account_id   = "github-actions"
      display_name = "github-actions"
    }
    
    locals {
      # https://cloud.google.com/run/docs/deploying-source-code#permissions_required_to_deploy
      github_actions_roles = [
        "roles/run.admin",
        "roles/cloudbuild.builds.editor",
        "roles/storage.admin",
        "roles/viewer",
        "roles/artifactregistry.admin",
      ]
    }
    resource "google_project_iam_member" "github_actions" {
      count = length(local.github_actions_roles)
    
      project = var.gcp_project
      role    = local.github_actions_roles[count.index]
      member  = "serviceAccount:${google_service_account.github_actions.email}"
    }
    

    As the same with the previous example, create a service account and give permissions needed.