Skip to content

Commit cd5e623

Browse files
authored
Merge pull request GoogleCloudPlatform#4572 from vikramvs-gg/lustre_integration
GKE Managed Lustre integration
2 parents 8298f82 + ee93cc3 commit cd5e623

16 files changed

Lines changed: 613 additions & 19 deletions

File tree

examples/gke-managed-lustre.yaml

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
# Copyright 2025 "Google LLC"
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
blueprint_name: gke-managed-lustre
16+
vars:
17+
project_id: # Set GCP Project ID Here
18+
deployment_name: # Set deployment name
19+
# Managed Lustre is only supported in specific regions and zones
20+
# Please refer https://cloud.google.com/managed-lustre/docs/locations
21+
# The GCP Region used for this deployment.
22+
region: us-central1
23+
# The GCP Zone used for this deployment.
24+
zone: us-central1-c
25+
# Cidr block containing the IP of the machine calling terraform.
26+
# The following line must be updated for this example to work.
27+
authorized_cidr: <your-ip-address>/32
28+
version_prefix: "1.33."
29+
base_network_name: $(vars.deployment_name)
30+
lustre_instance_id: gke-lustre-instance
31+
# The values of size_gib and per_unit_storage_throughput are co-related
32+
# Please refer https://cloud.google.com/managed-lustre/docs/create-instance#performance-tiers
33+
# Storage capacity of the lustre instance in GiB
34+
size_gib: 18000
35+
# Maximum throughput of the lustre instance in MBps per TiB
36+
per_unit_storage_throughput: 1000
37+
38+
deployment_groups:
39+
- group: primary
40+
modules:
41+
- id: network
42+
source: modules/network/vpc
43+
settings:
44+
network_name: $(vars.base_network_name)-net
45+
subnetwork_name: $(vars.base_network_name)-subnet
46+
secondary_ranges_list:
47+
- subnetwork_name: $(vars.base_network_name)-subnet
48+
ranges:
49+
- range_name: pods
50+
ip_cidr_range: 10.4.0.0/14
51+
- range_name: services
52+
ip_cidr_range: 10.0.32.0/20
53+
54+
# Private Service Access (PSA) requires the compute.networkAdmin role which is
55+
# included in the Owner role, but not Editor.
56+
# PSA is required for all Managed Lustre functionality.
57+
# https://cloud.google.com/vpc/docs/configure-private-services-access#permissions
58+
- id: private_service_access
59+
source: community/modules/network/private-service-access
60+
use: [network]
61+
settings:
62+
prefix_length: 24
63+
64+
# Firewall to allow Managed Lustre connection
65+
- id: lustre_firewall_rule
66+
source: modules/network/firewall-rules
67+
use: [network]
68+
settings:
69+
ingress_rules:
70+
- name: $(vars.base_network_name)-allow-lustre-traffic
71+
description: Allow Managed Lustre traffic
72+
source_ranges:
73+
- $(private_service_access.cidr_range)
74+
allow:
75+
- protocol: tcp
76+
ports:
77+
- "988"
78+
79+
- id: managed-lustre
80+
source: modules/file-system/managed-lustre
81+
use: [network, private_service_access]
82+
settings:
83+
name: $(vars.lustre_instance_id)
84+
local_mount: /lustre
85+
remote_mount: lustrefs
86+
size_gib: $(vars.size_gib)
87+
per_unit_storage_throughput: $(vars.per_unit_storage_throughput)
88+
89+
- id: node_pool_service_account
90+
source: community/modules/project/service-account
91+
settings:
92+
name: gke-np-sa
93+
project_roles:
94+
- logging.logWriter
95+
- monitoring.metricWriter
96+
- monitoring.viewer
97+
- stackdriver.resourceMetadata.writer
98+
- storage.objectViewer
99+
- artifactregistry.reader
100+
101+
- id: workload_service_account
102+
source: community/modules/project/service-account
103+
settings:
104+
name: gke-wl-sa
105+
project_roles:
106+
- logging.logWriter
107+
- monitoring.metricWriter
108+
- monitoring.viewer
109+
- stackdriver.resourceMetadata.writer
110+
- storage.objectAdmin
111+
- artifactregistry.reader
112+
113+
- id: gke_cluster
114+
source: modules/scheduler/gke-cluster
115+
use: [network, workload_service_account]
116+
settings:
117+
version_prefix: $(vars.version_prefix)
118+
release_channel: RAPID
119+
enable_managed_lustre_csi: true # Enable Managed Lustre for the cluster
120+
configure_workload_identity_sa: true
121+
enable_private_endpoint: false # Allows for access from authorized public IPs
122+
master_authorized_networks:
123+
- display_name: kubectl-access-network
124+
cidr_block: $(vars.authorized_cidr)
125+
maintenance_exclusions:
126+
- name: no-minor-or-node-upgrades-indefinite
127+
start_time: "2025-08-01T00:00:00Z"
128+
end_time: "2026-08-01T00:00:00Z"
129+
exclusion_scope: NO_MINOR_OR_NODE_UPGRADES
130+
outputs: [instructions]
131+
132+
- id: lustre-pv
133+
source: modules/file-system/gke-persistent-volume
134+
use: [managed-lustre, gke_cluster]
135+
settings:
136+
capacity_gb: $(vars.size_gib)
137+
138+
- id: gke-lustre-pool
139+
source: modules/compute/gke-node-pool
140+
use: [gke_cluster, node_pool_service_account]
141+
settings:
142+
name: gke-lustre-pool
143+
zones: [$(vars.zone)]
144+
machine_type: n2-standard-16
145+
auto_upgrade: true

