diff --git a/examples/simple/grant-permissions.sh b/examples/simple/grant-permissions.sh new file mode 100755 index 0000000..07cda4f --- /dev/null +++ b/examples/simple/grant-permissions.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +# Script to grant required IAM permissions to fix Terraform permission errors +# Replace PROJECT_ID and USER_EMAIL with your actual values + +PROJECT_ID= +USER_EMAIL= + +echo "Granting required IAM permissions to $USER_EMAIL for project $PROJECT_ID..." + +# Grant Service Account Admin role for IAM operations +echo "Granting Service Account Admin role..." +gcloud projects add-iam-policy-binding $PROJECT_ID \ + --member="user:$USER_EMAIL" \ + --role="roles/iam.serviceAccountAdmin" + +# Alternative: Grant specific workload identity permission (less permissive) +echo "Granting Workload Identity User role..." +gcloud projects add-iam-policy-binding $PROJECT_ID \ + --member="user:$USER_EMAIL" \ + --role="roles/iam.workloadIdentityUser" + +# Grant Container Admin role for full cluster access +echo "Granting Container Admin role for GKE cluster management..." +gcloud projects add-iam-policy-binding $PROJECT_ID \ + --member="user:$USER_EMAIL" \ + --role="roles/container.admin" + +echo "Permissions granted! You should now be able to run terraform apply successfully." diff --git a/examples/simple/main.tf b/examples/simple/main.tf index f9e09d4..536adb8 100644 --- a/examples/simple/main.tf +++ b/examples/simple/main.tf @@ -1,25 +1,3 @@ -terraform { - required_version = ">= 1.0" - - required_providers { - google = { - source = "hashicorp/google" - version = ">= 6.0" - } - kubernetes = { - source = "hashicorp/kubernetes" - version = "~> 2.0" - } - helm = { - source = "hashicorp/helm" - version = "~> 2.0" - } - random = { - source = "hashicorp/random" - version = "~> 3.0" - } - } -} provider "google" { project = var.project_id @@ -30,158 +8,228 @@ provider "google" { data "google_client_config" "default" {} provider "kubernetes" { - host = "https://${module.materialize.gke_cluster.endpoint}" + host = "https://${module.gke.cluster_endpoint}" token = data.google_client_config.default.access_token - cluster_ca_certificate = base64decode(module.materialize.gke_cluster.ca_certificate) + cluster_ca_certificate = base64decode(module.gke.cluster_ca_certificate) } provider "helm" { kubernetes { - host = "https://${module.materialize.gke_cluster.endpoint}" + host = "https://${module.gke.cluster_endpoint}" token = data.google_client_config.default.access_token - cluster_ca_certificate = base64decode(module.materialize.gke_cluster.ca_certificate) + cluster_ca_certificate = base64decode(module.gke.cluster_ca_certificate) } } -module "materialize" { - # Referencing the root module directory: - source = "../.." - # Alternatively, you can use the GitHub source URL: - # source = "github.com/MaterializeInc/terraform-google-materialize?ref=v0.1.0" +locals { + common_labels = merge(var.labels, { + managed_by = "terraform" + module = "materialize" + }) + + # Disk support configuration + disk_config = { + install_openebs = var.enable_disk_support ? lookup(var.disk_support_config, "install_openebs", true) : false + run_disk_setup_script = var.enable_disk_support ? lookup(var.disk_support_config, "run_disk_setup_script", true) : false + local_ssd_count = lookup(var.disk_support_config, "local_ssd_count", 1) + create_storage_class = var.enable_disk_support ? lookup(var.disk_support_config, "create_storage_class", true) : false + openebs_version = lookup(var.disk_support_config, "openebs_version", "4.2.0") + openebs_namespace = lookup(var.disk_support_config, "openebs_namespace", "openebs") + storage_class_name = lookup(var.disk_support_config, "storage_class_name", "openebs-lvm-instance-store-ext4") + storage_class_provisioner = "local.csi.openebs.io" + storage_class_parameters = { + storage = "lvm" + fsType = "ext4" + volgroup = "instance-store-vg" + } + } - project_id = var.project_id - region = var.region - prefix = var.prefix + metadata_backend_url = format( + "postgres://%s:%s@%s:5432/%s?sslmode=disable", + var.database_config.username, + random_password.database_password.result, + module.database.private_ip, + var.database_config.db_name + ) - network_config = { - subnet_cidr = "10.0.0.0/20" - pods_cidr = "10.48.0.0/14" - services_cidr = "10.52.0.0/20" - } + encoded_endpoint = urlencode("https://storage.googleapis.com") + encoded_secret = urlencode(module.storage.hmac_secret) - database_config = { - tier = "db-custom-2-4096" - version = "POSTGRES_15" - password = random_password.pass.result - } + persist_backend_url = format( + "s3://%s:%s@%s/materialize?endpoint=%s®ion=%s", + module.storage.hmac_access_id, + local.encoded_secret, + module.storage.bucket_name, + local.encoded_endpoint, + var.region + ) +} - labels = { - environment = "simple" - example = "true" - } +module "networking" { + source = "../../modules/networking" - install_materialize_operator = true - operator_version = var.operator_version - orchestratord_version = var.orchestratord_version + project_id = var.project_id + region = var.region + prefix = var.prefix + subnet_cidr = var.network_config.subnet_cidr + pods_cidr = var.network_config.pods_cidr + services_cidr = var.network_config.services_cidr +} - install_cert_manager = var.install_cert_manager - use_self_signed_cluster_issuer = var.use_self_signed_cluster_issuer +module "gke" { + source = "../../modules/gke" - # Once the operator is installed, you can define your Materialize instances here. - materialize_instances = var.materialize_instances + depends_on = [module.networking] - providers = { - google = google - kubernetes = kubernetes - helm = helm - } + project_id = var.project_id + region = var.region + prefix = var.prefix + network_name = module.networking.network_name + subnet_name = module.networking.subnet_name + namespace = var.namespace } -variable "project_id" { - description = "GCP Project ID" - type = string -} -variable "region" { - description = "GCP Region" - type = string - default = "us-central1" +module "nodepool" { + source = "../../modules/nodepool" + depends_on = [module.gke] + + nodepool_name = "${var.prefix}-node-pool" + region = var.region + enable_private_nodes = true + cluster_name = module.gke.cluster_name + project_id = var.project_id + node_count = var.gke_config.node_count + min_nodes = var.gke_config.min_nodes + max_nodes = var.gke_config.max_nodes + machine_type = var.gke_config.machine_type + disk_size_gb = var.gke_config.disk_size_gb + service_account_email = module.gke.service_account_email + labels = local.common_labels + + disk_setup_image = var.disk_setup_image + enable_disk_setup = local.disk_config.run_disk_setup_script + local_ssd_count = local.disk_config.local_ssd_count } -variable "prefix" { - description = "Used to prefix the names of the resources" - type = string - default = "mz-simple" +module "openebs" { + source = "../../modules/openebs" + depends_on = [ + module.gke, + module.nodepool + ] + + install_openebs = local.disk_config.install_openebs + create_namespace = true + openebs_namespace = local.disk_config.openebs_namespace + openebs_version = local.disk_config.openebs_version } -resource "random_password" "pass" { +resource "random_password" "database_password" { length = 20 special = false } -output "gke_cluster" { - description = "GKE cluster details" - value = module.materialize.gke_cluster - sensitive = true -} +module "database" { + source = "../../modules/database" -output "service_accounts" { - description = "Service account details" - value = module.materialize.service_accounts -} + depends_on = [ + module.networking, + ] -output "connection_strings" { - description = "Connection strings for metadata and persistence backends" - value = module.materialize.connection_strings - sensitive = true -} + database_name = var.database_config.db_name + database_user = var.database_config.username -output "load_balancer_details" { - description = "Details of the Materialize instance load balancers." - value = module.materialize.load_balancer_details -} + project_id = var.project_id + region = var.region + prefix = var.prefix + network_id = module.networking.network_id -variable "operator_version" { - description = "Version of the Materialize operator to install" - type = string - default = null -} + tier = var.database_config.tier + db_version = var.database_config.version + password = random_password.database_password.result -output "network" { - description = "Network details" - value = module.materialize.network + labels = local.common_labels } -variable "orchestratord_version" { - description = "Version of the Materialize orchestrator to install" - type = string - default = null -} +module "storage" { + source = "../../modules/storage" -variable "materialize_instances" { - description = "List of Materialize instances to be created." - type = list(object({ - name = string - namespace = optional(string) - database_name = string - create_database = optional(bool, true) - create_load_balancer = optional(bool, true) - internal_load_balancer = optional(bool, true) - environmentd_version = optional(string) - cpu_request = optional(string, "1") - memory_request = optional(string, "1Gi") - memory_limit = optional(string, "1Gi") - in_place_rollout = optional(bool, false) - request_rollout = optional(string) - force_rollout = optional(string) - balancer_memory_request = optional(string, "256Mi") - balancer_memory_limit = optional(string, "256Mi") - balancer_cpu_request = optional(string, "100m") - license_key = optional(string) - environmentd_extra_args = optional(list(string), []) - })) - default = [] -} + project_id = var.project_id + region = var.region + prefix = var.prefix + service_account = module.gke.workload_identity_sa_email + versioning = var.storage_bucket_versioning + version_ttl = var.storage_bucket_version_ttl -variable "install_cert_manager" { - description = "Whether to install cert-manager." - type = bool - default = true + labels = local.common_labels } -variable "use_self_signed_cluster_issuer" { - description = "Whether to install and use a self-signed ClusterIssuer for TLS. To work around limitations in Terraform, this will be treated as `false` if no materialize instances are defined." - type = bool - default = true +module "certificates" { + source = "../../modules/certificates" + + install_cert_manager = var.install_cert_manager + cert_manager_install_timeout = var.cert_manager_install_timeout + cert_manager_chart_version = var.cert_manager_chart_version + use_self_signed_cluster_issuer = var.install_materialize_instance + cert_manager_namespace = var.cert_manager_namespace + name_prefix = var.prefix + + depends_on = [ + module.gke, + module.nodepool, + ] +} + +module "operator" { + count = var.install_materialize_operator ? 1 : 0 + source = "../../modules/operator" + + name_prefix = var.prefix + use_self_signed_cluster_issuer = var.install_materialize_instance + region = var.region + + depends_on = [ + module.gke, + module.nodepool, + module.database, + module.storage, + module.certificates, + ] +} + +module "materialize_instance" { + count = var.install_materialize_instance ? 1 : 0 + + source = "../../modules/materialize-instance" + instance_name = "main" + instance_namespace = "materialize-environment" + metadata_backend_url = local.metadata_backend_url + persist_backend_url = local.persist_backend_url + + depends_on = [ + module.gke, + module.database, + module.storage, + module.networking, + module.certificates, + module.operator, + module.nodepool, + module.openebs, + ] +} + +module "load_balancers" { + count = var.install_materialize_instance ? 1 : 0 + + source = "../../modules/load_balancers" + + instance_name = "main" + namespace = "materialize-environment" + resource_id = module.materialize_instance[0].instance_resource_id + + depends_on = [ + module.materialize_instance, + ] } diff --git a/outputs.tf b/examples/simple/outputs.tf similarity index 74% rename from outputs.tf rename to examples/simple/outputs.tf index 0202443..ce0cf34 100644 --- a/outputs.tf +++ b/examples/simple/outputs.tf @@ -45,28 +45,6 @@ output "service_accounts" { } } -locals { - metadata_backend_url = format( - "postgres://%s:%s@%s:5432/%s?sslmode=disable", - var.database_config.username, - var.database_config.password, - module.database.private_ip, - var.database_config.db_name - ) - - encoded_endpoint = urlencode("https://storage.googleapis.com") - encoded_secret = urlencode(module.storage.hmac_secret) - - persist_backend_url = format( - "s3://%s:%s@%s/materialize?endpoint=%s®ion=%s", - module.storage.hmac_access_id, - local.encoded_secret, - module.storage.bucket_name, - local.encoded_endpoint, - var.region - ) -} - output "connection_strings" { description = "Formatted connection strings for Materialize" value = { @@ -82,8 +60,18 @@ output "operator" { namespace = module.operator[0].operator_namespace release_name = module.operator[0].operator_release_name release_status = module.operator[0].operator_release_status - instances = module.operator[0].materialize_instances - instance_resource_ids = module.operator[0].materialize_instance_resource_ids + } : null +} + +output "materialize_instance" { + description = "Materialize instance details" + sensitive = true + value = var.install_materialize_instance ? { + name = module.materialize_instance[0].instance_name + namespace = module.materialize_instance[0].instance_namespace + resource_id = module.materialize_instance[0].instance_resource_id + metadata_backend_url = module.materialize_instance[0].metadata_backend_url + persist_backend_url = module.materialize_instance[0].persist_backend_url } : null } diff --git a/examples/simple/terraform.tfvars.example b/examples/simple/terraform.tfvars.example deleted file mode 100644 index 61b82e1..0000000 --- a/examples/simple/terraform.tfvars.example +++ /dev/null @@ -1,32 +0,0 @@ -project_id = "enter-your-gcp-project-id" -prefix = "enter-a-prefix" // e.g., mz-simple, my-mz-demo -region = "us-central1" - -# Network configuration -network_config = { - subnet_cidr = "10.0.0.0/20" - pods_cidr = "10.48.0.0/14" - services_cidr = "10.52.0.0/20" -} - -# Once the operator is installed, you can define your Materialize instances here. -# Uncomment the following block (or provide your own instances) to configure them: - -# materialize_instances = [ -# { -# name = "analytics" -# namespace = "materialize-environment" -# database_name = "analytics_db" -# cpu_request = "2" -# memory_request = "4Gi" -# memory_limit = "4Gi" -# }, -# { -# name = "demo" -# namespace = "materialize-environment" -# database_name = "demo_db" -# cpu_request = "2" -# memory_request = "4Gi" -# memory_limit = "4Gi" -# } -# ] diff --git a/variables.tf b/examples/simple/variables.tf similarity index 74% rename from variables.tf rename to examples/simple/variables.tf index 3707b26..82d622c 100644 --- a/variables.tf +++ b/examples/simple/variables.tf @@ -22,6 +22,11 @@ variable "network_config" { pods_cidr = string services_cidr = string }) + default = { + subnet_cidr = "10.0.0.0/20" + pods_cidr = "10.48.0.0/14" + services_cidr = "10.52.0.0/20" + } } variable "gke_config" { @@ -38,7 +43,7 @@ variable "gke_config" { machine_type = "n2-highmem-8" disk_size_gb = 100 min_nodes = 1 - max_nodes = 2 + max_nodes = 5 } } @@ -47,14 +52,15 @@ variable "database_config" { type = object({ tier = optional(string, "db-custom-2-4096") version = optional(string, "POSTGRES_15") - password = string username = optional(string, "materialize") db_name = optional(string, "materialize") }) - validation { - condition = var.database_config.password != null - error_message = "database_config.password must be provided" + default = { + tier = "db-custom-2-4096" + version = "POSTGRES_15" + username = "materialize" + db_name = "materialize" } } @@ -95,58 +101,22 @@ variable "helm_values" { default = {} } -variable "orchestratord_version" { - description = "Version of the Materialize orchestrator to install" - type = string - default = null -} - -variable "materialize_instances" { - description = "Configuration for Materialize instances" - type = list(object({ - name = string - namespace = optional(string) - database_name = string - create_database = optional(bool, true) - create_load_balancer = optional(bool, true) - internal_load_balancer = optional(bool, true) - environmentd_version = optional(string) - cpu_request = optional(string, "1") - memory_request = optional(string, "1Gi") - memory_limit = optional(string, "1Gi") - in_place_rollout = optional(bool, false) - request_rollout = optional(string) - force_rollout = optional(string) - balancer_memory_request = optional(string, "256Mi") - balancer_memory_limit = optional(string, "256Mi") - balancer_cpu_request = optional(string, "100m") - license_key = optional(string) - environmentd_extra_args = optional(list(string), []) - })) - default = [] - - validation { - condition = alltrue([ - for instance in var.materialize_instances : - instance.request_rollout == null || - can(regex("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", instance.request_rollout)) - ]) - error_message = "Request rollout must be a valid UUID in the format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - } - - validation { - condition = alltrue([ - for instance in var.materialize_instances : - instance.force_rollout == null || - can(regex("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", instance.force_rollout)) - ]) - error_message = "Force rollout must be a valid UUID in the format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - } +variable "install_materialize_instance" { + description = "Whether to install the Materialize instance. Default is false as it requires the Kubernetes cluster to be created first." + type = bool + default = false } variable "operator_version" { description = "Version of the Materialize operator to install" type = string + default = "v25.1.12" # META: helm-chart version + nullable = false +} + +variable "orchestratord_version" { + description = "Version of the Materialize orchestrator to install" + type = string default = null } diff --git a/versions.tf b/examples/simple/versions.tf similarity index 79% rename from versions.tf rename to examples/simple/versions.tf index e495fd7..f43e900 100644 --- a/versions.tf +++ b/examples/simple/versions.tf @@ -14,5 +14,9 @@ terraform { source = "hashicorp/helm" version = "~> 2.0" } + random = { + source = "hashicorp/random" + version = "~> 3.0" + } } -} +} \ No newline at end of file diff --git a/main.tf b/main.tf deleted file mode 100644 index bfded57..0000000 --- a/main.tf +++ /dev/null @@ -1,268 +0,0 @@ -locals { - common_labels = merge(var.labels, { - managed_by = "terraform" - module = "materialize" - }) - - # Disk support configuration - disk_config = { - install_openebs = var.enable_disk_support ? lookup(var.disk_support_config, "install_openebs", true) : false - run_disk_setup_script = var.enable_disk_support ? lookup(var.disk_support_config, "run_disk_setup_script", true) : false - local_ssd_count = lookup(var.disk_support_config, "local_ssd_count", 1) - create_storage_class = var.enable_disk_support ? lookup(var.disk_support_config, "create_storage_class", true) : false - openebs_version = lookup(var.disk_support_config, "openebs_version", "4.2.0") - openebs_namespace = lookup(var.disk_support_config, "openebs_namespace", "openebs") - storage_class_name = lookup(var.disk_support_config, "storage_class_name", "openebs-lvm-instance-store-ext4") - storage_class_provisioner = "local.csi.openebs.io" - storage_class_parameters = { - storage = "lvm" - fsType = "ext4" - volgroup = "instance-store-vg" - } - } -} - -module "networking" { - source = "./modules/networking" - - project_id = var.project_id - region = var.region - prefix = var.prefix - subnet_cidr = var.network_config.subnet_cidr - pods_cidr = var.network_config.pods_cidr - services_cidr = var.network_config.services_cidr -} - -module "gke" { - source = "./modules/gke" - - depends_on = [module.networking] - - project_id = var.project_id - region = var.region - prefix = var.prefix - network_name = module.networking.network_name - subnet_name = module.networking.subnet_name - - node_count = var.gke_config.node_count - machine_type = var.gke_config.machine_type - disk_size_gb = var.gke_config.disk_size_gb - min_nodes = var.gke_config.min_nodes - max_nodes = var.gke_config.max_nodes - - # Disk support configuration - enable_disk_setup = local.disk_config.run_disk_setup_script - local_ssd_count = local.disk_config.local_ssd_count - install_openebs = local.disk_config.install_openebs - openebs_namespace = local.disk_config.openebs_namespace - openebs_version = local.disk_config.openebs_version - disk_setup_image = var.disk_setup_image - - namespace = var.namespace - labels = local.common_labels -} - -module "database" { - source = "./modules/database" - - depends_on = [ - module.networking, - ] - - database_name = var.database_config.db_name - database_user = var.database_config.username - - project_id = var.project_id - region = var.region - prefix = var.prefix - network_id = module.networking.network_id - - tier = var.database_config.tier - db_version = var.database_config.version - password = var.database_config.password - - labels = local.common_labels -} - -module "storage" { - source = "./modules/storage" - - project_id = var.project_id - region = var.region - prefix = var.prefix - service_account = module.gke.workload_identity_sa_email - versioning = var.storage_bucket_versioning - version_ttl = var.storage_bucket_version_ttl - - labels = local.common_labels -} - -module "certificates" { - source = "./modules/certificates" - - install_cert_manager = var.install_cert_manager - cert_manager_install_timeout = var.cert_manager_install_timeout - cert_manager_chart_version = var.cert_manager_chart_version - use_self_signed_cluster_issuer = var.use_self_signed_cluster_issuer && length(var.materialize_instances) > 0 - cert_manager_namespace = var.cert_manager_namespace - name_prefix = var.prefix - - depends_on = [ - module.gke, - ] -} - -module "operator" { - source = "github.com/MaterializeInc/terraform-helm-materialize?ref=v0.1.15" - - count = var.install_materialize_operator ? 1 : 0 - - install_metrics_server = var.install_metrics_server - - depends_on = [ - module.gke, - module.database, - module.storage, - module.certificates, - ] - - namespace = var.namespace - environment = var.prefix - operator_version = var.operator_version - operator_namespace = var.operator_namespace - - helm_values = local.merged_helm_values - - instances = local.instances - - // For development purposes, you can use a local Helm chart instead of fetching it from the Helm repository - use_local_chart = var.use_local_chart - helm_chart = var.helm_chart - - providers = { - kubernetes = kubernetes - helm = helm - } -} - -module "load_balancers" { - source = "./modules/load_balancers" - - for_each = { for idx, instance in local.instances : instance.name => instance if lookup(instance, "create_load_balancer", false) } - - instance_name = each.value.name - namespace = module.operator[0].materialize_instances[each.value.name].namespace - resource_id = module.operator[0].materialize_instance_resource_ids[each.value.name] - internal = each.value.internal_load_balancer - - depends_on = [ - module.operator, - module.gke, - ] -} - -locals { - default_helm_values = { - observability = { - podMetrics = { - enabled = true - } - } - operator = { - image = var.orchestratord_version == null ? {} : { - tag = var.orchestratord_version - }, - cloudProvider = { - type = "gcp" - region = var.region - providers = { - gcp = { - enabled = true - } - } - } - } - storage = var.enable_disk_support ? { - storageClass = { - create = local.disk_config.create_storage_class - name = local.disk_config.storage_class_name - provisioner = local.disk_config.storage_class_provisioner - parameters = local.disk_config.storage_class_parameters - } - } : {} - tls = (var.use_self_signed_cluster_issuer && length(var.materialize_instances) > 0) ? { - defaultCertificateSpecs = { - balancerdExternal = { - dnsNames = [ - "balancerd", - ] - issuerRef = { - name = module.certificates.cluster_issuer_name - kind = "ClusterIssuer" - } - } - consoleExternal = { - dnsNames = [ - "console", - ] - issuerRef = { - name = module.certificates.cluster_issuer_name - kind = "ClusterIssuer" - } - } - internal = { - issuerRef = { - name = module.certificates.cluster_issuer_name - kind = "ClusterIssuer" - } - } - } - } : {} - } - - merged_helm_values = merge(local.default_helm_values, var.helm_values) -} - -locals { - instances = [ - for instance in var.materialize_instances : { - name = instance.name - namespace = instance.namespace - database_name = instance.database_name - create_database = instance.create_database - create_load_balancer = instance.create_load_balancer - internal_load_balancer = instance.internal_load_balancer - environmentd_version = instance.environmentd_version - - environmentd_extra_args = instance.environmentd_extra_args - - metadata_backend_url = format( - "postgres://%s:%s@%s:5432/%s?sslmode=disable", - var.database_config.username, - urlencode(var.database_config.password), - module.database.private_ip, - coalesce(instance.database_name, instance.name) - ) - - persist_backend_url = format( - "s3://%s:%s@%s/materialize?endpoint=%s®ion=%s", - module.storage.hmac_access_id, - local.encoded_secret, - module.storage.bucket_name, - local.encoded_endpoint, - var.region - ) - - license_key = instance.license_key - - cpu_request = instance.cpu_request - memory_request = instance.memory_request - memory_limit = instance.memory_limit - - # Rollout options - in_place_rollout = instance.in_place_rollout - request_rollout = instance.request_rollout - force_rollout = instance.force_rollout - } - ] -} diff --git a/modules/certificates/variables.tf b/modules/certificates/variables.tf index 2de3f2f..914a8d1 100644 --- a/modules/certificates/variables.tf +++ b/modules/certificates/variables.tf @@ -6,6 +6,7 @@ variable "install_cert_manager" { variable "use_self_signed_cluster_issuer" { description = "Whether to install and use a self-signed ClusterIssuer for TLS. Due to limitations in Terraform, this may not be enabled before the cert-manager CRDs are installed." type = bool + default = false } variable "cert_manager_namespace" { diff --git a/modules/gke/main.tf b/modules/gke/main.tf index d194f01..b81411f 100644 --- a/modules/gke/main.tf +++ b/modules/gke/main.tf @@ -1,24 +1,3 @@ -locals { - node_labels = merge( - var.labels, - { - "materialize.cloud/disk" = var.enable_disk_setup ? "true" : "false" - "workload" = "materialize-instance" - }, - var.enable_disk_setup ? { - "materialize.cloud/disk-config-required" = "true" - } : {} - ) - - node_taints = var.enable_disk_setup ? [ - { - key = "disk-unconfigured" - value = "true" - effect = "NO_SCHEDULE" - } - ] : [] -} - resource "google_service_account" "gke_sa" { project = var.project_id account_id = "${var.prefix}-gke-sa" @@ -78,57 +57,6 @@ resource "google_container_cluster" "primary" { } } -resource "google_container_node_pool" "primary_nodes" { - provider = google - - name = "${var.prefix}-node-pool" - location = var.region - cluster = google_container_cluster.primary.name - project = var.project_id - - node_count = var.node_count - - autoscaling { - min_node_count = var.min_nodes - max_node_count = var.max_nodes - } - - node_config { - machine_type = var.machine_type - disk_size_gb = var.disk_size_gb - - labels = local.node_labels - - dynamic "taint" { - for_each = local.node_taints - content { - key = taint.value.key - value = taint.value.value - effect = taint.value.effect - } - } - - service_account = google_service_account.gke_sa.email - - oauth_scopes = [ - "https://www.googleapis.com/auth/cloud-platform" - ] - - local_nvme_ssd_block_config { - local_ssd_count = var.local_ssd_count - } - - workload_metadata_config { - mode = "GKE_METADATA" - } - } - - lifecycle { - create_before_destroy = true - prevent_destroy = false - } -} - resource "google_service_account_iam_binding" "workload_identity" { depends_on = [ google_service_account.workload_identity_sa, @@ -140,269 +68,3 @@ resource "google_service_account_iam_binding" "workload_identity" { "serviceAccount:${var.project_id}.svc.id.goog[${var.namespace}/orchestratord]" ] } - -resource "kubernetes_namespace" "openebs" { - count = var.install_openebs ? 1 : 0 - - metadata { - name = var.openebs_namespace - } - - depends_on = [ - google_container_cluster.primary, - google_container_node_pool.primary_nodes - ] -} - -resource "helm_release" "openebs" { - count = var.install_openebs ? 1 : 0 - - name = "openebs" - namespace = var.openebs_namespace - repository = "https://openebs.github.io/openebs" - chart = "openebs" - version = var.openebs_version - - set { - name = "engines.replicated.mayastor.enabled" - value = "false" - } - - # Unable to continue with install: CustomResourceDefinition "volumesnapshotclasses.snapshot.storage.k8s.io" - # in namespace "" exists and cannot be imported into the current release - # https://github.com/openebs/website/pull/506 - set { - name = "openebs-crds.csi.volumeSnapshots.enabled" - value = "false" - } - - depends_on = [ - google_container_cluster.primary, - google_container_node_pool.primary_nodes, - kubernetes_namespace.openebs - ] -} - -resource "kubernetes_namespace" "disk_setup" { - count = var.enable_disk_setup ? 1 : 0 - - metadata { - name = "disk-setup" - labels = { - "app.kubernetes.io/managed-by" = "terraform" - "app.kubernetes.io/part-of" = "materialize" - } - } - - depends_on = [ - google_container_node_pool.primary_nodes - ] -} - -resource "kubernetes_daemonset" "disk_setup" { - count = var.enable_disk_setup ? 1 : 0 - - metadata { - name = "disk-setup" - namespace = kubernetes_namespace.disk_setup[0].metadata[0].name - labels = { - "app.kubernetes.io/managed-by" = "terraform" - "app.kubernetes.io/part-of" = "materialize" - "app" = "disk-setup" - } - } - - spec { - selector { - match_labels = { - app = "disk-setup" - } - } - - template { - metadata { - labels = { - app = "disk-setup" - } - } - - spec { - security_context { - run_as_non_root = false - run_as_user = 0 - fs_group = 0 - seccomp_profile { - type = "RuntimeDefault" - } - } - - affinity { - node_affinity { - required_during_scheduling_ignored_during_execution { - node_selector_term { - match_expressions { - key = "materialize.cloud/disk" - operator = "In" - values = ["true"] - } - } - } - } - } - - toleration { - key = "disk-unconfigured" - operator = "Exists" - effect = "NoSchedule" - } - - # Use host network and PID namespace - host_network = true - host_pid = true - - init_container { - name = "disk-setup" - image = var.disk_setup_image - command = ["/usr/local/bin/configure-disks.sh"] - args = ["--cloud-provider", "gcp"] - resources { - limits = { - memory = "128Mi" - } - requests = { - memory = "128Mi" - cpu = "50m" - } - } - - security_context { - privileged = true - run_as_user = 0 - } - - env { - name = "NODE_NAME" - value_from { - field_ref { - field_path = "spec.nodeName" - } - } - } - - volume_mount { - name = "dev" - mount_path = "/dev" - } - - volume_mount { - name = "host-root" - mount_path = "/host" - } - - } - - init_container { - name = "taint-removal" - image = var.disk_setup_image - command = ["/usr/local/bin/remove-taint.sh"] - resources { - limits = { - memory = "64Mi" - } - requests = { - memory = "64Mi" - cpu = "10m" - } - } - security_context { - run_as_user = 0 - } - env { - name = "NODE_NAME" - value_from { - field_ref { - field_path = "spec.nodeName" - } - } - } - } - - container { - name = "pause" - image = "gcr.io/google_containers/pause:3.2" - - resources { - limits = { - memory = "8Mi" - } - requests = { - memory = "8Mi" - cpu = "1m" - } - } - - security_context { - allow_privilege_escalation = false - read_only_root_filesystem = true - run_as_non_root = true - run_as_user = 65534 - } - - } - - volume { - name = "dev" - host_path { - path = "/dev" - } - } - - volume { - name = "host-root" - host_path { - path = "/" - } - } - - service_account_name = kubernetes_service_account.disk_setup[0].metadata[0].name - } - } - } -} - -resource "kubernetes_service_account" "disk_setup" { - count = var.enable_disk_setup ? 1 : 0 - metadata { - name = "disk-setup" - namespace = kubernetes_namespace.disk_setup[0].metadata[0].name - } -} - -resource "kubernetes_cluster_role" "disk_setup" { - count = var.enable_disk_setup ? 1 : 0 - metadata { - name = "disk-setup" - } - rule { - api_groups = [""] - resources = ["nodes"] - verbs = ["get", "patch"] - } -} - -resource "kubernetes_cluster_role_binding" "disk_setup" { - count = var.enable_disk_setup ? 1 : 0 - metadata { - name = "disk-setup" - } - role_ref { - api_group = "rbac.authorization.k8s.io" - kind = "ClusterRole" - name = kubernetes_cluster_role.disk_setup[0].metadata[0].name - } - subject { - kind = "ServiceAccount" - name = kubernetes_service_account.disk_setup[0].metadata[0].name - namespace = kubernetes_namespace.disk_setup[0].metadata[0].name - } -} diff --git a/modules/gke/variables.tf b/modules/gke/variables.tf index 9e3b680..a2f063b 100644 --- a/modules/gke/variables.tf +++ b/modules/gke/variables.tf @@ -23,74 +23,7 @@ variable "subnet_name" { type = string } -variable "node_count" { - description = "Number of nodes in the GKE cluster" - type = number -} - -variable "machine_type" { - description = "Machine type for GKE nodes" - type = string -} - -variable "disk_size_gb" { - description = "Size of the disk attached to each node" - type = number -} - -variable "min_nodes" { - description = "Minimum number of nodes in the node pool" - type = number -} - -variable "max_nodes" { - description = "Maximum number of nodes in the node pool" - type = number -} - variable "namespace" { - description = "Kubernetes namespace for Materialize" - type = string -} - -variable "labels" { - description = "Labels to apply to resources" - type = map(string) - default = {} -} - -# Disk setup variables -variable "enable_disk_setup" { - description = "Whether to enable the local NVMe SSD disks setup script for NVMe storage" - type = bool - default = true -} - -variable "local_ssd_count" { - description = "Number of local NVMe SSDs to attach to each node. In GCP, each disk is 375GB. For Materialize, you need to have a 1:2 ratio of disk to memory. If you have 8 CPUs and 64GB of memory, you need 128GB of disk. This means you need at least 1 local NVMe SSD. If you go with a larger machine type, you can increase the number of local NVMe SSDs." - type = number - default = 1 -} - -variable "install_openebs" { - description = "Whether to install OpenEBS for NVMe storage" - type = bool - default = true -} - -variable "openebs_namespace" { - description = "Namespace for OpenEBS components" - type = string - default = "openebs" -} - -variable "openebs_version" { - description = "Version of OpenEBS Helm chart to install" - type = string - default = "4.2.0" -} - -variable "disk_setup_image" { - description = "Docker image for the disk setup script" + description = "The namespace where the GKE cluster will be created" type = string } diff --git a/modules/load_balancers/variables.tf b/modules/load_balancers/variables.tf index 65d451d..1bbbc8c 100644 --- a/modules/load_balancers/variables.tf +++ b/modules/load_balancers/variables.tf @@ -16,4 +16,5 @@ variable "resource_id" { variable "internal" { description = "Whether the load balancer is internal to the VPC." type = bool + default = true } diff --git a/modules/materialize-instance/main.tf b/modules/materialize-instance/main.tf new file mode 100644 index 0000000..adfad29 --- /dev/null +++ b/modules/materialize-instance/main.tf @@ -0,0 +1,99 @@ +# Create a namespace for this Materialize instance +resource "kubernetes_namespace" "instance" { + count = var.create_namespace ? 1 : 0 + + metadata { + name = var.instance_namespace + } +} + +# Create the Materialize instance using the kubernetes_manifest resource +resource "kubernetes_manifest" "materialize_instance" { + field_manager { + # force field manager conflicts to be overridden + name = "terraform" + force_conflicts = true + } + + manifest = { + apiVersion = "materialize.cloud/v1alpha1" + kind = "Materialize" + metadata = { + name = var.instance_name + namespace = var.instance_namespace + } + spec = { + environmentdImageRef = "materialize/environmentd:${var.environmentd_version}" + backendSecretName = "${var.instance_name}-materialize-backend" + inPlaceRollout = var.in_place_rollout + requestRollout = var.request_rollout + forceRollout = var.force_rollout + + environmentdExtraEnv = length(var.environmentd_extra_env) > 0 ? [{ + name = "MZ_SYSTEM_PARAMETER_DEFAULT" + value = join(";", [ + for item in var.environmentd_extra_env : + "${item.name}=${item.value}" + ]) + }] : null + + environmentdExtraArgs = length(var.environmentd_extra_args) > 0 ? var.environmentd_extra_args : null + + environmentdResourceRequirements = { + limits = { + memory = var.memory_limit + } + requests = { + cpu = var.cpu_request + memory = var.memory_request + } + } + balancerdResourceRequirements = { + limits = { + memory = var.balancer_memory_limit + } + requests = { + cpu = var.balancer_cpu_request + memory = var.balancer_memory_request + } + } + } + } + + depends_on = [ + kubernetes_secret.materialize_backend, + kubernetes_namespace.instance, + ] +} + +# Create a secret with connection information for the Materialize instance +resource "kubernetes_secret" "materialize_backend" { + metadata { + name = "${var.instance_name}-materialize-backend" + namespace = var.instance_namespace + } + + data = { + metadata_backend_url = var.metadata_backend_url + persist_backend_url = var.persist_backend_url + license_key = var.license_key == null ? "" : var.license_key + } + + depends_on = [ + kubernetes_namespace.instance + ] +} + +# Retrieve the resource ID of the Materialize instance +data "kubernetes_resource" "materialize_instance" { + api_version = "materialize.cloud/v1alpha1" + kind = "Materialize" + metadata { + name = var.instance_name + namespace = var.instance_namespace + } + + depends_on = [ + kubernetes_manifest.materialize_instance + ] +} diff --git a/modules/materialize-instance/outputs.tf b/modules/materialize-instance/outputs.tf new file mode 100644 index 0000000..b94360b --- /dev/null +++ b/modules/materialize-instance/outputs.tf @@ -0,0 +1,24 @@ +output "instance_name" { + description = "Name of the Materialize instance" + value = var.instance_name +} + +output "instance_namespace" { + description = "Namespace of the Materialize instance" + value = var.instance_namespace +} + +output "instance_resource_id" { + description = "Resource ID of the Materialize instance" + value = data.kubernetes_resource.materialize_instance.object.status.resourceId +} + +output "metadata_backend_url" { + description = "Metadata backend URL used by the Materialize instance" + value = var.metadata_backend_url +} + +output "persist_backend_url" { + description = "Persist backend URL used by the Materialize instance" + value = var.persist_backend_url +} diff --git a/modules/materialize-instance/variables.tf b/modules/materialize-instance/variables.tf new file mode 100644 index 0000000..493d890 --- /dev/null +++ b/modules/materialize-instance/variables.tf @@ -0,0 +1,122 @@ +variable "instance_name" { + description = "Name of the Materialize instance" + type = string +} + +variable "create_namespace" { + description = "Whether to create the Kubernetes namespace. Set to false if the namespace already exists." + type = bool + default = true +} + +variable "instance_namespace" { + description = "Kubernetes namespace for the instance." + type = string +} + +variable "metadata_backend_url" { + description = "PostgreSQL connection URL for metadata backend" + type = string + sensitive = true +} + +variable "persist_backend_url" { + description = "Object storage connection URL for persist backend" + type = string +} + +variable "license_key" { + description = "Materialize license key" + type = string + default = null + sensitive = true +} + +# Environmentd Configuration +variable "environmentd_version" { + description = "Version of environmentd to use" + type = string + default = "v0.130.13" # META: mz version +} + +variable "environmentd_extra_env" { + description = "Extra environment variables for environmentd" + type = list(object({ + name = string + value = string + })) + default = [] +} + +variable "environmentd_extra_args" { + description = "Extra command line arguments for environmentd" + type = list(string) + default = [] +} + +# Resource Requirements +variable "cpu_request" { + description = "CPU request for environmentd" + type = string + default = "1" +} + +variable "memory_request" { + description = "Memory request for environmentd" + type = string + default = "1Gi" +} + +variable "memory_limit" { + description = "Memory limit for environmentd" + type = string + default = "1Gi" +} + +# Rollout Configuration +variable "in_place_rollout" { + description = "Whether to perform in-place rollouts" + type = bool + default = true +} + +variable "request_rollout" { + description = "UUID to request a rollout" + type = string + default = "00000000-0000-0000-0000-000000000001" + + validation { + condition = can(regex("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", var.request_rollout)) + error_message = "Request rollout must be a valid UUID in the format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + } +} + +variable "force_rollout" { + description = "UUID to force a rollout" + type = string + default = "00000000-0000-0000-0000-000000000001" + + validation { + condition = can(regex("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", var.force_rollout)) + error_message = "Force rollout must be a valid UUID in the format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + } +} + +# Balancer Resource Requirements +variable "balancer_memory_request" { + description = "Memory request for balancer" + type = string + default = "256Mi" +} + +variable "balancer_memory_limit" { + description = "Memory limit for balancer" + type = string + default = "256Mi" +} + +variable "balancer_cpu_request" { + description = "CPU request for balancer" + type = string + default = "100m" +} diff --git a/modules/materialize-instance/versions.tf b/modules/materialize-instance/versions.tf new file mode 100644 index 0000000..ba8641a --- /dev/null +++ b/modules/materialize-instance/versions.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + kubernetes = { + source = "hashicorp/kubernetes" + version = "~> 2.0" + } + } +} diff --git a/modules/networking/main.tf b/modules/networking/main.tf index fad6509..6664169 100644 --- a/modules/networking/main.tf +++ b/modules/networking/main.tf @@ -2,14 +2,15 @@ resource "google_compute_network" "vpc" { name = "${var.prefix}-network" auto_create_subnetworks = false project = var.project_id + mtu = 1460 # Optimized for GKE lifecycle { create_before_destroy = true prevent_destroy = false } - } + resource "google_compute_route" "default_route" { name = "${var.prefix}-default-route" project = var.project_id @@ -34,6 +35,7 @@ resource "google_compute_subnetwork" "subnet" { region = var.region private_ip_google_access = true + purpose = "PRIVATE" secondary_ip_range { range_name = "pods" @@ -45,6 +47,41 @@ resource "google_compute_subnetwork" "subnet" { ip_cidr_range = var.services_cidr } + lifecycle { + create_before_destroy = true + } + +} + +# Cloud Router for NAT Gateway +resource "google_compute_router" "router" { + name = "${var.prefix}-router" + project = var.project_id + region = var.region + network = google_compute_network.vpc.name + + bgp { + asn = 64514 + } +} + +# Cloud NAT for outbound internet access from private nodes +resource "google_compute_router_nat" "nat" { + name = "${var.prefix}-nat" + project = var.project_id + router = google_compute_router.router.name + region = var.region + nat_ip_allocate_option = "AUTO_ONLY" + source_subnetwork_ip_ranges_to_nat = "ALL_SUBNETWORKS_ALL_IP_RANGES" + + log_config { + enable = true + filter = "ERRORS_ONLY" + } + + lifecycle { + create_before_destroy = true + } } resource "google_compute_global_address" "private_ip_address" { diff --git a/modules/networking/outputs.tf b/modules/networking/outputs.tf index 58a79ba..425b663 100644 --- a/modules/networking/outputs.tf +++ b/modules/networking/outputs.tf @@ -22,3 +22,13 @@ output "private_vpc_connection" { description = "The private VPC connection" value = google_service_networking_connection.private_vpc_connection } + +output "router_name" { + description = "The name of the Cloud Router" + value = google_compute_router.router.name +} + +output "nat_name" { + description = "The name of the Cloud NAT" + value = google_compute_router_nat.nat.name +} diff --git a/modules/nodepool/main.tf b/modules/nodepool/main.tf new file mode 100644 index 0000000..2bd61e5 --- /dev/null +++ b/modules/nodepool/main.tf @@ -0,0 +1,309 @@ +locals { + node_taints = var.enable_disk_setup ? [ + { + key = "disk-unconfigured" + value = "true" + effect = "NO_SCHEDULE" + } + ] : [] + + node_labels = merge( + var.labels, + { + "materialize.cloud/disk" = var.enable_disk_setup ? "true" : "false" + "workload" = "materialize-instance" + }, + var.enable_disk_setup ? { + "materialize.cloud/disk-config-required" = "true" + } : {} + ) + + disk_setup_name = "disk-setup" + + disk_setup_labels = merge( + var.labels, + { + "app" = local.disk_setup_name + } + ) +} + +resource "google_container_node_pool" "primary_nodes" { + provider = google + + name = "${var.nodepool_name}" + location = var.region + cluster = var.cluster_name + project = var.project_id + + node_count = var.node_count + + autoscaling { + min_node_count = var.min_nodes + max_node_count = var.max_nodes + } + + network_config { + enable_private_nodes = var.enable_private_nodes + } + + node_config { + machine_type = var.machine_type + disk_size_gb = var.disk_size_gb + + labels = local.node_labels + + dynamic "taint" { + for_each = local.node_taints + content { + key = taint.value.key + value = taint.value.value + effect = taint.value.effect + } + } + + service_account = var.service_account_email + + oauth_scopes = [ + "https://www.googleapis.com/auth/cloud-platform" + ] + + local_nvme_ssd_block_config { + local_ssd_count = var.local_ssd_count + } + + workload_metadata_config { + mode = "GKE_METADATA" + } + } + + lifecycle { + create_before_destroy = true + prevent_destroy = false + } +} + + +resource "kubernetes_namespace" "disk_setup" { + count = var.enable_disk_setup ? 1 : 0 + + metadata { + name = local.disk_setup_name + labels = local.disk_setup_labels + } + + depends_on = [ + google_container_node_pool.primary_nodes + ] +} + +resource "kubernetes_daemonset" "disk_setup" { + count = var.enable_disk_setup ? 1 : 0 + depends_on = [ + kubernetes_namespace.disk_setup + ] + + metadata { + name = local.disk_setup_name + namespace = kubernetes_namespace.disk_setup[0].metadata[0].name + labels = local.disk_setup_labels + } + + spec { + selector { + match_labels = { + app = local.disk_setup_name + } + } + + template { + metadata { + labels = local.disk_setup_labels + } + + spec { + security_context { + run_as_non_root = false + run_as_user = 0 + fs_group = 0 + seccomp_profile { + type = "RuntimeDefault" + } + } + + affinity { + node_affinity { + required_during_scheduling_ignored_during_execution { + node_selector_term { + match_expressions { + key = "materialize.cloud/disk" + operator = "In" + values = ["true"] + } + } + } + } + } + + toleration { + key = local.node_taints[0].key + operator = "Exists" + effect = "NoSchedule" + } + + # Use host network and PID namespace + host_network = true + host_pid = true + + init_container { + name = local.disk_setup_name + image = var.disk_setup_image + command = ["/usr/local/bin/configure-disks.sh"] + args = ["--cloud-provider", "gcp"] + resources { + limits = { + memory = "128Mi" + } + requests = { + memory = "128Mi" + cpu = "50m" + } + } + + security_context { + privileged = true + run_as_user = 0 + } + + env { + name = "NODE_NAME" + value_from { + field_ref { + field_path = "spec.nodeName" + } + } + } + + volume_mount { + name = "dev" + mount_path = "/dev" + } + + volume_mount { + name = "host-root" + mount_path = "/host" + } + + } + + init_container { + name = "taint-removal" + image = var.disk_setup_image + command = ["/usr/local/bin/remove-taint.sh"] + resources { + limits = { + memory = "64Mi" + } + requests = { + memory = "64Mi" + cpu = "10m" + } + } + security_context { + run_as_user = 0 + } + env { + name = "NODE_NAME" + value_from { + field_ref { + field_path = "spec.nodeName" + } + } + } + } + + container { + name = "pause" + image = "gcr.io/google_containers/pause:3.2" + + resources { + limits = { + memory = "8Mi" + } + requests = { + memory = "8Mi" + cpu = "1m" + } + } + + security_context { + allow_privilege_escalation = false + read_only_root_filesystem = true + run_as_non_root = true + run_as_user = 65534 + } + + } + + volume { + name = "dev" + host_path { + path = "/dev" + } + } + + volume { + name = "host-root" + host_path { + path = "/" + } + } + + service_account_name = kubernetes_service_account.disk_setup[0].metadata[0].name + } + } + } +} + +resource "kubernetes_service_account" "disk_setup" { + count = var.enable_disk_setup ? 1 : 0 + depends_on = [ + kubernetes_namespace.disk_setup + ] + metadata { + name = local.disk_setup_name + namespace = kubernetes_namespace.disk_setup[0].metadata[0].name + } +} + +resource "kubernetes_cluster_role" "disk_setup" { + count = var.enable_disk_setup ? 1 : 0 + depends_on = [ + kubernetes_namespace.disk_setup + ] + metadata { + name = local.disk_setup_name + } + rule { + api_groups = [""] + resources = ["nodes"] + verbs = ["get", "patch"] + } +} + +resource "kubernetes_cluster_role_binding" "disk_setup" { + count = var.enable_disk_setup ? 1 : 0 + metadata { + name = local.disk_setup_name + } + role_ref { + api_group = "rbac.authorization.k8s.io" + kind = "ClusterRole" + name = kubernetes_cluster_role.disk_setup[0].metadata[0].name + } + subject { + kind = "ServiceAccount" + name = kubernetes_service_account.disk_setup[0].metadata[0].name + namespace = kubernetes_namespace.disk_setup[0].metadata[0].name + } +} diff --git a/modules/nodepool/outputs.tf b/modules/nodepool/outputs.tf new file mode 100644 index 0000000..fecc25f --- /dev/null +++ b/modules/nodepool/outputs.tf @@ -0,0 +1,19 @@ +output "node_pool_name" { + description = "The name of the node pool" + value = google_container_node_pool.primary_nodes.name +} + +output "node_pool_id" { + description = "The ID of the node pool" + value = google_container_node_pool.primary_nodes.id +} + +output "instance_group_urls" { + description = "List of instance group URLs for the node pool" + value = google_container_node_pool.primary_nodes.instance_group_urls +} + +output "node_count" { + description = "The current number of nodes in the pool" + value = google_container_node_pool.primary_nodes.node_count +} diff --git a/modules/nodepool/variables.tf b/modules/nodepool/variables.tf new file mode 100644 index 0000000..0512499 --- /dev/null +++ b/modules/nodepool/variables.tf @@ -0,0 +1,84 @@ +variable "nodepool_name" { + description = "The name of the node pool" + type = string +} + +variable "region" { + description = "The region where the cluster is located" + type = string +} + +variable "enable_disk_setup" { + description = "Whether to enable the local NVMe SSD disks setup script for NVMe storage" + type = bool + default = true +} + +variable "cluster_name" { + description = "The name of the GKE cluster" + type = string +} + +variable "project_id" { + description = "The GCP project ID" + type = string +} + +variable "node_count" { + description = "The number of nodes in the node pool" + type = number + default = 3 +} + +variable "min_nodes" { + description = "The minimum number of nodes in the autoscaling group" + type = number + default = 1 +} + +variable "max_nodes" { + description = "The maximum number of nodes in the autoscaling group" + type = number + default = 10 +} + +variable "machine_type" { + description = "The machine type for the nodes" + type = string + default = "e2-medium" +} + +variable "disk_size_gb" { + description = "The disk size in GB for each node" + type = number + default = 100 +} + +variable "labels" { + description = "Labels to apply to the nodes" + type = map(string) + default = {} +} + +variable "service_account_email" { + description = "The email of the service account to use for the nodes" + type = string +} + +variable "local_ssd_count" { + description = "Number of local NVMe SSDs to attach to each node. In GCP, each disk is 375GB. For Materialize, you need to have a 1:2 ratio of disk to memory. If you have 8 CPUs and 64GB of memory, you need 128GB of disk. This means you need at least 1 local NVMe SSD. If you go with a larger machine type, you can increase the number of local NVMe SSDs." + type = number + default = 1 +} + +variable "enable_private_nodes" { + description = "Whether to enable private nodes" + type = bool + default = true +} + +variable "disk_setup_image" { + description = "Docker image for the disk setup script" + type = string + default = "materialize/ephemeral-storage-setup-image:v0.1.1" +} diff --git a/modules/openebs/main.tf b/modules/openebs/main.tf new file mode 100644 index 0000000..fdc9df5 --- /dev/null +++ b/modules/openebs/main.tf @@ -0,0 +1,36 @@ + + +resource "kubernetes_namespace" "openebs" { + count = var.install_openebs && var.create_namespace ? 1 : 0 + + metadata { + name = var.openebs_namespace + } +} + +resource "helm_release" "openebs" { + count = var.install_openebs ? 1 : 0 + + name = "openebs" + namespace = var.openebs_namespace + repository = "https://openebs.github.io/openebs" + chart = "openebs" + version = var.openebs_version + + set { + name = "engines.replicated.mayastor.enabled" + value = "false" + } + + # Unable to continue with install: CustomResourceDefinition "volumesnapshotclasses.snapshot.storage.k8s.io" + # in namespace "" exists and cannot be imported into the current release + # https://github.com/openebs/website/pull/506 + set { + name = "openebs-crds.csi.volumeSnapshots.enabled" + value = "false" + } + + depends_on = [ + kubernetes_namespace.openebs + ] +} diff --git a/modules/openebs/outputs.tf b/modules/openebs/outputs.tf new file mode 100644 index 0000000..be94294 --- /dev/null +++ b/modules/openebs/outputs.tf @@ -0,0 +1,24 @@ +output "openebs_namespace" { + description = "The namespace where OpenEBS is installed" + value = var.install_openebs ? var.openebs_namespace : null +} + +output "openebs_installed" { + description = "Whether OpenEBS is installed" + value = var.install_openebs +} + +output "helm_release_name" { + description = "The name of the OpenEBS Helm release" + value = var.install_openebs ? helm_release.openebs[0].name : null +} + +output "helm_release_version" { + description = "The version of the installed OpenEBS Helm chart" + value = var.install_openebs ? var.openebs_version : null +} + +output "helm_release_status" { + description = "The status of the OpenEBS Helm release" + value = var.install_openebs ? helm_release.openebs[0].status : null +} \ No newline at end of file diff --git a/modules/openebs/variables.tf b/modules/openebs/variables.tf new file mode 100644 index 0000000..2117ee7 --- /dev/null +++ b/modules/openebs/variables.tf @@ -0,0 +1,23 @@ +variable "install_openebs" { + description = "Whether to install OpenEBS for NVMe storage" + type = bool + default = true +} + +variable "openebs_namespace" { + description = "The namespace where OpenEBS will be installed" + type = string + default = "openebs" +} + +variable "create_namespace" { + description = "Whether to create the namespace where OpenEBS will be installed" + type = bool + default = true +} + +variable "openebs_version" { + description = "The version of OpenEBS Helm chart to install" + type = string + default = "3.9.0" +} diff --git a/modules/operator/main.tf b/modules/operator/main.tf new file mode 100644 index 0000000..ca4bff5 --- /dev/null +++ b/modules/operator/main.tf @@ -0,0 +1,131 @@ + + +locals { + default_helm_values = { + image = var.orchestratord_version == null ? {} : { + tag = var.orchestratord_version + }, + observability = { + podMetrics = { + enabled = true + } + } + operator = { + cloudProvider = { + type = "gcp" + region = var.region + providers = { + gcp = { + enabled = true + } + } + } + } + storage = var.enable_disk_support ? { + storageClass = { + create = local.disk_config.create_storage_class + name = local.disk_config.storage_class_name + provisioner = local.disk_config.storage_class_provisioner + parameters = local.disk_config.storage_class_parameters + } + } : {} + tls = var.use_self_signed_cluster_issuer ? { + defaultCertificateSpecs = { + balancerdExternal = { + dnsNames = [ + "balancerd", + ] + issuerRef = { + name = "${var.name_prefix}-root-ca" + kind = "ClusterIssuer" + } + } + consoleExternal = { + dnsNames = [ + "console", + ] + issuerRef = { + name = "${var.name_prefix}-root-ca" + kind = "ClusterIssuer" + } + } + internal = { + issuerRef = { + name = "${var.name_prefix}-root-ca" + kind = "ClusterIssuer" + } + } + } + } : {} + } + + # Requires OpenEBS to be installed + disk_config = { + create_storage_class = var.enable_disk_support ? lookup(var.disk_support_config, "create_storage_class", true) : false + storage_class_name = lookup(var.disk_support_config, "storage_class_name", "openebs-lvm-instance-store-ext4") + storage_class_provisioner = "local.csi.openebs.io" + storage_class_parameters = { + storage = "lvm" + fsType = "ext4" + volgroup = "instance-store-vg" + } + } +} + +resource "kubernetes_namespace" "materialize" { + metadata { + name = var.operator_namespace + } +} + +resource "kubernetes_namespace" "monitoring" { + metadata { + name = var.monitoring_namespace + } +} + +resource "helm_release" "materialize_operator" { + name = var.name_prefix + namespace = kubernetes_namespace.materialize.metadata[0].name + + repository = var.use_local_chart ? null : var.helm_repository + chart = var.helm_chart + version = var.use_local_chart ? null : var.operator_version + + values = [ + yamlencode(merge(local.default_helm_values, var.helm_values)) + ] + + depends_on = [kubernetes_namespace.materialize] +} + +# Install the metrics-server for monitoring +# Required for the Materialize Console to display cluster metrics +# Defaults to false because GKE provides metrics-server by default +# Enable this when metrics collection is disabled in the cluster +# https://cloud.google.com/kubernetes-engine/docs/how-to/configure-metrics +# TODO: we should rather rely on GKE metrics-server instead of installing our own, confirm with team +resource "helm_release" "metrics_server" { +count = var.install_metrics_server ? 1 : 0 + +name = "${var.name_prefix}-metrics-server" +namespace = kubernetes_namespace.monitoring.metadata[0].name +repository = "https://kubernetes-sigs.github.io/metrics-server/" +chart = "metrics-server" +version = var.metrics_server_version + +# Common configuration values +set { + name = "args[0]" + value = "--kubelet-insecure-tls" +} + +set { + name = "metrics.enabled" + value = "true" +} + +depends_on = [ + kubernetes_namespace.monitoring +] +} \ No newline at end of file diff --git a/modules/operator/outputs.tf b/modules/operator/outputs.tf new file mode 100644 index 0000000..e1073f9 --- /dev/null +++ b/modules/operator/outputs.tf @@ -0,0 +1,14 @@ +output "operator_namespace" { + description = "Namespace where the operator is installed" + value = kubernetes_namespace.materialize.metadata[0].name +} + +output "operator_release_name" { + description = "Helm release name of the operator" + value = helm_release.materialize_operator.name +} + +output "operator_release_status" { + description = "Status of the helm release" + value = helm_release.materialize_operator.status +} \ No newline at end of file diff --git a/modules/operator/variables.tf b/modules/operator/variables.tf new file mode 100644 index 0000000..da82237 --- /dev/null +++ b/modules/operator/variables.tf @@ -0,0 +1,98 @@ +variable "name_prefix" { + description = "Prefix for all resource names (replaces separate namespace and environment variables)" + type = string +} + +variable "operator_version" { + description = "Version of the Materialize operator to install" + type = string + default = "v25.1.12" # META: helm-chart version + nullable = false +} + +variable "orchestratord_version" { + description = "Version of the Materialize orchestrator to install" + type = string + default = null +} + +variable "helm_repository" { + description = "Repository URL for the Materialize operator Helm chart. Leave empty if using local chart." + type = string + default = "https://materializeinc.github.io/materialize/" +} + +variable "helm_chart" { + description = "Chart name from repository or local path to chart. For local charts, set the path to the chart directory." + type = string + default = "materialize-operator" +} + +variable "use_local_chart" { + description = "Whether to use a local chart instead of one from a repository" + type = bool + default = false +} + +variable "helm_values" { + description = "Values to pass to the Helm chart" + type = any + default = {} +} + +variable "operator_namespace" { + description = "Namespace for the Materialize operator" + type = string + default = "materialize" +} + +variable "monitoring_namespace" { + description = "Namespace for monitoring resources" + type = string + default = "monitoring" +} + +variable "metrics_server_version" { + description = "Version of metrics-server to install" + type = string + default = "3.12.2" +} + +variable "install_metrics_server" { + description = "Whether to install the metrics-server" + type = bool + default = false +} + +variable "region" { + description = "Region/Zone for the operator Helm values." + type = string + default = "us-central1" +} + +variable "use_self_signed_cluster_issuer" { + description = "Whether to use a self-signed cluster issuer for cert-manager." + type = bool + default = false +} + +variable "enable_disk_support" { + description = "Enable disk support for Materialize using OpenEBS and NVMe instance storage. When enabled, this configures OpenEBS, runs the disk setup script for NVMe devices, and creates appropriate storage classes." + type = bool + default = true +} + +variable "disk_support_config" { + description = "Advanced configuration for disk support (only used when enable_disk_support = true)" + type = object({ + create_storage_class = optional(bool, true) + storage_class_name = optional(string, "openebs-lvm-instance-store-ext4") + storage_class_provisioner = optional(string, "local.csi.openebs.io") + storage_class_parameters = optional(object({ + storage = optional(string, "lvm") + fsType = optional(string, "ext4") + volgroup = optional(string, "instance-store-vg") + }), {}) + }) + default = {} +}