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