modules/file-system/gke-persistent-volume/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ No modules.
157157
| <a name="input_filestore_id"></a> [filestore\_id](#input\_filestore\_id) | An identifier for a filestore with the format `projects/{{project}}/locations/{{location}}/instances/{{name}}`. | `string` | `null` | no |
158158
| <a name="input_gcs_bucket_name"></a> [gcs\_bucket\_name](#input\_gcs\_bucket\_name) | The gcs bucket to be used with the persistent volume. | `string` | `null` | no |
159159
| <a name="input_labels"></a> [labels](#input\_labels) | GCE resource labels to be applied to resources. Key-value pairs. | `map(string)` | n/a | yes |
160+
| <a name="input_lustre_id"></a> [lustre\_id](#input\_lustre\_id) | An identifier for a lustre with the format `projects/{{project}}/locations/{{location}}/instances/{{name}}`. | `string` | `null` | no |
160161
| <a name="input_namespace"></a> [namespace](#input\_namespace) | Kubernetes namespace to deploy the storage PVC/PV | `string` | `"default"` | no |
161162
| <a name="input_network_storage"></a> [network\_storage](#input\_network\_storage) | Network attached storage mount to be configured. | <pre>object({<br/> server_ip = string,<br/> remote_mount = string,<br/> local_mount = string,<br/> fs_type = string,<br/> mount_options = string,<br/> client_install_runner = map(string)<br/> mount_runner = map(string)<br/> })</pre> | n/a | yes |
162163
| <a name="input_pv_name"></a> [pv\_name](#input\_pv\_name) | The name for PV. IF not set, a name will be generated based on the storage name. | `string` | `null` | no |

modules/file-system/gke-persistent-volume/main.tf

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,28 @@ locals {
2020
}
2121

2222
locals {
23-
is_gcs = (var.gcs_bucket_name != null)
23+
is_gcs = (var.gcs_bucket_name != null)
24+
is_lustre = (var.lustre_id != null)
2425

2526
filestore_id = (
26-
!local.is_gcs ? # If not using gcs
27+
!local.is_gcs && !local.is_lustre ? # If not using gcs or lustre
2728
var.filestore_id : # Then filestore_id must be provided
2829
"projects/empty/locations/empty/instances/empty" # Otherwise use something arbitrary as it will not be used
2930
)
30-
location = split("/", local.filestore_id)[3]
31+
32+
lustre_id = (
33+
local.is_lustre ?
34+
var.lustre_id :
35+
"projects/empty/locations/empty/instances/empty"
36+
)
37+
location = local.is_lustre ? split("/", local.lustre_id)[3] : split("/", local.filestore_id)[3]
3138
filestore_name = split("/", local.filestore_id)[5]
3239
filestore_share_name = trimprefix(var.network_storage.remote_mount, "/")
33-
base_name = local.is_gcs ? var.gcs_bucket_name : local.filestore_name
34-
35-
pv_name = var.pv_name != null ? var.pv_name : "${local.base_name}-pv"
36-
pvc_name = var.pvc_name != null ? var.pvc_name : "${local.base_name}-pvc"
3740

41+
# Determine the base_name based on which storage type is used
42+
base_name = local.is_gcs ? var.gcs_bucket_name : (local.is_lustre ? split("/", local.lustre_id)[5] : local.filestore_name)
43+
pv_name = var.pv_name != null ? var.pv_name : "${local.base_name}-pv"
44+
pvc_name = var.pvc_name != null ? var.pvc_name : "${local.base_name}-pvc"
3845
list_mount_options = split(",", var.network_storage.mount_options)
3946

4047
filestore_pv_contents = templatefile(
@@ -87,6 +94,31 @@ locals {
8794
}
8895
)
8996

97+
lustre_pv_contents = templatefile(
98+
"${path.module}/templates/managed-lustre-pv.yaml.tftpl",
99+
{
100+
pv_name = local.pv_name
101+
capacity = "${var.capacity_gb}Gi"
102+
location = local.location
103+
project = split("/", var.cluster_id)[1]
104+
labels = local.labels
105+
instance_name = local.base_name
106+
server_ip = split("@", var.network_storage.server_ip)[0]
107+
filesystem_name = var.network_storage.remote_mount
108+
}
109+
)
110+
111+
lustre_pvc_contents = templatefile(
112+
"${path.module}/templates/managed-lustre-pvc.yaml.tftpl",
113+
{
114+
pv_name = local.pv_name
115+
pvc_name = local.pvc_name
116+
labels = local.labels
117+
capacity = "${var.capacity_gb}Gi"
118+
namespace = var.namespace
119+
}
120+
)
121+
90122
cluster_name = split("/", var.cluster_id)[5]
91123
cluster_location = split("/", var.cluster_id)[3]
92124
}
@@ -122,17 +154,17 @@ resource "kubectl_manifest" "pvc_namespace" {
122154
}
123155

124156
resource "kubectl_manifest" "pv" {
125-
yaml_body = local.is_gcs ? local.gcs_pv_contents : local.filestore_pv_contents
157+
yaml_body = local.is_gcs ? local.gcs_pv_contents : (local.is_lustre ? local.lustre_pv_contents : local.filestore_pv_contents)
126158

127159
lifecycle {
128160
precondition {
129-
condition = (var.gcs_bucket_name != null) != (var.filestore_id != null)
130-
error_message = "Either gcs_bucket_name or filestore_id must be set."
161+
condition = ((var.gcs_bucket_name != null ? 1 : 0) + (var.filestore_id != null ? 1 : 0) + (var.lustre_id != null ? 1 : 0)) == 1
162+
error_message = "Either gcs_bucket_name, filestore_id, or lustre_id must be set."
131163
}
132164
}
133165
}
134166

135167
resource "kubectl_manifest" "pvc" {
136-
yaml_body = local.is_gcs ? local.gcs_pvc_contents : local.filestore_pvc_contents
168+
yaml_body = local.is_gcs ? local.gcs_pvc_contents : (local.is_lustre ? local.lustre_pvc_contents : local.filestore_pvc_contents)
137169
depends_on = [kubectl_manifest.pv, kubectl_manifest.pvc_namespace]
138170
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
apiVersion: v1
3+
kind: PersistentVolume
4+
metadata:
5+
name: ${pv_name}
6+
labels:
7+
%{~ for key, val in labels ~}
8+
${key}: ${val}
9+
%{~ endfor ~}
10+
spec:
11+
storageClassName: ""
12+
capacity:
13+
storage: ${capacity}
14+
accessModes:
15+
- ReadWriteMany
16+
persistentVolumeReclaimPolicy: Retain
17+
volumeMode: Filesystem
18+
csi:
19+
driver: lustre.csi.storage.gke.io
20+
volumeHandle: "${project}/${location}/${instance_name}/default-pool/default-container"
21+
volumeAttributes:
22+
ip: ${server_ip}
23+
filesystem: ${filesystem_name}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
kind: PersistentVolumeClaim
3+
apiVersion: v1
4+
metadata:
5+
name: ${pvc_name}
6+
namespace: ${namespace}
7+
labels:
8+
%{~ for key, val in labels ~}
9+
${key}: ${val}
10+
%{~ endfor ~}
11+
spec:
12+
accessModes:
13+
- ReadWriteMany
14+
storageClassName: ""
15+
volumeName: ${pv_name}
16+
resources:
17+
requests:
18+
storage: ${capacity}

modules/file-system/gke-persistent-volume/variables.tf

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,19 @@ variable "filestore_id" {
4545
}
4646
}
4747

48+
variable "lustre_id" {
49+
description = "An identifier for a lustre with the format `projects/{{project}}/locations/{{location}}/instances/{{name}}`."
50+
type = string
51+
default = null
52+
validation {
53+
condition = (
54+
var.lustre_id == null ||
55+
try(length(split("/", var.lustre_id)), 0) == 6
56+
)
57+
error_message = "lustre_id must be in the format of 'projects/{{project}}/locations/{{location}}/instances/{{name}}'."
58+
}
59+
}
60+
4861
variable "gcs_bucket_name" {
4962
description = "The gcs bucket to be used with the persistent volume."
5063
type = string

modules/file-system/managed-lustre/outputs.tf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ output "install_managed_lustre_client" {
3434

3535
output "lustre_id" {
3636
description = "An identifier for the resource with format `projects/{{project}}/locations/{{location}}/instances/{{name}}`"
37-
value = google_lustre_instance.lustre_instance.instance_id
37+
value = google_lustre_instance.lustre_instance.id
3838
}
3939

4040
output "capacity_gib" {

modules/network/firewall-rules/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ limitations under the License.
7777
| Name | Version |
7878
|------|---------|
7979
| <a name="provider_google"></a> [google](#provider\_google) | >= 3.83 |
80+
| <a name="provider_terraform"></a> [terraform](#provider\_terraform) | n/a |
8081
8182
## Modules
8283
@@ -88,6 +89,7 @@ limitations under the License.
8889
8990
| Name | Type |
9091
|------|------|
92+
| [terraform_data.pga_check](https://registry.terraform.io/providers/hashicorp/terraform/latest/docs/resources/data) | resource |
9193
| [google_compute_subnetwork.subnetwork](https://registry.terraform.io/providers/hashicorp/google/latest/docs/data-sources/compute_subnetwork) | data source |
9294
9395
## Inputs
@@ -96,6 +98,8 @@ limitations under the License.
9698
|------|-------------|------|---------|:--------:|
9799
| <a name="input_egress_rules"></a> [egress\_rules](#input\_egress\_rules) | List of egress rules | <pre>list(object({<br/> name = string<br/> description = optional(string, null)<br/> disabled = optional(bool, null)<br/> priority = optional(number, null)<br/> destination_ranges = optional(list(string), [])<br/> source_ranges = optional(list(string), [])<br/> source_tags = optional(list(string))<br/> source_service_accounts = optional(list(string))<br/> target_tags = optional(list(string))<br/> target_service_accounts = optional(list(string))<br/><br/> allow = optional(list(object({<br/> protocol = string<br/> ports = optional(list(string))<br/> })), [])<br/> deny = optional(list(object({<br/> protocol = string<br/> ports = optional(list(string))<br/> })), [])<br/> log_config = optional(object({<br/> metadata = string<br/> }))<br/> }))</pre> | `[]` | no |
98100
| <a name="input_ingress_rules"></a> [ingress\_rules](#input\_ingress\_rules) | List of ingress rules | <pre>list(object({<br/> name = string<br/> description = optional(string, null)<br/> disabled = optional(bool, null)<br/> priority = optional(number, null)<br/> destination_ranges = optional(list(string), [])<br/> source_ranges = optional(list(string), [])<br/> source_tags = optional(list(string))<br/> source_service_accounts = optional(list(string))<br/> target_tags = optional(list(string))<br/> target_service_accounts = optional(list(string))<br/><br/> allow = optional(list(object({<br/> protocol = string<br/> ports = optional(list(string))<br/> })), [])<br/> deny = optional(list(object({<br/> protocol = string<br/> ports = optional(list(string))<br/> })), [])<br/> log_config = optional(object({<br/> metadata = string<br/> }))<br/> }))</pre> | `[]` | no |
101+
| <a name="input_network_name"></a> [network\_name](#input\_network\_name) | The name of the network to create firewall rules in | `string` | `null` | no |
102+
| <a name="input_project_id"></a> [project\_id](#input\_project\_id) | The project ID to host the network in | `string` | `null` | no |
99103
| <a name="input_subnetwork_self_link"></a> [subnetwork\_self\_link](#input\_subnetwork\_self\_link) | The self link of the subnetwork whose global network firewall rules will be modified. | `string` | n/a | yes |
100104

101105
## Outputs

modules/network/firewall-rules/main.tf

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,46 @@
1414
* limitations under the License.
1515
*/
1616

17+
locals {
18+
use_subnetwork_data = (var.project_id == null || var.network_name == null) && var.subnetwork_self_link != null
19+
}
20+
1721
# the google_compute_network data source does not allow identification by
1822
# self_link, which uniquely identifies subnet, project, and network
1923
data "google_compute_subnetwork" "subnetwork" {
24+
# Only instantiate this data source if needed
25+
count = local.use_subnetwork_data ? 1 : 0
2026
self_link = var.subnetwork_self_link
2127
}
2228

23-
# Module-level check for Private Google Access on the subnetwork
24-
check "private_google_access_enabled_subnetwork" {
25-
assert {
26-
condition = data.google_compute_subnetwork.subnetwork.private_ip_google_access
27-
error_message = "Private Google Access is disabled for subnetwork '${data.google_compute_subnetwork.subnetwork.name}'. This may cause connectivity issues for instances without external IPs trying to access Google APIs and services."
29+
locals {
30+
# Derived values from data source, null if data source is not used
31+
derived_project_id = local.use_subnetwork_data ? data.google_compute_subnetwork.subnetwork[0].project : null
32+
derived_network_name = local.use_subnetwork_data ? data.google_compute_subnetwork.subnetwork[0].network : null
33+
34+
# Effective values: Use var if provided, otherwise use derived value
35+
effective_project_id = coalesce(var.project_id, local.derived_project_id)
36+
effective_network_name = coalesce(var.network_name, local.derived_network_name)
37+
}
38+
39+
# Module-level check for Private Google Access on the subnetwork.
40+
# This check is only relevant if subnetwork_self_link was provided and used.
41+
resource "terraform_data" "pga_check" {
42+
count = local.use_subnetwork_data ? 1 : 0
43+
44+
lifecycle {
45+
precondition {
46+
condition = data.google_compute_subnetwork.subnetwork[0].private_ip_google_access
47+
error_message = "Private Google Access is disabled for subnetwork '${data.google_compute_subnetwork.subnetwork[0].name}'. This may cause connectivity issues for instances without external IPs trying to access Google APIs and services."
48+
}
2849
}
2950
}
3051

3152
module "firewall_rule" {
3253
source = "terraform-google-modules/network/google//modules/firewall-rules"
3354
version = "~> 9.0"
34-
project_id = data.google_compute_subnetwork.subnetwork.project
35-
network_name = data.google_compute_subnetwork.subnetwork.network
55+
project_id = local.effective_project_id
56+
network_name = local.effective_network_name
3657

3758
ingress_rules = var.ingress_rules
3859
egress_rules = var.egress_rules

0 commit comments

Comments
 (0)