Today I Learned, the Google Cloud Platform / Terraform Edition

🌍 This week I’ve been goofing around with Google Cloud Platform and Terraform to manage it. I’ve learned quite a few things, that might be of help to others, so here goes …


IAM for SA vs. IAM for Projects

There’s a difference between IAM policy for service account (google_service_account_iam) and IAM policy for projects (google_project_iam_member).

The former is used to allow other users to impersonate/control a certain Service Account, the second is used to define what a Service Account can access and do.

Should’ve RTFM‘d


The Project IAM Admin role

To apply IAM Policies onto a Service Account the invoker needs the “Project IAM Admin” (roles/resourcemanager.projectIamAdmin) role. If not you’ll get back a 403:

returned error: Error applying IAM policy for project "my-project": Error setting IAM policy for project "my-project":
googleapi: Error 403: The caller does not have permission, forbidden


Terraform doesn’t like Casing

There’s a bug in Terraform which prevents terraform apply from setting IAM Roles when you have a Service Account or User with an uppercase character in their e-mail address (e.g. Firstname.Lastname@domain.tld).

Terraform will automatically convert all e-mail address to all-lowercase variants (e.g. firstname.lastname@domain.tld). Therefore GCP’s Resource Manager won’t be able to apply the policies as the Rresource does not exist. Accounts that inherit access to the project are not affected.

The error you get back is quite obscure:

returned error: Error applying IAM policy for project "my-project": Error setting IAM policy for project "my-project":
googleapi: Error 400: Request contains an invalid argument., badRequest

To see whether you have such a conflicting user, check the output of gcloud projects get-iam-policy PROJECT-ID


You can partially apply Terraform Changes

You can partially apply a Terraform State by using Resource Targets. You need this, for example, when working with secrets which you can store onto Google Cloud Platform. To work with secrets you rely on a keyring+keycode, but you need that keyring/keycode to be available before you can generate encrypted secrets from plaintext strings.

Here’s how I did it, using Resource Targets:

  1. Run Terraform to only target the google_kms_key_ring and google_kms_crypto_key resources:

    terraform apply -target=google_kms_key_ring.my_key_ring -target=google_kms_crypto_key.my_crypto_key
  2. Generate your secrets and store them in the keyring using helper scripts such as gcloud-kms-scripts which I created just for that.
  3. Use the encrypted secrets in your .tf files
  4. Run terraform apply as you’d normally do

πŸ’‘ If you want to target a module, add a module. to its name, e.g. terraform apply -target=module.my_module


Everyone needs some sleep, even GCP

Applying IAM Policies to a newly created Service Account won’t always work, as it takes some time before the SA is available for use. There’s a workaround in which you trigger a sleep command using local-exec, yet I’m hoping that this will be solved in a future release of Terraform (perhaps with a delay Terraform Resource?).

resource "null_resource" "before" {

resource "null_resource" "delay" {
  provisioner "local-exec" {
    command = "sleep 10"
  triggers = {
    "before" = "${}"

resource "null_resource" "after" {
  depends_on = ["null_resource.delay"]


Service Account Impersonation

It’s possible to impersonate a Service Account from within your Terraform code. First you connect using your main account, and then generate a short lived token for the SA. From the Terraform docs I got this snippet:

provider "google" {
    scopes = [

data "google_service_account_access_token" "default" {
 provider = "google"
 target_service_account = ""
 scopes = ["userinfo-email", "cloud-platform"]
 lifetime = "300s"

data "google_client_openid_userinfo" "me" { }

output "source-email" {
  value = "${}"

provider "google" {
   alias  = "impersonated"
   access_token = "${data.google_service_account_access_token.default.access_token}"

data "google_project" "project" {
  provider = "google.impersonated"
  project_id = "target-project"

Due to the provider = "google.impersonated" part, the google_project will run as


That’s it! I hope these might have been of help to you, saving you some lookup work …

Did this help you out? Like what you see?
Thank me with a coffee.

I don\'t do this for profit but a small one-time donation would surely put a smile on my face. Thanks!

BuymeaCoffee (€3)

To stay in the loop you can follow @bramus or follow @bramusblog on Twitter.

Published by Bramus!

Bramus is a frontend web developer from Belgium, working as a Chrome Developer Relations Engineer at Google. From the moment he discovered view-source at the age of 14 (way back in 1997), he fell in love with the web and has been tinkering with it ever since (more …)

Unless noted otherwise, the contents of this post are licensed under the Creative Commons Attribution 4.0 License and code samples are licensed under the MIT License

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.