Terraform in GCP with service account impersonation

Michael Hannecke
Bluetuple.ai
Published in
9 min readAug 12, 2023

Introduction

Terraform is a great tool for managing Google Cloud resources.

When you’re first getting started, you just give yourself the owner role on the project you want to deploy resources in and run Terraform in the context of your account. That way, you don’t have to worry about any pesky IAM permissions getting in your way.

But once you’re past the beginner stage, or if you’re working on a project where you don’t have owner access, it’s a good idea to start using service accounts to run Terraform to increase security collaboration.

A service account is like a special kind of user account that’s used by applications and virtual machines to access Google Cloud APIs and services.

The downside to using service accounts is that you normally need to generate a service account key, and then distribute that key to Terraform so that it can authenticate as the service account. And that’s where the security risk comes in. Anyone with access to a service account key can authenticate as the service account and access all the resources that the service account has permissions to.

But don’t worry, there’s a better way! Service account impersonation allows you to run Terraform as a service account without having to generate or distribute a service account key. This is much more secure, because you never have to actually expose the service account’s credentials to Terraform.

So, if you’re serious about using Terraform to manage your Google Cloud infrastructure, I highly recommend using service account impersonation. It’s the safest and most secure way to run Terraform.

Here’s a little analogy to help you understand the difference between service accounts and service account impersonation:

Imagine you’re a celebrity, and you need to hire a bodyguard to protect you from your fans. You could give your bodyguard your house key, so that they can always get in to protect you. But that would also mean that they could break into your house and steal your stuff whenever they wanted.

A better option would be to give your bodyguard a temporary pass that only allows them to get into your house when they’re with you. This is like service account impersonation. Terraform is the bodyguard, and the temporary pass is the impersonation token. Terraform can only access Google Cloud resources when it’s impersonating a service account.

In the next steps I’ll show how to get started with service account impersonation and as a first use case, how to setup terraform remote state using impersonation.

Let’s get out fingers oiled.

Initialization Steps

Open a terminal and login to GCP via the CLI:

gcloud auth login

A browser page will pop-up, asking for your credentials and lastly a dialog will ask for confirmation that Google Auth Library wants to access your account:

Next set project and region values:

gcloud config set project <project-id>
gcloud config set compute/region <region>

You might want as-well export these values to environment variables:

export PROJECT_ID=<project_id>
export REGION=<region>

If it is a new project , you might have to enable two APIS:

  • Resource manager API
  • IAM Service Account Credentials API
gcloud services enable cloudresourcemanager.googleapis.com

gcloud services enable iamcredentials.googleapis.com

Now its time to create the service account we want to enable Terraform to deploy resources with:

gcloud iam service-accounts create
--description="Terrafrom service principal"
--display-name="terraform service account"

You can check a list of service account in the current project, the newly created account should show up in the list; note down email address of new service account, We will need that email later on.

E-mail address should look something like this:

<account-name>@<project-name>.iam.gserviceaccount.com

Impersonate service account

To allow a user (you in this case :) )to impersonate a service account, the user needs to have the iam.serviceAccountTokenCreator role. It would be possible to add this role binding to the projects policy but that would allow the user to impersonate all service accounts in that project. Following the least privilege principle, it is more secure to assign that role to the sa policy itself.

Run this:

gcloud iam service-accounts add-iam-policy-binding <service-account-email> \
--member="user:<your-account-email>"
--role="roles/iam.serviceAccountTokenCreator"

Even better would be not to assign the user directly but to use a group instead, but this will be out of scope for now.

Option “GOOGLE_IMPERSONATE_SERVICE_ACCOUNT” environment variable

If you set this environment variable to the service-account email address like this

export GOOGLE_IMPERSONATE_SERVICE_ACCOUNT=<service-account-email>

Applications like Google CLI and Terraform will look for this environment variable and if present will attempt to use the given service account when performing any actions. But that might not be the best option and can be error prone as you must ensure that the environment variable is set properly each new shell session.

Better Option: Provider blocks

It is also not the best approach when it comes to use CI/CD pipelines. Furthermore, you will be limited to use just this one service account for all the resources the terraform code will deploy. With the least privilege principle in mind, that might not be the best approach. For example, we will limit the specific service account to create storage buckets only in this project (to prepare the bucket we will use for the remote state) later.

To follow this option we will create a provider block for each service account we want to impersonate. For readability we will place these code blocks in a provider.tf file.



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

}


#receive short-lived access token
data "google_service_account_access_token" "default" {
provider = google.impersonation
target_service_account = var.terraform_service_account
scopes = ["cloud-platform", "userinfo-email"]
lifetime = "3600s"

}


# default provider to use the the token
provider "google" {
project = var.project_id
access_token = data.google_service_account_access_token.default.access_token
request_timeout = "60s"
}

This provider runs in the context of your personal user account — there are no other credentials or access tokens configured (less to mess around).

