Support context and add configurations factory to workstation cluster module, add FAST project template (#3401)

* add context to workstation-cluster module

* context test

* workstations project template
This commit is contained in:
Ludovico Magnocavallo
2025-10-10 18:59:37 +02:00
committed by GitHub
parent 9cf67755de
commit c996285b26
18 changed files with 1342 additions and 158 deletions

View File

@@ -0,0 +1,3 @@
# FAST Project Templates
Each of the folders contained here is a separate Terraform configuration, that can be used to bring up specific sets of resources. All the configurations include a `project.yaml` file in project factory format, that can be directly used after some light edits to bring up a project with the required prerequisites. The project file can also be used as documentation to create a suitable project via other means, where a project factory is not available.

View File

@@ -0,0 +1,97 @@
# Cloud Workstations Cluster
This simple setup allows creating and configuring one Cloud Workstation Cluster, and an arbitrary number of workstation configurations and workstations via a dedicated factory.
## Prerequisites
The [`project.yaml`](./project.yaml) file describes the project-level configuration needed in terms of API activation and IAM bindings.
If you are deploying this inside a FAST-enabled organization, the file can be lightly edited to match your configuration, and then used directly in the [project factory](../../stages/2-project-factory/).
This Terraform can of course be deployed using any pre-existing project. In that case use the YAML file to determine the configuration you need to set on the project:
- enable the APIs listed under `services`
- grant the permissions listed under `iam` to the principal running Terraform, either machine (service account) or human
## VPC-SC Integration
This example assumes a private cluster is needed, and provisions a PSC Endpoint for private connectivity. For more details on private clusters and VPC-SC see [this documentation page](https://cloud.google.com/workstations/docs/configure-vpc-service-controls-private-clusters).
An additional egress policy is needed to allow monitoring traffic for the cluster to the tenant project on the Google side. The following snippet can be added to the egress policy factory in the VPC-SC stage, and editied so that project numbers match. It should of course also be enabled in the perimeter definition.
```yaml
from:
identities:
- serviceAccount:service-1234567890@gcp-sa-workstations.iam.gserviceaccount.com
resources:
- projects/3456789012
to:
operations:
- service_name: monitoring.googleapis.com
method_selectors:
- "*"
resources:
- projects/1234567890
```
## Additional Configuration Steps
The workstations are accessible via the PSC Endpoint, once a DNS record for the cluster hostname has been configured. The cluster hostname is available from this example's outputs.
## Variable Configuration
This is an example of running this stage. Note that the `apt_remote_registries` has a default value that can be used when no IAM is needed at the registry level, and the default set of remotes is fine.
```hcl
project_id = "my-project"
location = "europe-west3"
network_config = {
network = "projects/ldj-prod-net-landing-0/global/networks/prod-landing-0"
subnetwork = "projects/ldj-prod-net-landing-0/regions/europe-west8/subnetworks/ws"
psc_endpoint_address = "10.0.18.10"
}
# tftest skip
```
<!-- BEGIN TFDOC -->
## Variables
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
| [network_config](variables.tf#L72) | VPC and subnet for the cluster. | <code title="object&#40;&#123;&#10; network &#61; string&#10; subnetwork &#61; string&#10; psc_endpoint_address &#61; optional&#40;string&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | |
| [project_id](variables.tf#L92) | Project id where the cluster will be created. | <code>string</code> | ✓ | |
| [annotations](variables.tf#L17) | Workstation cluster annotations. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> |
| [context](variables.tf#L23) | Context-specific interpolations. | <code title="object&#40;&#123;&#10; condition_vars &#61; optional&#40;map&#40;map&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; custom_roles &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; iam_principals &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; locations &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; networks &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; project_ids &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; subnetworks &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [display_name](variables.tf#L38) | Display name. | <code>string</code> | | <code>null</code> |
| [domain](variables.tf#L44) | Domain. | <code>string</code> | | <code>null</code> |
| [factories_config](variables.tf#L50) | Path to folder with YAML resource description data files. | <code title="object&#40;&#123;&#10; workstation_configs &#61; optional&#40;string, &#34;data&#47;workstation-configs&#34;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [id](variables.tf#L59) | Workstation cluster ID. | <code>string</code> | | <code>&#34;ws-cluster-0&#34;</code> |
| [labels](variables.tf#L66) | Workstation cluster labels. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> |
| [private_cluster_config](variables.tf#L82) | Private cluster config. | <code title="object&#40;&#123;&#10; allowed_projects &#61; optional&#40;list&#40;string&#41;&#41;&#10; enable_private_endpoint &#61; optional&#40;bool, true&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [service_accounts](variables.tf#L98) | Project factory managed service accounts to populate context. | <code title="map&#40;object&#40;&#123;&#10; email &#61; string&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
## Outputs
| name | description | sensitive |
|---|---|:---:|
| [hostname](outputs.tf#L17) | Cluster hostname. | |
<!-- END TFDOC -->
## Test
```hcl
module "test" {
source = "./fabric/fast/project-templates/os-apt-registries"
project_id = "my-project"
location = "europe-west3"
apt_remote_registries = [
{ path = "DEBIAN debian/dists/bookworm" },
{
path = "DEBIAN debian-security/dists/bookworm-security"
# grant specific access permissions to this registry
writer_principals = [
"serviceAccount:vm-default@prod-proj-0.iam.gserviceaccount.com"
]
}
]
}
# tftest modules=3 resources=4
```

View File

@@ -0,0 +1,34 @@
# 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.
# yaml-language-server: $schema=../../../../../modules/workstation-cluster/schemas/workstation-config.schema.json
display_name: Default configuration.
container:
image: us-central1-docker.pkg.dev/cloud-workstations-images/predefined/code-oss:latest
gce_instance:
disable_public_ip_addresses: true
machine_type: e2-medium
service_account: $iam_principals:service_accounts/prod-gce-ws-0/ws-default
service_account_scopes:
- https://www.googleapis.com/auth/cloud-platform
persistent_directories:
- mount_path: /home
gce_pd:
size_gb: 10
fs_type: ext4
disk_type: pd-balanced
reclaim_policy: DELETE
workstations:
test-0: {}

View File

@@ -0,0 +1,50 @@
/**
* 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 {
location = reverse(split("/", var.network_config.subnetwork))[2]
}
module "cluster" {
source = "../../../modules/workstation-cluster"
project_id = var.project_id
id = var.id
location = local.location
network_config = var.network_config
private_cluster_config = var.private_cluster_config
factories_config = var.factories_config
context = merge(var.context, {
iam_principals = merge(var.context.iam_principals, {
for k, v in var.service_accounts : "service_accounts/${k}" => v.email
})
})
}
module "ws-addresses" {
source = "../../../modules/net-address"
count = var.network_config.psc_endpoint_address != null ? 1 : 0
project_id = var.project_id
psc_addresses = {
ws-cluster-0 = {
address = var.network_config.psc_endpoint_address
subnet_self_link = var.network_config.subnetwork
region = local.location
service_attachment = {
psc_service_attachment_link = module.cluster.service_attachment_uri
}
}
}
}

View File

@@ -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
*
* 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 "hostname" {
description = "Cluster hostname."
value = module.cluster.cluster_hostname
}

View File

@@ -0,0 +1,69 @@
# 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.
# yaml-language-server: $schema=../..//stages/2-project-factory/schemas/project.schema.json
# TODO: edit and uncomment the following line to create the project in a folder
# parent: $folder_ids:shared
services:
- artifactregistry.googleapis.com
- compute.googleapis.com
- servicedirectory.googleapis.com
- workstations.googleapis.com
automation:
# TODO: edit the automation project and optionally edit resource names
project: $project_ids:iac-0
service_accounts:
rw:
description: Read/write automation service account for workstations.
bucket:
# this reuses the existing stage state bucket and creates a folder in it
name: iac-stage-state
create: false
managed_folders:
gce-workstation-cluster:
iam:
roles/storage.objectCreator:
# TODO: the project id in the service account ref matches this file name
- $iam_principals:service_accounts/gce-workstation-cluster/automation/rw
roles/storage.objectViewer:
- $iam_principals:service_accounts/gce-workstation-cluster/automation/rw
iam_by_principals:
# TODO: the project id in the service account ref matches this file name
$iam_principals:service_accounts/gce-workstation-cluster/automation/rw:
- roles/compute.admin
- roles/iam.serviceAccountUser
- roles/servicedirectory.admin
- roles/workstations.admin
$iam_principals:service_accounts/gce-workstation-cluster/ws-default:
- roles/logging.logWriter
- roles/monitoring.metricWriter
# org_policies:
# compute.restrictSharedVpcSubnetworks:
# rules:
# - allow:
# values:
# - ${subnet_self_links["prod-landing/europe-west8/ws"]}
service_accounts:
ws-default:
display_name: Workstations default service account.
shared_vpc_service_config:
# TODO: edit the host project
host_project: $project_ids:prod-landing
network_users:
- $iam_principals:service_accounts/gce-workstation-cluster/automation/rw
service_agent_iam:
roles/compute.networkUser:
- $service_agents:compute
- $service_agents:workstations

View File

@@ -0,0 +1,105 @@
/**
* 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.
*/
variable "annotations" {
description = "Workstation cluster annotations."
type = map(string)
default = {}
}
variable "context" {
description = "Context-specific interpolations."
type = object({
condition_vars = optional(map(map(string)), {})
custom_roles = optional(map(string), {})
iam_principals = optional(map(string), {})
locations = optional(map(string), {})
networks = optional(map(string), {})
project_ids = optional(map(string), {})
subnetworks = optional(map(string), {})
})
default = {}
nullable = false
}
variable "display_name" {
description = "Display name."
type = string
default = null
}
variable "domain" {
description = "Domain."
type = string
default = null
}
variable "factories_config" {
description = "Path to folder with YAML resource description data files."
type = object({
workstation_configs = optional(string, "data/workstation-configs")
})
nullable = false
default = {}
}
variable "id" {
description = "Workstation cluster ID."
type = string
nullable = false
default = "ws-cluster-0"
}
variable "labels" {
description = "Workstation cluster labels."
type = map(string)
default = {}
}
variable "network_config" {
description = "VPC and subnet for the cluster."
nullable = false
type = object({
network = string
subnetwork = string
psc_endpoint_address = optional(string)
})
}
variable "private_cluster_config" {
description = "Private cluster config."
type = object({
allowed_projects = optional(list(string))
enable_private_endpoint = optional(bool, true)
})
nullable = false
default = {}
}
variable "project_id" {
description = "Project id where the cluster will be created."
type = string
nullable = false
}
variable "service_accounts" {
description = "Project factory managed service accounts to populate context."
type = map(object({
email = string
}))
nullable = false
default = {}
}

View File

@@ -31,7 +31,6 @@ automation:
rw:
description: Read/write automation service account for apt registries.
bucket:
description: Terraform state bucket for apt registries.
# this reuses the existing stage state bucket and creates a folder in it
name: iac-stage-state
create: false
@@ -39,7 +38,7 @@ automation:
os-apt:
iam:
roles/storage.objectCreator:
# the project id in the service account ref matches this file name
# TODO: the project id in the service account ref matches this file name
- $iam_principals:service_accounts/os-apt-registries/automation/rw
roles/storage.objectViewer:
- $iam_principals:service_accounts/os-apt-registries/automation/rw

View File

@@ -140,18 +140,15 @@ locals {
)
shared_vpc_service_config = ( # type: object({...})
try(v.shared_vpc_service_config, null) != null
? merge(
{
host_project = null
iam_bindings_additive = {}
network_users = []
service_agent_iam = {}
service_agent_subnet_iam = {}
service_iam_grants = []
network_subnet_users = {}
},
v.shared_vpc_service_config
)
? {
host_project = try(v.shared_vpc_service_config.host_project, null)
iam_bindings_additive = try(v.shared_vpc_service_config.iam_bindings_additive, {})
network_users = try(v.shared_vpc_service_config.network_users, [])
service_agent_iam = try(v.shared_vpc_service_config.service_agent_iam, {})
service_agent_subnet_iam = try(v.shared_vpc_service_config.service_agent_subnet_iam, {})
service_iam_grants = try(v.shared_vpc_service_config.service_iam_grants, [])
network_subnet_users = try(v.shared_vpc_service_config.network_subnet_users, {})
}
: local.data_defaults.defaults.shared_vpc_service_config
)
tag_bindings = coalesce( # type: map(string)
@@ -267,27 +264,15 @@ locals {
)
service_encryption_key_ids = {}
services = []
shared_vpc_service_config = merge(
{
host_project = null
iam_bindings_additive = {}
network_users = []
service_agent_iam = {}
service_agent_subnet_iam = {}
service_iam_grants = []
network_subnet_users = {}
},
try(local._data_defaults.defaults.shared_vpc_service_config, {
host_project = null
iam_bindings_additive = {}
network_users = []
service_agent_iam = {}
service_agent_subnet_iam = {}
service_iam_grants = []
network_subnet_users = {}
}
)
)
shared_vpc_service_config = {
host_project = try(local._data_defaults.defaults.shared_vpc_service_config.host_project, null)
iam_bindings_additive = try(local._data_defaults.defaults.shared_vpc_service_config.iam_bindings_additive, {})
network_users = try(local._data_defaults.defaults.shared_vpc_service_config.network_users, [])
service_agent_iam = try(local._data_defaults.defaults.shared_vpc_service_config.service_agent_iam, {})
service_agent_subnet_iam = try(local._data_defaults.defaults.shared_vpc_service_config.service_agent_subnet_iam, {})
service_iam_grants = try(local._data_defaults.defaults.shared_vpc_service_config.service_iam_grants, [])
network_subnet_users = try(local._data_defaults.defaults.shared_vpc_service_config.network_subnet_users, {})
}
tag_bindings = {}
service_accounts = {}
universe = null

View File

@@ -172,16 +172,18 @@ module "workstation-cluster" {
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
| [id](variables.tf#L35) | Workstation cluster ID. | <code>string</code> | ✓ | |
| [location](variables.tf#L46) | Location. | <code>string</code> | ✓ | |
| [network_config](variables.tf#L51) | Network configuration. | <code title="object&#40;&#123;&#10; network &#61; string&#10; subnetwork &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | |
| [project_id](variables.tf#L69) | Cluster ID. | <code>string</code> | ✓ | |
| [workstation_configs](variables.tf#L74) | Workstation configurations. | <code title="map&#40;object&#40;&#123;&#10; annotations &#61; optional&#40;map&#40;string&#41;&#41;&#10; container &#61; optional&#40;object&#40;&#123;&#10; image &#61; optional&#40;string&#41;&#10; command &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; args &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; working_dir &#61; optional&#40;string&#41;&#10; env &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; run_as_user &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#10; display_name &#61; optional&#40;string&#41;&#10; enable_audit_agent &#61; optional&#40;bool&#41;&#10; encryption_key &#61; optional&#40;object&#40;&#123;&#10; kms_key &#61; string&#10; kms_key_service_account &#61; string&#10; &#125;&#41;&#41;&#10; gce_instance &#61; optional&#40;object&#40;&#123;&#10; machine_type &#61; optional&#40;string&#41;&#10; service_account &#61; optional&#40;string&#41;&#10; service_account_scopes &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; pool_size &#61; optional&#40;number&#41;&#10; boot_disk_size_gb &#61; optional&#40;number&#41;&#10; tags &#61; optional&#40;list&#40;string&#41;&#41;&#10; disable_public_ip_addresses &#61; optional&#40;bool, false&#41;&#10; enable_nested_virtualization &#61; optional&#40;bool, false&#41;&#10; shielded_instance_config &#61; optional&#40;object&#40;&#123;&#10; enable_secure_boot &#61; optional&#40;bool, false&#41;&#10; enable_vtpm &#61; optional&#40;bool, false&#41;&#10; enable_integrity_monitoring &#61; optional&#40;bool, false&#41;&#10; &#125;&#41;&#41;&#10; enable_confidential_compute &#61; optional&#40;bool, false&#41;&#10; accelerators &#61; optional&#40;list&#40;object&#40;&#123;&#10; type &#61; optional&#40;string&#41;&#10; count &#61; optional&#40;number&#41;&#10; &#125;&#41;&#41;, &#91;&#93;&#41;&#10; &#125;&#41;&#41;&#10; iam &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; iam_bindings &#61; optional&#40;map&#40;object&#40;&#123;&#10; role &#61; string&#10; members &#61; list&#40;string&#41;&#10; &#125;&#41;&#41;, &#123;&#125;&#41;&#10; iam_bindings_additive &#61; optional&#40;map&#40;object&#40;&#123;&#10; role &#61; string&#10; member &#61; string&#10; &#125;&#41;&#41;, &#123;&#125;&#41;&#10; labels &#61; optional&#40;map&#40;string&#41;&#41;&#10; max_workstations &#61; optional&#40;number&#41;&#10; persistent_directories &#61; optional&#40;list&#40;object&#40;&#123;&#10; mount_path &#61; optional&#40;string&#41;&#10; gce_pd &#61; optional&#40;object&#40;&#123;&#10; size_gb &#61; optional&#40;number&#41;&#10; fs_type &#61; optional&#40;string&#41;&#10; disk_type &#61; optional&#40;string&#41;&#10; source_snapshot &#61; optional&#40;string&#41;&#10; reclaim_policy &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#10; &#125;&#41;&#41;, &#91;&#93;&#41;&#10; replica_zones &#61; optional&#40;list&#40;string&#41;&#41;&#10; timeouts &#61; optional&#40;object&#40;&#123;&#10; idle &#61; optional&#40;number&#41;&#10; running &#61; optional&#40;number&#41;&#10; &#125;&#41;, &#123;&#125;&#41;&#10; workstations &#61; optional&#40;map&#40;object&#40;&#123;&#10; annotations &#61; optional&#40;map&#40;string&#41;&#41;&#10; display_name &#61; optional&#40;string&#41;&#10; env &#61; optional&#40;map&#40;string&#41;&#41;&#10; iam &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; iam_bindings &#61; optional&#40;map&#40;object&#40;&#123;&#10; role &#61; string&#10; members &#61; list&#40;string&#41;&#10; &#125;&#41;&#41;, &#123;&#125;&#41;&#10; iam_bindings_additive &#61; optional&#40;map&#40;object&#40;&#123;&#10; role &#61; string&#10; member &#61; string&#10; &#125;&#41;&#41;, &#123;&#125;&#41;&#10; labels &#61; optional&#40;map&#40;string&#41;&#41;&#10; &#125;&#41;&#41;, &#123;&#125;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | ✓ | |
| [id](variables.tf#L59) | Workstation cluster ID. | <code>string</code> | ✓ | |
| [location](variables.tf#L70) | Location. | <code>string</code> | ✓ | |
| [network_config](variables.tf#L75) | Network configuration. | <code title="object&#40;&#123;&#10; network &#61; string&#10; subnetwork &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | |
| [project_id](variables.tf#L93) | Cluster ID. | <code>string</code> | ✓ | |
| [annotations](variables.tf#L17) | Workstation cluster annotations. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> |
| [display_name](variables.tf#L23) | Display name. | <code>string</code> | | <code>null</code> |
| [domain](variables.tf#L29) | Domain. | <code>string</code> | | <code>null</code> |
| [labels](variables.tf#L40) | Workstation cluster labels. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> |
| [private_cluster_config](variables.tf#L59) | Private cluster config. | <code title="object&#40;&#123;&#10; enable_private_endpoint &#61; optional&#40;bool, false&#41;&#10; allowed_projects &#61; optional&#40;list&#40;string&#41;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [context](variables.tf#L23) | Context-specific interpolations. | <code title="object&#40;&#123;&#10; condition_vars &#61; optional&#40;map&#40;map&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; custom_roles &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; iam_principals &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; locations &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; networks &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; project_ids &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; subnetworks &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [display_name](variables.tf#L38) | Display name. | <code>string</code> | | <code>null</code> |
| [domain](variables.tf#L44) | Domain. | <code>string</code> | | <code>null</code> |
| [factories_config](variables.tf#L50) | Path to folder with YAML resource description data files. | <code title="object&#40;&#123;&#10; workstation_configs &#61; optional&#40;string&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [labels](variables.tf#L64) | Workstation cluster labels. | <code>map&#40;string&#41;</code> | | <code>&#123;&#125;</code> |
| [private_cluster_config](variables.tf#L83) | Private cluster config. | <code title="object&#40;&#123;&#10; enable_private_endpoint &#61; optional&#40;bool, false&#41;&#10; allowed_projects &#61; optional&#40;list&#40;string&#41;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [workstation_configs](variables.tf#L98) | Workstation configurations. | <code title="map&#40;object&#40;&#123;&#10; annotations &#61; optional&#40;map&#40;string&#41;&#41;&#10; display_name &#61; optional&#40;string&#41;&#10; enable_audit_agent &#61; optional&#40;bool&#41;&#10; labels &#61; optional&#40;map&#40;string&#41;&#41;&#10; max_workstations &#61; optional&#40;number&#41;&#10; replica_zones &#61; optional&#40;list&#40;string&#41;&#41;&#10; container &#61; optional&#40;object&#40;&#123;&#10; args &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; command &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; env &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; image &#61; optional&#40;string&#41;&#10; run_as_user &#61; optional&#40;string&#41;&#10; working_dir &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#10; encryption_key &#61; optional&#40;object&#40;&#123;&#10; kms_key &#61; string&#10; kms_key_service_account &#61; string&#10; &#125;&#41;&#41;&#10; gce_instance &#61; optional&#40;object&#40;&#123;&#10; boot_disk_size_gb &#61; optional&#40;number&#41;&#10; disable_public_ip_addresses &#61; optional&#40;bool, false&#41;&#10; enable_confidential_compute &#61; optional&#40;bool, false&#41;&#10; enable_nested_virtualization &#61; optional&#40;bool, false&#41;&#10; machine_type &#61; optional&#40;string&#41;&#10; pool_size &#61; optional&#40;number&#41;&#10; service_account &#61; optional&#40;string&#41;&#10; service_account_scopes &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; tags &#61; optional&#40;list&#40;string&#41;&#41;&#10; accelerators &#61; optional&#40;list&#40;object&#40;&#123;&#10; type &#61; optional&#40;string&#41;&#10; count &#61; optional&#40;number&#41;&#10; &#125;&#41;&#41;, &#91;&#93;&#41;&#10; shielded_instance_config &#61; optional&#40;object&#40;&#123;&#10; enable_secure_boot &#61; optional&#40;bool, false&#41;&#10; enable_vtpm &#61; optional&#40;bool, false&#41;&#10; enable_integrity_monitoring &#61; optional&#40;bool, false&#41;&#10; &#125;&#41;&#41;&#10; &#125;&#41;&#41;&#10; iam &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; iam_bindings &#61; optional&#40;map&#40;object&#40;&#123;&#10; role &#61; string&#10; members &#61; list&#40;string&#41;&#10; &#125;&#41;&#41;, &#123;&#125;&#41;&#10; iam_bindings_additive &#61; optional&#40;map&#40;object&#40;&#123;&#10; role &#61; string&#10; member &#61; string&#10; &#125;&#41;&#41;, &#123;&#125;&#41;&#10; persistent_directories &#61; optional&#40;list&#40;object&#40;&#123;&#10; mount_path &#61; optional&#40;string&#41;&#10; gce_pd &#61; optional&#40;object&#40;&#123;&#10; size_gb &#61; optional&#40;number&#41;&#10; fs_type &#61; optional&#40;string&#41;&#10; disk_type &#61; optional&#40;string&#41;&#10; source_snapshot &#61; optional&#40;string&#41;&#10; reclaim_policy &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#10; &#125;&#41;&#41;, &#91;&#93;&#41;&#10; timeouts &#61; optional&#40;object&#40;&#123;&#10; idle &#61; optional&#40;number&#41;&#10; running &#61; optional&#40;number&#41;&#10; &#125;&#41;, &#123;&#125;&#41;&#10; workstations &#61; optional&#40;map&#40;object&#40;&#123;&#10; annotations &#61; optional&#40;map&#40;string&#41;&#41;&#10; display_name &#61; optional&#40;string&#41;&#10; env &#61; optional&#40;map&#40;string&#41;&#41;&#10; iam &#61; optional&#40;map&#40;list&#40;string&#41;&#41;, &#123;&#125;&#41;&#10; iam_bindings &#61; optional&#40;map&#40;object&#40;&#123;&#10; role &#61; string&#10; members &#61; list&#40;string&#41;&#10; &#125;&#41;&#41;, &#123;&#125;&#41;&#10; iam_bindings_additive &#61; optional&#40;map&#40;object&#40;&#123;&#10; role &#61; string&#10; member &#61; string&#10; &#125;&#41;&#41;, &#123;&#125;&#41;&#10; labels &#61; optional&#40;map&#40;string&#41;&#41;&#10; &#125;&#41;&#41;, &#123;&#125;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
## Outputs

View File

@@ -0,0 +1,107 @@
/**
* 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 {
_f_files = try(fileset(local._f_paths.workstation_configs, "*.yaml"), [])
_f_paths = {
for k, v in var.factories_config : k => v == null ? null : pathexpand(v)
}
_f_raw = {
for f in local._f_files : trimsuffix(f, ".yaml") => yamldecode(file(
"${local._f_paths.workstation_configs}/${f}"
))
}
f_workstation_configs = {
for k, v in local._f_raw : k => {
annotations = try(v.annotations, null)
display_name = try(v.display_name, null)
enable_audit_agent = try(v.enable_audit_agent, null)
iam = try(v.iam, {})
iam_bindings = try(v.iam_bindings, {})
iam_bindings_additive = try(v.iam_bindings_additive, {})
labels = try(v.labels, null)
max_workstations = try(v.max_workstations, null)
replica_zones = try(v.replica_zones, null)
timeouts = try(v.timeouts, {})
container = (
lookup(v, "container", null) == null ? null : {
args = try(v.container.args, [])
command = try(v.container.command, [])
env = try(v.container.env, {})
image = try(v.container.image, null)
run_as_user = try(v.container.run_as_user, null)
working_dir = try(v.container.working_dir, null)
}
)
encryption_key = (
lookup(v, "encryption_key", null) == null ? null : {
kms_key = try(v.encryption_key.kms_key, null)
kms_key_service_account = try(v.encryption_key.kms_key_service_account, null)
}
)
gce_instance = (
lookup(v, "gce_instance", null) == null ? null : {
boot_disk_size_gb = try(v.gce_instance.boot_disk_size_gb, null)
disable_public_ip_addresses = try(v.gce_instance.disable_public_ip_addresses, false)
enable_confidential_compute = try(v.gce_instance.enable_confidential_compute, false)
enable_nested_virtualization = try(v.gce_instance.enable_nested_virtualization, false)
machine_type = try(v.gce_instance.machine_type, null)
pool_size = try(v.gce_instance.pool_size, null)
service_account = try(v.gce_instance.service_account, null)
service_account_scopes = try(v.gce_instance.service_account_scopes, null)
tags = try(v.gce_instance.tags, null)
accelerators = try(v.gce_instance.accelerators, [])
shielded_instance_config = (
try(v.gce_instance.shielded_instance_config, null) == null ? null : {
enable_secure_boot = try(
v.gce_instance.shielded_instance_config.enable_secure_boot, false
)
enable_vtpm = try(
v.gce_instance.shielded_instance_config.enable_vtpm, false
)
enable_integrity_monitoring = try(
v.gce_instance.shielded_instance_config.enable_integrity_monitoring, false
)
}
)
}
)
persistent_directories = [
for vv in try(v.persistent_directories, []) : {
mount_path = try(vv.mount_path, null)
gce_pd = try(vv.gce_pd, null) == null ? null : {
size_gb = try(vv.gce_pd.size_gb, null)
fs_type = try(vv.gce_pd.fs_type, null)
disk_type = try(vv.gce_pd.disk_type, null)
source_snapshot = try(vv.gce_pd.source_snapshot, null)
reclaim_policy = try(vv.gce_pd.reclaim_policy, null)
}
}
]
workstations = {
for kk, vv in try(v.workstations, {}) : kk => {
annotations = try(vv.annotations, null)
display_name = try(vv.display_name, null)
env = try(vv.env, null)
labels = try(vv.labels, null)
iam = try(vv.iam, {})
iam_bindings = try(vv.iam_bindings, {})
iam_bindings_additive = try(vv.iam_bindings_additive, {})
}
}
}
}
}

View File

@@ -17,105 +17,143 @@
# tfdoc:file:description IAM bindings
locals {
workstation_config_iam = merge([for k1, v1 in var.workstation_configs : { for k2, v2 in v1.iam :
"${k1}-${k2}" => {
workstation_config_id = k1
role = k2
members = v2
} }]...)
workstation_config_iam_bindings = merge([for k1, v1 in var.workstation_configs : { for k2, v2 in v1.iam_bindings :
"${k1}-${k2}" => merge(v2, {
workstation_config_id = k1
}) }]...)
workstation_config_iam_bindings_additive = merge([for k1, v1 in var.workstation_configs : { for k2, v2 in v1.iam_bindings_additive :
"${k1}-${k2}" => merge(v2, {
workstation_config_id = k1
}) }]...)
workstation_iam = merge(flatten([for k1, v1 in var.workstation_configs : [for k2, v2 in v1.workstations :
{ for k3, v3 in v2.iam : "${k1}-${k2}-${k3}" => {
workstation_config_id = k1
workstation_id = k2
role = k3
members = v3
} }]])...)
workstation_iam_bindings = merge(flatten([for k1, v1 in var.workstation_configs : [for k2, v2 in v1.workstations :
{ for k3, v3 in v2.iam_bindings : "${k1}-${k2}-${k3}" => merge(v3, {
workstation_config_id = k1
workstation_id = k2
}) }]])...)
workstation_iam_bindings_additive = merge(flatten([for k1, v1 in var.workstation_configs : [for k2, v2 in v1.workstations :
{ for k3, v3 in v2.iam_bindings_additive : "${k1}-${k2}-${k3}" => merge(v3, {
workstation_config_id = k1
workstation_id = k2
}) }]])...)
gwcc = google_workstations_workstation_config.configs
gwcw = google_workstations_workstation.workstations
workstation_config_iam = merge([
for k1, v1 in local.workstation_configs : {
for k2, v2 in v1.iam : "${k1}-${k2}" => {
workstation_config_id = k1
role = k2
members = v2
}
}
]...)
workstation_config_iam_bindings = merge([
for k1, v1 in local.workstation_configs : {
for k2, v2 in v1.iam_bindings : "${k1}-${k2}" => merge(v2, {
workstation_config_id = k1
})
}
]...)
workstation_config_iam_bindings_additive = merge([
for k1, v1 in local.workstation_configs : {
for k2, v2 in v1.iam_bindings_additive : "${k1}-${k2}" => merge(v2, {
workstation_config_id = k1
})
}
]...)
workstation_iam = merge(flatten([
for k1, v1 in local.workstation_configs : [
for k2, v2 in v1.workstations : {
for k3, v3 in v2.iam : "${k1}-${k2}-${k3}" => {
workstation_config_id = k1
workstation_id = k2
role = k3
members = v3
}
}
]
])...)
workstation_iam_bindings = merge(flatten([
for k1, v1 in local.workstation_configs : [
for k2, v2 in v1.workstations : {
for k3, v3 in v2.iam_bindings : "${k1}-${k2}-${k3}" => merge(v3, {
workstation_config_id = k1
workstation_id = k2
})
}
]
])...)
workstation_iam_bindings_additive = merge(flatten([
for k1, v1 in local.workstation_configs : [
for k2, v2 in v1.workstations : {
for k3, v3 in v2.iam_bindings_additive : "${k1}-${k2}-${k3}" => merge(v3, {
workstation_config_id = k1
workstation_id = k2
})
}
]
])...)
}
resource "google_workstations_workstation_config_iam_binding" "authoritative" {
provider = google-beta
for_each = local.workstation_config_iam
project = google_workstations_workstation_config.configs[each.value.workstation_config_id].project
location = google_workstations_workstation_config.configs[each.value.workstation_config_id].location
workstation_cluster_id = google_workstations_workstation_config.configs[each.value.workstation_config_id].workstation_cluster_id
workstation_config_id = google_workstations_workstation_config.configs[each.value.workstation_config_id].workstation_config_id
role = each.value.role
members = each.value.members
project = local.gwcc[each.value.workstation_config_id].project
location = local.gwcc[each.value.workstation_config_id].location
workstation_cluster_id = local.gwcc[each.value.workstation_config_id].workstation_cluster_id
workstation_config_id = local.gwcc[each.value.workstation_config_id].workstation_config_id
role = lookup(local.ctx.custom_roles, each.value.role, each.value.role)
members = [
for v in each.value.members : lookup(local.ctx.iam_principals, v, v)
]
}
resource "google_workstations_workstation_config_iam_binding" "bindings" {
provider = google-beta
for_each = local.workstation_config_iam_bindings
project = google_workstations_workstation_config.configs[each.value.workstation_config_id].project
location = google_workstations_workstation_config.configs[each.value.workstation_config_id].location
workstation_cluster_id = google_workstations_workstation_config.configs[each.value.workstation_config_id].workstation_cluster_id
workstation_config_id = google_workstations_workstation_config.configs[each.value.workstation_config_id].workstation_config_id
role = each.value.role
members = each.value.members
project = local.gwcc[each.value.workstation_config_id].project
location = local.gwcc[each.value.workstation_config_id].location
workstation_cluster_id = local.gwcc[each.value.workstation_config_id].workstation_cluster_id
workstation_config_id = local.gwcc[each.value.workstation_config_id].workstation_config_id
role = lookup(local.ctx.custom_roles, each.value.role, each.value.role)
members = [
for v in each.value.members : lookup(local.ctx.iam_principals, v, v)
]
}
resource "google_workstations_workstation_config_iam_member" "bindings" {
provider = google-beta
for_each = local.workstation_config_iam_bindings_additive
project = google_workstations_workstation_config.configs[each.value.workstation_config_id].project
location = google_workstations_workstation_config.configs[each.value.workstation_config_id].location
workstation_cluster_id = google_workstations_workstation_config.configs[each.value.workstation_config_id].workstation_cluster_id
workstation_config_id = google_workstations_workstation_config.configs[each.value.workstation_config_id].workstation_config_id
role = each.value.role
member = each.value.member
project = local.gwcc[each.value.workstation_config_id].project
location = local.gwcc[each.value.workstation_config_id].location
workstation_cluster_id = local.gwcc[each.value.workstation_config_id].workstation_cluster_id
workstation_config_id = local.gwcc[each.value.workstation_config_id].workstation_config_id
role = lookup(local.ctx.custom_roles, each.value.role, each.value.role)
member = lookup(
local.ctx.iam_principals, each.value.member, each.value.member
)
}
resource "google_workstations_workstation_iam_binding" "authoritative" {
provider = google-beta
for_each = local.workstation_iam
project = google_workstations_workstation.workstations["${each.value.workstation_config_id}-${each.value.workstation_id}"].project
location = google_workstations_workstation.workstations["${each.value.workstation_config_id}-${each.value.workstation_id}"].location
workstation_cluster_id = google_workstations_workstation.workstations["${each.value.workstation_config_id}-${each.value.workstation_id}"].workstation_cluster_id
workstation_config_id = google_workstations_workstation.workstations["${each.value.workstation_config_id}-${each.value.workstation_id}"].workstation_config_id
workstation_id = google_workstations_workstation.workstations["${each.value.workstation_config_id}-${each.value.workstation_id}"].workstation_id
role = each.value.role
members = each.value.members
project = local.gwcw["${each.value.workstation_config_id}-${each.value.workstation_id}"].project
location = local.gwcw["${each.value.workstation_config_id}-${each.value.workstation_id}"].location
workstation_cluster_id = local.gwcw["${each.value.workstation_config_id}-${each.value.workstation_id}"].workstation_cluster_id
workstation_config_id = local.gwcw["${each.value.workstation_config_id}-${each.value.workstation_id}"].workstation_config_id
workstation_id = local.gwcw["${each.value.workstation_config_id}-${each.value.workstation_id}"].workstation_id
role = lookup(local.ctx.custom_roles, each.value.role, each.value.role)
members = [
for v in each.value.members : lookup(local.ctx.iam_principals, v, v)
]
}
resource "google_workstations_workstation_iam_binding" "bindings" {
provider = google-beta
for_each = local.workstation_iam_bindings
project = google_workstations_workstation.workstations["${each.value.workstation_config_id}-${each.value.workstation_id}"].project
location = google_workstations_workstation.workstations["${each.value.workstation_config_id}-${each.value.workstation_id}"].location
workstation_cluster_id = google_workstations_workstation.workstations["${each.value.workstation_config_id}-${each.value.workstation_id}"].workstation_cluster_id
workstation_config_id = google_workstations_workstation.workstations["${each.value.workstation_config_id}-${each.value.workstation_id}"].workstation_config_id
workstation_id = google_workstations_workstation.workstations["${each.value.workstation_config_id}-${each.value.workstation_id}"].workstation_id
role = each.value.role
members = each.value.members
project = local.gwcw["${each.value.workstation_config_id}-${each.value.workstation_id}"].project
location = local.gwcw["${each.value.workstation_config_id}-${each.value.workstation_id}"].location
workstation_cluster_id = local.gwcw["${each.value.workstation_config_id}-${each.value.workstation_id}"].workstation_cluster_id
workstation_config_id = local.gwcw["${each.value.workstation_config_id}-${each.value.workstation_id}"].workstation_config_id
workstation_id = local.gwcw["${each.value.workstation_config_id}-${each.value.workstation_id}"].workstation_id
role = lookup(local.ctx.custom_roles, each.value.role, each.value.role)
members = [
for v in each.value.members : lookup(local.ctx.iam_principals, v, v)
]
}
resource "google_workstations_workstation_iam_member" "bindings" {
provider = google-beta
for_each = local.workstation_iam_bindings_additive
project = google_workstations_workstation.workstations["${each.value.workstation_config_id}-${each.value.workstation_id}"].project
location = google_workstations_workstation.workstations["${each.value.workstation_config_id}-${each.value.workstation_id}"].location
workstation_cluster_id = google_workstations_workstation.workstations["${each.value.workstation_config_id}-${each.value.workstation_id}"].workstation_cluster_id
workstation_config_id = google_workstations_workstation.workstations["${each.value.workstation_config_id}-${each.value.workstation_id}"].workstation_config_id
workstation_id = google_workstations_workstation.workstations["${each.value.workstation_config_id}-${each.value.workstation_id}"].workstation_id
role = each.value.role
member = each.value.member
project = local.gwcw["${each.value.workstation_config_id}-${each.value.workstation_id}"].project
location = local.gwcw["${each.value.workstation_config_id}-${each.value.workstation_id}"].location
workstation_cluster_id = local.gwcw["${each.value.workstation_config_id}-${each.value.workstation_id}"].workstation_cluster_id
workstation_config_id = local.gwcw["${each.value.workstation_config_id}-${each.value.workstation_id}"].workstation_config_id
workstation_id = local.gwcw["${each.value.workstation_config_id}-${each.value.workstation_id}"].workstation_id
role = lookup(local.ctx.custom_roles, each.value.role, each.value.role)
member = lookup(
local.ctx.iam_principals, each.value.member, each.value.member
)
}

View File

@@ -15,24 +15,43 @@
*/
locals {
workstations = merge(flatten([for k1, v1 in var.workstation_configs :
{ for k2, v2 in v1.workstations :
"${k1}-${k2}" => merge({
ctx = {
for k, v in var.context : k => {
for kk, vv in v : "${local.ctx_p}${k}:${kk}" => vv
} if k != "condition_vars"
}
ctx_p = "$"
workstation_configs = merge(
var.workstation_configs, local.f_workstation_configs
)
workstations = merge(flatten([
for k1, v1 in local.workstation_configs : {
for k2, v2 in v1.workstations : "${k1}-${k2}" => merge({
workstation_config_id = k1
workstation_id = k2
}, v2) }])...)
}, v2)
}
])...)
}
resource "google_workstations_workstation_cluster" "cluster" {
provider = google-beta
workstation_cluster_id = var.id
project = var.project_id
display_name = var.display_name
network = var.network_config.network
subnetwork = var.network_config.subnetwork
location = var.location
annotations = var.annotations
labels = var.labels
project = lookup(
local.ctx.project_ids, var.project_id, var.project_id
)
display_name = var.display_name
location = lookup(
local.ctx.locations, var.location, var.location
)
network = lookup(
local.ctx.networks, var.network_config.network, var.network_config.network
)
subnetwork = lookup(
local.ctx.subnetworks, var.network_config.subnetwork, var.network_config.subnetwork
)
annotations = var.annotations
labels = var.labels
dynamic "private_cluster_config" {
for_each = var.private_cluster_config == null ? [] : [""]
content {
@@ -49,29 +68,37 @@ resource "google_workstations_workstation_cluster" "cluster" {
}
resource "google_workstations_workstation_config" "configs" {
for_each = var.workstation_configs
for_each = local.workstation_configs
provider = google-beta
project = google_workstations_workstation_cluster.cluster.project
workstation_config_id = each.key
workstation_cluster_id = google_workstations_workstation_cluster.cluster.workstation_cluster_id
location = google_workstations_workstation_cluster.cluster.location
workstation_cluster_id = google_workstations_workstation_cluster.cluster.workstation_cluster_id
workstation_config_id = each.key
annotations = each.value.annotations
display_name = each.value.display_name
labels = each.value.labels
max_usable_workstations = each.value.max_workstations
replica_zones = each.value.replica_zones
idle_timeout = (
each.value.timeouts.idle == null ? null : "${each.value.timeouts.idle}s"
try(each.value.timeouts.idle, null) == null
? null
: "${each.value.timeouts.idle}s"
)
running_timeout = (
each.value.timeouts.running == null ? null : "${each.value.timeouts.running}s"
try(each.value.timeouts.running, null) == null
? null :
"${each.value.timeouts.running}s"
)
replica_zones = each.value.replica_zones
annotations = each.value.annotations
labels = each.value.labels
dynamic "host" {
for_each = each.value.gce_instance == null ? [] : [""]
content {
gce_instance {
machine_type = each.value.gce_instance.machine_type
service_account = each.value.gce_instance.service_account
machine_type = each.value.gce_instance.machine_type
service_account = each.value.gce_instance.service_account == null ? null : lookup(
local.ctx.iam_principals,
each.value.gce_instance.service_account,
each.value.gce_instance.service_account
)
service_account_scopes = each.value.gce_instance.service_account_scopes
pool_size = each.value.gce_instance.pool_size
boot_disk_size_gb = each.value.gce_instance.boot_disk_size_gb
@@ -116,8 +143,16 @@ resource "google_workstations_workstation_config" "configs" {
dynamic "encryption_key" {
for_each = each.value.encryption_key == null ? [] : [""]
content {
kms_key = each.value.encryption_key.kms_key
kms_key_service_account = each.value.encryption_key.kms_key_service_account
kms_key = each.value.encryption_key.kms_key
kms_key_service_account = (
each.value.encryption_key.kms_key_service_account == null
? null
: lookup(
local.ctx.iam_principals,
each.value.encryption_key.kms_key_service_account,
each.value.encryption_key.kms_key_service_account
)
)
}
}
dynamic "persistent_directories" {

View File

@@ -0,0 +1,409 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Workstation Config",
"type": "object",
"additionalProperties": false,
"properties": {
"annotations": {
"type": "object",
"description": "Annotations for the object (optional).",
"additionalProperties": false,
"patternProperties": {
"^[a-z0-9-]+$": {
"type": "string"
}
}
},
"container": {
"type": "object",
"additionalProperties": false,
"description": "Container configuration (optional).",
"properties": {
"args": {
"type": "array",
"description": "Container arguments (optional, defaults to []).",
"items": {
"type": "string"
},
"default": []
},
"command": {
"type": "array",
"description": "Container command (optional, defaults to []).",
"items": {
"type": "string"
},
"default": []
},
"env": {
"type": "object",
"description": "Container environment variables (optional, defaults to {}).",
"additionalProperties": {
"type": "string"
},
"default": {}
},
"image": {
"type": "string",
"description": "Container image URL (optional)."
},
"run_as_user": {
"type": "string",
"description": "User to run the container as (optional)."
},
"working_dir": {
"type": "string",
"description": "Container working directory (optional)."
}
}
},
"display_name": {
"type": "string",
"description": "Human-readable display name (optional)."
},
"enable_audit_agent": {
"type": "boolean",
"description": "Whether to enable the audit agent (optional)."
},
"encryption_key": {
"type": "object",
"additionalProperties": false,
"description": "Customer-managed encryption key configuration (optional).",
"properties": {
"kms_key": {
"type": "string",
"description": "The KMS key resource name (required)."
},
"kms_key_service_account": {
"type": "string",
"description": "The service account to use for the KMS key (required)."
}
},
"required": [
"kms_key",
"kms_key_service_account"
]
},
"gce_instance": {
"type": "object",
"additionalProperties": false,
"description": "GCE instance configuration (optional).",
"properties": {
"machine_type": {
"type": "string",
"description": "Machine type (optional)."
},
"service_account": {
"type": "string",
"description": "Service account for the GCE instance (optional)."
},
"service_account_scopes": {
"type": "array",
"description": "Service account scopes (optional, defaults to []).",
"items": {
"type": "string"
},
"default": []
},
"pool_size": {
"type": "number",
"description": "Size of the GCE instance pool (optional)."
},
"boot_disk_size_gb": {
"type": "number",
"description": "Boot disk size in GB (optional)."
},
"tags": {
"type": "array",
"description": "Network tags (optional).",
"items": {
"type": "string"
}
},
"disable_public_ip_addresses": {
"type": "boolean",
"description": "Whether to disable public IP addresses (optional, defaults to false).",
"default": false
},
"enable_nested_virtualization": {
"type": "boolean",
"description": "Whether to enable nested virtualization (optional, defaults to false).",
"default": false
},
"shielded_instance_config": {
"type": "object",
"additionalProperties": false,
"description": "Shielded instance configuration (optional).",
"properties": {
"enable_secure_boot": {
"type": "boolean",
"description": "Whether to enable Secure Boot (optional, defaults to false).",
"default": false
},
"enable_vtpm": {
"type": "boolean",
"description": "Whether to enable vTPM (optional, defaults to false).",
"default": false
},
"enable_integrity_monitoring": {
"type": "boolean",
"description": "Whether to enable integrity monitoring (optional, defaults to false).",
"default": false
}
}
},
"enable_confidential_compute": {
"type": "boolean",
"description": "Whether to enable Confidential Compute (optional, defaults to false).",
"default": false
},
"accelerators": {
"type": "array",
"description": "Accelerator configuration (optional, defaults to []).",
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"type": {
"type": "string",
"description": "Accelerator type (optional)."
},
"count": {
"type": "number",
"description": "Number of accelerators (optional)."
}
}
},
"default": []
}
}
},
"iam": {
"type": "object",
"description": "IAM policy per role for the resource (optional, defaults to {}).",
"additionalProperties": {
"type": "array",
"items": {
"type": "string"
}
},
"default": {}
},
"iam_bindings": {
"type": "object",
"description": "IAM bindings for the resource (optional, defaults to {}).",
"additionalProperties": {
"type": "object",
"properties": {
"role": {
"type": "string",
"description": "The role name (required)."
},
"members": {
"type": "array",
"description": "List of members (required).",
"items": {
"type": "string"
}
}
},
"required": [
"role",
"members"
]
},
"default": {}
},
"iam_bindings_additive": {
"type": "object",
"description": "Additive IAM bindings for the resource (optional, defaults to {}).",
"additionalProperties": {
"type": "object",
"properties": {
"role": {
"type": "string",
"description": "The role name (required)."
},
"member": {
"type": "string",
"description": "The member (required)."
}
},
"required": [
"role",
"member"
]
},
"default": {}
},
"labels": {
"type": "object",
"description": "Labels for the object (optional).",
"additionalProperties": {
"type": "string"
}
},
"max_workstations": {
"type": "number",
"description": "Maximum number of workstations (optional)."
},
"persistent_directories": {
"type": "array",
"description": "Persistent directory configurations (optional, defaults to []).",
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"mount_path": {
"type": "string",
"description": "Mount path for the directory (optional)."
},
"gce_pd": {
"type": "object",
"additionalProperties": false,
"description": "GCE persistent disk configuration (optional).",
"properties": {
"size_gb": {
"type": "number",
"description": "Size of the persistent disk in GB (optional)."
},
"fs_type": {
"type": "string",
"description": "Filesystem type (optional)."
},
"disk_type": {
"type": "string",
"description": "Disk type (optional)."
},
"source_snapshot": {
"type": "string",
"description": "Source snapshot (optional)."
},
"reclaim_policy": {
"type": "string",
"description": "Reclaim policy (optional)."
}
}
}
}
},
"default": []
},
"replica_zones": {
"type": "array",
"description": "Zones for replicas (optional).",
"items": {
"type": "string"
}
},
"timeouts": {
"type": "object",
"additionalProperties": false,
"description": "Timeout configuration (optional, defaults to {}).",
"properties": {
"idle": {
"type": "number",
"description": "Idle timeout in seconds (optional)."
},
"running": {
"type": "number",
"description": "Running timeout in seconds (optional)."
}
},
"default": {}
},
"workstations": {
"type": "object",
"description": "Workstation configurations by name (optional, defaults to {}).",
"additionalProperties": {
"type": "object",
"properties": {
"annotations": {
"type": "object",
"description": "Annotations for the workstation (optional).",
"additionalProperties": {
"type": "string"
}
},
"display_name": {
"type": "string",
"description": "Workstation display name (optional)."
},
"env": {
"type": "object",
"description": "Environment variables (optional).",
"additionalProperties": {
"type": "string"
}
},
"iam": {
"type": "object",
"description": "IAM policy per role for the workstation (optional, defaults to {}).",
"additionalProperties": {
"type": "array",
"items": {
"type": "string"
}
},
"default": {}
},
"iam_bindings": {
"type": "object",
"description": "IAM bindings for the workstation (optional, defaults to {}).",
"additionalProperties": {
"type": "object",
"properties": {
"role": {
"type": "string",
"description": "The role name (required)."
},
"members": {
"type": "array",
"description": "List of members (required).",
"items": {
"type": "string"
}
}
},
"required": [
"role",
"members"
]
},
"default": {}
},
"iam_bindings_additive": {
"type": "object",
"description": "Additive IAM bindings for the workstation (optional, defaults to {}).",
"additionalProperties": {
"type": "object",
"properties": {
"role": {
"type": "string",
"description": "The role name (required)."
},
"member": {
"type": "string",
"description": "The member (required)."
}
},
"required": [
"role",
"member"
]
},
"default": {}
},
"labels": {
"type": "object",
"description": "Labels for the workstation (optional).",
"additionalProperties": {
"type": "string"
}
}
}
},
"default": {}
}
}
}

View File

@@ -20,6 +20,21 @@ variable "annotations" {
default = {}
}
variable "context" {
description = "Context-specific interpolations."
type = object({
condition_vars = optional(map(map(string)), {})
custom_roles = optional(map(string), {})
iam_principals = optional(map(string), {})
locations = optional(map(string), {})
networks = optional(map(string), {})
project_ids = optional(map(string), {})
subnetworks = optional(map(string), {})
})
default = {}
nullable = false
}
variable "display_name" {
description = "Display name."
type = string
@@ -32,6 +47,15 @@ variable "domain" {
default = null
}
variable "factories_config" {
description = "Path to folder with YAML resource description data files."
type = object({
workstation_configs = optional(string)
})
nullable = false
default = {}
}
variable "id" {
description = "Workstation cluster ID."
type = string
@@ -74,40 +98,43 @@ variable "project_id" {
variable "workstation_configs" {
description = "Workstation configurations."
type = map(object({
annotations = optional(map(string))
container = optional(object({
image = optional(string)
command = optional(list(string), [])
args = optional(list(string), [])
working_dir = optional(string)
env = optional(map(string), {})
run_as_user = optional(string)
}))
annotations = optional(map(string))
display_name = optional(string)
enable_audit_agent = optional(bool)
labels = optional(map(string))
max_workstations = optional(number)
replica_zones = optional(list(string))
container = optional(object({
args = optional(list(string), [])
command = optional(list(string), [])
env = optional(map(string), {})
image = optional(string)
run_as_user = optional(string)
working_dir = optional(string)
}))
encryption_key = optional(object({
kms_key = string
kms_key_service_account = string
}))
gce_instance = optional(object({
boot_disk_size_gb = optional(number)
disable_public_ip_addresses = optional(bool, false)
enable_confidential_compute = optional(bool, false)
enable_nested_virtualization = optional(bool, false)
machine_type = optional(string)
pool_size = optional(number)
service_account = optional(string)
service_account_scopes = optional(list(string), [])
pool_size = optional(number)
boot_disk_size_gb = optional(number)
tags = optional(list(string))
disable_public_ip_addresses = optional(bool, false)
enable_nested_virtualization = optional(bool, false)
accelerators = optional(list(object({
type = optional(string)
count = optional(number)
})), [])
shielded_instance_config = optional(object({
enable_secure_boot = optional(bool, false)
enable_vtpm = optional(bool, false)
enable_integrity_monitoring = optional(bool, false)
}))
enable_confidential_compute = optional(bool, false)
accelerators = optional(list(object({
type = optional(string)
count = optional(number)
})), [])
}))
iam = optional(map(list(string)), {})
iam_bindings = optional(map(object({
@@ -118,8 +145,6 @@ variable "workstation_configs" {
role = string
member = string
})), {})
labels = optional(map(string))
max_workstations = optional(number)
persistent_directories = optional(list(object({
mount_path = optional(string)
gce_pd = optional(object({
@@ -130,7 +155,6 @@ variable "workstation_configs" {
reclaim_policy = optional(string)
}))
})), [])
replica_zones = optional(list(string))
timeouts = optional(object({
idle = optional(number)
running = optional(number)
@@ -151,4 +175,6 @@ variable "workstation_configs" {
labels = optional(map(string))
})), {})
}))
nullable = false
default = {}
}

View File

@@ -0,0 +1,71 @@
context = {
condition_vars = {
names = {
my-id = "myid"
}
}
custom_roles = {
myrole = "organizations/366118655033/roles/myRoleOne"
}
iam_principals = {
myuser = "user:test-user@example.com"
myuser2 = "user:test-user2@example.com"
}
locations = {
ew8 = "europe-west8"
}
networks = {
"dev-spoke-0" : "projects/foo-dev-net-spoke-0/global/networks/dev-spoke-0"
}
subnetworks = {
"default" : "projects/foo-dev-net-spoke-0/regions/europe-west8/subnetworks/default"
}
project_ids = {
test = "dev-test-0"
}
}
project_id = "$project_ids:test"
id = "test-0"
location = "$locations:ew8"
network_config = {
network = "$networks:dev-spoke-0"
subnetwork = "$subnetworks:default"
}
workstation_configs = {
my-workstation-config = {
workstations = {
my-workstation = {
labels = {
team = "my-team"
}
iam = {
"roles/workstations.user" = ["$iam_principals:myuser"]
}
}
}
iam = {
"roles/viewer" = ["$iam_principals:myuser2"]
}
iam_bindings = {
workstations-config-viewer = {
role = "$custom_roles:myrole"
members = ["$iam_principals:myuser"]
condition = {
title = "limited-access"
expression = "resource.name.startsWith('my-')"
}
}
}
iam_bindings_additive = {
workstations-config-editor = {
role = "roles/editor"
member = "group:group3@my-org.com"
condition = {
title = "limited-access"
expression = "resource.name.startsWith('$${names.my-id}-')"
}
}
}
}
}

View File

@@ -0,0 +1,117 @@
# 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.
values:
google_workstations_workstation.workstations["my-workstation-config-my-workstation"]:
annotations: null
display_name: null
effective_labels:
goog-terraform-provisioned: 'true'
team: my-team
env: null
labels:
team: my-team
location: europe-west8
project: dev-test-0
source_workstation: null
terraform_labels:
goog-terraform-provisioned: 'true'
team: my-team
timeouts: null
workstation_cluster_id: test-0
workstation_config_id: my-workstation-config
workstation_id: my-workstation
google_workstations_workstation_cluster.cluster:
annotations: null
display_name: null
domain_config: []
effective_labels:
goog-terraform-provisioned: 'true'
labels: null
location: europe-west8
network: projects/foo-dev-net-spoke-0/global/networks/dev-spoke-0
private_cluster_config:
- enable_private_endpoint: false
project: dev-test-0
subnetwork: projects/foo-dev-net-spoke-0/regions/europe-west8/subnetworks/default
tags: null
terraform_labels:
goog-terraform-provisioned: 'true'
timeouts: null
workstation_cluster_id: test-0
google_workstations_workstation_config.configs["my-workstation-config"]:
annotations: null
disable_tcp_connections: null
display_name: null
effective_labels:
goog-terraform-provisioned: 'true'
enable_audit_agent: null
encryption_key: []
idle_timeout: 1200s
labels: null
location: europe-west8
project: dev-test-0
readiness_checks: []
running_timeout: 43200s
terraform_labels:
goog-terraform-provisioned: 'true'
timeouts: null
workstation_cluster_id: test-0
workstation_config_id: my-workstation-config
google_workstations_workstation_config_iam_binding.authoritative["my-workstation-config-roles/viewer"]:
condition: []
location: europe-west8
members:
- user:test-user2@example.com
project: dev-test-0
role: roles/viewer
workstation_cluster_id: test-0
workstation_config_id: my-workstation-config
google_workstations_workstation_config_iam_binding.bindings["my-workstation-config-workstations-config-viewer"]:
condition: []
location: europe-west8
members:
- user:test-user@example.com
project: dev-test-0
role: organizations/366118655033/roles/myRoleOne
workstation_cluster_id: test-0
workstation_config_id: my-workstation-config
google_workstations_workstation_config_iam_member.bindings["my-workstation-config-workstations-config-editor"]:
condition: []
location: europe-west8
member: group:group3@my-org.com
project: dev-test-0
role: roles/editor
workstation_cluster_id: test-0
workstation_config_id: my-workstation-config
google_workstations_workstation_iam_binding.authoritative["my-workstation-config-my-workstation-roles/workstations.user"]:
condition: []
location: europe-west8
members:
- user:test-user@example.com
project: dev-test-0
role: roles/workstations.user
workstation_cluster_id: test-0
workstation_config_id: my-workstation-config
workstation_id: my-workstation
counts:
google_workstations_workstation: 1
google_workstations_workstation_cluster: 1
google_workstations_workstation_config: 1
google_workstations_workstation_config_iam_binding: 2
google_workstations_workstation_config_iam_member: 1
google_workstations_workstation_iam_binding: 1
modules: 0
resources: 7

View File

@@ -0,0 +1,17 @@
# 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.
module: modules/workstation-cluster
tests:
context: