Skip to content

Commit

Permalink
feat: Enables an existing GSA to be used when setting up Workload Ide…
Browse files Browse the repository at this point in the history
…ntity (#955)
  • Loading branch information
jackwhelpton committed Jul 20, 2021
1 parent e53a949 commit 712fc54
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 41 deletions.
14 changes: 12 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,22 +59,32 @@ 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"
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
```
Expand Down
50 changes: 37 additions & 13 deletions modules/workload-identity/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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:
Expand All @@ -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.

<!-- BEGINNING OF PRE-COMMIT-TERRAFORM DOCS HOOK -->
## Inputs

Expand All @@ -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
Expand Down
32 changes: 21 additions & 11 deletions modules/workload-identity/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
}
}
}
Expand All @@ -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
}
4 changes: 2 additions & 2 deletions modules/workload-identity/output.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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" {
Expand All @@ -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
}
38 changes: 25 additions & 13 deletions modules/workload-identity/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 = ""
}

0 comments on commit 712fc54

Please sign in to comment.