Skip to content

Commit 8ff41f7

Browse files
committed
gke: private nodes, DNS endpoint, Dataplane V2, cost allocation, monitoring, max_pods_per_node, nodepool labels/taints
GKE cluster hardening and observability options, plus per-nodepool max_pods_per_node, node_labels, and node_taints wired through the container cluster spec.
1 parent 8f474b0 commit 8ff41f7

7 files changed

Lines changed: 310 additions & 8 deletions

File tree

perfkitbenchmarker/configs/container_spec.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,9 @@ def __init__(
243243
self.vm_spec: virtual_machine_spec.BaseVmSpec
244244
self.machine_families: list[str] | None
245245
self.sandbox_config: SandboxSpec | None
246+
self.max_pods_per_node: int | None
247+
self.node_labels: dict[str, str] | None
248+
self.node_taints: list[str] | None
246249

247250
@classmethod
248251
def _GetOptionDecoderConstructions(cls):
@@ -273,6 +276,18 @@ def _GetOptionDecoderConstructions(cls):
273276
),
274277
'vm_spec': (spec.PerCloudConfigDecoder, {}),
275278
'sandbox_config': (_SandboxDecoder, {'default': None}),
279+
'max_pods_per_node': (
280+
option_decoders.IntDecoder,
281+
{'default': None, 'none_ok': True, 'min': 1},
282+
),
283+
'node_labels': (
284+
option_decoders.TypeVerifier,
285+
{'valid_types': (dict,), 'default': None, 'none_ok': True},
286+
),
287+
'node_taints': (
288+
option_decoders.TypeVerifier,
289+
{'valid_types': (list,), 'default': None, 'none_ok': True},
290+
),
276291
})
277292
return result
278293

perfkitbenchmarker/providers/gcp/flags.py