Important is the “impersonation” alias. We set a variable terraform_service_account to the email address of our service account above. The variable will be defined in a variable block in variable.tf and the value will be set in a terraform.tfvars file.

(All Source code can be found in this github repo)

Now we need a short-lived access token to authenticate as the target service account. To achieve this, we create a data block like this:

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

The lifetime parameter limits the period of validity, one hour should be ok, you can play around with this.

Now the stage is set and we can define a second “google” provider that will use exact this token to create resources on behalf of the terraform account. With no alias, this will be the default provider. More on that later.

Default provider to use the the token:

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

Adding Permissions to the service account

The service account created above has no roles assigned in the current project so terraform would not be able to deploy any configuration by impersonating this account. If you plan to deploy multiple resources in this project with terraform, you could assign editor role or another custom role that fit to your specific needs to the service account. Following the least privilege principle, the service account used by terraform should has as least privileges as possible…

With the following command we will assign storage admin role to the service account:

gcloud projects add-iam-policy-binding $PROJECT_ID \
--member "serviceAccount:" \
--role "roles/storage.admin"

If you want to verify that the permission has been added successfully, you can list the IAM policy bindings for the project using the following command:

gcloud projects get-iam-policy $PROJECT_ID

Now, any Google Cloud resource 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.

Let’s have a closer look into this.

Impersonate multiple service accounts with different roles assigned

Maybe you have two service accounts, one for Terraform, one for monitoring. The Terraform service account is meant for deploying general infrastructure, the monitoring service account might be responsible to handle monitoring configuration.

With impersonation you would have an approach like this:

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

# this account is for terraform deployments
data "google_service_account_access_token" "default" {
provider = google.impersonation
target_service_account = "<terraform-service account email"
scopes = ["userinfo-email", "cloud-platform"]
}

#this service account is for monitoring only
data "google_service_account_access_token" "monitoring" {
provider = google.impersonation
target_service_account = "<monitoring-sa-email"
scopes = ["userinfo-email", "cloud-platform"]
}

# token for terraform
provider "google" {
access_token = data.google_service_account_access_token.default.access_token
}

#token for monitoring
provider "google" {
alias = "monitoring"
access_token = data.google_service_account_access_token.monitoring.access_token
}

Any resource you deploy without explicit provider specification will be deployed by the default service account.

For example, the following code would instruct terraform to create a bucket with the default terraform service account.

resource "google_storage_bucket" "gcp_backend_bucket" { 
name = var.remote_state_bucket
project = var.project_id
location = var.region
force_destroy = true
storage_class = "STANDARD"
versioning {
enabled = false
}
}

If you want to deploy a resource by a different account, you have to specify the provider like this:

resource "google_logging_metric" "custom-metric" { 
name = "custom/metric xyz"

<other paramter for this resource goes here>

provider = google.monitoring
}

Advantages of impersonation

At first this might look more complicated than like just creating account keys. But the advantage is that you do not need to generate and maintain access keys. Therefor the security footprint is much smaller with impersonation. Administrators must not track and rotate keys, instead access to the service account is centralized to its corresponding IAM policy.

Furthermore, with impersonation the code becomes more portable and usable by any account on the project with the ServiceAccountTokenCreator role. This role can easily be granted or revoked by an administrator and misuse of credential files is not possible.

The final step for now: Bucket for Terraform remote stage

Terraform keeps track of the resources it manages in a state file. By default, this file is created locally in the working directory. Best practice is to store this file in a GCP bucket instead to allow collaboration for example. Let’s assume you have already deployed a bucket, maybe by the code described above .

Place this code in your main.tf file, setting actual values for the variables:

terraform {

backend "gcs" {
bucket = "<bucket-name>"
prefix = "<prefix>"

impersonate_service_account = "<serviceaccount-email>"

}

Unfortunately, in the backend definition in main.tf variables are not allowed in the actual Terraform version, but that might change with future versions, so we have to write the concrete values int tohe backend definition.

Run a

terraform init

and terraform will ask for permission (“yes” ) to transform the state file from your local disk to the GCP bucket.

(This will of course only work, if you have created the bucket upfront).

If your started from scratch, the workflow should be:

  1. Create Service Account
  2. Define main.tf. provider.tf , variables.tf and storage.tf with initial settings
  3. Run a first terraform init — plan — apply cycle to create the bucket
  4. Add the backend block to the main.tf
  5. Run terraform init again to move the state information to the remote backend

Conclusion

I hope this post spread some light in Googles approach of Service Account impersonation. You will never have to juggle with service account credential files .

“Man, it was always so much fun having to renew credentials for all accounts — you’ll miss it :)”

Have a virtual coffee with me .

If you have read to this point, thank you! You are a hero (and a Nerd ❤)! I try to keep my readers up to date with “interesting happenings in the AI world,” so please 🔔 clap | follow

Initially published on bluetuple.ai

--

--