diff --git a/0-bootstrap/cb.tf b/0-bootstrap/cb.tf index 8707e2ed3..b67e6993e 100644 --- a/0-bootstrap/cb.tf +++ b/0-bootstrap/cb.tf @@ -178,6 +178,8 @@ module "tf_cloud_builder" { worker_pool_id = module.tf_private_pool.private_worker_pool_id bucket_name = "${var.bucket_prefix}-${module.tf_source.cloudbuild_project_id}-tf-cloudbuilder-build-logs" workflow_deletion_protection = var.workflow_deletion_protection + + depends_on = [module.tf_source] } module "bootstrap_csr_repo" { diff --git a/0-bootstrap/sa.tf b/0-bootstrap/sa.tf index da23168b0..0802d507f 100644 --- a/0-bootstrap/sa.tf +++ b/0-bootstrap/sa.tf @@ -111,6 +111,7 @@ locals { ], "proj" = [ "roles/storage.objectAdmin", + "roles/storage.admin", ], } diff --git a/3-networks-hub-and-spoke/modules/base_env/main.tf b/3-networks-hub-and-spoke/modules/base_env/main.tf index 60e34e5af..f624bbbe6 100644 --- a/3-networks-hub-and-spoke/modules/base_env/main.tf +++ b/3-networks-hub-and-spoke/modules/base_env/main.tf @@ -57,6 +57,7 @@ locals { "cloudtrace.googleapis.com", "composer.googleapis.com", "compute.googleapis.com", + "confidentialcomputing.googleapis.com", "connectgateway.googleapis.com", "contactcenterinsights.googleapis.com", "container.googleapis.com", diff --git a/3-networks-svpc/modules/base_env/main.tf b/3-networks-svpc/modules/base_env/main.tf index ba632c9db..e94e5893e 100644 --- a/3-networks-svpc/modules/base_env/main.tf +++ b/3-networks-svpc/modules/base_env/main.tf @@ -39,7 +39,7 @@ locals { "adsdatahub.googleapis.com", "aiplatform.googleapis.com", "alloydb.googleapis.com", - "alpha-documentai.googleapis.com", + "documentai.googleapis.com", "analyticshub.googleapis.com", "apigee.googleapis.com", "apigeeconnect.googleapis.com", @@ -70,6 +70,7 @@ locals { "cloudtrace.googleapis.com", "composer.googleapis.com", "compute.googleapis.com", + "confidentialcomputing.googleapis.com", "connectgateway.googleapis.com", "contactcenterinsights.googleapis.com", "container.googleapis.com", diff --git a/4-projects/README.md b/4-projects/README.md index 96464bf9f..51e7af1e1 100644 --- a/4-projects/README.md +++ b/4-projects/README.md @@ -57,7 +57,7 @@ For an overview of the architecture and the parts, see the The purpose of this step is to set up the folder structure, projects, and infrastructure pipelines for applications that are connected as service projects to the shared VPC created in the previous stage. -For each business unit, a shared `infra-pipeline` project is created along with Cloud Build triggers, CSRs for application infrastructure code and Google Cloud Storage buckets for state storage. +For each business unit, a shared `infra-pipeline` project is created along with Cloud Build triggers, CSRs for application infrastructure code, Google Cloud Storage buckets for state storage, and a new Docker image will be built for the [Confidential Space](https://cloud.google.com/confidential-computing/confidential-space/docs/confidential-space-overview) environment, which will be used in the `5-app-infra` step. This step follows the same [conventions](https://github.com/terraform-google-modules/terraform-example-foundation#branching-strategy) as the Foundation pipeline deployed in [0-bootstrap](https://github.com/terraform-google-modules/terraform-example-foundation/blob/master/0-bootstrap/README.md). A custom [workspace](https://github.com/terraform-google-modules/terraform-google-bootstrap/blob/master/modules/tf_cloudbuild_workspace/README.md) (`bu1-example-app`) is created by this pipeline and necessary roles are granted to the Terraform Service Account of this workspace by enabling variable `sa_roles` as shown in this [example](https://github.com/terraform-google-modules/terraform-example-foundation/blob/master/4-projects/modules/base_env/example_shared_vpc_project.tf). @@ -201,7 +201,6 @@ grep -rl 10.3.64.0 business_unit_2/ | xargs sed -i 's/10.3.64.0/10.4.64.0/g' git checkout -b production git push origin production ``` - 1. After production has been applied, apply development. 1. Merge changes to development. Because this is a [named environment branch](../docs/FAQ.md#what-is-a-named-branch), pushing to this branch triggers both _terraform plan_ and _terraform apply_. Review the apply output in your Cloud Build project https://console.cloud.google.com/cloud-build/builds;region=DEFAULT_REGION?project=YOUR_CLOUD_BUILD_PROJECT_ID diff --git a/4-projects/business_unit_1/development/README.md b/4-projects/business_unit_1/development/README.md index fd653c107..cad2aa934 100644 --- a/4-projects/business_unit_1/development/README.md +++ b/4-projects/business_unit_1/development/README.md @@ -19,6 +19,9 @@ |------|-------------| | access\_context\_manager\_policy\_id | Access Context Manager Policy ID. | | bucket | The created storage bucket. | +| confidential\_space\_project | Confidential Space project id. | +| confidential\_space\_project\_number | Confidential Space project number. | +| confidential\_space\_workload\_sa | Workload Service Account for confidential space from base\_env | | default\_region | The default region for the project. | | floating\_project | Project sample floating project. | | iap\_firewall\_tags | The security tags created for IAP (SSH and RDP) firewall rules and to be used on the VM created on step 5-app-infra on the peering network project. | diff --git a/4-projects/business_unit_1/development/main.tf b/4-projects/business_unit_1/development/main.tf index e06845839..bd9bb2195 100644 --- a/4-projects/business_unit_1/development/main.tf +++ b/4-projects/business_unit_1/development/main.tf @@ -38,3 +38,4 @@ module "env" { project_deletion_policy = var.project_deletion_policy folder_deletion_protection = var.folder_deletion_protection } + diff --git a/4-projects/business_unit_1/development/outputs.tf b/4-projects/business_unit_1/development/outputs.tf index f92ed8578..e81e10580 100644 --- a/4-projects/business_unit_1/development/outputs.tf +++ b/4-projects/business_unit_1/development/outputs.tf @@ -93,3 +93,18 @@ output "default_region" { description = "The default region for the project." value = local.default_region } + +output "confidential_space_project" { + description = "Confidential Space project id." + value = module.env.confidential_space_project +} + +output "confidential_space_project_number" { + description = "Confidential Space project number." + value = module.env.confidential_space_project_number +} + +output "confidential_space_workload_sa" { + description = "Workload Service Account for confidential space from base_env" + value = module.env.confidential_space_workload_sa +} diff --git a/4-projects/business_unit_1/development/remote.tf b/4-projects/business_unit_1/development/remote.tf index 4a39ce1c3..43b2da73e 100644 --- a/4-projects/business_unit_1/development/remote.tf +++ b/4-projects/business_unit_1/development/remote.tf @@ -29,3 +29,4 @@ data "terraform_remote_state" "bootstrap" { prefix = "terraform/bootstrap/state" } } + diff --git a/4-projects/business_unit_1/nonproduction/README.md b/4-projects/business_unit_1/nonproduction/README.md index e0ff4ea94..802307a77 100644 --- a/4-projects/business_unit_1/nonproduction/README.md +++ b/4-projects/business_unit_1/nonproduction/README.md @@ -19,6 +19,9 @@ |------|-------------| | access\_context\_manager\_policy\_id | Access Context Manager Policy ID. | | bucket | The created storage bucket. | +| confidential\_space\_project | Confidential Space project id. | +| confidential\_space\_project\_number | Confidential Space project number. | +| confidential\_space\_workload\_sa | Workload Service Account for confidential space from base\_env | | default\_region | The default region for the project. | | floating\_project | Project sample floating project. | | iap\_firewall\_tags | The security tags created for IAP (SSH and RDP) firewall rules and to be used on the VM created on step 5-app-infra on the peering network project. | diff --git a/4-projects/business_unit_1/nonproduction/main.tf b/4-projects/business_unit_1/nonproduction/main.tf index 2086a4494..7786a9c1c 100644 --- a/4-projects/business_unit_1/nonproduction/main.tf +++ b/4-projects/business_unit_1/nonproduction/main.tf @@ -38,3 +38,4 @@ module "env" { project_deletion_policy = var.project_deletion_policy folder_deletion_protection = var.folder_deletion_protection } + diff --git a/4-projects/business_unit_1/nonproduction/outputs.tf b/4-projects/business_unit_1/nonproduction/outputs.tf index 120ff0ad1..ac13c9920 100644 --- a/4-projects/business_unit_1/nonproduction/outputs.tf +++ b/4-projects/business_unit_1/nonproduction/outputs.tf @@ -93,3 +93,18 @@ output "default_region" { description = "The default region for the project." value = local.default_region } + +output "confidential_space_project" { + description = "Confidential Space project id." + value = module.env.confidential_space_project +} + +output "confidential_space_project_number" { + description = "Confidential Space project number." + value = module.env.confidential_space_project_number +} + +output "confidential_space_workload_sa" { + description = "Workload Service Account for confidential space from base_env" + value = module.env.confidential_space_workload_sa +} diff --git a/4-projects/business_unit_1/nonproduction/remote.tf b/4-projects/business_unit_1/nonproduction/remote.tf index 4a39ce1c3..43b2da73e 100644 --- a/4-projects/business_unit_1/nonproduction/remote.tf +++ b/4-projects/business_unit_1/nonproduction/remote.tf @@ -29,3 +29,4 @@ data "terraform_remote_state" "bootstrap" { prefix = "terraform/bootstrap/state" } } + diff --git a/4-projects/business_unit_1/production/README.md b/4-projects/business_unit_1/production/README.md index d8cb23985..5dbe98f81 100644 --- a/4-projects/business_unit_1/production/README.md +++ b/4-projects/business_unit_1/production/README.md @@ -19,6 +19,9 @@ |------|-------------| | access\_context\_manager\_policy\_id | Access Context Manager Policy ID. | | bucket | The created storage bucket. | +| confidential\_space\_project | Confidential Space project id. | +| confidential\_space\_project\_number | Confidential Space project number. | +| confidential\_space\_workload\_sa | Workload Service Account for confidential space from base\_env | | default\_region | The default region for the project. | | floating\_project | Project sample floating project. | | iap\_firewall\_tags | The security tags created for IAP (SSH and RDP) firewall rules and to be used on the VM created on step 5-app-infra on the peering network project. | diff --git a/4-projects/business_unit_1/production/main.tf b/4-projects/business_unit_1/production/main.tf index 68d2a73ad..d28775daf 100644 --- a/4-projects/business_unit_1/production/main.tf +++ b/4-projects/business_unit_1/production/main.tf @@ -38,3 +38,4 @@ module "env" { project_deletion_policy = var.project_deletion_policy folder_deletion_protection = var.folder_deletion_protection } + diff --git a/4-projects/business_unit_1/production/outputs.tf b/4-projects/business_unit_1/production/outputs.tf index 0cd227d6a..99b461d78 100644 --- a/4-projects/business_unit_1/production/outputs.tf +++ b/4-projects/business_unit_1/production/outputs.tf @@ -94,3 +94,17 @@ output "default_region" { value = local.default_region } +output "confidential_space_project" { + description = "Confidential Space project id." + value = module.env.confidential_space_project +} + +output "confidential_space_project_number" { + description = "Confidential Space project number." + value = module.env.confidential_space_project_number +} + +output "confidential_space_workload_sa" { + description = "Workload Service Account for confidential space from base_env" + value = module.env.confidential_space_workload_sa +} diff --git a/4-projects/business_unit_1/production/remote.tf b/4-projects/business_unit_1/production/remote.tf index 4a39ce1c3..43b2da73e 100644 --- a/4-projects/business_unit_1/production/remote.tf +++ b/4-projects/business_unit_1/production/remote.tf @@ -29,3 +29,4 @@ data "terraform_remote_state" "bootstrap" { prefix = "terraform/bootstrap/state" } } + diff --git a/4-projects/business_unit_1/shared/Dockerfile b/4-projects/business_unit_1/shared/Dockerfile new file mode 100644 index 000000000..6ff4feae5 --- /dev/null +++ b/4-projects/business_unit_1/shared/Dockerfile @@ -0,0 +1,20 @@ +# Copyright 2025 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. + +FROM alpine:3.18 +WORKDIR /confidential +COPY confidential_data /confidential +ENTRYPOINT ["/confidential/confidential_data"] +CMD [] + diff --git a/4-projects/business_unit_1/shared/README.md b/4-projects/business_unit_1/shared/README.md index 011c44625..00cf1c983 100644 --- a/4-projects/business_unit_1/shared/README.md +++ b/4-projects/business_unit_1/shared/README.md @@ -15,9 +15,12 @@ |------|-------------| | apply\_triggers\_id | CB apply triggers | | artifact\_buckets | GCS Buckets to store Cloud Build Artifacts | +| artifact\_registry\_repository\_id | Artifact Registry ID. | +| bootstrap\_cloudbuild\_project\_id | Cloudbuild project ID. | | cloudbuild\_project\_id | n/a | | default\_region | Default region to create resources where applicable. | | enable\_cloudbuild\_deploy | Enable infra deployment using Cloud Build. | +| image\_name | Image path used by confidential space instance. | | log\_buckets | GCS Buckets to store Cloud Build logs | | plan\_triggers\_id | CB plan triggers | | repos | CSRs to store source code | diff --git a/4-projects/business_unit_1/shared/confidential_data b/4-projects/business_unit_1/shared/confidential_data new file mode 100755 index 000000000..a13064573 --- /dev/null +++ b/4-projects/business_unit_1/shared/confidential_data @@ -0,0 +1,17 @@ +#!/bin/bash + +# Copyright 2025 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. + +echo "Confidential space is running!" diff --git a/4-projects/business_unit_1/shared/example_infra_pipeline.tf b/4-projects/business_unit_1/shared/example_infra_pipeline.tf index 013b80bd4..0e4848598 100644 --- a/4-projects/business_unit_1/shared/example_infra_pipeline.tf +++ b/4-projects/business_unit_1/shared/example_infra_pipeline.tf @@ -15,7 +15,60 @@ */ locals { - repo_names = ["bu1-example-app"] + repo_names = ["bu1-example-app"] + cmd_prompt = "gcloud builds submit . --tag ${local.confidential_space_image_tag} --project=${local.cloudbuild_project_id} --service-account=projects/${local.cloudbuild_project_id}/serviceAccounts/tf-cb-builder-sa@${local.cloudbuild_project_id}.iam.gserviceaccount.com --gcs-log-dir=gs://${module.infra_pipelines[0].log_buckets["bu1-example-app"]} --worker-pool=${local.cloud_build_private_worker_pool_id} || ( sleep 46 && gcloud builds submit . --tag ${local.confidential_space_image_tag} --project=${local.cloudbuild_project_id} --service-account=projects/${local.cloudbuild_project_id}/serviceAccounts/tf-cb-builder-sa@${local.cloudbuild_project_id}.iam.gserviceaccount.com --gcs-log-dir=gs://${module.infra_pipelines[0].log_buckets["bu1-example-app"]} --worker-pool=${local.cloud_build_private_worker_pool_id})" + confidential_space_image_version = "latest" + confidential_space_image_tag = "${var.default_region}-docker.pkg.dev/${local.cloudbuild_project_id}/tf-runners/confidential_space_image:${local.confidential_space_image_version}" + + iam_roles_build = [ + "roles/storage.objectAdmin", + "roles/cloudbuild.builds.builder", + ] +} + +resource "google_project_iam_member" "build_roles" { + for_each = toset(local.iam_roles_build) + project = local.cloudbuild_project_id + role = each.key + member = "serviceAccount:tf-cb-builder-sa@${local.cloudbuild_project_id}.iam.gserviceaccount.com" +} + +resource "google_project_iam_member" "bucket_admin_binding" { + project = local.cloudbuild_project_id + role = "roles/storage.objectAdmin" + member = "serviceAccount:${local.projects_terraform_sa}" +} + +resource "google_artifact_registry_repository_iam_member" "builder_on_artifact_registry" { + project = local.cloudbuild_project_id + location = var.default_region + repository = "tf-runners" + role = "roles/artifactregistry.repoAdmin" + member = "serviceAccount:${module.app_infra_cloudbuild_project[0].sa}" +} + +resource "google_project_iam_member" "cloudbuild_logging" { + project = local.cloudbuild_project_id + role = "roles/logging.logWriter" + member = "serviceAccount:${module.app_infra_cloudbuild_project[0].sa}" +} + +resource "google_project_iam_member" "workload_identity_admin" { + project = module.app_infra_cloudbuild_project[0].project_id + role = "roles/iam.workloadIdentityPoolAdmin" + member = "serviceAccount:${module.app_infra_cloudbuild_project[0].sa}" +} + +resource "google_storage_bucket_iam_member" "cloudbuild_storage_read" { + bucket = module.infra_pipelines[0].log_buckets["bu1-example-app"] + role = "roles/storage.admin" + member = "serviceAccount:${module.app_infra_cloudbuild_project[0].sa}" +} + +resource "google_storage_bucket_iam_member" "cloudbuild_sa_storage_admin" { + bucket = module.infra_pipelines[0].log_buckets["bu1-example-app"] + role = "roles/storage.admin" + member = "serviceAccount:tf-cb-builder-sa@${local.cloudbuild_project_id}.iam.gserviceaccount.com" } module "app_infra_cloudbuild_project" { @@ -37,7 +90,8 @@ module "app_infra_cloudbuild_project" { "cloudkms.googleapis.com", "iam.googleapis.com", "artifactregistry.googleapis.com", - "cloudresourcemanager.googleapis.com" + "cloudresourcemanager.googleapis.com", + "confidentialcomputing.googleapis.com" ] # Metadata project_suffix = "infra-pipeline" @@ -62,6 +116,35 @@ module "infra_pipelines" { private_worker_pool_id = local.cloud_build_private_worker_pool_id } +resource "time_sleep" "wait_iam_propagation" { + create_duration = "60s" + + depends_on = [ + module.infra_pipelines, + module.app_infra_cloudbuild_project, + google_project_iam_member.bucket_admin_binding, + google_storage_bucket_iam_member.cloudbuild_storage_read, + google_artifact_registry_repository_iam_member.builder_on_artifact_registry, + google_project_iam_member.cloudbuild_logging, + google_storage_bucket_iam_member.cloudbuild_sa_storage_admin, + ] +} + +module "build_confidential_space_image" { + source = "terraform-google-modules/gcloud/google" + version = "~> 4.0" + upgrade = false + module_depends_on = [time_sleep.wait_iam_propagation] + + create_cmd_triggers = { + "tag_version" = local.confidential_space_image_version + "cmd_prompt" = local.cmd_prompt + } + + create_cmd_entrypoint = "bash" + create_cmd_body = "${local.cmd_prompt} || ( sleep 45 && ${local.cmd_prompt})" +} + /** * When Jenkins CI/CD is used for deployment this resource * is created to terraform validation works. @@ -72,3 +155,5 @@ module "infra_pipelines" { resource "null_resource" "jenkins_cicd" { count = !local.enable_cloudbuild_deploy ? 1 : 0 } + + diff --git a/4-projects/business_unit_1/shared/outputs.tf b/4-projects/business_unit_1/shared/outputs.tf index 5c3a84874..0cfc52f4f 100644 --- a/4-projects/business_unit_1/shared/outputs.tf +++ b/4-projects/business_unit_1/shared/outputs.tf @@ -62,3 +62,18 @@ output "enable_cloudbuild_deploy" { description = "Enable infra deployment using Cloud Build." value = local.enable_cloudbuild_deploy } + +output "artifact_registry_repository_id" { + description = "Artifact Registry ID." + value = module.infra_pipelines[0].artifact_registry_repository_id +} + +output "bootstrap_cloudbuild_project_id" { + description = "Cloudbuild project ID." + value = local.cloudbuild_project_id +} + +output "image_name" { + description = "Image path used by confidential space instance." + value = local.confidential_space_image_tag +} diff --git a/4-projects/business_unit_1/shared/remote.tf b/4-projects/business_unit_1/shared/remote.tf index 2597f19ba..7836b6649 100644 --- a/4-projects/business_unit_1/shared/remote.tf +++ b/4-projects/business_unit_1/shared/remote.tf @@ -27,6 +27,8 @@ locals { cloud_build_private_worker_pool_id = try(data.terraform_remote_state.bootstrap.outputs.cloud_build_private_worker_pool_id, "") cloud_builder_artifact_repo = try(data.terraform_remote_state.bootstrap.outputs.cloud_builder_artifact_repo, "") enable_cloudbuild_deploy = local.cloud_builder_artifact_repo != "" + cloudbuild_project_id = data.terraform_remote_state.bootstrap.outputs.cloudbuild_project_id + projects_terraform_sa = data.terraform_remote_state.bootstrap.outputs.projects_step_terraform_service_account_email } data "terraform_remote_state" "bootstrap" { diff --git a/4-projects/business_unit_1/shared/variables.tf b/4-projects/business_unit_1/shared/variables.tf index b372391ef..cf16b4c67 100644 --- a/4-projects/business_unit_1/shared/variables.tf +++ b/4-projects/business_unit_1/shared/variables.tf @@ -53,3 +53,4 @@ variable "project_deletion_policy" { type = string default = "PREVENT" } + diff --git a/4-projects/modules/base_env/README.md b/4-projects/modules/base_env/README.md index ef2760eff..74645b0b9 100644 --- a/4-projects/modules/base_env/README.md +++ b/4-projects/modules/base_env/README.md @@ -37,6 +37,9 @@ |------|-------------| | access\_context\_manager\_policy\_id | Access Context Manager Policy ID. | | bucket | The created storage bucket. | +| confidential\_space\_project | Confidential Space project id. | +| confidential\_space\_project\_number | Confidential Space project number. | +| confidential\_space\_workload\_sa | Workload Service Account for confidential space | | floating\_project | Project sample floating project. | | iap\_firewall\_tags | The security tags created for IAP (SSH and RDP) firewall rules and to be used on the VM created on step 5-app-infra on the peering network project. | | keyring | The name of the keyring. | diff --git a/4-projects/modules/base_env/example_confidential_space_project.tf b/4-projects/modules/base_env/example_confidential_space_project.tf new file mode 100644 index 000000000..d122e9bf5 --- /dev/null +++ b/4-projects/modules/base_env/example_confidential_space_project.tf @@ -0,0 +1,99 @@ +/** + * Copyright 2025 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 { + iam_roles = [ + "roles/iam.serviceAccountUser", + "roles/confidentialcomputing.workloadUser", + "roles/iam.workloadIdentityPoolAdmin", + "roles/storage.admin", + "roles/logging.logWriter", + ] +} + +resource "google_service_account" "workload_sa" { + account_id = "confidential-space-workload-sa" + display_name = "Workload Service Account for confidential space" + project = module.confidential_space_project.project_id +} + +resource "google_project_iam_member" "workload_sa_roles" { + for_each = toset(local.iam_roles) + project = module.confidential_space_project.project_id + role = each.key + member = "serviceAccount:${google_service_account.workload_sa.email}" +} + +resource "google_artifact_registry_repository_iam_member" "artifact_registry_reader" { + project = local.cloudbuild_project_id + repository = "tf-runners" + location = local.default_region + role = "roles/artifactregistry.reader" + member = "serviceAccount:${google_service_account.workload_sa.email}" +} + +module "confidential_space_project" { + source = "../single_project" + + org_id = local.org_id + billing_account = local.billing_account + folder_id = google_folder.env_business_unit.name + environment = var.env + vpc = "svpc" + shared_vpc_host_project_id = local.shared_vpc_host_project_id + shared_vpc_subnets = local.subnets_self_links + project_budget = var.project_budget + project_prefix = local.project_prefix + project_deletion_policy = var.project_deletion_policy + + enable_cloudbuild_deploy = local.enable_cloudbuild_deploy + app_infra_pipeline_service_accounts = local.app_infra_pipeline_service_accounts + + sa_roles = { + "${var.business_code}-example-app" = [ + "roles/compute.instanceAdmin.v1", + "roles/iam.serviceAccountUser", + "roles/iam.serviceAccountAdmin", + "roles/iam.workloadIdentityPoolAdmin", + "roles/serviceusage.serviceUsageAdmin", + "roles/cloudkms.admin", + "roles/storage.admin", + "roles/resourcemanager.projectIamAdmin", + ] + } + + activate_apis = [ + "accesscontextmanager.googleapis.com", + "artifactregistry.googleapis.com", + "iamcredentials.googleapis.com", + "compute.googleapis.com", + "confidentialcomputing.googleapis.com", + "cloudkms.googleapis.com" + ] + vpc_service_control_attach_enabled = local.enforce_vpcsc ? "true" : "false" + vpc_service_control_attach_dry_run = !local.enforce_vpcsc ? "true" : "false" + vpc_service_control_perimeter_name = "accessPolicies/${local.access_context_manager_policy_id}/servicePerimeters/${local.perimeter_name}" + vpc_service_control_sleep_duration = "60s" + + # Metadata + project_suffix = "conf-space" + application_name = "${var.business_code}-sample-instance" + billing_code = "1234" + primary_contact = "example@example.com" + secondary_contact = "example2@example.com" + business_code = var.business_code +} + diff --git a/4-projects/modules/base_env/outputs.tf b/4-projects/modules/base_env/outputs.tf index fa2f69d0b..5e3ac7c95 100644 --- a/4-projects/modules/base_env/outputs.tf +++ b/4-projects/modules/base_env/outputs.tf @@ -91,3 +91,19 @@ output "iap_firewall_tags" { "tagKeys/${google_tags_tag_key.firewall_tag_key_rdp[0].name}" = "tagValues/${google_tags_tag_value.firewall_tag_value_rdp[0].name}" } : {} } + +output "confidential_space_project" { + description = "Confidential Space project id." + value = module.confidential_space_project.project_id +} + +output "confidential_space_project_number" { + description = "Confidential Space project number." + value = module.confidential_space_project.project_number +} + +output "confidential_space_workload_sa" { + description = "Workload Service Account for confidential space" + value = google_service_account.workload_sa.email +} + diff --git a/4-projects/modules/base_env/remote.tf b/4-projects/modules/base_env/remote.tf index 5624b7fa6..869c39275 100644 --- a/4-projects/modules/base_env/remote.tf +++ b/4-projects/modules/base_env/remote.tf @@ -30,6 +30,8 @@ locals { enable_cloudbuild_deploy = data.terraform_remote_state.business_unit_shared.outputs.enable_cloudbuild_deploy kms_project_id = data.terraform_remote_state.environments_env.outputs.env_kms_project_id kms_project_number = data.terraform_remote_state.environments_env.outputs.env_kms_project_number + cloudbuild_project_id = data.terraform_remote_state.bootstrap.outputs.cloudbuild_project_id + default_region = data.terraform_remote_state.bootstrap.outputs.common_config.default_region } data "terraform_remote_state" "bootstrap" { diff --git a/4-projects/modules/infra_pipelines/README.md b/4-projects/modules/infra_pipelines/README.md index 22f84a7a6..3738b0a74 100644 --- a/4-projects/modules/infra_pipelines/README.md +++ b/4-projects/modules/infra_pipelines/README.md @@ -22,6 +22,7 @@ |------|-------------| | apply\_triggers\_id | CB apply triggers | | artifact\_buckets | GCS Buckets to store Cloud Build Artifacts | +| artifact\_registry\_repository\_id | Artifact Registry ID. | | default\_region | Default region to create resources where applicable. | | gar\_name | Artifact Registry (AR) repository name created to store runner images | | log\_buckets | GCS Buckets to store Cloud Build logs | diff --git a/4-projects/modules/infra_pipelines/main.tf b/4-projects/modules/infra_pipelines/main.tf index ab0da04ea..0197a4d22 100644 --- a/4-projects/modules/infra_pipelines/main.tf +++ b/4-projects/modules/infra_pipelines/main.tf @@ -99,7 +99,6 @@ module "tf_workspace" { /*********************************************** Cloud Build - IAM ***********************************************/ - resource "google_artifact_registry_repository_iam_member" "terraform-image-iam" { provider = google-beta for_each = toset(var.app_infra_repos) @@ -107,7 +106,7 @@ resource "google_artifact_registry_repository_iam_member" "terraform-image-iam" project = local.gar_project_id location = local.gar_region repository = local.gar_name - role = "roles/artifactregistry.reader" + role = "roles/artifactregistry.writer" member = "serviceAccount:${local.workspace_sa_email[each.key]}" } @@ -136,3 +135,4 @@ resource "google_sourcerepo_repository_iam_member" "member" { role = "roles/viewer" member = "serviceAccount:${local.workspace_sa_email[each.key]}" } + diff --git a/4-projects/modules/infra_pipelines/outputs.tf b/4-projects/modules/infra_pipelines/outputs.tf index c840d50e4..278eb822f 100644 --- a/4-projects/modules/infra_pipelines/outputs.tf +++ b/4-projects/modules/infra_pipelines/outputs.tf @@ -58,3 +58,9 @@ output "apply_triggers_id" { description = "CB apply triggers" value = local.apply_triggers_id } + +output "artifact_registry_repository_id" { + description = "Artifact Registry ID." + value = local.gar_project_id +} + diff --git a/4-projects/modules/infra_pipelines/variables.tf b/4-projects/modules/infra_pipelines/variables.tf index c9284d665..35eef8e23 100644 --- a/4-projects/modules/infra_pipelines/variables.tf +++ b/4-projects/modules/infra_pipelines/variables.tf @@ -79,3 +79,4 @@ variable "vpc_service_control_attach_dry_run" { type = bool default = false } + diff --git a/5-app-infra/README.md b/5-app-infra/README.md index e286ecf18..30a6538ed 100644 --- a/5-app-infra/README.md +++ b/5-app-infra/README.md @@ -58,8 +58,12 @@ file. The purpose of this step is to deploy a simple [Compute Engine](https://cloud.google.com/compute/) instance in one of the business unit projects using the infra pipeline set up in 4-projects. The infra pipeline is created in step `4-projects` within the shared env and has a [Cloud Build](https://cloud.google.com/build/docs) pipeline configured to manage infrastructure within projects. +As part of this deployment, the provided Terraform code automates the provisioning of a secure infrastructure in Google Cloud to run workloads in [Confidential Space](https://cloud.google.com/confidential-computing/confidential-space/docs/confidential-space-overview) — based on Confidential VMs with integrity verification, data isolation, trusted image execution, and integration with KMS for encryption and GCS for storage. In addition to this documentation, you can also follow Google's official tutorial to test each step directly: [Create your first Confidential Space environment](https://cloud.google.com/confidential-computing/confidential-space/docs/create-your-first-confidential-space-environment). + +To better understand the structure and content of the tokens used in Confidential Space, refer to the reference documentation on [Token Claims](https://cloud.google.com/confidential-computing/confidential-space/docs/reference/token-claims). This link details the information contained in the tokens and how they are used to ensure the security and integrity of the environment. + +This Compute Engine instance is created using the base network from step `3-networks` and is used to access private services. For the Confidential Space, a Docker image built in step `4-projects` serves as the base for the confidential instance. There is also a [Source Repository](https://cloud.google.com/source-repositories) configured with build triggers similar to the [CI/CD Pipeline](https://github.com/terraform-google-modules/terraform-example-foundation#0-bootstrap) setup in `0-bootstrap`. -This Compute Engine instance is created using the base network from step `3-networks` and is used to access private services. ## Prerequisites @@ -75,8 +79,10 @@ Please refer to [troubleshooting](../docs/TROUBLESHOOTING.md) if you run into is ## Usage -**Note:** If you are using MacOS, replace `cp -RT` with `cp -R` in the relevant -commands. The `-T` flag is needed for Linux, but causes problems for MacOS. +**Notes:** + +- For Confidential space, additional firewall rules and directional perimeter rules may be required, depending on the additional workloads to be deployed. +- If you are using MacOS, replace `cp -RT` with `cp -R` in the relevant commands. The `-T` flag is needed for Linux, but causes problems for MacOS. ### Deploying with Cloud Build @@ -153,6 +159,25 @@ Run `terraform output cloudbuild_project_id` in the `0-bootstrap` folder to get sed -i'' -e "s/REMOTE_STATE_BUCKET/${remote_state_bucket}/" ./common.auto.tfvars ``` +1. Get the `confidential_image_digest` value from the Docker image created in `gcp-projects` + +```bash +export CLOUD_BUILD_PROJECT_ID=$(terraform -chdir="terraform-example-foundation/0-bootstrap/" output -raw cloudbuild_project_id) +echo ${CLOUD_BUILD_PROJECT_ID} + +export DEFAULT_REGION=$(terraform -chdir="../gcp-projects/business_unit_1/shared" output -raw default_region) +echo ${DEFAULT_REGION} + +export confidential_image_digest=$(gcloud artifacts docker images describe ${DEFAULT_REGION}-docker.pkg.dev/${CLOUD_BUILD_PROJECT_ID}/tf-runners/confidential_space_image:latest --project=${CLOUD_BUILD_PROJECT_ID}) +echo "confidential_image_digest = ${confidential_image_digest}" +``` + +1. Update `IMAGE_DIGEST` value in the file `common.auto.tfvars`. + +```bash +sed -i'' -e "s/IMAGE_DIGEST/${confidential_image_digest}/" ./common.auto.tfvars +``` + 1. Commit changes. ```bash diff --git a/5-app-infra/business_unit_1/development/README.md b/5-app-infra/business_unit_1/development/README.md index 6809c65cf..24c6d4a6a 100644 --- a/5-app-infra/business_unit_1/development/README.md +++ b/5-app-infra/business_unit_1/development/README.md @@ -3,6 +3,7 @@ | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| +| confidential\_image\_digest | SHA256 digest of the Docker image to be used for running the workload in Confidential Space. This value ensures the integrity and immutability of the image, guaranteeing that only the expected and verified code is executed within the confidential environment. Expected format: `sha256:`. | `string` | `""` | no | | instance\_region | The region where compute instance will be created. A subnetwork must exists in the instance region. | `string` | `null` | no | | remote\_state\_bucket | Backend bucket to load remote state information from previous steps. | `string` | n/a | yes | @@ -11,11 +12,18 @@ | Name | Description | |------|-------------| | available\_zones | List of available zones in region | +| confidential\_available\_zones | List of available zones in region for confidential space. | +| confidential\_instances\_names | List of names for confidential compute instances | +| confidential\_instances\_zones | List of zone for confidential compute instances. | +| confidential\_space\_project\_id | Project where confidential compute instance was created | +| confidential\_space\_project\_number | Project number from confidential compute instance | | instances\_details | List of details for compute instances | | instances\_names | List of names for compute instances | | instances\_self\_links | List of self-links for compute instances | | instances\_zones | List of zone for compute instances | | project\_id | Project where compute instance was created | | region | Region where compute instance was created | +| workload\_identity\_pool\_id | Workload identity pool ID. | +| workload\_pool\_provider\_id | Workload pool provider used by confidential space. | diff --git a/5-app-infra/business_unit_1/development/main.tf b/5-app-infra/business_unit_1/development/main.tf index 3ab065855..e34ef35c3 100644 --- a/5-app-infra/business_unit_1/development/main.tf +++ b/5-app-infra/business_unit_1/development/main.tf @@ -38,3 +38,14 @@ module "peering_gce_instance" { region = coalesce(var.instance_region, local.default_region) remote_state_bucket = var.remote_state_bucket } + +module "confidential_space" { + source = "../../modules/confidential_space" + + environment = local.environment + confidential_image_digest = var.confidential_image_digest + business_unit = local.business_unit + project_suffix = "conf-space" + region = coalesce(var.instance_region, local.default_region) + remote_state_bucket = var.remote_state_bucket +} diff --git a/5-app-infra/business_unit_1/development/outputs.tf b/5-app-infra/business_unit_1/development/outputs.tf index 4fc64d26f..6eabecf6f 100644 --- a/5-app-infra/business_unit_1/development/outputs.tf +++ b/5-app-infra/business_unit_1/development/outputs.tf @@ -47,7 +47,45 @@ output "project_id" { value = module.gce_instance.project_id } +output "confidential_space_project_id" { + description = "Project where confidential compute instance was created" + value = module.confidential_space.confidential_space_project_id +} + +output "confidential_space_project_number" { + description = "Project number from confidential compute instance" + value = module.confidential_space.confidential_space_project_number +} + output "region" { description = "Region where compute instance was created" value = module.gce_instance.region } + +output "workload_pool_provider_id" { + description = "Workload pool provider used by confidential space." + value = module.confidential_space.workload_pool_provider_id +} + +output "workload_identity_pool_id" { + description = "Workload identity pool ID." + value = module.confidential_space.workload_identity_pool_id + +} + +output "confidential_instances_names" { + description = "List of names for confidential compute instances" + value = [for u in module.confidential_space.instances_details : u.name] + sensitive = true +} + +output "confidential_available_zones" { + description = "List of available zones in region for confidential space." + value = module.confidential_space.available_zones +} + +output "confidential_instances_zones" { + description = "List of zone for confidential compute instances." + value = [for u in module.confidential_space.instances_details : u.zone] + sensitive = true +} diff --git a/5-app-infra/business_unit_1/development/variables.tf b/5-app-infra/business_unit_1/development/variables.tf index 8aa99e86c..8537277b4 100644 --- a/5-app-infra/business_unit_1/development/variables.tf +++ b/5-app-infra/business_unit_1/development/variables.tf @@ -24,3 +24,9 @@ variable "remote_state_bucket" { description = "Backend bucket to load remote state information from previous steps." type = string } + +variable "confidential_image_digest" { + description = "SHA256 digest of the Docker image to be used for running the workload in Confidential Space. This value ensures the integrity and immutability of the image, guaranteeing that only the expected and verified code is executed within the confidential environment. Expected format: `sha256:`." + type = string + default = "" +} diff --git a/5-app-infra/business_unit_1/nonproduction/README.md b/5-app-infra/business_unit_1/nonproduction/README.md index 6809c65cf..24c6d4a6a 100644 --- a/5-app-infra/business_unit_1/nonproduction/README.md +++ b/5-app-infra/business_unit_1/nonproduction/README.md @@ -3,6 +3,7 @@ | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| +| confidential\_image\_digest | SHA256 digest of the Docker image to be used for running the workload in Confidential Space. This value ensures the integrity and immutability of the image, guaranteeing that only the expected and verified code is executed within the confidential environment. Expected format: `sha256:`. | `string` | `""` | no | | instance\_region | The region where compute instance will be created. A subnetwork must exists in the instance region. | `string` | `null` | no | | remote\_state\_bucket | Backend bucket to load remote state information from previous steps. | `string` | n/a | yes | @@ -11,11 +12,18 @@ | Name | Description | |------|-------------| | available\_zones | List of available zones in region | +| confidential\_available\_zones | List of available zones in region for confidential space. | +| confidential\_instances\_names | List of names for confidential compute instances | +| confidential\_instances\_zones | List of zone for confidential compute instances. | +| confidential\_space\_project\_id | Project where confidential compute instance was created | +| confidential\_space\_project\_number | Project number from confidential compute instance | | instances\_details | List of details for compute instances | | instances\_names | List of names for compute instances | | instances\_self\_links | List of self-links for compute instances | | instances\_zones | List of zone for compute instances | | project\_id | Project where compute instance was created | | region | Region where compute instance was created | +| workload\_identity\_pool\_id | Workload identity pool ID. | +| workload\_pool\_provider\_id | Workload pool provider used by confidential space. | diff --git a/5-app-infra/business_unit_1/nonproduction/main.tf b/5-app-infra/business_unit_1/nonproduction/main.tf index 9c456490a..e6a66d2e4 100644 --- a/5-app-infra/business_unit_1/nonproduction/main.tf +++ b/5-app-infra/business_unit_1/nonproduction/main.tf @@ -38,3 +38,14 @@ module "peering_gce_instance" { region = coalesce(var.instance_region, local.default_region) remote_state_bucket = var.remote_state_bucket } + +module "confidential_space" { + source = "../../modules/confidential_space" + + environment = local.environment + confidential_image_digest = var.confidential_image_digest + business_unit = local.business_unit + project_suffix = "conf-space" + region = coalesce(var.instance_region, local.default_region) + remote_state_bucket = var.remote_state_bucket +} diff --git a/5-app-infra/business_unit_1/nonproduction/outputs.tf b/5-app-infra/business_unit_1/nonproduction/outputs.tf index 4fc64d26f..6eabecf6f 100644 --- a/5-app-infra/business_unit_1/nonproduction/outputs.tf +++ b/5-app-infra/business_unit_1/nonproduction/outputs.tf @@ -47,7 +47,45 @@ output "project_id" { value = module.gce_instance.project_id } +output "confidential_space_project_id" { + description = "Project where confidential compute instance was created" + value = module.confidential_space.confidential_space_project_id +} + +output "confidential_space_project_number" { + description = "Project number from confidential compute instance" + value = module.confidential_space.confidential_space_project_number +} + output "region" { description = "Region where compute instance was created" value = module.gce_instance.region } + +output "workload_pool_provider_id" { + description = "Workload pool provider used by confidential space." + value = module.confidential_space.workload_pool_provider_id +} + +output "workload_identity_pool_id" { + description = "Workload identity pool ID." + value = module.confidential_space.workload_identity_pool_id + +} + +output "confidential_instances_names" { + description = "List of names for confidential compute instances" + value = [for u in module.confidential_space.instances_details : u.name] + sensitive = true +} + +output "confidential_available_zones" { + description = "List of available zones in region for confidential space." + value = module.confidential_space.available_zones +} + +output "confidential_instances_zones" { + description = "List of zone for confidential compute instances." + value = [for u in module.confidential_space.instances_details : u.zone] + sensitive = true +} diff --git a/5-app-infra/business_unit_1/nonproduction/variables.tf b/5-app-infra/business_unit_1/nonproduction/variables.tf index 8aa99e86c..8ce106706 100644 --- a/5-app-infra/business_unit_1/nonproduction/variables.tf +++ b/5-app-infra/business_unit_1/nonproduction/variables.tf @@ -24,3 +24,10 @@ variable "remote_state_bucket" { description = "Backend bucket to load remote state information from previous steps." type = string } + +variable "confidential_image_digest" { + description = "SHA256 digest of the Docker image to be used for running the workload in Confidential Space. This value ensures the integrity and immutability of the image, guaranteeing that only the expected and verified code is executed within the confidential environment. Expected format: `sha256:`." + type = string + default = "" +} + diff --git a/5-app-infra/business_unit_1/production/README.md b/5-app-infra/business_unit_1/production/README.md index 6809c65cf..44995cae9 100644 --- a/5-app-infra/business_unit_1/production/README.md +++ b/5-app-infra/business_unit_1/production/README.md @@ -3,6 +3,7 @@ | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| +| confidential\_image\_digest | SHA256 digest of the Docker image to be used for running the workload in Confidential Space. This value ensures the integrity and immutability of the image, guaranteeing that only the expected and verified code is executed within the confidential environment. Expected format: `sha256:`. | `string` | `""` | no | | instance\_region | The region where compute instance will be created. A subnetwork must exists in the instance region. | `string` | `null` | no | | remote\_state\_bucket | Backend bucket to load remote state information from previous steps. | `string` | n/a | yes | @@ -11,11 +12,18 @@ | Name | Description | |------|-------------| | available\_zones | List of available zones in region | +| confidential\_available\_zones | List of available zones in region for confidential space. | +| confidential\_instances\_names | List of names for confidential compute instances. | +| confidential\_instances\_zones | List of zone for confidential compute instances. | +| confidential\_space\_project\_id | Project where confidential compute instance was created | +| confidential\_space\_project\_number | Project number from confidential compute instance | | instances\_details | List of details for compute instances | | instances\_names | List of names for compute instances | | instances\_self\_links | List of self-links for compute instances | | instances\_zones | List of zone for compute instances | | project\_id | Project where compute instance was created | | region | Region where compute instance was created | +| workload\_identity\_pool\_id | Workload identity pool ID. | +| workload\_pool\_provider\_id | Workload pool provider used by confidential space. | diff --git a/5-app-infra/business_unit_1/production/main.tf b/5-app-infra/business_unit_1/production/main.tf index 1efdbb0f5..56b27f29e 100644 --- a/5-app-infra/business_unit_1/production/main.tf +++ b/5-app-infra/business_unit_1/production/main.tf @@ -38,3 +38,14 @@ module "peering_gce_instance" { region = coalesce(var.instance_region, local.default_region) remote_state_bucket = var.remote_state_bucket } + +module "confidential_space" { + source = "../../modules/confidential_space" + + environment = local.environment + confidential_image_digest = var.confidential_image_digest + business_unit = local.business_unit + project_suffix = "conf-space" + region = coalesce(var.instance_region, local.default_region) + remote_state_bucket = var.remote_state_bucket +} diff --git a/5-app-infra/business_unit_1/production/outputs.tf b/5-app-infra/business_unit_1/production/outputs.tf index 4fc64d26f..c23e6c992 100644 --- a/5-app-infra/business_unit_1/production/outputs.tf +++ b/5-app-infra/business_unit_1/production/outputs.tf @@ -47,7 +47,45 @@ output "project_id" { value = module.gce_instance.project_id } +output "confidential_space_project_id" { + description = "Project where confidential compute instance was created" + value = module.confidential_space.confidential_space_project_id +} + +output "confidential_space_project_number" { + description = "Project number from confidential compute instance" + value = module.confidential_space.confidential_space_project_number +} + output "region" { description = "Region where compute instance was created" value = module.gce_instance.region } + +output "workload_pool_provider_id" { + description = "Workload pool provider used by confidential space." + value = module.confidential_space.workload_pool_provider_id +} + +output "workload_identity_pool_id" { + description = "Workload identity pool ID." + value = module.confidential_space.workload_identity_pool_id + +} + +output "confidential_instances_names" { + description = "List of names for confidential compute instances." + value = [for u in module.confidential_space.instances_details : u.name] + sensitive = true +} + +output "confidential_available_zones" { + description = "List of available zones in region for confidential space." + value = module.confidential_space.available_zones +} + +output "confidential_instances_zones" { + description = "List of zone for confidential compute instances." + value = [for u in module.confidential_space.instances_details : u.zone] + sensitive = true +} diff --git a/5-app-infra/business_unit_1/production/variables.tf b/5-app-infra/business_unit_1/production/variables.tf index 8aa99e86c..8ce106706 100644 --- a/5-app-infra/business_unit_1/production/variables.tf +++ b/5-app-infra/business_unit_1/production/variables.tf @@ -24,3 +24,10 @@ variable "remote_state_bucket" { description = "Backend bucket to load remote state information from previous steps." type = string } + +variable "confidential_image_digest" { + description = "SHA256 digest of the Docker image to be used for running the workload in Confidential Space. This value ensures the integrity and immutability of the image, guaranteeing that only the expected and verified code is executed within the confidential environment. Expected format: `sha256:`." + type = string + default = "" +} + diff --git a/5-app-infra/common.auto.example.tfvars b/5-app-infra/common.auto.example.tfvars index 8ba24df06..f518bcadd 100644 --- a/5-app-infra/common.auto.example.tfvars +++ b/5-app-infra/common.auto.example.tfvars @@ -16,4 +16,5 @@ # instance_region = "us-central1" // should be one of the regions used to create network on step 3-networks -remote_state_bucket = "REMOTE_STATE_BUCKET" +remote_state_bucket = "REMOTE_STATE_BUCKET" +confidential_image_digest = "IMAGE_DIGEST" diff --git a/5-app-infra/modules/confidential_space/README.md b/5-app-infra/modules/confidential_space/README.md new file mode 100644 index 000000000..8b89d9dcb --- /dev/null +++ b/5-app-infra/modules/confidential_space/README.md @@ -0,0 +1,35 @@ + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| business\_unit | The business (ex. business\_unit\_1). | `string` | `"business_unit_1"` | no | +| confidential\_hostname | Hostname of confidential instance. | `string` | `"confidential-instance"` | no | +| confidential\_image\_digest | SHA256 digest of the Docker image to be used for running the workload in Confidential Space. This value ensures the integrity and immutability of the image, guaranteeing that only the expected and verified code is executed within the confidential environment. Expected format: `sha256:`. | `string` | n/a | yes | +| confidential\_instance\_type | (Optional) Defines the confidential computing technology the instance uses. SEV is an AMD feature. TDX is an Intel feature. One of the following values is required: SEV, SEV\_SNP, TDX. | `string` | `"SEV"` | no | +| confidential\_machine\_type | Machine type to create for confidential instance. | `string` | `"n2d-standard-2"` | no | +| cpu\_platform | The CPU platform used by this instance. If confidential\_instance\_type is set as SEV, then it is an AMD feature. TDX is an Intel feature. | `string` | `"AMD Milan"` | no | +| environment | The environment the single project belongs to | `string` | n/a | yes | +| num\_instances | Number of instances to create | `number` | `1` | no | +| project\_suffix | The name of the GCP project. Max 16 characters with 3 character business unit code. | `string` | n/a | yes | +| region | The GCP region to create and test resources in | `string` | `"us-central1"` | no | +| remote\_state\_bucket | Backend bucket to load remote state information from previous steps. | `string` | n/a | yes | +| source\_image\_family | Source image family used for confidential instance. The default is confidential-space. | `string` | `"confidential-space"` | no | +| source\_image\_project | Project where the source image comes from. The default project contains confidential-space-images images. See: https://cloud.google.com/confidential-computing/confidential-space/docs/confidential-space-images | `string` | `"confidential-space-images"` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| available\_zones | List of available zones in region | +| confidential\_image\_digest | SHA256 digest of the Docker image. | +| confidential\_space\_project\_id | Project where confidential compute instance was created | +| confidential\_space\_project\_number | Project number from confidential compute instance | +| instances\_details | List of details for compute instances | +| instances\_self\_links | List of self-links for compute instances | +| project\_id | Project where compute instance was created | +| workload\_identity\_pool\_id | Workload identity pool ID. | +| workload\_pool\_provider\_id | Workload pool provider used by confidential space. | + + + diff --git a/5-app-infra/modules/confidential_space/main.tf b/5-app-infra/modules/confidential_space/main.tf new file mode 100644 index 000000000..39cc04e81 --- /dev/null +++ b/5-app-infra/modules/confidential_space/main.tf @@ -0,0 +1,155 @@ +/** + * Copyright 2025 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 { + default_tee_image_reference = "${local.default_region}-docker.pkg.dev/${local.cloudbuild_project_id}/${local.artifact_registry_repository}/confidential_space_image:latest" + env_project_ids = { + "conf-space" = data.terraform_remote_state.projects_env.outputs.confidential_space_project, + } + env_project_subnets = { + "conf-space" = local.svpc_subnetwork_self_link, + } + env_project_resource_manager_tags = { + "conf-space" = null, + } + + subnetwork_self_links = data.terraform_remote_state.projects_env.outputs.subnets_self_links + svpc_subnetwork_self_link = [for subnet in local.subnetwork_self_links : subnet if length(regexall("regions/${var.region}/subnetworks", subnet)) > 0][0] + + cloudbuild_project_id = data.terraform_remote_state.business_unit_shared.outputs.bootstrap_cloudbuild_project_id + default_region = data.terraform_remote_state.projects_env.outputs.default_region + env_project_id = local.env_project_ids[var.project_suffix] + subnetwork_self_link = local.env_project_subnets[var.project_suffix] + subnetwork_project = element(split("/", local.subnetwork_self_link), index(split("/", local.subnetwork_self_link), "projects") + 1, ) + resource_manager_tags = local.env_project_resource_manager_tags[var.project_suffix] + artifact_registry_repository = "tf-runners" + confidential_space_project_id = data.terraform_remote_state.projects_env.outputs.confidential_space_project + confidential_space_project_number = data.terraform_remote_state.projects_env.outputs.confidential_space_project_number + confidential_space_workload_sa = data.terraform_remote_state.projects_env.outputs.confidential_space_workload_sa +} + +data "terraform_remote_state" "projects_env" { + backend = "gcs" + + config = { + bucket = var.remote_state_bucket + prefix = "terraform/projects/${var.business_unit}/${var.environment}" + } +} + +data "terraform_remote_state" "business_unit_shared" { + backend = "gcs" + + config = { + bucket = var.remote_state_bucket + prefix = "terraform/projects/${var.business_unit}/shared" + } +} + +resource "google_iam_workload_identity_pool" "confidential_space_pool" { + workload_identity_pool_id = "confidential-space-pool" + disabled = false + project = local.env_project_id +} + +resource "google_iam_workload_identity_pool_provider" "attestation_verifier" { + workload_identity_pool_id = google_iam_workload_identity_pool.confidential_space_pool.workload_identity_pool_id + workload_identity_pool_provider_id = "attestation-verifier" + display_name = "attestation-verifier" + description = "OIDC provider for confidential computing attestation" + project = local.env_project_id + + oidc { + issuer_uri = "https://confidentialcomputing.googleapis.com/" + allowed_audiences = ["https://sts.googleapis.com"] + } + + attribute_mapping = { + "google.subject" = "\"gcpcs::\" + assertion.submods.container.image_digest + \"::\" + assertion.submods.gce.project_number + \"::\" + assertion.submods.gce.instance_id" + "attribute.image_digest" = "assertion.submods.container.image_digest" + } + + attribute_condition = < 0 } + +// Gets the digest of a Docker image in Artifact Registry. +func (g GCP) GetDockerImageDigest(t testing.TB, project, imageName string) (string, error) { + + cmd := fmt.Sprintf("artifacts docker images describe %s --project=%s", imageName, project) + result := g.Runf(t, cmd) + + digest := result.Get("image_summary.digest").String() + if digest == "" { + return "", fmt.Errorf("failed to retrieve digest for image %s in project %s", imageName, project) + } + + return digest, nil +} diff --git a/helpers/foundation-deployer/stages/apply.go b/helpers/foundation-deployer/stages/apply.go index f83aeb877..0667d2c9a 100644 --- a/helpers/foundation-deployer/stages/apply.go +++ b/helpers/foundation-deployer/stages/apply.go @@ -404,12 +404,17 @@ func DeployProjectsStage(t testing.TB, s steps.Steps, tfvars GlobalTFVars, outpu } func DeployExampleAppStage(t testing.TB, s steps.Steps, tfvars GlobalTFVars, outputs InfraPipelineOutputs, c CommonConf) error { + digest, err := gcp.NewGCP().GetDockerImageDigest(t, outputs.BootstrapCloudbuildProjectID, outputs.ImageName) + if err != nil { + return err + } // create tfvars file commonTfvars := AppInfraCommonTfvars{ InstanceRegion: tfvars.DefaultRegion, RemoteStateBucket: outputs.RemoteStateBucket, + ImageDigest: digest, } - err := utils.WriteTfvars(filepath.Join(c.FoundationPath, AppInfraStep, "common.auto.tfvars"), commonTfvars) + err = utils.WriteTfvars(filepath.Join(c.FoundationPath, AppInfraStep, "common.auto.tfvars"), commonTfvars) if err != nil { return err } diff --git a/helpers/foundation-deployer/stages/data.go b/helpers/foundation-deployer/stages/data.go index 867f3df17..046d0cf0f 100644 --- a/helpers/foundation-deployer/stages/data.go +++ b/helpers/foundation-deployer/stages/data.go @@ -87,11 +87,13 @@ type BootstrapOutputs struct { } type InfraPipelineOutputs struct { - RemoteStateBucket string - InfraPipeProj string - DefaultRegion string - TerraformSA string - StateBucket string + RemoteStateBucket string + InfraPipeProj string + DefaultRegion string + TerraformSA string + StateBucket string + ImageName string + BootstrapCloudbuildProjectID string } // ServerAddress is the element for TargetNameServerAddresses @@ -280,6 +282,7 @@ type ProjEnvTfvars struct { type AppInfraCommonTfvars struct { InstanceRegion string `hcl:"instance_region"` RemoteStateBucket string `hcl:"remote_state_bucket"` + ImageDigest string `hcl:"confidential_image_digest"` } func GetBootstrapStepOutputs(t testing.TB, foundationPath string) BootstrapOutputs { @@ -309,10 +312,12 @@ func GetInfraPipelineOutputs(t testing.TB, checkoutPath, workspace string) Infra NoColor: true, } return InfraPipelineOutputs{ - InfraPipeProj: terraform.Output(t, options, "cloudbuild_project_id"), - DefaultRegion: terraform.Output(t, options, "default_region"), - TerraformSA: terraform.OutputMap(t, options, "terraform_service_accounts")["bu1-example-app"], - StateBucket: terraform.OutputMap(t, options, "state_buckets")["bu1-example-app"], + InfraPipeProj: terraform.Output(t, options, "cloudbuild_project_id"), + DefaultRegion: terraform.Output(t, options, "default_region"), + TerraformSA: terraform.OutputMap(t, options, "terraform_service_accounts")["bu1-example-app"], + StateBucket: terraform.OutputMap(t, options, "state_buckets")["bu1-example-app"], + ImageName: terraform.Output(t, options, "image_name"), + BootstrapCloudbuildProjectID: terraform.Output(t, options, "bootstrap_cloudbuild_project_id"), } } diff --git a/policy-library/policies/constraints/serviceusage_allow_basic_apis.yaml b/policy-library/policies/constraints/serviceusage_allow_basic_apis.yaml index 9e42f0e4c..8e4e35b62 100644 --- a/policy-library/policies/constraints/serviceusage_allow_basic_apis.yaml +++ b/policy-library/policies/constraints/serviceusage_allow_basic_apis.yaml @@ -45,6 +45,7 @@ spec: - "cloudscheduler.googleapis.com" - "cloudtrace.googleapis.com" - "compute.googleapis.com" + - "confidentialcomputing.googleapis.com" - "container.googleapis.com" - "datastore.googleapis.com" - "dns.googleapis.com" diff --git a/test/integration/app-infra/app_infra_test.go b/test/integration/app-infra/app_infra_test.go index 00d6de448..4e6dd38b9 100644 --- a/test/integration/app-infra/app_infra_test.go +++ b/test/integration/app-infra/app_infra_test.go @@ -70,6 +70,8 @@ func TestAppInfra(t *testing.T) { appInfra.DefineVerify( func(assert *assert.Assertions) { projectID := appInfra.GetStringOutput("project_id") + workloadPoolProvider := appInfra.GetStringOutput("workload_pool_provider_id") + workloadIdentityPool := appInfra.GetStringOutput("workload_identity_pool_id") instanceName := terraform.OutputList(t, appInfra.GetTFOptions(), "instances_names")[0] instanceZone := terraform.OutputList(t, appInfra.GetTFOptions(), "instances_zones")[0] machineType := fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/zones/%s/machineTypes/f1-micro", projectID, instanceZone) @@ -77,6 +79,34 @@ func TestAppInfra(t *testing.T) { gcOps := gcloud.WithCommonArgs([]string{"--project", projectID, "--zone", instanceZone, "--format", "json"}) instance := gcloud.Run(t, fmt.Sprintf("compute instances describe %s", instanceName), gcOps) assert.Equal(machineType, instance.Get("machineType").String(), "should have machine_type f1-micro") + + confidentialProjectID := appInfra.GetStringOutput("confidential_space_project_id") + confidentialInstanceName := terraform.OutputList(t, appInfra.GetTFOptions(), "confidential_instances_names")[0] + confidentialInstanceZone := terraform.OutputList(t, appInfra.GetTFOptions(), "confidential_instances_zones")[0] + confidentialProjectNumber := appInfra.GetStringOutput("confidential_space_project_number") + + gcPoolOps := gcloud.WithCommonArgs([]string{"--project", confidentialProjectID, "--format", "json"}) + poolDetails := gcloud.Run(t, fmt.Sprintf("iam workload-identity-pools describe %s --location=global", workloadIdentityPool), gcPoolOps) + name := poolDetails.Get("name").String() + expectedName := fmt.Sprintf("projects/%s/locations/global/workloadIdentityPools/%s", confidentialProjectNumber, workloadIdentityPool) + assert.Equal(expectedName, name, "Workload Identity Pool full name should match") + + gcPoolProviderOps := gcloud.WithCommonArgs([]string{fmt.Sprintf("--workload-identity-pool=%s", workloadIdentityPool), "--location=global", "--project", confidentialProjectID, "--format", "json"}) + workloadIdentityPoolProviderID := gcloud.Run(t, fmt.Sprintf("iam workload-identity-pools providers describe %s", workloadPoolProvider), gcPoolProviderOps) + assert.Equal(workloadPoolProvider, workloadIdentityPoolProviderID.Get("displayName").String(), fmt.Sprintf("workload identity pool provider should have name equals to %s", workloadPoolProvider)) + + gcInstanceOps := gcloud.WithCommonArgs([]string{"--project", confidentialProjectID, "--zone", confidentialInstanceZone, "--format", "json"}) + computeInstanceList := gcloud.Run(t, fmt.Sprintf("compute instances describe %s", confidentialInstanceName), gcInstanceOps) + assert.NotEmpty(computeInstanceList, "Expected instance details to be present") + computeInstance := computeInstanceList + assert.Equal(confidentialInstanceName, computeInstance.Get("name").String(), "Confidential instance name must match expected") + confidentialInstanceConfig := computeInstance.Get("confidentialInstanceConfig") + assert.True(confidentialInstanceConfig.Get("enableConfidentialCompute").Bool(), "Confidential Compute should be enabled") + assert.Equal("SEV", confidentialInstanceConfig.Get("confidentialInstanceType").String()) + assert.Equal("MIGRATE", computeInstance.Get("scheduling").Get("onHostMaintenance").String()) + serviceAccounts := computeInstance.Get("serviceAccounts").Array() + assert.Len(serviceAccounts, 1) + assert.Equal(fmt.Sprintf("confidential-space-workload-sa@%s.iam.gserviceaccount.com", confidentialProjectID), serviceAccounts[0].Get("email").String()) }) appInfra.Test() diff --git a/test/integration/networks/networks_test.go b/test/integration/networks/networks_test.go index 48b401c91..e2f9c2039 100644 --- a/test/integration/networks/networks_test.go +++ b/test/integration/networks/networks_test.go @@ -95,7 +95,7 @@ func TestNetworks(t *testing.T) { "adsdatahub.googleapis.com", "aiplatform.googleapis.com", "alloydb.googleapis.com", - "alpha-documentai.googleapis.com", + "documentai.googleapis.com", "analyticshub.googleapis.com", "apigee.googleapis.com", "apigeeconnect.googleapis.com", diff --git a/test/integration/projects/projects_test.go b/test/integration/projects/projects_test.go index def083e8d..4edf875d7 100644 --- a/test/integration/projects/projects_test.go +++ b/test/integration/projects/projects_test.go @@ -64,6 +64,10 @@ func TestProjects(t *testing.T) { "billingbudgets.googleapis.com", } + var confidentialRestrictedApisEnabled = []string{ + "confidentialcomputing.googleapis.com", + } + var project_sa_roles = []string{ "roles/compute.instanceAdmin.v1", "roles/iam.serviceAccountAdmin", @@ -71,27 +75,27 @@ func TestProjects(t *testing.T) { } for _, tt := range []struct { - name string - repo string - baseDir string + name string + repo string + baseDir string sharedNetwork string }{ { - name: "bu1_development", - repo: "bu1-example-app", - baseDir: "../../../4-projects/business_unit_1/%s", + name: "bu1_development", + repo: "bu1-example-app", + baseDir: "../../../4-projects/business_unit_1/%s", sharedNetwork: fmt.Sprintf("vpc-d-svpc%s", networkMode), }, { - name: "bu1_nonproduction", - repo: "bu1-example-app", - baseDir: "../../../4-projects/business_unit_1/%s", + name: "bu1_nonproduction", + repo: "bu1-example-app", + baseDir: "../../../4-projects/business_unit_1/%s", sharedNetwork: fmt.Sprintf("vpc-n-svpc%s", networkMode), }, { - name: "bu1_production", - repo: "bu1-example-app", - baseDir: "../../../4-projects/business_unit_1/%s", + name: "bu1_production", + repo: "bu1-example-app", + baseDir: "../../../4-projects/business_unit_1/%s", sharedNetwork: fmt.Sprintf("vpc-p-svpc%s", networkMode), }, } { @@ -144,6 +148,7 @@ func TestProjects(t *testing.T) { "floating_project", "peering_project", "shared_vpc_project", + "confidential_space_project", } { projectID := projects.GetStringOutput(projectOutput) prj := gcloud.Runf(t, "projects describe %s", projectID) @@ -173,6 +178,18 @@ func TestProjects(t *testing.T) { } + if projectOutput == "confidential_space_project" { + + enabledAPIS := gcloud.Runf(t, "services list --project %s --impersonate-service-account %s", projectID, terraformSA).Array() + listApis := testutils.GetResultFieldStrSlice(enabledAPIS, "config.name") + assert.Subset(listApis, confidentialRestrictedApisEnabled, "API should have been enabled") + + confidentialSpaceWorkloadSAEmail := projects.GetStringOutput("confidential_space_workload_sa") + confidentialSpaceSAName := fmt.Sprintf("projects/%s/serviceAccounts/%s", projectID, confidentialSpaceWorkloadSAEmail) + confidentialSpaceSA := gcloud.Runf(t, "iam service-accounts describe %s --project %s", confidentialSpaceWorkloadSAEmail, projectID) + assert.Equal(confidentialSpaceSAName, confidentialSpaceSA.Get("name").String(), fmt.Sprintf("service account %s should exist", confidentialSpaceWorkloadSAEmail)) + } + if projectOutput == "floating_project" { sharedVPC := gcloud.Runf(t, "compute shared-vpc get-host-project %s", projectID) assert.Empty(sharedVPC.Map()) diff --git a/test/integration/testutils/retry.go b/test/integration/testutils/retry.go index d70ca41a6..362d3cdb5 100644 --- a/test/integration/testutils/retry.go +++ b/test/integration/testutils/retry.go @@ -44,7 +44,7 @@ var ( ".*Error 400.*Service account.*does not exist*": "Error setting IAM policy", // Error waiting for creating service network connection. This happens randomly for development, production and non-production environments - ".*Error code 16.*Error waiting for Create Service Networking Connection*": "Request had invalid authentication credentials", + ".*Error waiting for Create Service Networking Connection.*Error code 16.*Expected OAuth 2 access token.*": "Request had invalid authentication credentials.", // Error 400: The eTag provided {} does not match the eTag of the current version of the Access Policy, which is {}. ".*Error 400: The eTag provided.*does not match the eTag of the current version of the Access Policy, which is.*": "Conflict during Access Policy configuration.",