From 37263008f9e92e0363d417956f69bdda6432f668 Mon Sep 17 00:00:00 2001 From: Angus Hollands Date: Mon, 29 Jun 2026 15:13:33 +0100 Subject: [PATCH 1/9] feat: add neurohackademy --- eksctl/neurohackademy.jsonnet | 290 +++++++++++++++++++ terraform/aws/projects/neurohackademy.tfvars | 25 ++ 2 files changed, 315 insertions(+) create mode 100644 eksctl/neurohackademy.jsonnet create mode 100644 terraform/aws/projects/neurohackademy.tfvars diff --git a/eksctl/neurohackademy.jsonnet b/eksctl/neurohackademy.jsonnet new file mode 100644 index 0000000000..5824794d28 --- /dev/null +++ b/eksctl/neurohackademy.jsonnet @@ -0,0 +1,290 @@ +/* + This file is a jsonnet template of a eksctl's cluster configuration file, + that is used with the eksctl CLI to both update and initialize an AWS EKS + based cluster. + + This file has in turn been generated from eksctl/template.jsonnet which is + relevant to compare with for changes over time. + + To use jsonnet to generate an eksctl configuration file from this, do: + + jsonnet neurohackademy.jsonnet > neurohackademy.eksctl.yaml + + References: + - https://eksctl.io/usage/schema/ +*/ +local ng = import './libsonnet/nodegroup.jsonnet'; + +// place all cluster nodes here +local clusterRegion = 'us-west-2'; +local masterAzs = ['us-west-2a', 'us-west-2b', 'us-west-2c']; +local nodeAz = 'us-west-2a'; + +// Node definitions for notebook nodes. Config here is merged +// with our notebook node definition. +// A `node.kubernetes.io/instance-type label is added, so pods +// can request a particular kind of node with a nodeSelector +local notebookNodes = [ + // staging + { + instanceType: 'r5.xlarge', + namePrefix: 'nb-staging', + labels+: { '2i2c/hub-name': 'staging' }, + tags+: { '2i2c:hub-name': 'staging' }, + }, + { + instanceType: 'r5.4xlarge', + namePrefix: 'nb-staging', + labels+: { '2i2c/hub-name': 'staging' }, + tags+: { '2i2c:hub-name': 'staging' }, + }, + { + instanceType: 'r5.16xlarge', + namePrefix: 'nb-staging', + labels+: { '2i2c/hub-name': 'staging' }, + tags+: { '2i2c:hub-name': 'staging' }, + }, + // prod + { + instanceType: 'r5.xlarge', + namePrefix: 'nb-prod', + labels+: { '2i2c/hub-name': 'prod' }, + tags+: { '2i2c:hub-name': 'prod' }, + }, + { + instanceType: 'r5.4xlarge', + namePrefix: 'nb-prod', + labels+: { '2i2c/hub-name': 'prod' }, + tags+: { '2i2c:hub-name': 'prod' }, + }, + { + instanceType: 'r5.16xlarge', + namePrefix: 'nb-prod', + labels+: { '2i2c/hub-name': 'prod' }, + tags+: { '2i2c:hub-name': 'prod' }, + }, + { + instanceType: 'g4dn.xlarge', + namePrefix: 'gpu-staging', + minSize: 0, + labels+: { + '2i2c/hub-name': 'staging', + '2i2c/has-gpu': 'true', + 'k8s.amazonaws.com/accelerator': 'nvidia-tesla-t4', + }, + tags+: { + '2i2c:hub-name': 'staging', + 'k8s.io/cluster-autoscaler/node-template/resources/nvidia.com/gpu': '1', + 'k8s.io/cluster-autoscaler/node-template/label/k8s.amazonaws.com/accelerator': 'nvidia-tesla-t4', + }, + taints+: { + 'nvidia.com/gpu': 'present:NoSchedule', + }, + // Allow provisioning GPUs across all AZs, to prevent situation where all + // GPUs in a single AZ are in use and no new nodes can be spawned + availabilityZones: masterAzs, + }, + { + instanceType: 'g4dn.xlarge', + namePrefix: 'gpu-prod', + minSize: 0, + labels+: { + '2i2c/hub-name': 'prod', + '2i2c/has-gpu': 'true', + 'k8s.amazonaws.com/accelerator': 'nvidia-tesla-t4', + }, + tags+: { + '2i2c:hub-name': 'prod', + 'k8s.io/cluster-autoscaler/node-template/resources/nvidia.com/gpu': '1', + 'k8s.io/cluster-autoscaler/node-template/label/k8s.amazonaws.com/accelerator': 'nvidia-tesla-t4', + }, + taints+: { + 'nvidia.com/gpu': 'present:NoSchedule', + }, + // Allow provisioning GPUs across all AZs, to prevent situation where all + // GPUs in a single AZ are in use and no new nodes can be spawned + availabilityZones: masterAzs, + }, + { + instanceType: 'g4dn.2xlarge', + namePrefix: 'gpu-prod-2xlarge', + minSize: 0, + labels+: { + '2i2c/hub-name': 'prod', + '2i2c/has-gpu': 'true', + 'k8s.amazonaws.com/accelerator': 'nvidia-tesla-t4', + }, + tags+: { + '2i2c:hub-name': 'prod', + 'k8s.io/cluster-autoscaler/node-template/resources/nvidia.com/gpu': '1', + 'k8s.io/cluster-autoscaler/node-template/label/k8s.amazonaws.com/accelerator': 'nvidia-tesla-t4', + }, + taints+: { + 'nvidia.com/gpu': 'present:NoSchedule', + }, + // Allow provisioning GPUs across all AZs, to prevent situation where all + // GPUs in a single AZ are in use and no new nodes can be spawned + availabilityZones: masterAzs, + }, + { + instanceType: 'g4dn.4xlarge', + namePrefix: 'gpu-prod-4xlarge', + minSize: 0, + labels+: { + '2i2c/hub-name': 'prod', + '2i2c/has-gpu': 'true', + 'k8s.amazonaws.com/accelerator': 'nvidia-tesla-t4', + }, + tags+: { + '2i2c:hub-name': 'prod', + 'k8s.io/cluster-autoscaler/node-template/resources/nvidia.com/gpu': '1', + 'k8s.io/cluster-autoscaler/node-template/label/k8s.amazonaws.com/accelerator': 'nvidia-tesla-t4', + }, + taints+: { + 'nvidia.com/gpu': 'present:NoSchedule', + }, + // Allow provisioning GPUs across all AZs, to prevent situation where all + // GPUs in a single AZ are in use and no new nodes can be spawned + availabilityZones: masterAzs, + }, +]; + +local daskNodes = []; + + +{ + apiVersion: 'eksctl.io/v1alpha5', + kind: 'ClusterConfig', + metadata+: { + name: 'neurohackademy', + region: clusterRegion, + version: '1.32', + tags+: { + ManagedBy: '2i2c', + '2i2c.org/cluster-name': $.metadata.name, + }, + }, + availabilityZones: masterAzs, + iam: { + withOIDC: true, + }, + // If you add an addon to this config, run the create addon command. + // + // eksctl create addon --config-file=neurohackademy.eksctl.yaml + // + addons: [ + { version: 'latest', tags: $.metadata.tags } + addon + for addon in + [ + { name: 'coredns' }, + { name: 'kube-proxy' }, + { + // vpc-cni is a Amazon maintained container networking interface + // (CNI), where a CNI is required for k8s networking. The aws-node + // DaemonSet in kube-system stems from installing this. + // + // Related docs: https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/network-plugins/ + // https://docs.aws.amazon.com/eks/latest/userguide/managing-vpc-cni.html + // + name: 'vpc-cni', + attachPolicyARNs: ['arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy'], + // configurationValues ref: https://github.com/aws/amazon-vpc-cni-k8s/blob/HEAD/charts/aws-vpc-cni/values.yaml + configurationValues: ||| + enableNetworkPolicy: "false" + |||, + }, + { + // aws-ebs-csi-driver ensures that our PVCs are bound to PVs that + // couple to AWS EBS based storage, without it expect to see pods + // mounting a PVC failing to schedule and PVC resources that are + // unbound. + // + // Related docs: https://docs.aws.amazon.com/eks/latest/userguide/managing-ebs-csi.html + // + name: 'aws-ebs-csi-driver', + wellKnownPolicies: { + ebsCSIController: true, + }, + // We enable detailed metrics collection to watch for issues with + // jupyterhub-home-nfs + // configurationValues ref: https://github.com/kubernetes-sigs/aws-ebs-csi-driver/blob/HEAD/charts/aws-ebs-csi-driver/values.yaml + configurationValues: ||| + defaultStorageClass: + enabled: true + controller: + enableMetrics: true + node: + enableMetrics: true + |||, + }, + ] + ], + nodeGroups: [ + n { clusterName: $.metadata.name } + for n in + [ + ng { + namePrefix: 'core', + nameSuffix: 'a', + nameIncludeInstanceType: false, + availabilityZones: [nodeAz], + instanceType: 'r5.xlarge', + minSize: 1, + maxSize: 6, + labels+: { + 'hub.jupyter.org/node-purpose': 'core', + 'k8s.dask.org/node-purpose': 'core', + }, + tags+: { + '2i2c:node-purpose': 'core', + }, + }, + ] + [ + ng { + namePrefix: 'nb', + availabilityZones: [nodeAz], + minSize: 0, + maxSize: 500, + instanceType: n.instanceType, + labels+: { + 'hub.jupyter.org/node-purpose': 'user', + 'k8s.dask.org/node-purpose': 'scheduler', + }, + taints+: { + 'hub.jupyter.org_dedicated': 'user:NoSchedule', + 'hub.jupyter.org/dedicated': 'user:NoSchedule', + }, + tags+: { + '2i2c:node-purpose': 'user', + }, + } + n + for n in notebookNodes + ] + ( + if daskNodes != null then + [ + ng { + namePrefix: 'dask', + availabilityZones: [nodeAz], + minSize: 0, + maxSize: 500, + labels+: { + 'k8s.dask.org/node-purpose': 'worker', + }, + taints+: { + 'k8s.dask.org_dedicated': 'worker:NoSchedule', + 'k8s.dask.org/dedicated': 'worker:NoSchedule', + }, + tags+: { + '2i2c:node-purpose': 'worker', + }, + instancesDistribution+: { + onDemandBaseCapacity: 0, + onDemandPercentageAboveBaseCapacity: 0, + spotAllocationStrategy: 'capacity-optimized', + }, + } + n + for n in daskNodes + ] else [] + ) + ], +} diff --git a/terraform/aws/projects/neurohackademy.tfvars b/terraform/aws/projects/neurohackademy.tfvars new file mode 100644 index 0000000000..52d372010e --- /dev/null +++ b/terraform/aws/projects/neurohackademy.tfvars @@ -0,0 +1,25 @@ +region = "us-west-2" +cluster_name = "neurohackademy" +cluster_nodes_location = "us-west-2a" + +enable_jupyterhub_cost_monitoring = true + +enable_nfs_backup = true + +filestores = {} + +ebs_volumes = { + "staging" = { + size = 5 # in GB + type = "gp3" + name_suffix = "staging" + tags = { "2i2c:hub-name" : "staging" } + } + "prod" = { + size = 5200 # in GB + type = "gp3" + name_suffix = "prod" + tags = { "2i2c:hub-name" : "prod" } + iops = 10000 + } +} From 7d9e77bc3cd980ec818163c6ea30c2575fe40008 Mon Sep 17 00:00:00 2001 From: Angus Hollands Date: Mon, 29 Jun 2026 16:43:11 +0100 Subject: [PATCH 2/9] wip: build out Terraform config for EKS Cluster --- terraform/aws/cluster.tf | 164 +++++++++++++++++++ terraform/aws/projects/neurohackademy.tfvars | 2 +- terraform/aws/variables.tf | 33 +++- 3 files changed, 197 insertions(+), 2 deletions(-) create mode 100644 terraform/aws/cluster.tf diff --git a/terraform/aws/cluster.tf b/terraform/aws/cluster.tf new file mode 100644 index 0000000000..52c9e2bcc7 --- /dev/null +++ b/terraform/aws/cluster.tf @@ -0,0 +1,164 @@ +resource "aws_eks_cluster" "cluster" { + name = "${var.cluster_name}-cluster" + region = var.region + + access_config { + authentication_mode = "API" + } + + role_arn = aws_iam_role.cluster.arn + version = var.k8s_versions.min_master_version + + vpc_config { + subnet_ids = concat(aws_subnet.private[*].id, aws_subnet.public[*].idu) + } + + # Ensure that IAM Role permissions are created before and deleted + # after EKS Cluster handling. Otherwise, EKS will not be able to + # properly delete EKS managed EC2 infrastructure such as Security Groups. + depends_on = [ + aws_iam_role_policy_attachment.cluster_AmazonEKSClusterPolicy, + ] +} + +resource "aws_iam_role" "cluster" { + name = "${var.cluster_name}-role" + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = [ + "sts:AssumeRole", + "sts:TagSession" + ] + Effect = "Allow" + Principal = { + Service = "eks.amazonaws.com" + } + }, + ] + }) +} + +resource "aws_iam_role_policy_attachment" "cluster_AmazonEKSClusterPolicy" { + policy_arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy" + role = aws_iam_role.cluster.name +} + + +# Declare the data source +data "aws_availability_zones" "available" { + state = "available" + region = var.region +} + + +resource "aws_vpc" "main" { + region = var.region + cidr_block = "192.168.0.0/16" + enable_dns_hostnames = true + enable_dns_support = true + + tags = { + Name = "${var.cluster_name}-vpc" + } +} + +resource "aws_subnet" "private" { + region = var.region + count = count(aws_availability_zones.available.names) + vpc_id = aws_vpc.main.id + cidr_block = "192.168.${count.index + 1}.0/24" + availability_zone = aws_availability_zones.available.names[count.index] + + tags = { + Name = "${var.cluster_name}-private-${count.index + 1}" + "kubernetes.io/role/internal-elb" = "1" + } +} + +resource "aws_subnet" "public" { + region = var.region + count = count(aws_availability_zones.available.names) + vpc_id = aws_vpc.main.id + cidr_block = "192.168.${count.index + 10}.0/24" + availability_zone = aws_availability_zones.available.names[count.index] + map_public_ip_on_launch = true + + tags = { + Name = "${var.cluster_name}-public-${count.index + 1}" + "kubernetes.io/role/elb" = "1" + } +} + +resource "aws_iam_role" "node" { + name = "${var.cluster_name}-node" + + assume_role_policy = jsonencode({ + Statement = [{ + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "ec2.amazonaws.com" + } + }] + Version = "2012-10-17" + }) +} + +# TODO: one role per node group? +resource "aws_iam_role_policy_attachment" "node-AmazonEKSWorkerNodePolicy" { + policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy" + role = aws_iam_role.node.name +} + +resource "aws_iam_role_policy_attachment" "node-AmazonEKS_CNI_Policy" { + policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy" + role = aws_iam_role.node.name +} + +resource "aws_iam_role_policy_attachment" "node-AmazonEC2ContainerRegistryReadOnly" { + policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly" + role = aws_iam_role.node.name +} + +resource "aws_eks_node_group" "core" { + cluster_name = aws_eks_cluster.cluster.name + region = var.region + node_group_name = "${var.cluster-name}-core-pool" + node_role_arn = aws_iam_role.cluster.arn + subnet_ids = aws_subnet.public[*].id + + ami_type = "AL2023_x86_64_STANDARD" + disk_size = 80 + node_repair_config { + enabled = true + } + instance_types = [var.core_node_machine_type] + + scaling_config { + min_size = 1 + max_size = var.core_node_max_count + desired_size = 1 + } + + update_config { + max_unavailable = 1 + } + + # Ensure that IAM Role permissions are created before and deleted after EKS Node Group handling. + # Otherwise, EKS will not be able to properly delete EC2 Instances and Elastic Network Interfaces. + depends_on = [ + aws_iam_role_policy_attachment.node-AmazonEKSWorkerNodePolicy, + aws_iam_role_policy_attachment.node-AmazonEKS_CNI_Policy, + aws_iam_role_policy_attachment.node-AmazonEC2ContainerRegistryReadOnly, + ] + + labels = { + "hub.jupyter.org/node-purpose" = "core", + "k8s.dask.org/node-purpose" = "core" + } + kubelet_config { + single_process_oom_kill = var.single_process_oom_kill + } +} diff --git a/terraform/aws/projects/neurohackademy.tfvars b/terraform/aws/projects/neurohackademy.tfvars index 52d372010e..0781f88452 100644 --- a/terraform/aws/projects/neurohackademy.tfvars +++ b/terraform/aws/projects/neurohackademy.tfvars @@ -16,7 +16,7 @@ ebs_volumes = { tags = { "2i2c:hub-name" : "staging" } } "prod" = { - size = 5200 # in GB + size = 1000 # in GB type = "gp3" name_suffix = "prod" tags = { "2i2c:hub-name" : "prod" } diff --git a/terraform/aws/variables.tf b/terraform/aws/variables.tf index 13cff9528e..00af3c581c 100644 --- a/terraform/aws/variables.tf +++ b/terraform/aws/variables.tf @@ -31,7 +31,7 @@ variable "user_buckets" { description = <<-EOT S3 Buckets to be created. - The key for each entry will be prefixed with {var.prefix}- to form + The key for each entry will be prefixed with {var.cluster_name}- to form the name of the bucket. The value is a map, with the following accepted keys: @@ -323,3 +323,34 @@ variable "enable_ebs_alarms" { Enable alerts for IOPs and throughput EOT } + + +variable "k8s_versions" { + type = object({ + min_master_version : optional(string, null), + core_nodes_version : optional(string, null), + notebook_nodes_version : optional(string, null), + dask_nodes_version : optional(string, null), + }) + default = {} + description = <<-EOT + Configuration of the k8s cluster's version and node pools' versions. To specify these + + - min_master_nodes is passthrough configuration of https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/eks_cluster#version-1 + - [core|notebook|dask]_nodes_version is passthrough configuration of https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/eks_node_group#version-2 + EOT +} + +variable "core_node_machine_type" { + type = string + description = <<-EOT + Machine type to use for core nodes. + + Core nodes will always be on, and count as 'base cost' + for a cluster. We should try to run with as few of them + as possible. + + For single-tenant clusters, a single r8i-flex.large node can be + enough. + EOT +} From 53af44ff9a776939df0d3bfccae1b3d77fc9a3ee Mon Sep 17 00:00:00 2001 From: Angus Hollands Date: Mon, 29 Jun 2026 17:51:00 +0100 Subject: [PATCH 3/9] wip: progress on pools --- terraform/aws/cluster.tf | 101 +++++++++++++++++++++++++++++++++++-- terraform/aws/variables.tf | 35 ++++++++----- 2 files changed, 119 insertions(+), 17 deletions(-) diff --git a/terraform/aws/cluster.tf b/terraform/aws/cluster.tf index 52c9e2bcc7..85c775e109 100644 --- a/terraform/aws/cluster.tf +++ b/terraform/aws/cluster.tf @@ -122,15 +122,35 @@ resource "aws_iam_role_policy_attachment" "node-AmazonEC2ContainerRegistryReadOn role = aws_iam_role.node.name } +resource "aws_launch_template" "core" { + name = "${var.cluster-name}-core-machine" + + block_device_mappings { + device_name = "/dev/xvda" + + ebs { + volume_size = var.core_nodes.disk_size_gb + volume_type = var.core_nodes.disk_type + iops = var.core_nodes.disk_iops + throughput = var.core_nodes.disk_throughput + } + } +} + resource "aws_eks_node_group" "core" { cluster_name = aws_eks_cluster.cluster.name region = var.region node_group_name = "${var.cluster-name}-core-pool" node_role_arn = aws_iam_role.cluster.arn subnet_ids = aws_subnet.public[*].id + ami_type = "AL2023_x86_64_STANDARD" + + launch_template { + id = aws_launch_template.core.id + # TODO: check this is correct + version = aws_launch_template.core.default_version + } - ami_type = "AL2023_x86_64_STANDARD" - disk_size = 80 node_repair_config { enabled = true } @@ -154,11 +174,82 @@ resource "aws_eks_node_group" "core" { aws_iam_role_policy_attachment.node-AmazonEC2ContainerRegistryReadOnly, ] - labels = { + labels = merge({ "hub.jupyter.org/node-purpose" = "core", "k8s.dask.org/node-purpose" = "core" + }, each.value.labels + ) + + tags = merge({ + "ManagedBy" : "2i2c", + "2i2c.org/cluster-name" : aws_eks_cluster.cluster.name, + "2i2c:node-purpose" : "core", + }, each.value.tags + ) +} + +resource "aws_launch_template" "notebook" { + name = "${var.cluster-name}-notebook-machine" + + block_device_mappings { + device_name = "/dev/xvda" + + ebs { + volume_size = var.notebook_nodes.disk_size_gb + volume_type = var.notebook_nodes.disk_type + iops = var.notebook_nodes.disk_iops + throughput = var.notebook_nodes.disk_throughput + } + } +} + +resource "aws_eks_node_group" "notebook" { + cluster_name = aws_eks_cluster.cluster.name + region = var.region + node_group_name = "${var.cluster-name}-notebook-pool" + node_role_arn = aws_iam_role.cluster.arn + subnet_ids = aws_subnet.public[*].id + ami_type = "AL2023_x86_64_STANDARD" + + launch_template { + id = aws_launch_template.notebook.id + # TODO: check this is correct + version = aws_launch_template.notebook.default_version + } + + node_repair_config { + enabled = true + } + instance_types = [var.notebook_node_machine_type] + + scaling_config { + min_size = 1 + max_size = var.notebook_node_max_count + desired_size = 1 } - kubelet_config { - single_process_oom_kill = var.single_process_oom_kill + + update_config { + max_unavailable = 1 } + + # Ensure that IAM Role permissions are created before and deleted after EKS Node Group handling. + # Otherwise, EKS will not be able to properly delete EC2 Instances and Elastic Network Interfaces. + depends_on = [ + aws_iam_role_policy_attachment.node-AmazonEKSWorkerNodePolicy, + aws_iam_role_policy_attachment.node-AmazonEKS_CNI_Policy, + aws_iam_role_policy_attachment.node-AmazonEC2ContainerRegistryReadOnly, + ] + + labels = merge({ + "hub.jupyter.org/node-purpose" = "user", + "k8s.dask.org/node-purpose" = "scheduler" + }, each.value.labels + ) + + tags = merge({ + "ManagedBy" : "2i2c", + "2i2c.org/cluster-name" : aws_eks_cluster.cluster.name, + "2i2c:node-purpose" : "user", + }, each.value.tags + ) } diff --git a/terraform/aws/variables.tf b/terraform/aws/variables.tf index 00af3c581c..9af21a8c09 100644 --- a/terraform/aws/variables.tf +++ b/terraform/aws/variables.tf @@ -341,16 +341,27 @@ variable "k8s_versions" { EOT } -variable "core_node_machine_type" { - type = string - description = <<-EOT - Machine type to use for core nodes. - - Core nodes will always be on, and count as 'base cost' - for a cluster. We should try to run with as few of them - as possible. - - For single-tenant clusters, a single r8i-flex.large node can be - enough. - EOT +variable "core_nodes" { + type = object({ + min : number, + max : number, + machine_type : string, + tags : optional(map(string), {}), + labels : optional(map(string), {}), + taints : optional(list(object({ + key : string, + value : string, + effect : string + })), []) + # Balanced disks are much faster than standard disks, and much cheaper + # than SSD disks. It contributes heavily to how fast new nodes spin up, + # as images being pulled takes up a lot of new node spin up time. + # Faster disks provide faster image pulls! + disk_type : optional(string, "gp3"), + disk_size_gb : optional(number, 80), + disk_throughput : optional(number, null), + disk_iops : optional(number, null), + node_version : optional(string, ""), + }) + description = "Core node pool to create" } From 123aaf3a455980ddced280d9d1b09151cb6facda Mon Sep 17 00:00:00 2001 From: Angus Hollands Date: Mon, 29 Jun 2026 18:44:45 +0100 Subject: [PATCH 4/9] wip: more work towards node pools --- terraform/aws/cluster.tf | 72 ++++++++++++++++++++++++++++---------- terraform/aws/variables.tf | 26 ++++++++++++++ 2 files changed, 79 insertions(+), 19 deletions(-) diff --git a/terraform/aws/cluster.tf b/terraform/aws/cluster.tf index 85c775e109..2388ee9e9f 100644 --- a/terraform/aws/cluster.tf +++ b/terraform/aws/cluster.tf @@ -125,6 +125,9 @@ resource "aws_iam_role_policy_attachment" "node-AmazonEC2ContainerRegistryReadOn resource "aws_launch_template" "core" { name = "${var.cluster-name}-core-machine" + instance_type = var.core_nodes.machine_type + + block_device_mappings { device_name = "/dev/xvda" @@ -154,7 +157,6 @@ resource "aws_eks_node_group" "core" { node_repair_config { enabled = true } - instance_types = [var.core_node_machine_type] scaling_config { min_size = 1 @@ -174,36 +176,58 @@ resource "aws_eks_node_group" "core" { aws_iam_role_policy_attachment.node-AmazonEC2ContainerRegistryReadOnly, ] + /** + Generate EC2 resource tags representing node taints that cluster autoscaler's autodiscovery understands + https://github.com/kubernetes/autoscaler/tree/master/cluster-autoscaler/cloudprovider/aws#auto-discovery-setup + */ labels = merge({ - "hub.jupyter.org/node-purpose" = "core", - "k8s.dask.org/node-purpose" = "core" - }, each.value.labels + "node.kubernetes.io/instance-type" = "${var.core_nodes.machine_type}", + "hub.jupyter.org/node-purpose" = "core", + "k8s.dask.org/node-purpose" = "core" + }, var.core_nodes.labels ) + taint = var.core_nodes.taints + + # https://github.com/kubernetes/autoscaler/tree/master/cluster-autoscaler/cloudprovider/aws#auto-discovery-setup tags = merge({ "ManagedBy" : "2i2c", "2i2c.org/cluster-name" : aws_eks_cluster.cluster.name, "2i2c:node-purpose" : "core", - }, each.value.tags + }, + var.core_nodes.tags, + { + for k, v in var.core_nodes.labels : + "k8s.io/cluster-autoscaler/node-template/label/${k}" => v + }, + { + for k, v in var.core_nodes.taints : + "k8s.io/cluster-autoscaler/node-template/taint/${k}" => v + } ) } resource "aws_launch_template" "notebook" { - name = "${var.cluster-name}-notebook-machine" + for_each = var.notebook_nodes + name = "${var.cluster-name}-notebook-${each.key}" + + instance_type = each.value.machine_type block_device_mappings { device_name = "/dev/xvda" ebs { - volume_size = var.notebook_nodes.disk_size_gb - volume_type = var.notebook_nodes.disk_type - iops = var.notebook_nodes.disk_iops - throughput = var.notebook_nodes.disk_throughput + volume_size = each.value.disk_size_gb + volume_type = each.value.disk_type + iops = each.value.disk_iops + throughput = each.value.disk_throughput } } } resource "aws_eks_node_group" "notebook" { + for_each = var.notebook_nodes + cluster_name = aws_eks_cluster.cluster.name region = var.region node_group_name = "${var.cluster-name}-notebook-pool" @@ -212,20 +236,20 @@ resource "aws_eks_node_group" "notebook" { ami_type = "AL2023_x86_64_STANDARD" launch_template { - id = aws_launch_template.notebook.id + # TODO fixme this seems long winded + id = aws_launch_template.notebook["${var.cluster-name}-notebook-${each.key}"].id # TODO: check this is correct - version = aws_launch_template.notebook.default_version + version = aws_launch_template.notebook["${var.cluster-name}-notebook-${each.key}"].default_version } node_repair_config { enabled = true } - instance_types = [var.notebook_node_machine_type] scaling_config { - min_size = 1 - max_size = var.notebook_node_max_count - desired_size = 1 + min_size = each.value.min + max_size = each.value.max + desired_size = each.value.min } update_config { @@ -241,8 +265,9 @@ resource "aws_eks_node_group" "notebook" { ] labels = merge({ - "hub.jupyter.org/node-purpose" = "user", - "k8s.dask.org/node-purpose" = "scheduler" + "node.kubernetes.io/instance-type" = "${each.value.machine_type}", + "hub.jupyter.org/node-purpose" = "user", + "k8s.dask.org/node-purpose" = "scheduler" }, each.value.labels ) @@ -250,6 +275,15 @@ resource "aws_eks_node_group" "notebook" { "ManagedBy" : "2i2c", "2i2c.org/cluster-name" : aws_eks_cluster.cluster.name, "2i2c:node-purpose" : "user", - }, each.value.tags + }, + each.value.tags, + { + for k, v in each.value.labels : + "k8s.io/cluster-autoscaler/node-template/label/${k}" => v + }, + { + for k, v in each.value.taints : + "k8s.io/cluster-autoscaler/node-template/taint/${k}" => v + } ) } diff --git a/terraform/aws/variables.tf b/terraform/aws/variables.tf index 9af21a8c09..2f401d4b3d 100644 --- a/terraform/aws/variables.tf +++ b/terraform/aws/variables.tf @@ -365,3 +365,29 @@ variable "core_nodes" { }) description = "Core node pool to create" } + +variable "notebook_nodes" { + type = map(object({ + min : number, + max : number, + machine_type : string, + tags : optional(map(string), {}), + labels : optional(map(string), {}), + taints : optional(list(object({ + key : string, + value : string, + effect : string + })), []) + # Balanced disks are much faster than standard disks, and much cheaper + # than SSD disks. It contributes heavily to how fast new nodes spin up, + # as images being pulled takes up a lot of new node spin up time. + # Faster disks provide faster image pulls! + disk_type : optional(string, "gp3"), + disk_size_gb : optional(number, 80), + disk_throughput : optional(number, null), + disk_iops : optional(number, null), + node_version : optional(string, ""), + })) + description = "Notebook node pools to create" + default = {} +} From 0db0679ad3677fc0821999258dfbbadd55144339 Mon Sep 17 00:00:00 2001 From: Angus Hollands Date: Tue, 30 Jun 2026 10:50:29 +0100 Subject: [PATCH 5/9] fix: handle taints --- terraform/aws/cluster.tf | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/terraform/aws/cluster.tf b/terraform/aws/cluster.tf index 2388ee9e9f..97fde14314 100644 --- a/terraform/aws/cluster.tf +++ b/terraform/aws/cluster.tf @@ -176,6 +176,13 @@ resource "aws_eks_node_group" "core" { aws_iam_role_policy_attachment.node-AmazonEC2ContainerRegistryReadOnly, ] + taint { + for_each = each.value.taints + key = each.key + effect = each.value.effect + value = each.value.value + } + /** Generate EC2 resource tags representing node taints that cluster autoscaler's autodiscovery understands https://github.com/kubernetes/autoscaler/tree/master/cluster-autoscaler/cloudprovider/aws#auto-discovery-setup @@ -202,7 +209,7 @@ resource "aws_eks_node_group" "core" { }, { for k, v in var.core_nodes.taints : - "k8s.io/cluster-autoscaler/node-template/taint/${k}" => v + "k8s.io/cluster-autoscaler/node-template/taint/${k}" => "${v.value}:${v.effect}" } ) } @@ -264,6 +271,13 @@ resource "aws_eks_node_group" "notebook" { aws_iam_role_policy_attachment.node-AmazonEC2ContainerRegistryReadOnly, ] + taint { + for_each = each.value.taints + key = each.key + effect = each.value.effect + value = each.value.value + } + labels = merge({ "node.kubernetes.io/instance-type" = "${each.value.machine_type}", "hub.jupyter.org/node-purpose" = "user", @@ -283,7 +297,7 @@ resource "aws_eks_node_group" "notebook" { }, { for k, v in each.value.taints : - "k8s.io/cluster-autoscaler/node-template/taint/${k}" => v + "k8s.io/cluster-autoscaler/node-template/taint/${k}" => "${v.value}:${v.effect}" } ) } From 3316a48d90928b5f00aea7033ca0dfc6981c8c1d Mon Sep 17 00:00:00 2001 From: Angus Hollands Date: Tue, 30 Jun 2026 12:42:27 +0100 Subject: [PATCH 6/9] feat: add singleProcessOOMKill for launch template --- terraform/aws/cluster.tf | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/terraform/aws/cluster.tf b/terraform/aws/cluster.tf index 97fde14314..5e9da06dea 100644 --- a/terraform/aws/cluster.tf +++ b/terraform/aws/cluster.tf @@ -230,6 +230,17 @@ resource "aws_launch_template" "notebook" { throughput = each.value.disk_throughput } } + + user_data = base64encode(<<-EOT +--- +apiVersion: node.eks.aws/v1alpha1 +kind: NodeConfig +spec: + kubelet: + config: + singleProcessOOMKill: true + EOT + ) } resource "aws_eks_node_group" "notebook" { From c95f242ec7f13957a94cc7ba3e10b3c3df45d4ca Mon Sep 17 00:00:00 2001 From: Angus Hollands Date: Tue, 30 Jun 2026 13:31:05 +0100 Subject: [PATCH 7/9] feat: add pools --- terraform/aws/projects/neurohackademy.tfvars | 21 ++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/terraform/aws/projects/neurohackademy.tfvars b/terraform/aws/projects/neurohackademy.tfvars index 0781f88452..fc0f7965ca 100644 --- a/terraform/aws/projects/neurohackademy.tfvars +++ b/terraform/aws/projects/neurohackademy.tfvars @@ -23,3 +23,24 @@ ebs_volumes = { iops = 10000 } } + +core_nodes = { + machine_type : "r8i-flex.large" +} +notebook_nodes = { + "r5-xlarge" : { + min : 0, + max : 100, + machine_type : "r5.xlarge", + }, + "r5-4xlarge" : { + min : 0, + max : 100, + machine_type : "r5.4xlarge", + }, + "r5-16xlarge" : { + min : 0, + max : 100, + machine_type : "r5.16xlarge", + } +} From 4178872055b44f0b2d950bcb8fb0e77cf1b09fdf Mon Sep 17 00:00:00 2001 From: Angus Hollands Date: Tue, 30 Jun 2026 14:32:08 +0100 Subject: [PATCH 8/9] wip: more progress --- terraform/aws/cluster.tf | 128 ++++++++++--------- terraform/aws/main.tf | 2 +- terraform/aws/projects/neurohackademy.tfvars | 2 + terraform/aws/variables.tf | 10 +- 4 files changed, 82 insertions(+), 60 deletions(-) diff --git a/terraform/aws/cluster.tf b/terraform/aws/cluster.tf index 5e9da06dea..5544866f67 100644 --- a/terraform/aws/cluster.tf +++ b/terraform/aws/cluster.tf @@ -1,16 +1,18 @@ + resource "aws_eks_cluster" "cluster" { - name = "${var.cluster_name}-cluster" - region = var.region + count = var.use_eksctl ? 0 : 1 + + name = "${var.cluster_name}-cluster" access_config { authentication_mode = "API" } - role_arn = aws_iam_role.cluster.arn + role_arn = aws_iam_role.cluster[0].arn version = var.k8s_versions.min_master_version vpc_config { - subnet_ids = concat(aws_subnet.private[*].id, aws_subnet.public[*].idu) + subnet_ids = concat(aws_subnet.private[*].id, aws_subnet.public[*].id) } # Ensure that IAM Role permissions are created before and deleted @@ -21,7 +23,10 @@ resource "aws_eks_cluster" "cluster" { ] } + resource "aws_iam_role" "cluster" { + count = var.use_eksctl ? 0 : 1 + name = "${var.cluster_name}-role" assume_role_policy = jsonencode({ Version = "2012-10-17" @@ -41,20 +46,23 @@ resource "aws_iam_role" "cluster" { } resource "aws_iam_role_policy_attachment" "cluster_AmazonEKSClusterPolicy" { + count = var.use_eksctl ? 0 : 1 + + policy_arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy" - role = aws_iam_role.cluster.name + role = aws_iam_role.cluster[0].name } # Declare the data source data "aws_availability_zones" "available" { - state = "available" - region = var.region + state = "available" } resource "aws_vpc" "main" { - region = var.region + count = var.use_eksctl ? 0 : 1 + cidr_block = "192.168.0.0/16" enable_dns_hostnames = true enable_dns_support = true @@ -65,11 +73,10 @@ resource "aws_vpc" "main" { } resource "aws_subnet" "private" { - region = var.region - count = count(aws_availability_zones.available.names) - vpc_id = aws_vpc.main.id + count = var.use_eksctl ? 0 : length(data.aws_availability_zones.available.names) + vpc_id = aws_vpc.main[0].id cidr_block = "192.168.${count.index + 1}.0/24" - availability_zone = aws_availability_zones.available.names[count.index] + availability_zone = data.aws_availability_zones.available.names[count.index] tags = { Name = "${var.cluster_name}-private-${count.index + 1}" @@ -78,11 +85,10 @@ resource "aws_subnet" "private" { } resource "aws_subnet" "public" { - region = var.region - count = count(aws_availability_zones.available.names) - vpc_id = aws_vpc.main.id + count = var.use_eksctl ? 0 : length(data.aws_availability_zones.available.names) + vpc_id = aws_vpc.main[0].id cidr_block = "192.168.${count.index + 10}.0/24" - availability_zone = aws_availability_zones.available.names[count.index] + availability_zone = data.aws_availability_zones.available.names[count.index] map_public_ip_on_launch = true tags = { @@ -92,7 +98,8 @@ resource "aws_subnet" "public" { } resource "aws_iam_role" "node" { - name = "${var.cluster_name}-node" + count = var.use_eksctl ? 0 : 1 + name = "${var.cluster_name}-node" assume_role_policy = jsonencode({ Statement = [{ @@ -108,22 +115,26 @@ resource "aws_iam_role" "node" { # TODO: one role per node group? resource "aws_iam_role_policy_attachment" "node-AmazonEKSWorkerNodePolicy" { + count = var.use_eksctl ? 0 : 1 policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy" - role = aws_iam_role.node.name + role = aws_iam_role.node[0].name } resource "aws_iam_role_policy_attachment" "node-AmazonEKS_CNI_Policy" { + count = var.use_eksctl ? 0 : 1 policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy" - role = aws_iam_role.node.name + role = aws_iam_role.node[0].name } resource "aws_iam_role_policy_attachment" "node-AmazonEC2ContainerRegistryReadOnly" { + count = var.use_eksctl ? 0 : 1 policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly" - role = aws_iam_role.node.name + role = aws_iam_role.node[0].name } resource "aws_launch_template" "core" { - name = "${var.cluster-name}-core-machine" + count = var.use_eksctl ? 0 : 1 + name = "${var.cluster_name}-core-machine" instance_type = var.core_nodes.machine_type @@ -141,17 +152,17 @@ resource "aws_launch_template" "core" { } resource "aws_eks_node_group" "core" { - cluster_name = aws_eks_cluster.cluster.name - region = var.region - node_group_name = "${var.cluster-name}-core-pool" - node_role_arn = aws_iam_role.cluster.arn + count = var.use_eksctl ? 0 : 1 + cluster_name = aws_eks_cluster.cluster[0].name + node_group_name = "${var.cluster_name}-core-pool" + node_role_arn = aws_iam_role.cluster[0].arn subnet_ids = aws_subnet.public[*].id ami_type = "AL2023_x86_64_STANDARD" launch_template { - id = aws_launch_template.core.id + id = aws_launch_template.core[0].id # TODO: check this is correct - version = aws_launch_template.core.default_version + version = aws_launch_template.core[0].default_version } node_repair_config { @@ -159,9 +170,9 @@ resource "aws_eks_node_group" "core" { } scaling_config { - min_size = 1 - max_size = var.core_node_max_count - desired_size = 1 + min_size = var.core_nodes.min + max_size = var.core_nodes.max + desired_size = var.core_nodes.min } update_config { @@ -171,16 +182,18 @@ resource "aws_eks_node_group" "core" { # Ensure that IAM Role permissions are created before and deleted after EKS Node Group handling. # Otherwise, EKS will not be able to properly delete EC2 Instances and Elastic Network Interfaces. depends_on = [ - aws_iam_role_policy_attachment.node-AmazonEKSWorkerNodePolicy, - aws_iam_role_policy_attachment.node-AmazonEKS_CNI_Policy, - aws_iam_role_policy_attachment.node-AmazonEC2ContainerRegistryReadOnly, + aws_iam_role_policy_attachment.node-AmazonEKSWorkerNodePolicy[0], + aws_iam_role_policy_attachment.node-AmazonEKS_CNI_Policy[0], + aws_iam_role_policy_attachment.node-AmazonEC2ContainerRegistryReadOnly[0], ] - taint { - for_each = each.value.taints - key = each.key - effect = each.value.effect - value = each.value.value + dynamic "taint" { + for_each = var.core_nodes.taints + content { + key = each.key + effect = each.value.effect + value = each.value.value + } } /** @@ -194,12 +207,10 @@ resource "aws_eks_node_group" "core" { }, var.core_nodes.labels ) - taint = var.core_nodes.taints - # https://github.com/kubernetes/autoscaler/tree/master/cluster-autoscaler/cloudprovider/aws#auto-discovery-setup tags = merge({ "ManagedBy" : "2i2c", - "2i2c.org/cluster-name" : aws_eks_cluster.cluster.name, + "2i2c.org/cluster_name" : aws_eks_cluster.cluster[0].name, "2i2c:node-purpose" : "core", }, var.core_nodes.tags, @@ -215,8 +226,8 @@ resource "aws_eks_node_group" "core" { } resource "aws_launch_template" "notebook" { - for_each = var.notebook_nodes - name = "${var.cluster-name}-notebook-${each.key}" + for_each = var.use_eksctl ? {} : var.notebook_nodes + name = "${var.cluster_name}-notebook-${each.key}" instance_type = each.value.machine_type @@ -244,20 +255,19 @@ spec: } resource "aws_eks_node_group" "notebook" { - for_each = var.notebook_nodes + for_each = var.use_eksctl ? {} : var.notebook_nodes - cluster_name = aws_eks_cluster.cluster.name - region = var.region - node_group_name = "${var.cluster-name}-notebook-pool" - node_role_arn = aws_iam_role.cluster.arn + cluster_name = aws_eks_cluster.cluster[0].name + node_group_name = "${var.cluster_name}-notebook-pool" + node_role_arn = aws_iam_role.cluster[0].arn subnet_ids = aws_subnet.public[*].id ami_type = "AL2023_x86_64_STANDARD" launch_template { # TODO fixme this seems long winded - id = aws_launch_template.notebook["${var.cluster-name}-notebook-${each.key}"].id + id = aws_launch_template.notebook[each.key].id # TODO: check this is correct - version = aws_launch_template.notebook["${var.cluster-name}-notebook-${each.key}"].default_version + version = aws_launch_template.notebook[each.key].default_version } node_repair_config { @@ -277,16 +287,18 @@ resource "aws_eks_node_group" "notebook" { # Ensure that IAM Role permissions are created before and deleted after EKS Node Group handling. # Otherwise, EKS will not be able to properly delete EC2 Instances and Elastic Network Interfaces. depends_on = [ - aws_iam_role_policy_attachment.node-AmazonEKSWorkerNodePolicy, - aws_iam_role_policy_attachment.node-AmazonEKS_CNI_Policy, - aws_iam_role_policy_attachment.node-AmazonEC2ContainerRegistryReadOnly, + aws_iam_role_policy_attachment.node-AmazonEKSWorkerNodePolicy[0], + aws_iam_role_policy_attachment.node-AmazonEKS_CNI_Policy[0], + aws_iam_role_policy_attachment.node-AmazonEC2ContainerRegistryReadOnly[0], ] - taint { + dynamic "taint" { for_each = each.value.taints - key = each.key - effect = each.value.effect - value = each.value.value + content { + key = each.key + effect = each.value.effect + value = each.value.value + } } labels = merge({ @@ -298,7 +310,7 @@ resource "aws_eks_node_group" "notebook" { tags = merge({ "ManagedBy" : "2i2c", - "2i2c.org/cluster-name" : aws_eks_cluster.cluster.name, + "2i2c.org/cluster_name" : aws_eks_cluster.cluster[0].name, "2i2c:node-purpose" : "user", }, each.value.tags, diff --git a/terraform/aws/main.tf b/terraform/aws/main.tf index 4cd9a71a96..492838ecf4 100644 --- a/terraform/aws/main.tf +++ b/terraform/aws/main.tf @@ -47,5 +47,5 @@ provider "aws" { # ref: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster data "aws_eks_cluster" "cluster" { - name = var.cluster_name + name = var.use_eksctl ? var.cluster_name : aws_eks_cluster.cluster[0].id } diff --git a/terraform/aws/projects/neurohackademy.tfvars b/terraform/aws/projects/neurohackademy.tfvars index fc0f7965ca..f540a46e8f 100644 --- a/terraform/aws/projects/neurohackademy.tfvars +++ b/terraform/aws/projects/neurohackademy.tfvars @@ -1,6 +1,7 @@ region = "us-west-2" cluster_name = "neurohackademy" cluster_nodes_location = "us-west-2a" +use_eksctl = false enable_jupyterhub_cost_monitoring = true @@ -26,6 +27,7 @@ ebs_volumes = { core_nodes = { machine_type : "r8i-flex.large" + max : 1 } notebook_nodes = { "r5-xlarge" : { diff --git a/terraform/aws/variables.tf b/terraform/aws/variables.tf index 2f401d4b3d..9d99262bf4 100644 --- a/terraform/aws/variables.tf +++ b/terraform/aws/variables.tf @@ -19,6 +19,14 @@ variable "cluster_nodes_location" { EOT } +variable "use_eksctl" { + type = bool + description = <<-EOT + Use eksctl to provision infra. + EOT + default = true +} + variable "user_buckets" { type = map( object({ @@ -343,7 +351,7 @@ variable "k8s_versions" { variable "core_nodes" { type = object({ - min : number, + min : optional(number, 1), max : number, machine_type : string, tags : optional(map(string), {}), From 14df1f28fa65c5e05814d96fd29ea0991559ef39 Mon Sep 17 00:00:00 2001 From: Angus Hollands Date: Tue, 30 Jun 2026 14:33:26 +0100 Subject: [PATCH 9/9] chore: remove unused files --- eksctl/neurohackademy.jsonnet | 290 ------------------- terraform/aws/projects/neurohackademy.tfvars | 48 --- 2 files changed, 338 deletions(-) delete mode 100644 eksctl/neurohackademy.jsonnet delete mode 100644 terraform/aws/projects/neurohackademy.tfvars diff --git a/eksctl/neurohackademy.jsonnet b/eksctl/neurohackademy.jsonnet deleted file mode 100644 index 5824794d28..0000000000 --- a/eksctl/neurohackademy.jsonnet +++ /dev/null @@ -1,290 +0,0 @@ -/* - This file is a jsonnet template of a eksctl's cluster configuration file, - that is used with the eksctl CLI to both update and initialize an AWS EKS - based cluster. - - This file has in turn been generated from eksctl/template.jsonnet which is - relevant to compare with for changes over time. - - To use jsonnet to generate an eksctl configuration file from this, do: - - jsonnet neurohackademy.jsonnet > neurohackademy.eksctl.yaml - - References: - - https://eksctl.io/usage/schema/ -*/ -local ng = import './libsonnet/nodegroup.jsonnet'; - -// place all cluster nodes here -local clusterRegion = 'us-west-2'; -local masterAzs = ['us-west-2a', 'us-west-2b', 'us-west-2c']; -local nodeAz = 'us-west-2a'; - -// Node definitions for notebook nodes. Config here is merged -// with our notebook node definition. -// A `node.kubernetes.io/instance-type label is added, so pods -// can request a particular kind of node with a nodeSelector -local notebookNodes = [ - // staging - { - instanceType: 'r5.xlarge', - namePrefix: 'nb-staging', - labels+: { '2i2c/hub-name': 'staging' }, - tags+: { '2i2c:hub-name': 'staging' }, - }, - { - instanceType: 'r5.4xlarge', - namePrefix: 'nb-staging', - labels+: { '2i2c/hub-name': 'staging' }, - tags+: { '2i2c:hub-name': 'staging' }, - }, - { - instanceType: 'r5.16xlarge', - namePrefix: 'nb-staging', - labels+: { '2i2c/hub-name': 'staging' }, - tags+: { '2i2c:hub-name': 'staging' }, - }, - // prod - { - instanceType: 'r5.xlarge', - namePrefix: 'nb-prod', - labels+: { '2i2c/hub-name': 'prod' }, - tags+: { '2i2c:hub-name': 'prod' }, - }, - { - instanceType: 'r5.4xlarge', - namePrefix: 'nb-prod', - labels+: { '2i2c/hub-name': 'prod' }, - tags+: { '2i2c:hub-name': 'prod' }, - }, - { - instanceType: 'r5.16xlarge', - namePrefix: 'nb-prod', - labels+: { '2i2c/hub-name': 'prod' }, - tags+: { '2i2c:hub-name': 'prod' }, - }, - { - instanceType: 'g4dn.xlarge', - namePrefix: 'gpu-staging', - minSize: 0, - labels+: { - '2i2c/hub-name': 'staging', - '2i2c/has-gpu': 'true', - 'k8s.amazonaws.com/accelerator': 'nvidia-tesla-t4', - }, - tags+: { - '2i2c:hub-name': 'staging', - 'k8s.io/cluster-autoscaler/node-template/resources/nvidia.com/gpu': '1', - 'k8s.io/cluster-autoscaler/node-template/label/k8s.amazonaws.com/accelerator': 'nvidia-tesla-t4', - }, - taints+: { - 'nvidia.com/gpu': 'present:NoSchedule', - }, - // Allow provisioning GPUs across all AZs, to prevent situation where all - // GPUs in a single AZ are in use and no new nodes can be spawned - availabilityZones: masterAzs, - }, - { - instanceType: 'g4dn.xlarge', - namePrefix: 'gpu-prod', - minSize: 0, - labels+: { - '2i2c/hub-name': 'prod', - '2i2c/has-gpu': 'true', - 'k8s.amazonaws.com/accelerator': 'nvidia-tesla-t4', - }, - tags+: { - '2i2c:hub-name': 'prod', - 'k8s.io/cluster-autoscaler/node-template/resources/nvidia.com/gpu': '1', - 'k8s.io/cluster-autoscaler/node-template/label/k8s.amazonaws.com/accelerator': 'nvidia-tesla-t4', - }, - taints+: { - 'nvidia.com/gpu': 'present:NoSchedule', - }, - // Allow provisioning GPUs across all AZs, to prevent situation where all - // GPUs in a single AZ are in use and no new nodes can be spawned - availabilityZones: masterAzs, - }, - { - instanceType: 'g4dn.2xlarge', - namePrefix: 'gpu-prod-2xlarge', - minSize: 0, - labels+: { - '2i2c/hub-name': 'prod', - '2i2c/has-gpu': 'true', - 'k8s.amazonaws.com/accelerator': 'nvidia-tesla-t4', - }, - tags+: { - '2i2c:hub-name': 'prod', - 'k8s.io/cluster-autoscaler/node-template/resources/nvidia.com/gpu': '1', - 'k8s.io/cluster-autoscaler/node-template/label/k8s.amazonaws.com/accelerator': 'nvidia-tesla-t4', - }, - taints+: { - 'nvidia.com/gpu': 'present:NoSchedule', - }, - // Allow provisioning GPUs across all AZs, to prevent situation where all - // GPUs in a single AZ are in use and no new nodes can be spawned - availabilityZones: masterAzs, - }, - { - instanceType: 'g4dn.4xlarge', - namePrefix: 'gpu-prod-4xlarge', - minSize: 0, - labels+: { - '2i2c/hub-name': 'prod', - '2i2c/has-gpu': 'true', - 'k8s.amazonaws.com/accelerator': 'nvidia-tesla-t4', - }, - tags+: { - '2i2c:hub-name': 'prod', - 'k8s.io/cluster-autoscaler/node-template/resources/nvidia.com/gpu': '1', - 'k8s.io/cluster-autoscaler/node-template/label/k8s.amazonaws.com/accelerator': 'nvidia-tesla-t4', - }, - taints+: { - 'nvidia.com/gpu': 'present:NoSchedule', - }, - // Allow provisioning GPUs across all AZs, to prevent situation where all - // GPUs in a single AZ are in use and no new nodes can be spawned - availabilityZones: masterAzs, - }, -]; - -local daskNodes = []; - - -{ - apiVersion: 'eksctl.io/v1alpha5', - kind: 'ClusterConfig', - metadata+: { - name: 'neurohackademy', - region: clusterRegion, - version: '1.32', - tags+: { - ManagedBy: '2i2c', - '2i2c.org/cluster-name': $.metadata.name, - }, - }, - availabilityZones: masterAzs, - iam: { - withOIDC: true, - }, - // If you add an addon to this config, run the create addon command. - // - // eksctl create addon --config-file=neurohackademy.eksctl.yaml - // - addons: [ - { version: 'latest', tags: $.metadata.tags } + addon - for addon in - [ - { name: 'coredns' }, - { name: 'kube-proxy' }, - { - // vpc-cni is a Amazon maintained container networking interface - // (CNI), where a CNI is required for k8s networking. The aws-node - // DaemonSet in kube-system stems from installing this. - // - // Related docs: https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/network-plugins/ - // https://docs.aws.amazon.com/eks/latest/userguide/managing-vpc-cni.html - // - name: 'vpc-cni', - attachPolicyARNs: ['arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy'], - // configurationValues ref: https://github.com/aws/amazon-vpc-cni-k8s/blob/HEAD/charts/aws-vpc-cni/values.yaml - configurationValues: ||| - enableNetworkPolicy: "false" - |||, - }, - { - // aws-ebs-csi-driver ensures that our PVCs are bound to PVs that - // couple to AWS EBS based storage, without it expect to see pods - // mounting a PVC failing to schedule and PVC resources that are - // unbound. - // - // Related docs: https://docs.aws.amazon.com/eks/latest/userguide/managing-ebs-csi.html - // - name: 'aws-ebs-csi-driver', - wellKnownPolicies: { - ebsCSIController: true, - }, - // We enable detailed metrics collection to watch for issues with - // jupyterhub-home-nfs - // configurationValues ref: https://github.com/kubernetes-sigs/aws-ebs-csi-driver/blob/HEAD/charts/aws-ebs-csi-driver/values.yaml - configurationValues: ||| - defaultStorageClass: - enabled: true - controller: - enableMetrics: true - node: - enableMetrics: true - |||, - }, - ] - ], - nodeGroups: [ - n { clusterName: $.metadata.name } - for n in - [ - ng { - namePrefix: 'core', - nameSuffix: 'a', - nameIncludeInstanceType: false, - availabilityZones: [nodeAz], - instanceType: 'r5.xlarge', - minSize: 1, - maxSize: 6, - labels+: { - 'hub.jupyter.org/node-purpose': 'core', - 'k8s.dask.org/node-purpose': 'core', - }, - tags+: { - '2i2c:node-purpose': 'core', - }, - }, - ] + [ - ng { - namePrefix: 'nb', - availabilityZones: [nodeAz], - minSize: 0, - maxSize: 500, - instanceType: n.instanceType, - labels+: { - 'hub.jupyter.org/node-purpose': 'user', - 'k8s.dask.org/node-purpose': 'scheduler', - }, - taints+: { - 'hub.jupyter.org_dedicated': 'user:NoSchedule', - 'hub.jupyter.org/dedicated': 'user:NoSchedule', - }, - tags+: { - '2i2c:node-purpose': 'user', - }, - } + n - for n in notebookNodes - ] + ( - if daskNodes != null then - [ - ng { - namePrefix: 'dask', - availabilityZones: [nodeAz], - minSize: 0, - maxSize: 500, - labels+: { - 'k8s.dask.org/node-purpose': 'worker', - }, - taints+: { - 'k8s.dask.org_dedicated': 'worker:NoSchedule', - 'k8s.dask.org/dedicated': 'worker:NoSchedule', - }, - tags+: { - '2i2c:node-purpose': 'worker', - }, - instancesDistribution+: { - onDemandBaseCapacity: 0, - onDemandPercentageAboveBaseCapacity: 0, - spotAllocationStrategy: 'capacity-optimized', - }, - } + n - for n in daskNodes - ] else [] - ) - ], -} diff --git a/terraform/aws/projects/neurohackademy.tfvars b/terraform/aws/projects/neurohackademy.tfvars deleted file mode 100644 index f540a46e8f..0000000000 --- a/terraform/aws/projects/neurohackademy.tfvars +++ /dev/null @@ -1,48 +0,0 @@ -region = "us-west-2" -cluster_name = "neurohackademy" -cluster_nodes_location = "us-west-2a" -use_eksctl = false - -enable_jupyterhub_cost_monitoring = true - -enable_nfs_backup = true - -filestores = {} - -ebs_volumes = { - "staging" = { - size = 5 # in GB - type = "gp3" - name_suffix = "staging" - tags = { "2i2c:hub-name" : "staging" } - } - "prod" = { - size = 1000 # in GB - type = "gp3" - name_suffix = "prod" - tags = { "2i2c:hub-name" : "prod" } - iops = 10000 - } -} - -core_nodes = { - machine_type : "r8i-flex.large" - max : 1 -} -notebook_nodes = { - "r5-xlarge" : { - min : 0, - max : 100, - machine_type : "r5.xlarge", - }, - "r5-4xlarge" : { - min : 0, - max : 100, - machine_type : "r5.4xlarge", - }, - "r5-16xlarge" : { - min : 0, - max : 100, - machine_type : "r5.16xlarge", - } -}