diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8e19a632f..734520fa6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -59,15 +59,24 @@ Six test-kitchen instances are defined: The test-kitchen instances in `test/fixtures/` wrap identically-named examples in the `examples/` directory.` ### Test Environment -The easiest way to test the module is in an isolated test project. The setup for such a project is defined in [test/setup](./test/setup/) directory. +The easiest way to test the module is in an isolated test project. The +setup for such a project is defined in [test/setup](./test/setup/) +directory. -To use this setup, you need a service account with Project Creator access on a folder. Export the Service Account credentials to your environment like so: +To use this setup, you need a service account with Project Creator access +on a folder; the Billing Account User role is also required. Export the +Service Account credentials to your environment like so: ``` export SERVICE_ACCOUNT_JSON=$(< credentials.json) ``` +Note that `SERVICE_ACCOUNT_JSON` holds the _contents_ of the credentials +file; if you see errors pertaining to credential type, ensure this variable +contains valid JSON, and not, for example, a path. + You will also need to set a few environment variables: + ``` export TF_VAR_org_id="your_org_id" export TF_VAR_folder_id="your_folder_id" @@ -75,6 +84,7 @@ export TF_VAR_billing_account="your_billing_account_id" ``` With these settings in place, you can prepare a test project using Docker: + ``` make docker_test_prepare ``` diff --git a/modules/workload-identity/README.md b/modules/workload-identity/README.md index e40dc0d05..543a32cda 100644 --- a/modules/workload-identity/README.md +++ b/modules/workload-identity/README.md @@ -4,13 +4,15 @@ This module creates: -* GCP Service Account * IAM Service Account binding to `roles/iam.workloadIdentityUser` +* Optionally, a Google Service Account * Optionally, a Kubernetes Service Account ## Usage -The `terraform-google-workload-identity` can create a kubernetes service account for you, or use an existing kubernetes service account. +The `terraform-google-workload-identity` can create service accounts for you, +or you can use existing accounts; this applies for both the Google and +Kubernetes accounts. ### Creating a Workload Identity @@ -20,17 +22,17 @@ module "my-app-workload-identity" { name = "my-application-name" namespace = "default" project_id = "my-gcp-project-name" - roles = ["roles/storage.Admin", "roles/compute.Admin"] + roles = ["roles/storage.Admin", "roles/compute.Admin"] } ``` This will create: -* GCP Service Account named: `my-application-name@my-gcp-project-name.iam.gserviceaccount.com` +* Google Service Account named: `my-application-name@my-gcp-project-name.iam.gserviceaccount.com` * Kubernetes Service Account named: `my-application-name` in the `default` namespace * IAM Binding (`roles/iam.workloadIdentityUser`) between the service accounts -Usage from a kubernetes deployment: +Usage from a Kubernetes deployment: ```yaml metadata: @@ -43,27 +45,47 @@ spec: serviceAccountName: my-application-name ``` +### Using an existing Google Service Account + +An existing Google service account can optionally be used. + +```hcl +resource "google_service_account" "preexisting" { + account_id = "preexisting-sa" +} + +module "my-app-workload-identity" { + source = "terraform-google-modules/kubernetes-engine/google//modules/workload-identity" + use_existing_gcp_sa = true + name = google_service_account.preexisting.account_id + project_id = var.project_id +} +``` + ### Using an existing Kubernetes Service Account -An existing kubernetes service account can optionally be used. When using an existing k8s servicea account the annotation `"iam.gke.io/gcp-service-account"` must be set. +An existing Kubernetes service account can optionally be used. ```hcl resource "kubernetes_service_account" "preexisting" { metadata { - name = "preexisting-sa" + name = "preexisting-sa" namespace = "prod" } } module "my-app-workload-identity" { - source = "terraform-google-modules/kubernetes-engine/google//modules/workload-identity" + source = "terraform-google-modules/kubernetes-engine/google//modules/workload-identity" use_existing_k8s_sa = true - name = "preexisting-sa" - namespace = "prod" + name = kubernetes_service_account.preexisting.metadata[0].name + namespace = kubernetes_service_account.preexisting.metadata[0].namespace project_id = var.project_id } ``` +If annotation is disabled (via `annotate_k8s_sa = false`), the existing Kubernetes service account must +already bear the `"iam.gke.io/gcp-service-account"` annotation. + ## Inputs @@ -72,13 +94,15 @@ module "my-app-workload-identity" { | annotate\_k8s\_sa | Annotate the kubernetes service account with 'iam.gke.io/gcp-service-account' annotation. Valid in cases when an existing SA is used. | `bool` | `true` | no | | automount\_service\_account\_token | Enable automatic mounting of the service account token | `bool` | `false` | no | | cluster\_name | Cluster name. Required if using existing KSA. | `string` | `""` | no | +| gcp\_sa\_name | Name for the Google service account; overrides `var.name`. | `string` | `null` | no | | impersonate\_service\_account | An optional service account to impersonate for gcloud commands. If this service account is not specified, the module will use Application Default Credentials. | `string` | `""` | no | -| k8s\_sa\_name | Name for the existing Kubernetes service account | `string` | `null` | no | +| k8s\_sa\_name | Name for the Kubernetes service account; overrides `var.name`. | `string` | `null` | no | | location | Cluster location (region if regional cluster, zone if zonal cluster). Required if using existing KSA. | `string` | `""` | no | | name | Name for both service accounts. The GCP SA will be truncated to the first 30 chars if necessary. | `string` | n/a | yes | -| namespace | Namespace for k8s service account | `string` | `"default"` | no | +| namespace | Namespace for the Kubernetes service account | `string` | `"default"` | no | | project\_id | GCP project ID | `string` | n/a | yes | -| roles | (optional) A list of roles to be added to the created Service account | `list(string)` | `[]` | no | +| roles | A list of roles to be added to the created service account | `list(string)` | `[]` | no | +| use\_existing\_gcp\_sa | Use an existing Google service account instead of creating one | `bool` | `false` | no | | use\_existing\_k8s\_sa | Use an existing kubernetes service account instead of creating one | `bool` | `false` | no | ## Outputs diff --git a/modules/workload-identity/main.tf b/modules/workload-identity/main.tf index bbd4c0775..2215540a1 100644 --- a/modules/workload-identity/main.tf +++ b/modules/workload-identity/main.tf @@ -15,19 +15,30 @@ */ locals { - k8s_sa_gcp_derived_name = "serviceAccount:${var.project_id}.svc.id.goog[${var.namespace}/${local.output_k8s_name}]" - gcp_sa_email = google_service_account.cluster_service_account.email + # GCP service account ids must be < 30 chars matching regex ^[a-z](?:[-a-z0-9]{4,28}[a-z0-9])$ + # KSAs do not have this naming restriction. + gcp_given_name = var.gcp_sa_name != null ? var.gcp_sa_name : substr(var.name, 0, 30) + gcp_sa_email = data.google_service_account.cluster_service_account.email + gcp_sa_fqn = "serviceAccount:${local.gcp_sa_email}" - # This will cause terraform to block returning outputs until the service account is created + # This will cause Terraform to block returning outputs until the service account is created k8s_given_name = var.k8s_sa_name != null ? var.k8s_sa_name : var.name output_k8s_name = var.use_existing_k8s_sa ? local.k8s_given_name : kubernetes_service_account.main[0].metadata[0].name output_k8s_namespace = var.use_existing_k8s_sa ? var.namespace : kubernetes_service_account.main[0].metadata[0].namespace + + k8s_sa_gcp_derived_name = "serviceAccount:${var.project_id}.svc.id.goog[${var.namespace}/${local.output_k8s_name}]" } -resource "google_service_account" "cluster_service_account" { - # GCP service account ids must be < 30 chars matching regex ^[a-z](?:[-a-z0-9]{4,28}[a-z0-9])$ - # KSA do not have this naming restriction. - account_id = substr(var.name, 0, 30) +data "google_service_account" "cluster_service_account" { + # This will cause Terraform to block looking up details until the service account is created + account_id = var.use_existing_gcp_sa ? local.gcp_given_name : google_service_account.main[0].account_id + project = var.project_id +} + +resource "google_service_account" "main" { + count = var.use_existing_gcp_sa ? 0 : 1 + + account_id = local.gcp_given_name display_name = substr("GCP SA bound to K8S SA ${local.k8s_given_name}", 0, 100) project = var.project_id } @@ -40,7 +51,7 @@ resource "kubernetes_service_account" "main" { name = var.name namespace = var.namespace annotations = { - "iam.gke.io/gcp-service-account" = google_service_account.cluster_service_account.email + "iam.gke.io/gcp-service-account" = local.gcp_sa_email } } } @@ -61,16 +72,15 @@ module "annotate-sa" { } resource "google_service_account_iam_member" "main" { - service_account_id = google_service_account.cluster_service_account.name + service_account_id = data.google_service_account.cluster_service_account.name role = "roles/iam.workloadIdentityUser" member = local.k8s_sa_gcp_derived_name } - resource "google_project_iam_member" "workload_identity_sa_bindings" { for_each = toset(var.roles) project = var.project_id role = each.value - member = "serviceAccount:${google_service_account.cluster_service_account.email}" + member = local.gcp_sa_fqn } diff --git a/modules/workload-identity/output.tf b/modules/workload-identity/output.tf index 9965e0b16..fd0554d46 100644 --- a/modules/workload-identity/output.tf +++ b/modules/workload-identity/output.tf @@ -31,7 +31,7 @@ output "gcp_service_account_email" { output "gcp_service_account_fqn" { description = "FQN of GCP service account." - value = "serviceAccount:${google_service_account.cluster_service_account.email}" + value = local.gcp_sa_fqn } output "gcp_service_account_name" { @@ -41,5 +41,5 @@ output "gcp_service_account_name" { output "gcp_service_account" { description = "GCP service account." - value = google_service_account.cluster_service_account + value = data.google_service_account.cluster_service_account } diff --git a/modules/workload-identity/variables.tf b/modules/workload-identity/variables.tf index f903a61aa..315c85fd5 100644 --- a/modules/workload-identity/variables.tf +++ b/modules/workload-identity/variables.tf @@ -19,6 +19,23 @@ variable "name" { type = string } +variable "project_id" { + description = "GCP project ID" + type = string +} + +variable "gcp_sa_name" { + description = "Name for the Google service account; overrides `var.name`." + type = string + default = null +} + +variable "use_existing_gcp_sa" { + description = "Use an existing Google service account instead of creating one" + type = bool + default = false +} + variable "cluster_name" { description = "Cluster name. Required if using existing KSA." type = string @@ -32,48 +49,43 @@ variable "location" { } variable "k8s_sa_name" { - description = "Name for the existing Kubernetes service account" + description = "Name for the Kubernetes service account; overrides `var.name`." type = string default = null } variable "namespace" { - description = "Namespace for k8s service account" - default = "default" - type = string -} - -variable "project_id" { - description = "GCP project ID" + description = "Namespace for the Kubernetes service account" type = string + default = "default" } variable "use_existing_k8s_sa" { description = "Use an existing kubernetes service account instead of creating one" - default = false type = bool + default = false } variable "annotate_k8s_sa" { description = "Annotate the kubernetes service account with 'iam.gke.io/gcp-service-account' annotation. Valid in cases when an existing SA is used." - default = true type = bool + default = true } variable "automount_service_account_token" { description = "Enable automatic mounting of the service account token" - default = false type = bool + default = false } variable "roles" { + description = "A list of roles to be added to the created service account" type = list(string) default = [] - description = "(optional) A list of roles to be added to the created Service account" } variable "impersonate_service_account" { - type = string description = "An optional service account to impersonate for gcloud commands. If this service account is not specified, the module will use Application Default Credentials." + type = string default = "" }