Service Account in GCP with Terraform
Entrée
In this article we will see how to create Service Account with RSA key pairs in Google Cloud Platform (GCP) with Terraform
There are a lot ways to create Service Accounts in Google Cloud Platform (GCP), and one of those method that I do not definitely prefer is clicking buttons on their GUI.
Thanks to Google they already provide program libraries -Google SA documentation, in order to create Service Accounts programmatically.
Create SA in Golang
import (
"context"
"fmt"
"io"
iam "google.golang.org/api/iam/v1"
)
// createServiceAccount creates a service account.
func createServiceAccount(w io.Writer, projectID, name, displayName string) (*iam.ServiceAccount, error) {
ctx := context.Background()
service, err := iam.NewService(ctx)
if err != nil {
return nil, fmt.Errorf("iam.NewService: %v", err)
}
request := &iam.CreateServiceAccountRequest{
AccountId: name,
ServiceAccount: &iam.ServiceAccount{
DisplayName: displayName,
},
}
account, err := service.Projects.ServiceAccounts.Create("projects/"+projectID, request).Do()
if err != nil {
return nil, fmt.Errorf("Projects.ServiceAccounts.Create: %v", err)
}
fmt.Fprintf(w, "Created service account: %v", account)
return account, nil
}
However this is not the method that I would go for it. I prefer IaC (Infrastructure as Code) because of it’s advantages. And in this article we’re going to see this method for creating Service Account in Google Cloud Platform (GCP).
GCP Service Accounts with Terraform
Project Structure
Before we start I’d like to mention that all the code you will see can be
written in a single main.tf
file. However I always tend to design any software
with minimalist — Weniger, aber Besser, and atomic modules, like UNIX
Philosophy encapsulates.
We’ll have 5 files instead of one main file.
$ tree -L 3 .
.
├── apis.tf
├── output.tf
├── providers.tf
├── sa.tf
└── variable.tf
And the whole project structure will look like this after creating Service Accounts.
$ tree -L 3 .
.
├── apis.tf
├── main.tfplan
├── main.tfvars
├── output.tf
├── providers.tf
├── sa.tf
├── .terraform
│ ├── modules
│ │ ├── enabled_apis
│ │ └── modules.json
│ └── providers
│ └── registry.terraform.io
├── .terraform.lock.hcl
├── terraform.tfstate
├── terraform.tfstate.backup
└── variable.tf
Terraform Providers
First things first, we have to start with required terraform providers. I’m
going to use google
and google-beta
rightmost version of 4.0
, and
terraform minimum 0.13
.
For more detail on provider version control syntax you can look at official documentation on version constraints syntax.
# resources.tf
terraform {
required_providers {
google = {
source = "hashicorp/google"
version = "~> 4.0"
}
google-beta = {
source = "hashicorp/google-beta"
version = "~> 4.0"
}
}
required_version = ">= 0.13"
}
Enable Google Services
Then we have to enable necessary Google Services. Most articles suggest you to go GCP console and clicking one by one in order to enable the services you need. As you can guess, for sure I’m not going to click those either.
Let’s create api.tf
file and add below content:
# apis.tf
locals {
all_project_services = concat(var.gcp_service_list, [
"serviceusage.googleapis.com",
"iam.googleapis.com",
])
}
resource "google_project_service" "enabled_apis" {
project = var.project_id
for_each = toset(locals.all_project_services)
service = each.key
disable_on_destroy = false
}
If you want to disable after destroying your terraform resources by terraform destroy
, enable disable_on_destroy
.
Then let’s define variables needed by this apis
file.
# variables.tf
variable "gcp_service_list" {
type = list(string)
description = "The list of apis necessary for the project"
default = []
}
variable "project_id" {
type = string
}
First two services which we defined in api
is a-MUST then I’m providing them
as preequisite. Whatever services you add to gcp_service_list
will be merged
into all_project_services
local.
Service Account
Now let’s add the core module:
# sa.tf
resource "google_service_account" "sa" {
project = var.project_id
account_id = var.account_id
display_name = var.description
depends_on = [
google_project_service.enabled_apis,
]
}
resource "google_project_iam_member" "sa_iam" {
for_each = toset(var.roles)
project = var.project_id
role = each.value
member = "serviceAccount:${google_service_account.sa.email}"
depends_on = [
google_project_service.enabled_apis,
]
}
In this module we create a Service Account and add provided roles from
roles
variables to it.
Here is the related variables, add them to variables.tf
file.
# variables.tf
variable "account_id" {
description = "The service account ID. Changing this forces a new service account to be created."
}
variable "description" {
description = "The display name for the service account. Can be updated without creating a new resource."
default = "managed-by-terraform"
}
variable "roles" {
type = list(string)
description = "The roles that will be granted to the service account."
default = []
}
Also let’s add an output file to get necessary information after running our IaC code.
# output.tf
# ===================================================================
# service account: email, name. unique_id
# ===================================================================
output "email" {
value = google_service_account.sa.email
description = "The e-mail address of the service account."
}
output "name" {
value = google_service_account.sa.name
description = "The fully-qualified name of the service account."
}
# output "account_id" {
output "unique_id" {
value = google_service_account.sa.unique_id
description = "The unique id of the service account."
}
Service Account Key
One thing that we need is a related public/private RSA key for this Service Accounts. Since it does not have have passwords, and cannot log in via browsers or cookies.
Add below codes in order to create key pairs associated with the SA we create.
# sa.tf
resource "google_service_account_key" "sa_key" {
service_account_id = google_service_account.sa.name
}
And also output info:
# output.tf
# ===================================================================
# private key
# ===================================================================
output "private_key" {
value = google_service_account_key.sa_key.private_key
sensitive = true
}
output "decoded_private_key" {
value = base64decode(google_service_account_key.sa_key.private_key)
sensitive = true
}
I’d like to emphasize that the sensitive
parameter is very critical. From
sensitive variables:
Setting the sensitive flag helps avoid accidental exposure of sensitive or secret values. You must also keep them secure while passing them into Terraform configuration, and protect them in your state file.
Note
Marking variables as sensitive is not sufficient to secure them. You should use secrets management tools and secure your state in addition to marking variables as sensitive.
Terraform Run
You can provide variables via entering manually on cli
or via --var
flags,
but let’s use a more proper way: create main.tfvars
file to provide our
variables.
# main.tfvars
gcp_service_list = [
"storage.googleapis.com",
]
project_id = "some-project-id"
account_id = "bucket-admin"
description = "Bucket Admin"
roles = [
"roles/storage.admin",
]
As an example I’m creating a Bucket Admin Service Account to control our
GCP Cloud Storage Bucket (equivalent of AWS S3), so we’re adding
storage.admin
role and enabling storage
api.
Now run init
to get all terraform provider modules.
$ terraform init
Plan
Before creating any resource let’s plan
and examine resources that will be
created:
$ terraform plan --var-file=main.tfvars --out=main.tfplan
Apply
If evertying seems good as you intented, then apply those plans:
$ terraform apply main.tfplan
You will see an output like below:
Apply complete! Resources: 6 added, 0 changed, 0 destroyed.
Outputs:
decoded_private_key = <sensitive>
email = "[email protected]"
name = "projects/some-project-id/serviceAccounts/[email protected]"
private_key = <sensitive>
unique_id = "111111111111111111111"
As you can see private_key
and decoded_private_key
are hidden. In order to
get them you need to run:
# private_key
$ terraform output --state=terraform.tfstate private_key
# decoded_private_key
$ terraform output --state=terraform.tfstate decoded_private_key
Destroy
In order to destroy the resources we created just run destroy
:
$ terraform destroy --var-file=main.tfvars
You can alse skip the approve part by providing --auto-approve
flag.
Approve Part
Do you really want to destroy all resources?
Terraform will destroy all your managed infrastructure, as shown above.
There is no undo. Only 'yes' will be accepted to confirm.
Enter a value:
$ terraform destroy --var-file=main.tfvars --auto-approve
Conclusion
In this article we have seen how to create Service Account with RSA key pairs in Google Cloud Platform (GCP) with Terraform.
Note
You can also use this as a remote Terraform module. For detail you can look at the article gcp bucket service account with remote terraform module
All done!
Changelog
- 2022-07-15 : Added reference to gcp bucket service account with remote terraform module