Lines changed: 92 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,7 @@
6767
'Whether or not we create a Confidential VM Instance',
6868
)
6969
GCE_CONFIDENTIAL_COMPUTE_TYPE = flags.DEFINE_string(
70-
'gce_confidential_compute_type',
71-
'sev',
72-
'Type of Confidential VM Instance'
70+
'gce_confidential_compute_type', 'sev', 'Type of Confidential VM Instance'
7371
)
7472
GCE_NETWORK_NAMES = flags.DEFINE_list(
7573
'gce_network_name',
@@ -561,6 +559,97 @@
561559
False,
562560
'Whether to enable shielded nodes.',
563561
)
562+
GKE_ENABLE_PRIVATE_NODES = flags.DEFINE_boolean(
563+
'gke_enable_private_nodes',
564+
False,
565+
'Whether to create the cluster with private nodes (nodes have only internal'
566+
' IPs).',
567+
)
568+
GKE_ENABLE_DNS_ACCESS = flags.DEFINE_boolean(
569+
'gke_enable_dns_access',
570+
False,
571+
'Whether to enable DNS-based control plane access (replaces the'
572+
' public/private IP endpoint model).',
573+
)
574+
GKE_ENABLE_IP_ACCESS = flags.DEFINE_boolean(
575+
'gke_enable_ip_access',
576+
True,
577+
'Whether to enable IP-based control plane access. Disabling requires DNS'
578+
' access and is mutually exclusive with public clusters (nodes with public'
579+
' IPs).',
580+
)
581+
GKE_MASTER_IPV4_CIDR = flags.DEFINE_string(
582+
'gke_master_ipv4_cidr',
583+
None,
584+
'CIDR range to use for the hosted master network. Required when private'
585+
' nodes are enabled without DNS access.',
586+
)
587+
GKE_ENABLE_DATAPLANE_V2 = flags.DEFINE_boolean(
588+
'gke_enable_dataplane_v2',
589+
False,
590+
'Whether to enable GKE Dataplane V2 (eBPF-based datapath, Cilium under the'
591+
' hood). Requires cluster recreation; cannot be toggled on an existing'
592+
' cluster.',
593+
)
594+
GKE_ENABLE_MANAGED_PROMETHEUS = flags.DEFINE_boolean(
595+
'gke_enable_managed_prometheus',
596+
False,
597+
'Whether to enable Google Cloud Managed Service for Prometheus on the'
598+
' cluster.',
599+
)
600+
GKE_ENABLE_COST_ALLOCATION = flags.DEFINE_boolean(
601+
'gke_enable_cost_allocation',
602+
False,
603+
'Whether to enable GKE cost allocation tracking.',
604+
)
605+
GKE_MONITORING_COMPONENTS = flags.DEFINE_string(
606+
'gke_monitoring_components',
607+
'SYSTEM,API_SERVER,SCHEDULER,CONTROLLER_MANAGER',
608+
'Comma-separated list of GKE monitoring components to enable '
609+
'(e.g. SYSTEM,API_SERVER,SCHEDULER,CONTROLLER_MANAGER,POD,DEPLOYMENT,'
610+
'STATEFULSET,DAEMONSET,HPA,STORAGE,CADVISOR,KUBELET).',
611+
)
612+
GKE_ENABLE_AGENT_SANDBOX = flags.DEFINE_boolean(
613+
'gke_enable_agent_sandbox',
614+
False,
615+
'Whether to enable the GKE Agent Sandbox controller on the cluster. '
616+
'Installs the managed agent-sandbox controller and CRDs, enabling '
617+
'SandboxClaim/Sandbox/SandboxWarmPool reconciliation by GKE. This is '
618+
'separate from the gvisor sandbox runtime (--sandbox=type=gvisor on a '
619+
'node pool). Requires GKE 1.35.2-gke.1269000 or later. See '
620+
'https://docs.cloud.google.com/kubernetes-engine/docs/how-to/agent-sandbox.',
621+
)
622+
623+
624+
def _ValidateGkePrivateNodeFlags(flags_dict):
625+
if (
626+
not flags_dict['gke_enable_ip_access']
627+
and not flags_dict['gke_enable_dns_access']
628+
):
629+
raise flags.ValidationError(
630+
'--no-gke_enable_ip_access requires --gke_enable_dns_access.'
631+
)
632+
if (
633+
flags_dict['gke_enable_private_nodes']
634+
and not flags_dict['gke_enable_dns_access']
635+
and not flags_dict['gke_master_ipv4_cidr']
636+
):
637+
raise flags.ValidationError(
638+
'--gke_enable_private_nodes without --gke_enable_dns_access requires'
639+
' --gke_master_ipv4_cidr.'
640+
)
641+
return True
642+
643+
644+
flags.register_multi_flags_validator(
645+
[
646+
'gke_enable_ip_access',
647+
'gke_enable_dns_access',
648+
'gke_enable_private_nodes',
649+
'gke_master_ipv4_cidr',
650+
],
651+
_ValidateGkePrivateNodeFlags,
652+
)
564653
GKE_ADDONS = flags.DEFINE_string(
565654
'gke_addons',
566655
'',

perfkitbenchmarker/providers/gcp/gce_network.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -914,6 +914,7 @@ class GceNetwork(network.BaseNetwork):
914914
def __init__(self, network_spec: GceNetworkSpec):
915915
super().__init__(network_spec)
916916
self.project: str | None = network_spec.project
917+
self._zone: str = network_spec.zone
917918
self.vpn_gateway: Dict[str, GceVpnGateway] = {}
918919

919920
# Figuring out the type of network here.
@@ -1231,6 +1232,60 @@ def _GetNumberVms(self) -> int:
12311232
for group_spec in benchmark_spec.config.vm_groups.values()
12321233
)
12331234

