Developers & Practitioners

Using Google Cloud Service Account impersonation in your Terraform code

Terraform is one of the most popular open source infrastructure-as-code tools out there, and it works great for managing resources on Google Cloud.  When you’re just kicking the tires and learning how to use Terraform with Google Cloud, having the owner role on the project and running Terraform yourself makes things very easy.  That’s because with unlimited permissions, you can focus on understanding the syntax and functionality without getting distracted by any issues caused by missing IAM permissions.

However, once you’re past that, or if it’s just not possible in the project you’re working from, it’s a good idea to limit your own permissions and get into the habit of running your Terraform code as one or more service accounts with just the right set of IAM roles.  A service account is a special kind of account that is typically used by applications and virtual machines in your Google Cloud project to access APIs and services.  Applications and users can authenticate as a service account using generated service account keys. 

The downside to this approach is that it creates a security risk as soon as the key is generated and distributed. Any user with access to a service account key, whether authorized or not, will be able to authenticate as the service account and access all the resources for which the service account has permissions.  Fortunately, there’s another way to run Terraform code as a service that’s generally safer - service account impersonation. 

Creating resources as a service account

To begin creating resources as a service account you’ll need two things. First, you’ll need a service account in your project that you’ll use to run the Terraform code.  This service account will need to have the permissions to create the resources referenced in your code. Second,  you’ll need to have the Service Account Token Creator IAM role granted to your own user account.  This role enables you to impersonate service accounts to access APIs and resources.  The IAM role can be granted on the project’s IAM policy, thereby giving you impersonation permissions on all service accounts in the project. However, if you’re adhering to the principle of least privilege, the role should be granted to you on the service account’s IAM policy instead.

Once you have a service account and the Service Account Token Creator role, you can impersonate service accounts in Terraform in two ways: set an environment variable to the service account’s email or add an extra provider block in your Terraform code.

For the first method, set the GOOGLE_IMPERSONATE_SERVICE_ACCOUNT environment variable to that service account’s email. For example:

  export GOOGLE_IMPERSONATE_SERVICE_ACCOUNT=YOUR_SERVICE_ACCOUNT@YOUR_PROJECT.iam.gserviceaccount.com

After that, any Terraform code you run in your current terminal session will use the service account’s credentials instead of your own.  It’s a quick and easy way to run Terraform as a service account, but of course, you’ll have to remember to set that variable each time you restart your terminal session. You’ll also be limited to using just one service account for all of the resources your Terraform code creates.  

For the second method, you will need to add a few blocks into your Terraform code (preferably in the provider.tf file) that will retrieve the service account credentials.  First, set a local variable to the service account email:

  locals {
 terraform_service_account = "YOUR_SERVICE_ACCOUNT@YOUR_PROJECT.iam.gserviceaccount.com"
}

You can also set this variable by writing a variable block and setting the value in the terraform.tfvars file.  Either way works fine.  Next, create a provider that will be used to retrieve an access token for the service account. The provider is “google” but note the “impersonation” alias that’s assigned to it:

  provider "google" {
 alias = "impersonation"
 scopes = [
   "https://www.googleapis.com/auth/cloud-platform",
   "https://www.googleapis.com/auth/userinfo.email",
 ]
}

Next, add a data block to retrieve the access token that will be used to authenticate as the service account.  Notice that the block references the impersonation provider and the service account specified above:

  data "google_service_account_access_token" "default" {
 provider               	= google.impersonation
 target_service_account 	= local.terraform_service_account
 scopes                 	= ["userinfo-email", "cloud-platform"]
 lifetime               	= "1200s"
}

And finally, include a second “google” provider that will use the access token of your service account. With no alias, it’ll be the default provider used for any Google resources in your Terraform code:

  provider "google" {
 project 		= YOUR_PROJECT_ID
 access_token	= data.google_service_account_access_token.default.access_token
 request_timeout 	= "60s"
}

Now, any Google Cloud resources your Terraform code creates will use the service account instead of your own credentials without the need to set any environment variables. With this method, you also have the option of using more than one service account by specifying additional provider blocks with unique aliases.

Updating remote state files with a service account

When you run Terraform code, it keeps track of the Google Cloud resources it manages in a state file. By default, the state file is generated in your working directory, but as a best practice the state file should be kept in a GCS bucket instead.  When you specify a backend, you need to provide an existing bucket and an optional prefix (directory) to keep your state file in.  If this bucket exists but your user account doesn’t have access to it, a service account that does have access can be used instead.  

Once again, you’ll need the Service Account Token Creator role granted via the service account’s policy.  This service account can be different from the one you’ll use to execute your Terraform code. Specifying the service account here is as simple as adding the impersonate_service_account argument to your backend block:

  terraform {
 backend "gcs" {
   bucket                      = "GCS_BUCKET_NAME"
   prefix                      = "OPTIONAL_PREFIX" 
   impersonate_service_account = "YOUR_SERVICE_ACCOUNT@YOUR_PROJECT.iam.gserviceaccount.com"
 }
}

With this one argument added to your backend block, a service account will read and update your state file when changes are made to your infrastructure, and your user account won’t need any access to the bucket, only to the service account.

The advantages of impersonation

The methods above don’t require any service account keys to be generated or distributed.  While Terraform does support the use of service account keys, generating and distributing those keys introduces some security risks that are minimized with impersonation.  Instead of administrators creating, tracking, and rotating keys, the access to the service account is centralized to its corresponding IAM policy.  By using impersonation, the code becomes portable and usable by anyone on the project with the Service Account Token Creator role, which can be easily granted and revoked by an administrator.