diff --git a/modules/gke-nodepool/README.md b/modules/gke-nodepool/README.md index 0828190e..17df73a8 100644 --- a/modules/gke-nodepool/README.md +++ b/modules/gke-nodepool/README.md @@ -4,6 +4,10 @@ This module allows simplified creation and management of individual GKE nodepool ## Example usage +### Module defaults + +If no specific node configuration is set via variables, the module uses the provider's defaults only setting OAuth scopes to a minimal working set (devstorage read-only, logging and monitoring write) and the node machine type to `n1-standard-1`. The service account set by the provider in this case is the GCE default service account. + ```hcl module "cluster-1-nodepool-1" { source = "../modules/gke-nodepool" @@ -14,6 +18,21 @@ module "cluster-1-nodepool-1" { } ``` +### Internally managed service account + +To have the module auto-create a service account for the nodes, set the `node_service_account_create` variable to `true`. When a service account is created by the module, OAuth scopes are set to `cloud-platform` by default. The service account resource and email (in both plain and IAM formats) are then available in outputs to assign IAM roles from your own code. + +```hcl +module "cluster-1-nodepool-1" { + source = "../modules/gke-nodepool" + project_id = "myproject" + cluster_name = "cluster-1" + location = "europe-west1-b" + name = "nodepool-1" + node_service_account_create = true +} +``` + ## Variables @@ -28,23 +47,24 @@ module "cluster-1-nodepool-1" { | *management_config* | Optional node management configuration. | object({...}) | | null | | *max_pods_per_node* | Maximum number of pods per node. | number | | null | | *name* | Optional nodepool name. | string | | null | -| *node_config_disk_size* | Node disk size, defaults to 100GB. | number | | 100 | -| *node_config_disk_type* | Node disk type, defaults to pd-standard. | string | | pd-standard | -| *node_config_guest_accelerator* | Map of type and count of attached accelerator cards. | map(number) | | {} | -| *node_config_image_type* | Nodes image type. | string | | null | -| *node_config_labels* | Kubernetes labels attached to nodes. | map(string) | | {} | -| *node_config_local_ssd_count* | Number of local SSDs attached to nodes. | number | | 0 | -| *node_config_machine_type* | Nodes machine type. | string | | n1-standard-1 | -| *node_config_metadata* | Metadata key/value pairs assigned to nodes. Set disable-legacy-endpoints to true when using this variable. | map(string) | | null | -| *node_config_min_cpu_platform* | Minimum CPU platform for nodes. | string | | null | -| *node_config_oauth_scopes* | Set of Google API scopes for the nodes service account. Include logging-write, monitoring, and storage-ro when using this variable. | list(string) | | ["logging-write", "monitoring", "monitoring-write", "storage-ro"] | -| *node_config_preemptible* | Use preemptible VMs for nodes. | bool | | null | -| *node_config_sandbox_config* | GKE Sandbox configuration. Needs image_type set to COS_CONTAINERD and node_version set to 1.12.7-gke.17 when using this variable. | string | | null | -| *node_config_service_account* | Service account used for nodes. | string | | null | -| *node_config_shielded_instance_config* | Shielded instance options. | object({...}) | | null | -| *node_config_tags* | Network tags applied to nodes. | list(string) | | null | | *node_count* | Number of nodes per instance group, can be updated after creation. Ignored when autoscaling is set. | number | | null | +| *node_disk_size* | Node disk size, defaults to 100GB. | number | | 100 | +| *node_disk_type* | Node disk type, defaults to pd-standard. | string | | pd-standard | +| *node_guest_accelerator* | Map of type and count of attached accelerator cards. | map(number) | | {} | +| *node_image_type* | Nodes image type. | string | | null | +| *node_labels* | Kubernetes labels attached to nodes. | map(string) | | {} | +| *node_local_ssd_count* | Number of local SSDs attached to nodes. | number | | 0 | | *node_locations* | Optional list of zones in which nodes should be located. Uses cluster locations if unset. | list(string) | | null | +| *node_machine_type* | Nodes machine type. | string | | n1-standard-1 | +| *node_metadata* | Metadata key/value pairs assigned to nodes. Set disable-legacy-endpoints to true when using this variable. | map(string) | | null | +| *node_min_cpu_platform* | Minimum CPU platform for nodes. | string | | null | +| *node_preemptible* | Use preemptible VMs for nodes. | bool | | null | +| *node_sandbox_config* | GKE Sandbox configuration. Needs image_type set to COS_CONTAINERD and node_version set to 1.12.7-gke.17 when using this variable. | string | | null | +| *node_service_account* | Service account email. Unused if service account is auto-created. | string | | null | +| *node_service_account_create* | Auto-create service account. | bool | | false | +| *node_service_account_scopes* | Scopes applied to service account. Default to: 'cloud-platform' when creating a service account; 'devstorage.read_only', 'logging.write', 'monitoring.write' otherwise. | list(string) | | [] | +| *node_shielded_instance_config* | Shielded instance options. | object({...}) | | null | +| *node_tags* | Network tags applied to nodes. | list(string) | | null | | *upgrade_config* | Optional node upgrade configuration. | object({...}) | | null | | *workload_metadata_config* | Metadata configuration to expose to workloads on the node pool. | string | | GKE_METADATA_SERVER | @@ -53,4 +73,7 @@ module "cluster-1-nodepool-1" { | name | description | sensitive | |---|---|:---:| | name | Nodepool name. | | +| service_account | Service account resource. | | +| service_account_email | Service account email. | | +| service_account_iam_email | Service account email. | | diff --git a/modules/gke-nodepool/main.tf b/modules/gke-nodepool/main.tf index 73eb8850..51823954 100644 --- a/modules/gke-nodepool/main.tf +++ b/modules/gke-nodepool/main.tf @@ -14,6 +14,38 @@ * limitations under the License. */ +locals { + service_account_email = ( + var.node_service_account_create + ? ( + length(google_service_account.service_account) > 0 + ? google_service_account.service_account[0].email + : null + ) + : var.node_service_account + ) + service_account_scopes = ( + length(var.node_service_account_scopes) > 0 + ? var.node_service_account_scopes + : ( + var.node_service_account_create + ? ["https://www.googleapis.com/auth/cloud-platform"] + : [ + "https://www.googleapis.com/auth/devstorage.read_only", + "https://www.googleapis.com/auth/logging.write", + "https://www.googleapis.com/auth/monitoring.write" + ] + ) + ) +} + +resource "google_service_account" "service_account" { + count = var.node_service_account_create ? 1 : 0 + project = var.project_id + account_id = "tf-gke-${var.cluster_name}-${var.name}" + display_name = "Terraform GKE ${var.cluster_name} ${var.name}." +} + resource "google_container_node_pool" "nodepool" { provider = google-beta @@ -29,21 +61,21 @@ resource "google_container_node_pool" "nodepool" { version = var.gke_version node_config { - disk_size_gb = var.node_config_disk_size - disk_type = var.node_config_disk_type - image_type = var.node_config_image_type - labels = var.node_config_labels - local_ssd_count = var.node_config_local_ssd_count - machine_type = var.node_config_machine_type - metadata = var.node_config_metadata - min_cpu_platform = var.node_config_min_cpu_platform - oauth_scopes = var.node_config_oauth_scopes - preemptible = var.node_config_preemptible - service_account = var.node_config_service_account - tags = var.node_config_tags + disk_size_gb = var.node_disk_size + disk_type = var.node_disk_type + image_type = var.node_image_type + labels = var.node_labels + local_ssd_count = var.node_local_ssd_count + machine_type = var.node_machine_type + metadata = var.node_metadata + min_cpu_platform = var.node_min_cpu_platform + oauth_scopes = local.service_account_scopes + preemptible = var.node_preemptible + service_account = local.service_account_email + tags = var.node_tags dynamic guest_accelerator { - for_each = var.node_config_guest_accelerator + for_each = var.node_guest_accelerator iterator = config content { type = config.key @@ -53,8 +85,8 @@ resource "google_container_node_pool" "nodepool" { dynamic sandbox_config { for_each = ( - var.node_config_sandbox_config != null - ? [var.node_config_sandbox_config] + var.node_sandbox_config != null + ? [var.node_sandbox_config] : [] ) iterator = config @@ -65,8 +97,8 @@ resource "google_container_node_pool" "nodepool" { dynamic shielded_instance_config { for_each = ( - var.node_config_shielded_instance_config != null - ? [var.node_config_shielded_instance_config] + var.node_shielded_instance_config != null + ? [var.node_shielded_instance_config] : [] ) iterator = config diff --git a/modules/gke-nodepool/outputs.tf b/modules/gke-nodepool/outputs.tf index 96ccb269..c96301a8 100644 --- a/modules/gke-nodepool/outputs.tf +++ b/modules/gke-nodepool/outputs.tf @@ -18,3 +18,25 @@ output "name" { description = "Nodepool name." value = google_container_node_pool.nodepool.name } + +output "service_account" { + description = "Service account resource." + value = ( + var.node_service_account_create + ? google_service_account.service_account[0] + : null + ) +} + +output "service_account_email" { + description = "Service account email." + value = local.service_account_email +} + +output "service_account_iam_email" { + description = "Service account email." + value = join("", [ + "serviceAccount:", + local.service_account_email == null ? "" : local.service_account_email + ]) +} diff --git a/modules/gke-nodepool/variables.tf b/modules/gke-nodepool/variables.tf index e8685c7d..dd823309 100644 --- a/modules/gke-nodepool/variables.tf +++ b/modules/gke-nodepool/variables.tf @@ -66,85 +66,93 @@ variable "name" { default = null } -variable "node_config_disk_size" { +variable "node_disk_size" { description = "Node disk size, defaults to 100GB." type = number default = 100 } -variable "node_config_disk_type" { +variable "node_disk_type" { description = "Node disk type, defaults to pd-standard." type = string default = "pd-standard" } -variable "node_config_guest_accelerator" { +variable "node_guest_accelerator" { description = "Map of type and count of attached accelerator cards." type = map(number) default = {} } -variable "node_config_image_type" { +variable "node_image_type" { description = "Nodes image type." type = string default = null } -variable "node_config_labels" { +variable "node_labels" { description = "Kubernetes labels attached to nodes." type = map(string) default = {} } -variable "node_config_local_ssd_count" { +variable "node_local_ssd_count" { description = "Number of local SSDs attached to nodes." type = number default = 0 } -variable "node_config_machine_type" { +variable "node_machine_type" { description = "Nodes machine type." type = string default = "n1-standard-1" } -variable "node_config_metadata" { +variable "node_metadata" { description = "Metadata key/value pairs assigned to nodes. Set disable-legacy-endpoints to true when using this variable." type = map(string) default = null } -variable "node_config_min_cpu_platform" { +variable "node_min_cpu_platform" { description = "Minimum CPU platform for nodes." type = string default = null } -variable "node_config_oauth_scopes" { - description = "Set of Google API scopes for the nodes service account. Include logging-write, monitoring, and storage-ro when using this variable." - type = list(string) - default = ["logging-write", "monitoring", "monitoring-write", "storage-ro"] -} - -variable "node_config_preemptible" { +variable "node_preemptible" { description = "Use preemptible VMs for nodes." type = bool default = null } -variable "node_config_sandbox_config" { +variable "node_sandbox_config" { description = "GKE Sandbox configuration. Needs image_type set to COS_CONTAINERD and node_version set to 1.12.7-gke.17 when using this variable." type = string default = null } -variable "node_config_service_account" { - description = "Service account used for nodes." +variable "node_service_account" { + description = "Service account email. Unused if service account is auto-created." type = string default = null } -variable "node_config_shielded_instance_config" { +variable "node_service_account_create" { + description = "Auto-create service account." + type = bool + default = false +} + +# scopes and scope aliases list +# https://cloud.google.com/sdk/gcloud/reference/compute/instances/create#--scopes +variable "node_service_account_scopes" { + description = "Scopes applied to service account. Default to: 'cloud-platform' when creating a service account; 'devstorage.read_only', 'logging.write', 'monitoring.write' otherwise." + type = list(string) + default = [] +} + +variable "node_shielded_instance_config" { description = "Shielded instance options." type = object({ enable_secure_boot = bool @@ -153,13 +161,13 @@ variable "node_config_shielded_instance_config" { default = null } -variable "node_config_tags" { +variable "node_tags" { description = "Network tags applied to nodes." type = list(string) default = null } -# variable "node_config_taint" { +# variable "node_taint" { # description = "Kubernetes taints applied to nodes." # type = string # default = null diff --git a/networking/hub-and-spoke-peering/main.tf b/networking/hub-and-spoke-peering/main.tf index d5c09cc6..a84ddef6 100644 --- a/networking/hub-and-spoke-peering/main.tf +++ b/networking/hub-and-spoke-peering/main.tf @@ -220,12 +220,12 @@ module "cluster-1" { } module "cluster-1-nodepool-1" { - source = "../../modules/gke-nodepool" - name = "nodepool-1" - project_id = var.project_id - location = module.cluster-1.location - cluster_name = module.cluster-1.name - node_config_service_account = module.service-account-gke-node.email + source = "../../modules/gke-nodepool" + name = "nodepool-1" + project_id = var.project_id + location = module.cluster-1.location + cluster_name = module.cluster-1.name + node_service_account = module.service-account-gke-node.email } # roles assigned via this module use non-authoritative IAM bindings at the diff --git a/networking/shared-vpc-gke/README.md b/networking/shared-vpc-gke/README.md index 2bc72fda..dcb1384a 100644 --- a/networking/shared-vpc-gke/README.md +++ b/networking/shared-vpc-gke/README.md @@ -39,7 +39,6 @@ There's a minor glitch that can surface running `terraform destroy`, where the s |---|---|:---:| | gke_clusters | GKE clusters information. | | | projects | Project ids. | | -| service_accounts | GCE and GKE service accounts. | | | vms | GCE VMs. | | | vpc | Shared VPC. | | diff --git a/networking/shared-vpc-gke/main.tf b/networking/shared-vpc-gke/main.tf index bee0d814..bf38397b 100644 --- a/networking/shared-vpc-gke/main.tf +++ b/networking/shared-vpc-gke/main.tf @@ -74,8 +74,8 @@ module "project-svc-gke" { } iam = { "roles/container.developer" = [module.vm-bastion.service_account_iam_email], - "roles/logging.logWriter" = [module.service-account-gke-node.iam_email], - "roles/monitoring.metricWriter" = [module.service-account-gke-node.iam_email], + "roles/logging.logWriter" = [module.cluster-1-nodepool-1.service_account_iam_email], + "roles/monitoring.metricWriter" = [module.cluster-1-nodepool-1.service_account_iam_email], "roles/owner" = var.owners_gke } } @@ -220,14 +220,5 @@ module "cluster-1-nodepool-1" { project_id = module.project-svc-gke.project_id location = module.cluster-1.location cluster_name = module.cluster-1.name - node_config_service_account = module.service-account-gke-node.email -} - -# roles assigned via this module use non-authoritative IAM bindings at the -# project level, with no risk of conflicts with pre-existing roles - -module "service-account-gke-node" { - source = "../../modules/iam-service-account" - project_id = module.project-svc-gke.project_id - name = "gke-node" + node_service_account_create = true } diff --git a/networking/shared-vpc-gke/outputs.tf b/networking/shared-vpc-gke/outputs.tf index 26acf421..d3f0e80a 100644 --- a/networking/shared-vpc-gke/outputs.tf +++ b/networking/shared-vpc-gke/outputs.tf @@ -28,14 +28,6 @@ output "projects" { } } -output "service_accounts" { - description = "GCE and GKE service accounts." - value = { - bastion = module.vm-bastion.service_account_email - gke_node = module.service-account-gke-node.email - } -} - output "vpc" { description = "Shared VPC." value = { diff --git a/tests/modules/gke_nodepool/__init__.py b/tests/modules/gke_nodepool/__init__.py new file mode 100644 index 00000000..6913f02e --- /dev/null +++ b/tests/modules/gke_nodepool/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/modules/gke_nodepool/fixture/main.tf b/tests/modules/gke_nodepool/fixture/main.tf new file mode 100644 index 00000000..c77136e4 --- /dev/null +++ b/tests/modules/gke_nodepool/fixture/main.tf @@ -0,0 +1,26 @@ +/** + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +module "test" { + source = "../../../../modules/gke-nodepool" + project_id = "my-project" + cluster_name = "cluster-1" + location = "europe-west1-b" + name = "nodepool-1" + node_service_account = var.node_service_account + node_service_account_create = var.node_service_account_create + node_service_account_scopes = var.node_service_account_scopes +} diff --git a/tests/modules/gke_nodepool/fixture/variables.tf b/tests/modules/gke_nodepool/fixture/variables.tf new file mode 100644 index 00000000..c0d3c922 --- /dev/null +++ b/tests/modules/gke_nodepool/fixture/variables.tf @@ -0,0 +1,32 @@ +/** + * Copyright 2020 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 "node_service_account" { + type = string + default = null +} + +variable "node_service_account_create" { + type = bool + default = false +} + +# scopes and scope aliases list +# https://cloud.google.com/sdk/gcloud/reference/compute/instances/create#--scopes +variable "node_service_account_scopes" { + type = list(string) + default = [] +} diff --git a/tests/modules/gke_nodepool/test_plan.py b/tests/modules/gke_nodepool/test_plan.py new file mode 100644 index 00000000..1a71b4d4 --- /dev/null +++ b/tests/modules/gke_nodepool/test_plan.py @@ -0,0 +1,64 @@ +# Copyright 2020 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. + + +import os +import pytest + + +FIXTURES_DIR = os.path.join(os.path.dirname(__file__), 'fixture') +OAUTH_SCOPE = ['https://www.googleapis.com/auth/cloud-platform'] +OAUTH_SCOPES = [ + 'https://www.googleapis.com/auth/devstorage.read_only', + 'https://www.googleapis.com/auth/logging.write', + 'https://www.googleapis.com/auth/monitoring.write'] + + +def test_defaults(plan_runner): + "Test resources created with variable defaults." + _, resources = plan_runner(FIXTURES_DIR) + assert len(resources) == 1 + node_config = resources[0]['values']['node_config'][0] + assert node_config['oauth_scopes'] == OAUTH_SCOPES + assert 'service_account' not in node_config + + +def test_external_sa(plan_runner): + "Test resources created with externally managed sa." + _, resources = plan_runner( + FIXTURES_DIR, node_service_account='foo@example.org') + assert len(resources) == 1 + node_config = resources[0]['values']['node_config'][0] + assert node_config['oauth_scopes'] == OAUTH_SCOPES + assert node_config['service_account'] == 'foo@example.org' + + +def test_external_scopes(plan_runner): + "Test resources created with externally defined scopes." + oauth_scopes = '["https://www.googleapis.com/auth/cloud-platform"]' + _, resources = plan_runner( + FIXTURES_DIR, node_service_account_scopes=oauth_scopes) + assert len(resources) == 1 + node_config = resources[0]['values']['node_config'][0] + assert node_config['oauth_scopes'] == OAUTH_SCOPE + assert 'service_account' not in node_config + + +def test_internal_sa(plan_runner): + "Test resources created with internally managed sa." + _, resources = plan_runner(FIXTURES_DIR, node_service_account_create='true') + assert len(resources) == 2 + node_config = resources[0]['values']['node_config'][0] + assert node_config['oauth_scopes'] == OAUTH_SCOPE + assert 'service_account' not in node_config diff --git a/tests/networking/shared_vpc_gke/test_plan.py b/tests/networking/shared_vpc_gke/test_plan.py index c0c0b1c6..7f0bf42f 100644 --- a/tests/networking/shared_vpc_gke/test_plan.py +++ b/tests/networking/shared_vpc_gke/test_plan.py @@ -23,5 +23,5 @@ FIXTURES_DIR = os.path.join(os.path.dirname(__file__), 'fixture') def test_resources(e2e_plan_runner): "Test that plan works and the numbers of resources is as expected." modules, resources = e2e_plan_runner(FIXTURES_DIR) - assert len(modules) == 11 + assert len(modules) == 10 assert len(resources) == 43