Playing around with Workload Identity on GKE

TechStack

Google recently beta-released Workload Identity, a solution to use Google Service Accounts (GSA) from Workloads in Kubernetes using Kubernetes Service Accounts (KSA).

Setting this up requires some steps and using it with other technologies requires some wiring. My current setup involves Terraform for cloud infrastructure setup, Helm charts to bundle applications and FluxCD to manage GKE clusters in a GitOps way.

Setup of GSA and KSA

So you have a GKE cluster with Workload Identity enabled on the cluster and Node Pool. Now how to create a KSA which can use a GSA? Of course, write a Terraform module!

provider "google" {}
provider "kubernetes" {}

variable "name" {
  description = "Name of the Google and Kubernetes Account that is created"
  type        = string
}

variable "namespace" {
  description = "Kubernetes namespace where the SA is created"
  type        = string
}

data "google_project" "this" {}

resource "google_service_account" "this" {
  account_id = var.name
}

resource "kubernetes_service_account" "this" {
  metadata {
    annotations = {
      managed_by_terraform = true

      "iam.gke.io/gcp-service-account" = google_service_account.this.email
    }

    name      = var.name
    namespace = var.namespace
  }

  automount_service_account_token = true
}


resource "google_service_account_iam_member" "workload_identity_user" {
  service_account_id = google_service_account.this.name
  role               = "roles/iam.workloadIdentityUser"
  member             = "serviceAccount:${data.google_project.this.project_id}.svc.id.goog[${var.namespace}/${kubernetes_service_account.this.metadata.0.name}]"
}

output "kubernetes_service_account" {
  description = "The created kubernetes service account"
  value       = kubernetes_service_account.this
}

output "google_service_account" {
  description = "The created google service account"
  value       = google_service_account.this
}

Now you can use this module wherever you need to create a KSA for a workload that has to access some Google API:

module "workload_identity_service_account" {
  source = "../workload_identity_service_account" # or something else

  providers = {
    google     = google
    kubernetes = kubernetes
  }

  name      = "workload-name"
  namespace = "default"
}

And now to the really fun part: automagically making a Helm chart using this KSA when managed via Flux!

Sharing Terraform knowledge with Flux

So whats the problem? Terraform knows how our KSA is named, our Helm chart needs this info. Hopefully our Helm chart allows us to pass something akin to serviceAccount.name: whatever (if not, make it so!).

Thankfully FluxCD allows us, to get values from one (or multiple) configMap and pass them to a helmRelease:

apiVersion: flux.weave.works/v1beta1
kind: HelmRelease
metadata:
  name: workload
  namespace: default
spec:
  releaseName: workload
  chart:
    git: SOME_GIT_REPO
    path: charts/workload
  valuesFrom:
    - configMapKeyRef:
        name: workload-values
    - configMapKeyRef:
        name: other-values
  values:
    otherStuff: true

So let’s also create this configMap from Terraform:

resource "kubernetes_config_map" "workload_values" {
  metadata {
    name      = "workload-values"
    namespace = "default"
  }

  data = {
    "values.yaml" = <<-YAML
    app:
      serviceAccount:
        create: false
        name: ${module.workload_identity_service_account.kubernetes_service_account.metadata.0.name}
    YAML
  }
}

Conclusion

With this setup the following happens:

  • Terraform creates a KSA and a GSA, the KSA is allowed to impersonate the GSA.
  • Terraform will also create a configMap which holds values for the Helm chart.
  • FluxCD picks up these values, merges them with others and deploys the helmRelease.
  • The workload will now identify as the GSA when calling Google APIs

Therefore you can now also use Terraform to grant IAM permissions to the GSA, e.g.:

resource "google_project_iam_member" "storage_object_viewer" {
  project = data.google_project.this.project_id
  role    = "roles/storage.objectViewer"
  member  = "serviceAccount:${module.workload_identity_service_account.google_service_account.email}"
}
comments powered by Disqus