From 991cd1324d3ed4dd5997303867a5fa930a1cc2b5 Mon Sep 17 00:00:00 2001 From: Aleksandr Averbukh Date: Tue, 25 Oct 2022 13:04:27 +0200 Subject: [PATCH] feat: TFE OIDC with GCP WIF blueprint added. --- blueprints/cloud-operations/README.md | 5 + .../terraform-enterprise-wif/README.md | 115 ++++++++++++++++++ .../terraform-enterprise-wif/diagram.png | Bin 0 -> 29084 bytes .../gcp-workload-identity-provider/README.md | 33 +++++ .../gcp-workload-identity-provider/main.tf | 83 +++++++++++++ .../gcp-workload-identity-provider/outputs.tf | 34 ++++++ .../terraform.auto.tfvars.template | 20 +++ .../variables.tf | 69 +++++++++++ .../tfc-workflow-using-wif/README.md | 19 +++ .../backend.tf.template | 29 +++++ .../tfc-workflow-using-wif/main.tf | 25 ++++ .../tfc-workflow-using-wif/provider.tf | 25 ++++ .../terraform.auto.tfvars.template | 17 +++ .../tfc-workflow-using-wif/tfc-oidc/README.md | 40 ++++++ .../tfc-workflow-using-wif/tfc-oidc/main.tf | 23 ++++ .../tfc-oidc/outputs.tf | 26 ++++ .../tfc-oidc/variables.tf | 31 +++++ .../tfc-oidc/versions.tf | 17 +++ .../tfc-oidc/write_token.sh | 23 ++++ .../tfc-workflow-using-wif/variables.tf | 29 +++++ .../__init__.py | 13 ++ .../fixture/main.tf | 28 +++++ .../fixture/variables.tf | 68 +++++++++++ .../test_plan.py | 19 +++ 24 files changed, 791 insertions(+) create mode 100644 blueprints/cloud-operations/terraform-enterprise-wif/README.md create mode 100644 blueprints/cloud-operations/terraform-enterprise-wif/diagram.png create mode 100644 blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/README.md create mode 100644 blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/main.tf create mode 100644 blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/outputs.tf create mode 100644 blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/terraform.auto.tfvars.template create mode 100644 blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/variables.tf create mode 100644 blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/README.md create mode 100644 blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/backend.tf.template create mode 100644 blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/main.tf create mode 100644 blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/provider.tf create mode 100644 blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/terraform.auto.tfvars.template create mode 100644 blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/README.md create mode 100644 blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/main.tf create mode 100644 blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/outputs.tf create mode 100644 blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/variables.tf create mode 100644 blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/versions.tf create mode 100644 blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/write_token.sh create mode 100644 blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/variables.tf create mode 100644 tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/__init__.py create mode 100644 tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/fixture/main.tf create mode 100644 tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/fixture/variables.tf create mode 100644 tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/test_plan.py diff --git a/blueprints/cloud-operations/README.md b/blueprints/cloud-operations/README.md index 36e4c41b..88d55d4e 100644 --- a/blueprints/cloud-operations/README.md +++ b/blueprints/cloud-operations/README.md @@ -62,3 +62,8 @@ This [blueprint](./onprem-sa-key-management) shows how to manage IAM Service Acc This [blueprint](./unmanaged-instances-healthcheck) shows how to leverage [Serverless VPC Access](https://cloud.google.com/vpc/docs/configure-serverless-vpc-access) and Cloud Functions to organize a highly performant TCP healtheck for unmanaged GCE instances.
+ +## Workload identity federation for Terraform Enterprise workflow + This [blueprint](./terraform-enterprise-wif) shows how to configure [Wokload Identity Federation](https://cloud.google.com/iam/docs/workload-identity-federation) between [Terraform Cloud/Enterprise](https://developer.hashicorp.com/terraform/enterprise) instance and Google Cloud. + +
diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/README.md b/blueprints/cloud-operations/terraform-enterprise-wif/README.md new file mode 100644 index 00000000..4bb282c5 --- /dev/null +++ b/blueprints/cloud-operations/terraform-enterprise-wif/README.md @@ -0,0 +1,115 @@ +# Configuring workload identity federation for Terraform Cloud/Enterprise workflow + +The most common way to use Terraform Cloud for GCP deployments is to store a GCP Service Account Key as a part of TFE Workflow configuration, as we all know there are security risks due to the fact that keys are long term credentials that could be compromised. + +Workload identity federation enables applications running outside of Google Cloud to replace long-lived service account keys with short-lived access tokens. This is achieved by configuring Google Cloud to trust an external identity provider, so applications can use the credentials issued by the external identity provider to impersonate a service account. + +This blueprint shows how to set up [Workload Identity Federation](https://cloud.google.com/iam/docs/workload-identity-federation) between [Terraform Cloud/Enterprise](https://developer.hashicorp.com/terraform/enterprise) instance and Google Cloud. This will be possible by configuring workload identity federation to trust oidc tokens generated for a specific workflow in a Terraform Enterprise organization. + +The following diagram illustrates how the VM will get a short-lived access token and use it to access a resource: + + ![Sequence diagram](diagram.png) + +## Running the blueprint + +### Create Terraform Enterprise Workflow +If you don't have an existing Terraform Enterprise organization you can sign up for a [free trial](https://app.terraform.io/public/signup/account) account. + +Create a new Workspace for a `CLI-driven workflow` (Identity Federation will work for any workflow type, but for simplicity of the blueprint we use CLI driven workflow). + +Note workspace name and id (id starts with `ws-`), we will use them on a later stage. + +Go to the organization settings and note the org name and id (id starts with `org-`). + +### Deploy GCP Workload Identity Pool Provider for Terraform Enterprise + +> **_NOTE:_** This is a preparation part and should be executed on behalf of a user with enough permissions. + +Required permissions when new project is created: + - Project Creator on the parent folder/org. + + Required permissions when an existing project is used: + - Workload Identity Admin on the project level + - Project IAM Admin on the project level + +Fill out required variables, use TFE Org and Workspace IDs from the previous steps (IDs are not the names). +```bash +cd gcp-workload-identity-provider + +mv terraform.auto.tfvars.template terraform.auto.tfvars + +vi terraform.auto.tfvars +``` + +Authenticate using application default credentials, execute terraform code and deploy resources +``` +gcloud auth application-default login + +terraform init + +terraform apply +``` + +As a result a set of outputs will be provided (your values will be different), note the output since we will use it on the next steps. + +``` +impersonate_service_account_email = "sa-tfe@fe-test-oidc.iam.gserviceaccount.com" +project_id = "tfe-test-oidc" +workload_identity_audience = "//iam.googleapis.com/projects/476538149566/locations/global/workloadIdentityPools/tfe-pool/providers/tfe-provider" +workload_identity_pool_provider_id = "projects/476538149566/locations/global/workloadIdentityPools/tfe-pool/providers/tfe-provider" +``` + +### Configure OIDC provider for your TFE Workflow + +To enable OIDC for a TFE workflow it's enough to setup an environment variable `TFC_WORKLOAD_IDENTITY_AUDIENCE`. + +Go the the Workflow -> Variables and add a new variable `TFC_WORKLOAD_IDENTITY_AUDIENCE` equal to the value of `workload_identity_audience` output, in our example it's: + +``` +TFC_WORKLOAD_IDENTITY_AUDIENCE = "//iam.googleapis.com/projects/476538149566/locations/global/workloadIdentityPools/tfe-pool/providers/tfe-provider" +``` + +At that point we setup GCP Identity Federation to trust TFE generated OIDC tokens, so the TFE workflow can use the token to impersonate a GCP Service Account. + +## Testing the blueprint + +In order to test the setup we will deploy a GCS bucket from TFE Workflow using OIDC token for Service Account Impersonation. + +### Configure backend and variables + +First, we need to configure TFE Remote backend for our testing terraform code, use TFE Organization name and workspace name (names are not the same as ids) + +``` +cd ../tfc-workflow-using-wif + +mv backend.tf.template backend.tf + + +vi backend.tf + +``` + +Fill out variables based on the output from the preparation steps: + +``` +mv terraform.auto.tfvars.template terraform.auto.tfvars + +vi terraform.auto.tfvars + +``` + +### Authenticate terraform for triggering CLI-driven workflow + +Follow this [documentation](https://learn.hashicorp.com/tutorials/terraform/cloud-login) to login ti terraform cloud from the CLI. + +### Trigger the workflow + +``` +terraform init + +terraform apply +``` + +As a result we have a successfully deployed GCS bucket from Terraform Enterprise workflow using Workload Identity Federation. + +Once done testing, you can clean up resources by running `terraform destroy` first in the `tfc-workflow-using-wif` and then `gcp-workload-identity-provider` folders. diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/diagram.png b/blueprints/cloud-operations/terraform-enterprise-wif/diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..d4e6f82e9d0bfbc41c85ed9e3e5b4aaa5b72ac25 GIT binary patch literal 29084 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

w%=&~_s@(isigS^wDyk9P9`wMxNtgc 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 + +## Variables + +| name | description | type | required | default | +|---|---|:---:|:---:|:---:| +| [billing_account](variables.tf#L16) | Billing account id used as default for new projects. | string | ✓ | | +| [project_id](variables.tf#L38) | Existing project id. | string | ✓ | | +| [tfe_organization_id](variables.tf#L43) | | | ✓ | | +| [tfe_workspace_id](variables.tf#L48) | | | ✓ | | +| [issuer_uri](variables.tf#L65) | Terraform Enterprise uri. Replace the uri if a self hosted instance is used. | string | | "https://app.terraform.io/" | +| [parent](variables.tf#L27) | Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format. | string | | null | +| [project_create](variables.tf#L21) | Create project instead of using an existing one. | bool | | true | +| [workload_identity_pool_id](variables.tf#L53) | Workload identity pool id. | string | | "tfe-pool" | +| [workload_identity_pool_provider_id](variables.tf#L59) | Workload identity pool provider id. | string | | "tfe-provider" | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| [impersonate_service_account_email](outputs.tf#L31) | | | +| [project_id](outputs.tf#L16) | | | +| [workload_identity_audience](outputs.tf#L26) | | | +| [workload_identity_pool_provider_id](outputs.tf#L21) | GCP workload identity pool provider ID. | | + + diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/main.tf b/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/main.tf new file mode 100644 index 00000000..6ca48696 --- /dev/null +++ b/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/main.tf @@ -0,0 +1,83 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +############################################################################### +# GCP PROJECT # +############################################################################### + +module "project" { + source = "../../../../modules/project" + name = var.project_id + project_create = var.project_create + parent = var.parent + billing_account = var.billing_account + services = [ + "iam.googleapis.com", + "cloudresourcemanager.googleapis.com", + "iamcredentials.googleapis.com", + "sts.googleapis.com", + "storage.googleapis.com" + ] +} + +############################################################################### +# Workload Identity Pool and Provider # +############################################################################### + +resource "google_iam_workload_identity_pool" "tfe-pool" { + project = module.project.project_id + workload_identity_pool_id = var.workload_identity_pool_id + display_name = "TFE Pool" + description = "Identity pool for Terraform Enterprise OIDC integration" +} + +resource "google_iam_workload_identity_pool_provider" "tfe-pool-provider" { + project = module.project.project_id + workload_identity_pool_id = google_iam_workload_identity_pool.tfe-pool.workload_identity_pool_id + workload_identity_pool_provider_id = var.workload_identity_pool_provider_id + display_name = "TFE Pool Provider" + description = "OIDC identity pool provider for TFE Integration" + # Use condition to make sure only token generated for a specific TFE Org and workspace can be used + attribute_condition = "attribute.terraform_workspace_id == \"${var.tfe_workspace_id}\" && attribute.terraform_organization_id == \"${var.tfe_organization_id}\"" + attribute_mapping = { + "google.subject" = "assertion.sub" + "attribute.terraform_organization_id" = "assertion.terraform_organization_id" + "attribute.terraform_workspace_id" = "assertion.terraform_workspace_id" + } + oidc { + # Should be different if self hosted TFE instance is used + issuer_uri = var.issuer_uri + } +} + +############################################################################### +# Service Account and IAM bindings # +############################################################################### + +module "sa-tfe" { + source = "../../../../modules/iam-service-account" + project_id = module.project.project_id + name = "sa-tfe" + + iam = { + "roles/iam.workloadIdentityUser" = ["principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.tfe-pool.name}/*"] + } + + iam_project_roles = { + "${module.project.project_id}" = [ + "roles/storage.admin" + ] + } +} \ No newline at end of file diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/outputs.tf b/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/outputs.tf new file mode 100644 index 00000000..f28dc3a9 --- /dev/null +++ b/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/outputs.tf @@ -0,0 +1,34 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +output "project_id" { + description = "GCP Project ID." + value = module.project.project_id +} + +output "workload_identity_pool_provider_id" { + description = "GCP workload identity pool provider ID." + value = google_iam_workload_identity_pool_provider.tfe-pool-provider.name +} + +output "workload_identity_audience" { + description = "TFC Workload Identity Audience." + value = "//iam.googleapis.com/${google_iam_workload_identity_pool_provider.tfe-pool-provider.name}" +} + +output "impersonate_service_account_email" { + description = "Service account to be impersonated by workload identity." + value = module.sa-tfe.email +} \ No newline at end of file diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/terraform.auto.tfvars.template b/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/terraform.auto.tfvars.template new file mode 100644 index 00000000..645eea0b --- /dev/null +++ b/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/terraform.auto.tfvars.template @@ -0,0 +1,20 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +parent = "folders/437102807785" +project_id = "my-project-id" +tfe_organization_id = "org-W3bz9neazHrZz99U" +tfe_workspace_id = "ws-DFxEE3NmeMdaAvoK" +billing_account = "015617-1B8CBC-AF10D9" diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/variables.tf b/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/variables.tf new file mode 100644 index 00000000..6df8dbcb --- /dev/null +++ b/blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider/variables.tf @@ -0,0 +1,69 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +variable "billing_account" { + description = "Billing account id used as default for new projects." + type = string +} + +variable "project_create" { + description = "Create project instead of using an existing one." + type = bool + default = true +} + +variable "parent" { + description = "Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format." + type = string + default = null + validation { + condition = var.parent == null || can(regex("(organizations|folders)/[0-9]+", var.parent)) + error_message = "Parent must be of the form folders/folder_id or organizations/organization_id." + } +} + + +variable "project_id" { + description = "Existing project id." + type = string +} + +variable "tfe_organization_id" { + description = "TFE organization id." + type = string +} + +variable "tfe_workspace_id" { + description = "TFE workspace id." + type = string +} + +variable "workload_identity_pool_id" { + description = "Workload identity pool id." + type = string + default = "tfe-pool" +} + +variable "workload_identity_pool_provider_id" { + description = "Workload identity pool provider id." + type = string + default = "tfe-provider" +} + +variable "issuer_uri" { + description = "Terraform Enterprise uri. Replace the uri if a self hosted instance is used." + type = string + default = "https://app.terraform.io/" +} \ No newline at end of file diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/README.md b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/README.md new file mode 100644 index 00000000..5226dd64 --- /dev/null +++ b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/README.md @@ -0,0 +1,19 @@ +# GCP Workload Identity Provider for Terraform Enterprise + +This terraform code is a part of [GCP Workload Identity Federation for Terraform Enterprise](../) blueprint. For instructions please refer to the blueprint [readme](../README.md). + +The codebase provisions the following list of resources: + +- GCS Bucket + + + +## Variables + +| name | description | type | required | default | +|---|---|:---:|:---:|:---:| +| [impersonate_service_account_email](variables.tf#L26) | | | ✓ | | +| [project_id](variables.tf#L16) | | | ✓ | | +| [workload_identity_pool_provider_id](variables.tf#L21) | GCP workload identity pool provider ID. | string | ✓ | | + + diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/backend.tf.template b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/backend.tf.template new file mode 100644 index 00000000..87d4737d --- /dev/null +++ b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/backend.tf.template @@ -0,0 +1,29 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +# The block below configures Terraform to use the 'remote' backend with Terraform Cloud. +# For more information, see https://www.terraform.io/docs/backends/types/remote.html + +terraform { + backend "remote" { + organization = "" + + workspaces { + name = "" + } + } + + required_version = ">= 0.14.0" +} diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/main.tf b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/main.tf new file mode 100644 index 00000000..5e03ada5 --- /dev/null +++ b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/main.tf @@ -0,0 +1,25 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +############################################################################### +# TEST RESOURCE TO VALIDATE WIF # +############################################################################### + +resource "google_storage_bucket" "test-bucket" { + project = var.project_id + name = "${var.project_id}-tfe-oidc-test-bucket" + location = "US" + force_destroy = true +} diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/provider.tf b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/provider.tf new file mode 100644 index 00000000..47f24620 --- /dev/null +++ b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/provider.tf @@ -0,0 +1,25 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +module "tfe_oidc" { + source = "./tfc-oidc" + + workload_identity_pool_provider_id = var.workload_identity_pool_provider_id + impersonate_service_account_email = var.impersonate_service_account_email +} + +provider "google" { + credentials = module.tfe_oidc.credentials +} diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/terraform.auto.tfvars.template b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/terraform.auto.tfvars.template new file mode 100644 index 00000000..efea4cc9 --- /dev/null +++ b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/terraform.auto.tfvars.template @@ -0,0 +1,17 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +project_id = "tfe-oidc-workflow" +workload_identity_pool_provider_id = "projects/683987109094/locations/global/workloadIdentityPools/tfe-pool/providers/tfe-provider" +impersonate_service_account_email = "sa-tfe@tfe-oidc-workflow2.iam.gserviceaccount.com" diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/README.md b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/README.md new file mode 100644 index 00000000..bb8d7983 --- /dev/null +++ b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/README.md @@ -0,0 +1,40 @@ +# Terraform Enterprise OIDC Credential for GCP Workload Identity Federation + +This is a helper module to prepare GCP Credentials from Terraform Enterprise workload identity token. For more information see [Terraform Enterprise Workload Identity Federation](../) blueprint. + +## Example +```hcl +module "tfe_oidc" { + source = "./tfe_oidc" + + workload_identity_pool_provider_id = "projects/683987109094/locations/global/workloadIdentityPools/tfe-pool/providers/tfe-provider" + impersonate_service_account_email = "tfe-test@tfe-test-wif.iam.gserviceaccount.com" +} + +provider "google" { + credentials = module.tfe_oidc.credentials +} + +provider "google-beta" { + credentials = module.tfe_oidc.credentials +} + +# tftest skip +``` + + +## Variables + +| name | description | type | required | default | +|---|---|:---:|:---:|:---:| +| [impersonate_service_account_email](variables.tf#L22) | Service account to be impersonated by workload identity federation. | string | ✓ | | +| [workload_identity_pool_provider_id](variables.tf#L17) | GCP workload identity pool provider ID. | string | ✓ | | +| [tmp_oidc_token_path](variables.tf#L27) | Name of the temporary file where TFC OIDC token will be stored to authentificate terraform provider google. | string | | ".oidc_token" | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| [credentials](outputs.tf#L17) | | | + + diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/main.tf b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/main.tf new file mode 100644 index 00000000..2c510a6a --- /dev/null +++ b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/main.tf @@ -0,0 +1,23 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +locals { + audience = "//iam.googleapis.com/${var.workload_identity_pool_provider_id}" +} + +data "external" "oidc_token_file" { + program = ["bash", "${path.module}/write_token.sh", "${var.tmp_oidc_token_path}"] +} diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/outputs.tf b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/outputs.tf new file mode 100644 index 00000000..fbcea8c2 --- /dev/null +++ b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/outputs.tf @@ -0,0 +1,26 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +output "credentials" { + value = jsonencode({ + "type" : "external_account", + "audience" : "${local.audience}", + "subject_token_type" : "urn:ietf:params:oauth:token-type:jwt", + "token_url" : "https://sts.googleapis.com/v1/token", + "credential_source" : data.external.oidc_token_file.result + "service_account_impersonation_url" : "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${var.impersonate_service_account_email}:generateAccessToken" + }) +} diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/variables.tf b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/variables.tf new file mode 100644 index 00000000..06f310da --- /dev/null +++ b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/variables.tf @@ -0,0 +1,31 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +variable "workload_identity_pool_provider_id" { + description = "GCP workload identity pool provider ID." + type = string +} + +variable "impersonate_service_account_email" { + description = "Service account to be impersonated by workload identity federation." + type = string +} + +variable "tmp_oidc_token_path" { + description = "Name of the temporary file where TFC OIDC token will be stored to authentificate terraform provider google." + type = string + default = ".oidc_token" +} diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/versions.tf b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/versions.tf new file mode 100644 index 00000000..a079e99c --- /dev/null +++ b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/versions.tf @@ -0,0 +1,17 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +terraform { + required_version = ">= 1.3.1" +} diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/write_token.sh b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/write_token.sh new file mode 100644 index 00000000..2f7e30a2 --- /dev/null +++ b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/tfc-oidc/write_token.sh @@ -0,0 +1,23 @@ +#!/bin/bash +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Exit if any of the intermediate steps fail +set -e + +FILENAME=$@ + +echo $TFC_WORKLOAD_IDENTITY_TOKEN > $FILENAME + +echo -n "{\"file\":\"${FILENAME}\"}" diff --git a/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/variables.tf b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/variables.tf new file mode 100644 index 00000000..55a8c691 --- /dev/null +++ b/blueprints/cloud-operations/terraform-enterprise-wif/tfc-workflow-using-wif/variables.tf @@ -0,0 +1,29 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +variable "project_id" { + description = "GCP project ID." + type = string +} + +variable "workload_identity_pool_provider_id" { + description = "GCP workload identity pool provider ID." + type = string +} + +variable "impersonate_service_account_email" { + description = "Service account to be impersonated by workload identity." + type = string +} \ No newline at end of file diff --git a/tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/__init__.py b/tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/__init__.py new file mode 100644 index 00000000..6d6d1266 --- /dev/null +++ b/tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/fixture/main.tf b/tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/fixture/main.tf new file mode 100644 index 00000000..3552740c --- /dev/null +++ b/tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/fixture/main.tf @@ -0,0 +1,28 @@ +/** + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +module "test" { + source = "../../../../../../blueprints/cloud-operations/terraform-enterprise-wif/gcp-workload-identity-provider" + billing_account = var.billing_account + project_create = var.project_create + project_id = var.project_id + parent = var.parent + tfe_organization_id = var.tfe_organization_id + tfe_workspace_id = var.tfe_workspace_id + workload_identity_pool_id = var.workload_identity_pool_id + workload_identity_pool_provider_id = var.workload_identity_pool_provider_id + issuer_uri = var.issuer_uri +} diff --git a/tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/fixture/variables.tf b/tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/fixture/variables.tf new file mode 100644 index 00000000..134e4aff --- /dev/null +++ b/tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/fixture/variables.tf @@ -0,0 +1,68 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +variable "billing_account" { + type = string + default = "1234-ABCD-1234" +} + +variable "project_create" { + type = bool + default = true +} + +variable "project_id" { + type = string + default = "project-1" +} + +variable "parent" { + description = "Parent folder or organization in 'folders/folder_id' or 'organizations/org_id' format." + type = string + default = null + validation { + condition = var.parent == null || can(regex("(organizations|folders)/[0-9]+", var.parent)) + error_message = "Parent must be of the form folders/folder_id or organizations/organization_id." + } +} + +variable "tfe_organization_id" { + description = "TFE organization id." + type = string + default = "org-123" +} + +variable "tfe_workspace_id" { + description = "TFE workspace id." + type = string + default = "ws-123" +} + +variable "workload_identity_pool_id" { + description = "Workload identity pool id." + type = string + default = "tfe-pool" +} + +variable "workload_identity_pool_provider_id" { + description = "Workload identity pool provider id." + type = string + default = "tfe-provider" +} + +variable "issuer_uri" { + description = "Terraform Enterprise uri. Replace the uri if a self hosted instance is used." + type = string + default = "https://app.terraform.io/" +} \ No newline at end of file diff --git a/tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/test_plan.py b/tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/test_plan.py new file mode 100644 index 00000000..e6a8bde4 --- /dev/null +++ b/tests/blueprints/cloud_operations/terraform-enterprise-wif/gcp-workload-identity-provider/test_plan.py @@ -0,0 +1,19 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +def test_resources(e2e_plan_runner): + "Test that plan works and the numbers of resources is as expected." + modules, resources = e2e_plan_runner() + assert len(modules) == 2 + assert len(resources) == 13