1235+
def _CreateCloudNat(self):
1236+
"""Provision a Cloud Router + NAT so private resources can egress.
1237+
1238+
Called during network provisioning so NAT has time to fully propagate
1239+
before any cluster lifecycle code starts. Shared across all resources
1240+
in the network.
1241+
"""
1242+
region = util.GetRegionFromZone(self._zone)
1243+
router_name = f'{self.primary_subnet_name}-router'
1244+
nat_name = f'{self.primary_subnet_name}-nat'
1245+
1246+
router_cmd = util.GcloudCommand(
1247+
self, 'compute', 'routers', 'create', router_name
1248+
)
1249+
router_cmd.flags['network'] = self.primary_subnet_name
1250+
router_cmd.flags['region'] = region
1251+
router_cmd.flags.pop('zone', None)
1252+
_, stderr, retcode = router_cmd.Issue(raise_on_failure=False)
1253+
if retcode and 'already exists' not in stderr:
1254+
logging.warning('Cloud Router create failed: %s', stderr)
1255+
1256+
nat_cmd = util.GcloudCommand(
1257+
self, 'compute', 'routers', 'nats', 'create', nat_name
1258+
)
1259+
nat_cmd.flags['router'] = router_name
1260+
nat_cmd.flags['region'] = region
1261+
nat_cmd.flags.pop('zone', None)
1262+
nat_cmd.args.append('--auto-allocate-nat-external-ips')
1263+
nat_cmd.args.append('--nat-all-subnet-ip-ranges')
1264+
_, stderr, retcode = nat_cmd.Issue(raise_on_failure=False)
1265+
if retcode and 'already exists' not in stderr:
1266+
logging.warning('Cloud NAT create failed: %s', stderr)
1267+
1268+
def _DeleteCloudNat(self):
1269+
"""Best-effort teardown of the NAT and router this network created."""
1270+
region = util.GetRegionFromZone(self._zone)
1271+
router_name = f'{self.primary_subnet_name}-router'
1272+
nat_name = f'{self.primary_subnet_name}-nat'
1273+
1274+
nat_cmd = util.GcloudCommand(
1275+
self, 'compute', 'routers', 'nats', 'delete', nat_name
1276+
)
1277+
nat_cmd.flags['router'] = router_name
1278+
nat_cmd.flags['region'] = region
1279+
nat_cmd.flags.pop('zone', None)
1280+
nat_cmd.Issue(raise_on_failure=False)
1281+
1282+
router_cmd = util.GcloudCommand(
1283+
self, 'compute', 'routers', 'delete', router_name
1284+
)
1285+
router_cmd.flags['region'] = region
1286+
router_cmd.flags.pop('zone', None)
1287+
router_cmd.Issue(raise_on_failure=False)
1288+
12341289
def Create(self):
12351290
"""Creates the actual network."""
12361291
if not self.is_existing_network:
@@ -1244,6 +1299,8 @@ def Create(self):
12441299
lambda rule: self.external_nets_rules[rule].Create(),
12451300
list(self.external_nets_rules.keys()),
12461301
)
1302+
if gcp_flags.GKE_ENABLE_PRIVATE_NODES.value:
1303+
self._CreateCloudNat()
12471304
if getattr(self, 'vpn_gateway', False):
12481305
background_tasks.RunThreaded(
12491306
lambda gateway: self.vpn_gateway[gateway].Create(),
@@ -1257,6 +1314,12 @@ def Delete(self):
12571314
if self.placement_group:
12581315
self.placement_group.Delete()
12591316
if not self.is_existing_network:
1317+
# Always attempt NAT+router cleanup: both gcloud calls use
1318+
# raise_on_failure=False so this is a no-op when none was created.
1319+
# Checking the live flag here is wrong because teardown runs from a
1320+
# restored pickle and the flag may default to False even when a NAT
1321+
# was created at provision time.
1322+
self._DeleteCloudNat()
12601323
if getattr(self, 'vpn_gateway', False):
12611324
background_tasks.RunThreaded(
12621325
lambda gateway: self.vpn_gateway[gateway].Delete(),

perfkitbenchmarker/providers/gcp/google_kubernetes_engine.py

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,10 +168,37 @@ def _RunClusterCreateCommand(self, cmd: util.GcloudCommand):
168168
)
169169
cmd.flags['release-channel'] = self.release_channel
170170

171+
if gcp_flags.GKE_ENABLE_PRIVATE_NODES.value:
172+
cmd.args.append('--enable-private-nodes')
173+
# GKE requires VPC-native (alias IPs) when private nodes are enabled.
174+
# Without this gcloud rejects the create with:
175+
# Cannot specify --enable-private-nodes without --enable-ip-alias.
176+
cmd.args.append('--enable-ip-alias')
177+
else:
178+
cmd.args.append('--no-enable-private-nodes')
179+
if gcp_flags.GKE_ENABLE_DNS_ACCESS.value:
180+
cmd.args.append('--enable-dns-access')
181+
else:
182+
cmd.args.append('--no-enable-dns-access')
183+
if gcp_flags.GKE_ENABLE_IP_ACCESS.value:
184+
cmd.args.append('--enable-ip-access')
185+
else:
186+
cmd.args.append('--no-enable-ip-access')
187+
if gcp_flags.GKE_ENABLE_DATAPLANE_V2.value:
188+
cmd.args.append('--enable-dataplane-v2')
189+
if gcp_flags.GKE_ENABLE_AGENT_SANDBOX.value:
190+
cmd.args.append('--enable-agent-sandbox')
191+
if gcp_flags.GKE_MASTER_IPV4_CIDR.value:
192+
cmd.flags['master-ipv4-cidr'] = gcp_flags.GKE_MASTER_IPV4_CIDR.value
193+
171194
if FLAGS.gke_enable_alpha:
172195
cmd.args.append('--enable-kubernetes-alpha')
173196
cmd.args.append('--no-enable-autorepair')
174-
cmd.flags['monitoring'] = 'SYSTEM,API_SERVER,SCHEDULER,CONTROLLER_MANAGER'
197+
cmd.flags['monitoring'] = gcp_flags.GKE_MONITORING_COMPONENTS.value
198+
if gcp_flags.GKE_ENABLE_MANAGED_PROMETHEUS.value:
199+
cmd.args.append('--enable-managed-prometheus')
200+
if gcp_flags.GKE_ENABLE_COST_ALLOCATION.value:
201+
cmd.args.append('--enable-cost-allocation')
175202

176203
user = util.GetDefaultUser()
177204
if FLAGS.gcp_service_account:
@@ -209,6 +236,10 @@ def _GetKubeconfig(self):
209236
cmd = self._GcloudCommand(
210237
'container', 'clusters', 'get-credentials', self.name
211238
)
239+
if gcp_flags.GKE_ENABLE_DNS_ACCESS.value:
240+
# Private-node clusters are unreachable via the IP endpoint; use the
241+
# DNS-based control plane endpoint instead.
242+
cmd.args.append('--dns-endpoint')
212243
env = os.environ.copy()
213244
env['KUBECONFIG'] = FLAGS.kubeconfig
214245
cmd.IssueRetryable(env=env)
@@ -377,6 +408,10 @@ def _Create(self):
377408
cmd = self._GcloudCommand('container', 'clusters', 'create', self.name)
378409
if self.default_nodepool.network:
379410
cmd.flags['network'] = self.default_nodepool.network.network_resource.name
411+
if self.default_nodepool.network.subnet_resource:
412+
cmd.flags['subnetwork'] = (
413+
self.default_nodepool.network.subnet_resource.name
414+
)
380415

381416
if gcp_flags.GKE_ENABLE_SHIELDED_NODES.value:
382417
cmd.args.append('--enable-shielded-nodes')
@@ -576,10 +611,19 @@ def _AddNodeParamsToCmd(
576611
if nodepool_config.sandbox_config is not None:
577612
cmd.flags['sandbox'] = nodepool_config.sandbox_config.ToSandboxFlag()
578613

614+
if nodepool_config.max_pods_per_node is not None:
615+
cmd.flags['max-pods-per-node'] = nodepool_config.max_pods_per_node
616+
579617
if self.image_type:
580618
cmd.flags['image-type'] = self.image_type
581619

582-
cmd.flags['node-labels'] = f'pkb_nodepool={nodepool_config.name}'
620+
labels = {}
621+
if nodepool_config.node_labels:
622+
labels.update(nodepool_config.node_labels)
623+
labels['pkb_nodepool'] = nodepool_config.name
624+
cmd.flags['node-labels'] = ','.join(f'{k}={v}' for k, v in labels.items())
625+
if nodepool_config.node_taints:
626+
cmd.flags['node-taints'] = ','.join(nodepool_config.node_taints)
583627
if nodepool_config.min_nodes != nodepool_config.max_nodes:
584628
cmd.args.append('--enable-autoscaling')
585629
cmd.flags['min-nodes'] = nodepool_config.min_nodes
@@ -673,6 +717,10 @@ def _Create(self):
673717
)
674718
if self.default_nodepool.network:
675719
cmd.flags['network'] = self.default_nodepool.network.network_resource.name
720+
if self.default_nodepool.network.subnet_resource:
721+
cmd.flags['subnetwork'] = (
722+
self.default_nodepool.network.subnet_resource.name
723+
)
676724
cmd.flags['labels'] = util.MakeFormattedDefaultTags()
677725

678726
if self.enable_aam:

perfkitbenchmarker/resources/container_service/container.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,9 @@ def __init__(
187187
# Defined by GceVirtualMachineConfig. Used by google_kubernetes_engine
188188
# pylint: disable=g-missing-from-attributes
189189
self.sandbox_config: container_spec_lib.SandboxSpec | None = None
190+
self.node_labels: dict[str, str] | None = None
191+
self.node_taints: list[str] | None = None
192+
self.max_pods_per_node: int | None = None
190193
self.max_local_disks: int | None
191194
self.ssd_interface: str | None
192195
self.threads_per_core: int

perfkitbenchmarker/resources/container_service/container_cluster.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ def _InitializeNodePool(
116116
nodepool_spec.machine_families,
117117
)
118118
nodepool_config.sandbox_config = nodepool_spec.sandbox_config
119+
nodepool_config.node_labels = nodepool_spec.node_labels
120+
nodepool_config.node_taints = nodepool_spec.node_taints
121+
nodepool_config.max_pods_per_node = nodepool_spec.max_pods_per_node
119122
nodepool_config.zone = zone
120123
nodepool_config.num_nodes = nodepool_spec.vm_count
121124
if nodepool_spec.min_vm_count is None:

0 commit comments

Comments
 (0)