Using Google Cloud Service Account impersonation with Terraform
Google Cloud service accounts are a special kind of account typically used by applications and other resources in your Google Cloud project to access APIs and services. Best practice suggests that you should limit your personal account permissions and run your Terraform code with dedicated service accounts which have just the right permissions to perform the configuration required. In this post I will demonstrate how to configure a Terraform project to use service account impersonation and avoid managing service account keys.
Service account keys vs impersonation
There are two main ways to use service accounts in Google Cloud, service account keys and service account impersonation.
Using service account keys involves creating and downloading a JSON keyfile and setting the
GOOGLE_APPLICATION_CREDENTIALS
environment variable to the file location on your filesystem. Applications such as the
gcloud CLI and Terraform are aware of this variable and will use the private key found in the keyfile to authenticate as
the service account when performing actions.
This method is a security nightmare. As you can imagine, service accounts running Terraform configurations are often very powerful. So, having multiple plain text keyfiles stored all over the place, increasing the risk of private keys getting into the wrong hands, probably isn’t the best idea.
This brings me onto service account impersonation. Service account impersonation is where a principal, such as a user or another service account, creates short-lived credentials to authenticate as a service account. These credentials do not require service account keys making it a much more secure solution.
Configuring service account impersonation IAM bindings
To allow a principal to impersonate a service account they must be granted the iam.serviceAccountTokenCreator
role on
the service account. This can be assigned at the project level to allow a principal to impersonate all service accounts
in a project. However, it is best practice to assign the role at the service account level itself, to adhere to the
principal of least privilege.
For example, to allow the user joe.bloggs@example.com
to impersonate the service account
terraform-admin@my-project.iam.gserviceaccount.com
you can assign the iam.serviceAccountTokenCreator
role as
follows.
gcloud iam service-accounts add-iam-policy-binding \
terraform-admin@my-project.iam.gserviceaccount.com \
--member='user:joe.bloggs@example.com' \
--role='roles/iam.serviceAccountTokenCreator'
The principal can then impersonate the terraform-admin@my-project.iam.gserviceaccount.com
service account be setting
the GOOGLE_IMPERSONATE_SERVICE_ACCOUNT
environment variable, for example:
export GOOGLE_IMPERSONATE_SERVICE_ACCOUNT=terraform-admin@my-project.iam.gserviceaccount.com
Applications such as the gcloud CLI and Terraform are aware of this environment variable and will attempt to impersonate the service account when performing any actions. However, this is not always the best option when impersonating service accounts with Terraform, as the following section will explain.
Impersonating service accounts in Terraform providers
As mentioned above, exporting the GOOGLE_IMPERSONATE_SERVICE_ACCOUNT
environment variable is not always the best way
to impersonate service accounts. For one thing, it can be cumbersome to have to remember to export the
GOOGLE_IMPERSONATE_SERVICE_ACCOUNT
environment variable for every new shell session. But also, it can often be useful
in complex Terraform configurations to impersonate multiple service accounts. For example, maybe you have a specific
service account which has permissions to create monitoring resources in a central monitoring project, but which doesn’t
have permission to create resources in the main project. This is where configuring the impersonation directly in the
Terraform providers can be useful.
First, create a google
provider as follows.
provider "google" {
alias = "impersonation"
scopes = [
"https://www.googleapis.com/auth/cloud-platform",
"https://www.googleapis.com/auth/userinfo.email",
]
}
This provider will run in the context of your personal user account (as it doesn’t have any other credentials or access
tokens configured) and has the impersonation
alias.
Next, add a data block to retrieve the short-lived access token which will be used to authenticate as the target service
account. Notice that you’re explicitly telling the data source to use the google.impersonation
aliased provider, thus
requesting the access token via your personal user account.
data "google_service_account_access_token" "default" {
target_service_account = "terraform-admin@my-project.iam.gserviceaccount.com"
scopes = ["userinfo-email", "cloud-platform"]
provider = google.impersonation
}
And finally, create a second google
provider that will use the access token of your service account. Notice that this
provider doesn’t have an alias, meaning it’ll be the default provider used for any Google resources in your Terraform
code.
provider "google" {
access_token = data.google_service_account_access_token.default.access_token
}
Using aliases to configure multiple providers with different service accounts
I mentioned in the previous section that it can be useful in complex Terraform configurations to impersonate multiple service accounts. Take the example of creating monitoring resources in a central monitoring project using a different service account to the one which can create resources in the main project. You could expand on the previous example as follows.
provider "google" {
alias = "impersonation"
scopes = [
"https://www.googleapis.com/auth/cloud-platform",
"https://www.googleapis.com/auth/userinfo.email",
]
}
data "google_service_account_access_token" "default" {
provider = google.impersonation
target_service_account = "terraform-admin@my-project.iam.gserviceaccount.com"
scopes = ["userinfo-email", "cloud-platform"]
}
data "google_service_account_access_token" "monitoring" {
provider = google.impersonation
target_service_account = "terraform-monitoring@my-project.iam.gserviceaccount.com"
scopes = ["userinfo-email", "cloud-platform"]
}
provider "google" {
access_token = data.google_service_account_access_token.default.access_token
}
provider "google" {
alias = "monitoring"
access_token = data.google_service_account_access_token.monitoring.access_token
}
By creating another google
provider, this time with the alias monitoring
, and setting the access_token
to that of
a second impersonated service account (terraform-monitoring@my-project.iam.gserviceaccount.com
in the example above),
you’re now able to specify that certain resources be created using the terraform-monitoring
service account via the
provider
meta-argument on the resource.
For example, the following logging metric would be creating using the default provider, which is impersonating the
terraform-admin@my-project.iam.gserviceaccount.com
service account.
resource "google_logging_metric" "main" {
name = "my-custom/metric"
# More arguments...
}
And the following alert policy uses the google.monitoring
aliased provider, meaning that it will be created using the
terraform-monitoring@my-project.iam.gserviceaccount.com
service account. It specifies the google.monitoring
provider
using the provider
meta-argument of the
google_monitoring_alert_policy
resource.
resource "google_monitoring_alert_policy" "main" {
display_name = "My Alert Policy"
combiner = "OR"
conditions {
display_name = "test condition"
# More arguments...
}
provider = google.monitoring
}
Hopefully this demonstrates how flexible these configurations can be in allowing you to configure service accounts with just the right level of permissions to perform their required tasks.
Impersonating service accounts when accessing remote state files
There is one gotcha though. Remote state.
When using the gcs
backend, you’ll need to
explicitly tell it to impersonate a service account using the impersonate_service_account
argument. This is because
Terraform needs to access the state bucket before it even thinks about loading any of the provider configurations that
you configured above. However, this is pretty trivial to configure as you can see in the example below.
terraform {
backend "gcs" {
bucket = "my-bucket-name"
impersonate_service_account = "terraform-state@my-project.iam.gserviceaccount.com"
}
}
It’s worth noting that the argument values in the backend "gcs"
block cannot be variables, so you’ll need to hard code
the service account email. This can be a pain if you’re using workspaces with different service accounts per-workspace.
In this specific situation I’ve often opted to create a dedicated terraform-state
service account which only has
permissions to manage the state files for all workspaces of a project, nothing else.
Conclusion
I hope this post has been helpful in explaining the pros of using Google’s service account impersonation in Terraform, and in demonstrating how easily it can be configured. Thanks for reading!