diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 48d04253a..aba839fcc 100755
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.4.0
+ rev: v4.5.0
hooks:
- id: trailing-whitespace
args: ['--markdown-linebreak-ext=md']
@@ -10,7 +10,7 @@ repos:
- id: detect-aws-credentials
args: ['--allow-missing-credentials']
- repo: https://github.com/antonbabenko/pre-commit-terraform
- rev: v1.81.0
+ rev: v1.83.5
hooks:
- id: terraform_fmt
- id: terraform_docs
diff --git a/README.md b/README.md
index 34e72367b..e4b06c80b 100755
--- a/README.md
+++ b/README.md
@@ -1,5 +1,7 @@
![Data on EKS](website/static/img/doeks-logo-green.png)
# [Data on Amazon EKS (DoEKS)](https://awslabs.github.io/data-on-eks/)
+(pronounce Do.eks)
+
[![plan-examples](https://github.com/awslabs/data-on-eks/actions/workflows/plan-examples.yml/badge.svg?branch=main)](https://github.com/awslabs/data-on-eks/actions/workflows/plan-examples.yml)
diff --git a/ai-ml/emr-spark-rapids/eks.tf b/ai-ml/emr-spark-rapids/eks.tf
index 02c60f12d..e1cf92e39 100644
--- a/ai-ml/emr-spark-rapids/eks.tf
+++ b/ai-ml/emr-spark-rapids/eks.tf
@@ -9,6 +9,7 @@ module "eks" {
cluster_name = local.name
cluster_version = var.eks_cluster_version
+ #WARNING: Avoid using this option (cluster_endpoint_public_access = true) in preprod or prod accounts. This feature is designed for sandbox accounts, simplifying cluster deployment and testing.
cluster_endpoint_public_access = true # if true, Your cluster API server is accessible from the internet. You can, optionally, limit the CIDR blocks that can access the public endpoint.
vpc_id = module.vpc.vpc_id
diff --git a/ai-ml/jark-stack/terraform/README.md b/ai-ml/jark-stack/terraform/README.md
new file mode 100644
index 000000000..e7567f85a
--- /dev/null
+++ b/ai-ml/jark-stack/terraform/README.md
@@ -0,0 +1,62 @@
+# JupyterHub, Argo, Ray, Kubernetes
+
+Docs coming soon...
+
+
+## Requirements
+
+| Name | Version |
+|------|---------|
+| [terraform](#requirement\_terraform) | >= 1.0.0 |
+| [aws](#requirement\_aws) | >= 3.72 |
+| [helm](#requirement\_helm) | >= 2.4.1 |
+| [http](#requirement\_http) | >= 3.3 |
+| [kubectl](#requirement\_kubectl) | >= 1.14 |
+| [kubernetes](#requirement\_kubernetes) | >= 2.10 |
+| [random](#requirement\_random) | >= 3.1 |
+
+## Providers
+
+| Name | Version |
+|------|---------|
+| [aws](#provider\_aws) | >= 3.72 |
+| [kubernetes](#provider\_kubernetes) | >= 2.10 |
+
+## Modules
+
+| Name | Source | Version |
+|------|--------|---------|
+| [data\_addons](#module\_data\_addons) | aws-ia/eks-data-addons/aws | ~> 1.1 |
+| [ebs\_csi\_driver\_irsa](#module\_ebs\_csi\_driver\_irsa) | terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks | ~> 5.20 |
+| [eks](#module\_eks) | terraform-aws-modules/eks/aws | ~> 19.15 |
+| [eks\_blueprints\_addons](#module\_eks\_blueprints\_addons) | aws-ia/eks-blueprints-addons/aws | ~> 1.2 |
+| [vpc](#module\_vpc) | terraform-aws-modules/vpc/aws | ~> 5.0 |
+
+## Resources
+
+| Name | Type |
+|------|------|
+| [kubernetes_annotations.disable_gp2](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/annotations) | resource |
+| [kubernetes_config_map_v1.notebook](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/config_map_v1) | resource |
+| [kubernetes_namespace_v1.jupyterhub](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/namespace_v1) | resource |
+| [kubernetes_secret_v1.huggingface_token](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/secret_v1) | resource |
+| [kubernetes_storage_class.default_gp3](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/storage_class) | resource |
+| [aws_eks_cluster_auth.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/eks_cluster_auth) | data source |
+
+## Inputs
+
+| Name | Description | Type | Default | Required |
+|------|-------------|------|---------|:--------:|
+| [eks\_cluster\_version](#input\_eks\_cluster\_version) | EKS Cluster version | `string` | `"1.27"` | no |
+| [huggingface\_token](#input\_huggingface\_token) | Hugging Face Secret Token | `string` | `"DUMMY_TOKEN_REPLACE_ME"` | no |
+| [name](#input\_name) | Name of the VPC and EKS Cluster | `string` | `"jark-stack"` | no |
+| [region](#input\_region) | region | `string` | `"us-west-2"` | no |
+| [secondary\_cidr\_blocks](#input\_secondary\_cidr\_blocks) | Secondary CIDR blocks to be attached to VPC | `list(string)` |
[
"100.64.0.0/16"
]
| no |
+| [vpc\_cidr](#input\_vpc\_cidr) | VPC CIDR. This should be a valid private (RFC 1918) CIDR range | `string` | `"10.1.0.0/21"` | no |
+
+## Outputs
+
+| Name | Description |
+|------|-------------|
+| [configure\_kubectl](#output\_configure\_kubectl) | Configure kubectl: make sure you're logged in with the correct AWS profile and run the following command to update your kubeconfig |
+
diff --git a/ai-ml/jark-stack/terraform/addons.tf b/ai-ml/jark-stack/terraform/addons.tf
new file mode 100644
index 000000000..3f2a91a2e
--- /dev/null
+++ b/ai-ml/jark-stack/terraform/addons.tf
@@ -0,0 +1,186 @@
+#---------------------------------------------------------------
+# GP3 Encrypted Storage Class
+#---------------------------------------------------------------
+resource "kubernetes_annotations" "disable_gp2" {
+ annotations = {
+ "storageclass.kubernetes.io/is-default-class" : "false"
+ }
+ api_version = "storage.k8s.io/v1"
+ kind = "StorageClass"
+ metadata {
+ name = "gp2"
+ }
+ force = true
+
+ depends_on = [module.eks.eks_cluster_id]
+}
+
+resource "kubernetes_storage_class" "default_gp3" {
+ metadata {
+ name = "gp3"
+ annotations = {
+ "storageclass.kubernetes.io/is-default-class" : "true"
+ }
+ }
+
+ storage_provisioner = "ebs.csi.aws.com"
+ reclaim_policy = "Delete"
+ allow_volume_expansion = true
+ volume_binding_mode = "WaitForFirstConsumer"
+ parameters = {
+ fsType = "ext4"
+ encrypted = true
+ type = "gp3"
+ }
+
+ depends_on = [kubernetes_annotations.disable_gp2]
+}
+
+#---------------------------------------------------------------
+# IRSA for EBS CSI Driver
+#---------------------------------------------------------------
+module "ebs_csi_driver_irsa" {
+ source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks"
+ version = "~> 5.20"
+ role_name_prefix = format("%s-%s-", local.name, "ebs-csi-driver")
+ attach_ebs_csi_policy = true
+ oidc_providers = {
+ main = {
+ provider_arn = module.eks.oidc_provider_arn
+ namespace_service_accounts = ["kube-system:ebs-csi-controller-sa"]
+ }
+ }
+ tags = local.tags
+}
+
+#---------------------------------------------------------------
+# EKS Blueprints Addons
+#---------------------------------------------------------------
+module "eks_blueprints_addons" {
+ source = "aws-ia/eks-blueprints-addons/aws"
+ version = "~> 1.2"
+
+ cluster_name = module.eks.cluster_name
+ cluster_endpoint = module.eks.cluster_endpoint
+ cluster_version = module.eks.cluster_version
+ oidc_provider_arn = module.eks.oidc_provider_arn
+
+ #---------------------------------------
+ # Amazon EKS Managed Add-ons
+ #---------------------------------------
+ eks_addons = {
+ aws-ebs-csi-driver = {
+ service_account_role_arn = module.ebs_csi_driver_irsa.iam_role_arn
+ }
+ coredns = {
+ preserve = true
+ }
+ kube-proxy = {
+ preserve = true
+ }
+ # VPC CNI uses worker node IAM role policies
+ vpc-cni = {
+ preserve = true
+ }
+ }
+
+ #---------------------------------------
+ # AWS Load Balancer Controller Add-on
+ #---------------------------------------
+ enable_aws_load_balancer_controller = true
+ # turn off the mutating webhook for services because we are using
+ # service.beta.kubernetes.io/aws-load-balancer-type: external
+ aws_load_balancer_controller = {
+ set = [{
+ name = "enableServiceMutatorWebhook"
+ value = "false"
+ }]
+ }
+
+ #---------------------------------------
+ # Ingress Nginx Add-on
+ #---------------------------------------
+ enable_ingress_nginx = true
+ ingress_nginx = {
+ values = [templatefile("${path.module}/helm-values/ingress-nginx-values.yaml", {})]
+ }
+
+ helm_releases = {
+ #---------------------------------------
+ # NVIDIA Device Plugin Add-on
+ #---------------------------------------
+ nvidia-device-plugin = {
+ description = "A Helm chart for NVIDIA Device Plugin"
+ namespace = "nvidia-device-plugin"
+ create_namespace = true
+ chart = "nvidia-device-plugin"
+ chart_version = "0.14.0"
+ repository = "https://nvidia.github.io/k8s-device-plugin"
+ values = [file("${path.module}/helm-values/nvidia-values.yaml")]
+ }
+ }
+}
+
+#---------------------------------------------------------------
+# Data on EKS Kubernetes Addons
+#---------------------------------------------------------------
+module "data_addons" {
+ source = "aws-ia/eks-data-addons/aws"
+ version = "~> 1.1" # ensure to update this to the latest/desired version
+
+ oidc_provider_arn = module.eks.oidc_provider_arn
+
+ #---------------------------------------------------------------
+ # JupyterHub Add-on
+ #---------------------------------------------------------------
+ enable_jupyterhub = true
+ jupyterhub_helm_config = {
+ namespace = kubernetes_namespace_v1.jupyterhub.id
+ create_namespace = false
+ values = [file("${path.module}/helm-values/jupyterhub-values.yaml")]
+ }
+
+ #---------------------------------------------------------------
+ # KubeRay Operator Add-on
+ #---------------------------------------------------------------
+ enable_kuberay_operator = true
+
+ depends_on = [
+ kubernetes_secret_v1.huggingface_token,
+ kubernetes_config_map_v1.notebook
+ ]
+}
+
+
+#---------------------------------------------------------------
+# Additional Resources
+#---------------------------------------------------------------
+
+resource "kubernetes_namespace_v1" "jupyterhub" {
+ metadata {
+ name = "jupyterhub"
+ }
+}
+
+
+resource "kubernetes_secret_v1" "huggingface_token" {
+ metadata {
+ name = "hf-token"
+ namespace = kubernetes_namespace_v1.jupyterhub.id
+ }
+
+ data = {
+ token = var.huggingface_token
+ }
+}
+
+resource "kubernetes_config_map_v1" "notebook" {
+ metadata {
+ name = "notebook"
+ namespace = kubernetes_namespace_v1.jupyterhub.id
+ }
+
+ data = {
+ "dogbooth.ipynb" = file("${path.module}/src/notebook/dogbooth.ipynb")
+ }
+}
diff --git a/ai-ml/jark-stack/terraform/cleanup.sh b/ai-ml/jark-stack/terraform/cleanup.sh
new file mode 100755
index 000000000..797c2de67
--- /dev/null
+++ b/ai-ml/jark-stack/terraform/cleanup.sh
@@ -0,0 +1,74 @@
+#!/bin/bash
+
+read -p "Enter the region: " region
+export AWS_DEFAULT_REGION=$region
+
+echo "Destroying RayService..."
+
+# Delete the Ingress/SVC before removing the addons
+TMPFILE=$(mktemp)
+terraform -chdir=$SCRIPTDIR output -raw configure_kubectl > "$TMPFILE"
+# check if TMPFILE contains the string "No outputs found"
+if [[ ! $(cat $TMPFILE) == *"No outputs found"* ]]; then
+ echo "No outputs found, skipping kubectl delete"
+ source "$TMPFILE"
+ kubectl delete -f src/service/ray-service.yaml
+fi
+
+
+# List of Terraform modules to apply in sequence
+targets=(
+ "module.data_addons"
+ "module.eks_blueprints_addons"
+ "module.eks"
+ "module.vpc"
+)
+
+# Destroy modules in sequence
+for target in "${targets[@]}"
+do
+ echo "Destroying module $target..."
+ destroy_output=$(terraform destroy -target="$target" -var="region=$region" -auto-approve 2>&1 | tee /dev/tty)
+ if [[ ${PIPESTATUS[0]} -eq 0 && $destroy_output == *"Destroy complete"* ]]; then
+ echo "SUCCESS: Terraform destroy of $target completed successfully"
+ else
+ echo "FAILED: Terraform destroy of $target failed"
+ exit 1
+ fi
+done
+
+echo "Destroying Load Balancers..."
+
+for arn in $(aws resourcegroupstaggingapi get-resources \
+ --resource-type-filters elasticloadbalancing:loadbalancer \
+ --tag-filters "Key=elbv2.k8s.aws/cluster,Values=jark-stack" \
+ --query 'ResourceTagMappingList[].ResourceARN' \
+ --output text); do \
+ aws elbv2 delete-load-balancer --load-balancer-arn "$arn"; \
+ done
+
+echo "Destroying Target Groups..."
+for arn in $(aws resourcegroupstaggingapi get-resources \
+ --resource-type-filters elasticloadbalancing:targetgroup \
+ --tag-filters "Key=elbv2.k8s.aws/cluster,Values=jark-stack" \
+ --query 'ResourceTagMappingList[].ResourceARN' \
+ --output text); do \
+ aws elbv2 delete-target-group --target-group-arn "$arn"; \
+ done
+
+echo "Destroying Security Groups..."
+for sg in $(aws ec2 describe-security-groups \
+ --filters "Name=tag:elbv2.k8s.aws/cluster,Values=jark-stack" \
+ --query 'SecurityGroups[].GroupId' --output text); do \
+ aws ec2 delete-security-group --group-id "$sg"; \
+ done
+
+## Final destroy to catch any remaining resources
+echo "Destroying remaining resources..."
+destroy_output=$(terraform destroy -var="region=$region"-auto-approve 2>&1 | tee /dev/tty)
+if [[ ${PIPESTATUS[0]} -eq 0 && $destroy_output == *"Destroy complete"* ]]; then
+ echo "SUCCESS: Terraform destroy of all modules completed successfully"
+else
+ echo "FAILED: Terraform destroy of all modules failed"
+ exit 1
+fi
diff --git a/ai-ml/jark-stack/terraform/eks.tf b/ai-ml/jark-stack/terraform/eks.tf
new file mode 100644
index 000000000..04c7fd409
--- /dev/null
+++ b/ai-ml/jark-stack/terraform/eks.tf
@@ -0,0 +1,151 @@
+#---------------------------------------------------------------
+# EKS Cluster
+#---------------------------------------------------------------
+module "eks" {
+ source = "terraform-aws-modules/eks/aws"
+ version = "~> 19.15"
+
+ cluster_name = local.name
+ cluster_version = var.eks_cluster_version
+
+ # if true, Your cluster API server is accessible from the internet.
+ # You can, optionally, limit the CIDR blocks that can access the public endpoint.
+ #WARNING: Avoid using this option (cluster_endpoint_public_access = true) in preprod or prod accounts. This feature is designed for sandbox accounts, simplifying cluster deployment and testing.
+ cluster_endpoint_public_access = true
+
+ vpc_id = module.vpc.vpc_id
+ # Filtering only Secondary CIDR private subnets starting with "100.".
+ # Subnet IDs where the EKS Control Plane ENIs will be created
+ subnet_ids = compact([for subnet_id, cidr_block in zipmap(module.vpc.private_subnets, module.vpc.private_subnets_cidr_blocks) :
+ substr(cidr_block, 0, 4) == "100." ? subnet_id : null])
+
+ manage_aws_auth_configmap = true
+ aws_auth_roles = [
+ # We need to add in the Karpenter node IAM role for nodes launched by Karpenter
+ {
+ rolearn = module.eks_blueprints_addons.karpenter.node_iam_role_arn
+ username = "system:node:{{EC2PrivateDNSName}}"
+ groups = [
+ "system:bootstrappers",
+ "system:nodes",
+ ]
+ }
+ ]
+ #---------------------------------------
+ # Note: This can further restricted to specific required for each Add-on and your application
+ #---------------------------------------
+ # Extend cluster security group rules
+ cluster_security_group_additional_rules = {
+ ingress_nodes_ephemeral_ports_tcp = {
+ description = "Nodes on ephemeral ports"
+ protocol = "tcp"
+ from_port = 0
+ to_port = 65535
+ type = "ingress"
+ source_node_security_group = true
+ }
+ }
+
+ node_security_group_additional_rules = {
+ # Allows Control Plane Nodes to talk to Worker nodes on all ports.
+ # Added this to simplify the example and further avoid issues with Add-ons communication with Control plane.
+ # This can be restricted further to specific port based on the requirement for each Add-on
+ # e.g., coreDNS 53, metrics-server 4443.
+ # Update this according to your security requirements if needed
+ ingress_cluster_to_node_all_traffic = {
+ description = "Cluster API to Nodegroup all traffic"
+ protocol = "-1"
+ from_port = 0
+ to_port = 0
+ type = "ingress"
+ source_cluster_security_group = true
+ }
+ }
+
+ eks_managed_node_group_defaults = {
+ iam_role_additional_policies = {
+ # Not required, but used in the example to access the nodes to inspect mounted volumes
+ AmazonSSMManagedInstanceCore = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
+ }
+
+ ebs_optimized = true
+ # This block device is used only for root volume. Adjust volume according to your size.
+ # NOTE: Don't use this volume for ML workloads
+ block_device_mappings = {
+ xvda = {
+ device_name = "/dev/xvda"
+ ebs = {
+ volume_size = 100
+ volume_type = "gp3"
+ }
+ }
+ }
+ }
+
+ eks_managed_node_groups = {
+ # It's recommended to have a Managed Node group for hosting critical add-ons
+ # It's recommeded to use Karpenter to place your workloads instead of using Managed Node groups
+ # You can leverage nodeSelector and Taints/tolerations to distribute workloads across Managed Node group or Karpenter nodes.
+ core_node_group = {
+ name = "core-node-group"
+ description = "EKS Core node group for hosting system add-ons"
+ # Filtering only Secondary CIDR private subnets starting with "100.".
+ # Subnet IDs where the nodes/node groups will be provisioned
+ subnet_ids = compact([for subnet_id, cidr_block in zipmap(module.vpc.private_subnets, module.vpc.private_subnets_cidr_blocks) :
+ substr(cidr_block, 0, 4) == "100." ? subnet_id : null]
+ )
+
+ # aws ssm get-parameters --names /aws/service/eks/optimized-ami/1.27/amazon-linux-2/recommended/image_id --region us-west-2
+ ami_type = "AL2_x86_64" # Use this for Graviton AL2_ARM_64
+ min_size = 2
+ max_size = 8
+ desired_size = 2
+
+ instance_types = ["m5.xlarge"]
+
+ labels = {
+ WorkerType = "ON_DEMAND"
+ NodeGroupType = "core"
+ }
+
+ tags = merge(local.tags, {
+ Name = "core-node-grp"
+ })
+ }
+
+ # GPU Nodegroup for JupyterHub Notebook and Ray Service
+ gpu1 = {
+ name = "gpu-node-grp"
+ description = "EKS Node Group to run GPU workloads"
+ # Filtering only Secondary CIDR private subnets starting with "100.".
+ # Subnet IDs where the nodes/node groups will be provisioned
+ subnet_ids = compact([for subnet_id, cidr_block in zipmap(module.vpc.private_subnets, module.vpc.private_subnets_cidr_blocks) :
+ substr(cidr_block, 0, 4) == "100." ? subnet_id : null]
+ )
+
+ ami_type = "AL2_x86_64_GPU"
+ min_size = 1
+ max_size = 1
+ desired_size = 1
+
+ instance_types = ["g5.12xlarge"]
+
+ labels = {
+ WorkerType = "ON_DEMAND"
+ NodeGroupType = "gpu"
+ }
+
+ taints = {
+ gpu = {
+ key = "nvidia.com/gpu"
+ effect = "NO_SCHEDULE"
+ operator = "EXISTS"
+ }
+ }
+
+ tags = merge(local.tags, {
+ Name = "gpu-node-grp"
+ })
+ }
+ }
+}
diff --git a/ai-ml/jark-stack/terraform/helm-values/ingress-nginx-values.yaml b/ai-ml/jark-stack/terraform/helm-values/ingress-nginx-values.yaml
new file mode 100644
index 000000000..c8b1a5d74
--- /dev/null
+++ b/ai-ml/jark-stack/terraform/helm-values/ingress-nginx-values.yaml
@@ -0,0 +1,11 @@
+controller:
+ service:
+ externalTrafficPolicy: "Local"
+ annotations:
+ service.beta.kubernetes.io/aws-load-balancer-type: external
+ service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
+ service.beta.kubernetes.io/aws-load-balancer-backend-protocol: http
+ service.beta.kubernetes.io/aws-load-balancer-scheme: internal # Private Load Balancer can only be accessed within the VPC
+ targetPorts:
+ http: http
+ https: http
diff --git a/ai-ml/jark-stack/terraform/helm-values/jupyterhub-values.yaml b/ai-ml/jark-stack/terraform/helm-values/jupyterhub-values.yaml
new file mode 100644
index 000000000..fcad06b62
--- /dev/null
+++ b/ai-ml/jark-stack/terraform/helm-values/jupyterhub-values.yaml
@@ -0,0 +1,59 @@
+hub:
+ config:
+ Authenticator:
+ admin_users:
+ - admin1
+ allowed_users:
+ - user1
+ # testing only - do not do this for production
+ DummyAuthenticator:
+ password: never-do-this
+ JupyterHub:
+ authenticator_class: dummy
+proxy:
+ service:
+ annotations:
+ service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
+ service.beta.kubernetes.io/aws-load-balancer-scheme: internal # Private Load Balancer can only be accessed within the VPC
+ service.beta.kubernetes.io/aws-load-balancer-type: external
+ service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: 'true'
+ service.beta.kubernetes.io/aws-load-balancer-ip-address-type: ipv4
+singleuser:
+ image:
+ name: public.ecr.aws/h3o5n2r0/gpu-jupyter
+ tag: v1.5_cuda-11.6_ubuntu-20.04_python-only
+ pullPolicy: Always
+ cmd: null
+ startTimeout: 600
+ memory:
+ guarantee: 24G
+ extraResource:
+ limits:
+ nvidia.com/gpu: "1"
+ extraEnv:
+ HUGGING_FACE_HUB_TOKEN:
+ valueFrom:
+ secretKeyRef:
+ name: hf-token
+ key: token
+ storage:
+ capacity: 100Gi
+ extraVolumes:
+ - name: shm-volume
+ emptyDir:
+ medium: Memory
+ - name: notebook
+ configMap:
+ name: notebook
+ extraVolumeMounts:
+ - name: shm-volume
+ mountPath: /dev/shm
+ - name: notebook
+ mountPath: /home/jovyan/dogbooth
+ extraTolerations:
+ - key: nvidia.com/gpu
+ operator: Exists
+ effect: NoSchedule
+scheduling:
+ userScheduler:
+ enabled: false
diff --git a/ai-ml/jark-stack/terraform/helm-values/nvidia-values.yaml b/ai-ml/jark-stack/terraform/helm-values/nvidia-values.yaml
new file mode 100644
index 000000000..9fa59599e
--- /dev/null
+++ b/ai-ml/jark-stack/terraform/helm-values/nvidia-values.yaml
@@ -0,0 +1,10 @@
+gfd:
+ enabled: true
+nfd:
+ enabled: true
+ worker:
+ tolerations:
+ - key: nvidia.com/gpu
+ operator: Exists
+ effect: NoSchedule
+ - operator: "Exists"
diff --git a/ai-ml/jark-stack/terraform/install.sh b/ai-ml/jark-stack/terraform/install.sh
new file mode 100755
index 000000000..18f2a94d3
--- /dev/null
+++ b/ai-ml/jark-stack/terraform/install.sh
@@ -0,0 +1,36 @@
+#!/bin/bash
+
+read -p "Enter the region: " region
+export AWS_DEFAULT_REGION=$region
+
+# List of Terraform modules to apply in sequence
+targets=(
+ "module.vpc"
+ "module.eks"
+)
+
+# Initialize Terraform
+terraform init -upgrade
+
+# Apply modules in sequence
+for target in "${targets[@]}"
+do
+ echo "Applying module $target..."
+ apply_output=$(terraform apply -target="$target" -var="region=$region" -auto-approve 2>&1 | tee /dev/tty)
+ if [[ ${PIPESTATUS[0]} -eq 0 && $apply_output == *"Apply complete"* ]]; then
+ echo "SUCCESS: Terraform apply of $target completed successfully"
+ else
+ echo "FAILED: Terraform apply of $target failed"
+ exit 1
+ fi
+done
+
+# Final apply to catch any remaining resources
+echo "Applying remaining resources..."
+apply_output=$(terraform apply -var="region=$region" -auto-approve 2>&1 | tee /dev/tty)
+if [[ ${PIPESTATUS[0]} -eq 0 && $apply_output == *"Apply complete"* ]]; then
+ echo "SUCCESS: Terraform apply of all modules completed successfully"
+else
+ echo "FAILED: Terraform apply of all modules failed"
+ exit 1
+fi
diff --git a/ai-ml/jark-stack/terraform/main.tf b/ai-ml/jark-stack/terraform/main.tf
new file mode 100644
index 000000000..bbbb966cd
--- /dev/null
+++ b/ai-ml/jark-stack/terraform/main.tf
@@ -0,0 +1,38 @@
+provider "aws" {
+ region = local.region
+}
+
+provider "kubernetes" {
+ host = module.eks.cluster_endpoint
+ cluster_ca_certificate = base64decode(module.eks.cluster_certificate_authority_data)
+ token = data.aws_eks_cluster_auth.this.token
+}
+
+provider "helm" {
+ kubernetes {
+ host = module.eks.cluster_endpoint
+ cluster_ca_certificate = base64decode(module.eks.cluster_certificate_authority_data)
+ token = data.aws_eks_cluster_auth.this.token
+ }
+}
+provider "kubectl" {
+ apply_retry_count = 30
+ host = module.eks.cluster_endpoint
+ cluster_ca_certificate = base64decode(module.eks.cluster_certificate_authority_data)
+ token = data.aws_eks_cluster_auth.this.token
+ load_config_file = false
+}
+
+data "aws_eks_cluster_auth" "this" {
+ name = module.eks.cluster_name
+}
+
+locals {
+ name = var.name
+ region = var.region
+ azs = ["${local.region}c", "${local.region}d"]
+ tags = {
+ Blueprint = local.name
+ GithubRepo = "github.com/awslabs/data-on-eks"
+ }
+}
diff --git a/ai-ml/jark-stack/terraform/outputs.tf b/ai-ml/jark-stack/terraform/outputs.tf
new file mode 100644
index 000000000..f6444daab
--- /dev/null
+++ b/ai-ml/jark-stack/terraform/outputs.tf
@@ -0,0 +1,4 @@
+output "configure_kubectl" {
+ description = "Configure kubectl: make sure you're logged in with the correct AWS profile and run the following command to update your kubeconfig"
+ value = "aws eks --region ${var.region} update-kubeconfig --name ${var.name}"
+}
diff --git a/ai-ml/jark-stack/terraform/src/app/Dockerfile b/ai-ml/jark-stack/terraform/src/app/Dockerfile
new file mode 100644
index 000000000..afa9fc5ee
--- /dev/null
+++ b/ai-ml/jark-stack/terraform/src/app/Dockerfile
@@ -0,0 +1,24 @@
+FROM python:3.8-slim
+
+RUN groupadd --gid 1000 appuser \
+ && useradd --uid 1000 --gid 1000 -ms /bin/bash appuser
+
+RUN pip3 install --no-cache-dir --upgrade \
+ pip \
+ virtualenv
+
+RUN apt-get update && apt-get install -y
+
+USER appuser
+WORKDIR /home/appuser
+
+ENV VIRTUAL_ENV=/home/appuser/venv
+RUN virtualenv ${VIRTUAL_ENV}
+RUN . ${VIRTUAL_ENV}/bin/activate && \
+ pip install requests streamlit Pillow
+
+EXPOSE 8501
+
+COPY streamlit.py /home/appuser/
+COPY run.sh /home/appuser
+ENTRYPOINT ["./run.sh"]
diff --git a/ai-ml/jark-stack/terraform/src/app/run.sh b/ai-ml/jark-stack/terraform/src/app/run.sh
new file mode 100644
index 000000000..0c52b8acc
--- /dev/null
+++ b/ai-ml/jark-stack/terraform/src/app/run.sh
@@ -0,0 +1,26 @@
+#!/bin/bash
+
+APP_PID=
+stopRunningProcess() {
+ # Based on https://linuxconfig.org/how-to-propagate-a-signal-to-child-processes-from-a-bash-script
+ if test ! "${APP_PID}" = '' && ps -p ${APP_PID} > /dev/null ; then
+ > /proc/1/fd/1 echo "Stopping ${COMMAND_PATH} which is running with process ID ${APP_PID}"
+
+ kill -TERM ${APP_PID}
+ > /proc/1/fd/1 echo "Waiting for ${COMMAND_PATH} to process SIGTERM signal"
+
+ wait ${APP_PID}
+ > /proc/1/fd/1 echo "All processes have stopped running"
+ else
+ > /proc/1/fd/1 echo "${COMMAND_PATH} was not started when the signal was sent or it has already been stopped"
+ fi
+}
+
+trap stopRunningProcess EXIT TERM
+
+source ${VIRTUAL_ENV}/bin/activate
+
+streamlit run ${HOME}/streamlit_app.py &
+APP_ID=${!}
+
+wait ${APP_ID}
diff --git a/ai-ml/jark-stack/terraform/src/app/streamlit.py b/ai-ml/jark-stack/terraform/src/app/streamlit.py
new file mode 100644
index 000000000..2448e114e
--- /dev/null
+++ b/ai-ml/jark-stack/terraform/src/app/streamlit.py
@@ -0,0 +1,33 @@
+import streamlit as st
+import requests
+from urllib.parse import urlencode
+from PIL import Image
+import tempfile
+
+
+### Update Hostname before building image
+base_url=""
+
+st.title("Welcome to dogbooth! :dog:")
+st.header("_a place to create images of [v]dog in beautiful scenes._")
+
+
+prompt = st.chat_input("a photo of a [v]dog ...")
+if prompt:
+ query_params = {
+ "prompt": prompt
+ }
+ encoded_query = urlencode(query_params)
+ image_url = f"{base_url}?{encoded_query}"
+
+ with st.spinner("Wait for it..."):
+ response = requests.get(image_url, timeout=180)
+
+ if response.status_code == 200:
+ content_size = len(response.content)
+ with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as f:
+ f.write(response.content)
+ st.image(Image.open(f.name), caption=prompt)
+ st.balloons()
+ else:
+ st.error(f"Failed to download image. Status code: {response.status_code}")
diff --git a/ai-ml/jark-stack/terraform/src/app/streamlit.yaml b/ai-ml/jark-stack/terraform/src/app/streamlit.yaml
new file mode 100644
index 000000000..ae3fc0df5
--- /dev/null
+++ b/ai-ml/jark-stack/terraform/src/app/streamlit.yaml
@@ -0,0 +1,83 @@
+---
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: dogbooth-app
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: streamlit-deployment
+ namespace: dogbooth-app
+ labels:
+ app: streamlit
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: streamlit
+ template:
+ metadata:
+ labels:
+ app: streamlit
+ spec:
+ containers:
+ - name: streamlit
+ image: public.ecr.aws/h3o5n2r0/gen-ai-demo/dogbooth-app:0.0.2
+ imagePullPolicy: Always
+ ports:
+ - containerPort: 8501
+ livenessProbe:
+ httpGet:
+ path: /_stcore/health
+ port: 8501
+ scheme: HTTP
+ timeoutSeconds: 1
+ readinessProbe:
+ httpGet:
+ path: /_stcore/health
+ port: 8501
+ scheme: HTTP
+ timeoutSeconds: 1
+ resources:
+ limits:
+ cpu: 1
+ memory: 2Gi
+ requests:
+ cpu: 100m
+ memory: 745Mi
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: streamlit-service
+ namespace: dogbooth-app
+spec:
+ type: ClusterIP
+ selector:
+ app: streamlit
+ ports:
+ - name: streamlit-port
+ protocol: TCP
+ port: 8501
+ targetPort: 8501
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: dogbooth-app
+ namespace: dogbooth-app
+ annotations:
+ nginx.ingress.kubernetes.io/rewrite-target: "/$1"
+spec:
+ ingressClassName: nginx
+ rules:
+ - http:
+ paths:
+ - path: /dogbooth/app/(.*)
+ pathType: ImplementationSpecific
+ backend:
+ service:
+ name: streamlit-service
+ port:
+ number: 8501
diff --git a/ai-ml/jark-stack/terraform/src/notebook/Dockerfile b/ai-ml/jark-stack/terraform/src/notebook/Dockerfile
new file mode 100644
index 000000000..da5c32dd7
--- /dev/null
+++ b/ai-ml/jark-stack/terraform/src/notebook/Dockerfile
@@ -0,0 +1,10 @@
+FROM cschranz/gpu-jupyter:v1.5_cuda-11.6_ubuntu-20.04_python-only
+
+USER root
+
+RUN conda install -c anaconda libstdcxx-ng
+
+RUN cp /opt/conda/lib/libstdc++.so.6.0.31 /usr/lib/x86_64-linux-gnu/ && \
+ cd /usr/lib/x86_64-linux-gnu && \
+ rm -f libstdc++.so.6 && \
+ ln -s libstdc++.so.6.0.31 libstdc++.so.6
diff --git a/ai-ml/jark-stack/terraform/src/notebook/dogbooth.ipynb b/ai-ml/jark-stack/terraform/src/notebook/dogbooth.ipynb
new file mode 100644
index 000000000..0e54206b9
--- /dev/null
+++ b/ai-ml/jark-stack/terraform/src/notebook/dogbooth.ipynb
@@ -0,0 +1,275 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "vscode": {
+ "languageId": "plaintext"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "# Verify NVIDIA GPU is visible\n",
+ "!nvidia-smi"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "vscode": {
+ "languageId": "plaintext"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "import os\n",
+ "os.chdir(\"/home/jovyan\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "vscode": {
+ "languageId": "plaintext"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "# Clone the diffusers repo\n",
+ "!git clone https://github.com/huggingface/diffusers"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "vscode": {
+ "languageId": "plaintext"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "# Change the directory\n",
+ "os.chdir(\"diffusers\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "vscode": {
+ "languageId": "plaintext"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "# Install requirements\n",
+ "! pip install -e .\n",
+ "! pip install xformers==0.0.16 diffusers[torch]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "vscode": {
+ "languageId": "plaintext"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "# Fix for bitsandbytes https://github.com/TimDettmers/bitsandbytes/blob/main/how_to_use_nonpytorch_cuda.md\n",
+ "! wget https://raw.githubusercontent.com/TimDettmers/bitsandbytes/main/cuda_install.sh\n",
+ "! bash cuda_install.sh 117 ~/local 1"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "vscode": {
+ "languageId": "plaintext"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "# Install bitsandbytes for optimizations\n",
+ "! pip install bitsandbytes==0.41.0"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "vscode": {
+ "languageId": "plaintext"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "# Use the newly installed CUDA version for bitsandbytes\n",
+ "os.environ[\"BNB_CUDA_VERSION\"] = \"117\"\n",
+ "os.environ[\"LD_LIBRARY_PATH\"] = os.getenv(\"LD_LIBRARY_PATH\") + \":/home/jovyan/local/cuda-11.7\""
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "vscode": {
+ "languageId": "plaintext"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "# Validate successful install of bitsandbytes\n",
+ "! python -m bitsandbytes"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "vscode": {
+ "languageId": "plaintext"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "# Install requirements for dreambooth\n",
+ "os.chdir(\"examples/dreambooth\")\n",
+ "! pip install -r requirements.txt"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "vscode": {
+ "languageId": "plaintext"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "# Setup default configuration for accelerate\n",
+ "! accelerate config default"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "vscode": {
+ "languageId": "plaintext"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "# Login to huggingface associated with your account (please create one if it doesn't exist)\n",
+ "! huggingface-cli login --token $HUGGING_FACE_HUB_TOKEN"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "vscode": {
+ "languageId": "plaintext"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "# Download sample dataset of the subject. See the sample images here https://huggingface.co/datasets/diffusers/dog-example\n",
+ "from huggingface_hub import snapshot_download\n",
+ "\n",
+ "local_dir = \"./dog\"\n",
+ "snapshot_download(\n",
+ " \"diffusers/dog-example\",\n",
+ " local_dir=local_dir, repo_type=\"dataset\",\n",
+ " ignore_patterns=\".gitattributes\",\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "vscode": {
+ "languageId": "plaintext"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "# Export environment variables to provide input model, dataset directory and output directory for the tuned model\n",
+ "os.environ[\"MODEL_NAME\"] = \"stabilityai/stable-diffusion-2-1\"\n",
+ "os.environ[\"INSTANCE_DIR\"] = \"dog\"\n",
+ "os.environ[\"OUTPUT_DIR\"] = \"dogbooth\"\n",
+ "os.environ[\"RESOLUTION\"] = \"768\"\n",
+ "os.environ[\"PYTORCH_CUDA_ALLOC_CONF\"] = \"garbage_collection_threshold:0.6,max_split_size_mb:128\""
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "vscode": {
+ "languageId": "plaintext"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "# Launch the training and push the output model to huggingface\n",
+ "! accelerate launch train_dreambooth.py \\\n",
+ " --pretrained_model_name_or_path=$MODEL_NAME \\\n",
+ " --instance_data_dir=$INSTANCE_DIR \\\n",
+ " --output_dir=$OUTPUT_DIR \\\n",
+ " --instance_prompt=\"a photo of [v]dog\" \\\n",
+ " --resolution=768 \\\n",
+ " --train_batch_size=1 \\\n",
+ " --gradient_accumulation_steps=1 \\\n",
+ " --gradient_checkpointing \\\n",
+ " --learning_rate=1e-6 \\\n",
+ " --lr_scheduler=\"constant\" \\\n",
+ " --enable_xformers_memory_efficient_attention \\\n",
+ " --use_8bit_adam \\\n",
+ " --lr_warmup_steps=0 \\\n",
+ " --max_train_steps=800 \\\n",
+ " --push_to_hub"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "vscode": {
+ "languageId": "plaintext"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "# Run a sample inference\n",
+ "from diffusers import StableDiffusionPipeline\n",
+ "import torch\n",
+ "\n",
+ "model_id = \"./dogbooth\"\n",
+ "pipe = StableDiffusionPipeline.from_pretrained(model_id).to(\"cuda\")\n",
+ "\n",
+ "prompt = \"a photo of [v]dog on the moon\"\n",
+ "image = pipe(prompt, num_inference_steps=100, guidance_scale=7.5).images[0]\n",
+ "\n",
+ "image.save(\"dog-bucket.png\")"
+ ]
+ }
+ ],
+ "metadata": {
+ "language_info": {
+ "name": "python"
+ },
+ "orig_nbformat": 4
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/ai-ml/jark-stack/terraform/src/service/Dockerfile b/ai-ml/jark-stack/terraform/src/service/Dockerfile
new file mode 100644
index 000000000..1f348eaeb
--- /dev/null
+++ b/ai-ml/jark-stack/terraform/src/service/Dockerfile
@@ -0,0 +1,8 @@
+FROM rayproject/ray-ml:2.6.0-gpu
+
+RUN pip install -U \
+ git+https://github.com/huggingface/transformers diffusers
+
+WORKDIR /serve_app
+
+COPY dogbooth.py /serve_app/dogbooth.py
diff --git a/ai-ml/jark-stack/terraform/src/service/dogbooth.py b/ai-ml/jark-stack/terraform/src/service/dogbooth.py
new file mode 100644
index 000000000..f3b42b474
--- /dev/null
+++ b/ai-ml/jark-stack/terraform/src/service/dogbooth.py
@@ -0,0 +1,58 @@
+from io import BytesIO
+from fastapi import FastAPI
+from fastapi.responses import Response
+import torch
+import os
+from ray import serve
+
+
+app = FastAPI()
+
+
+@serve.deployment(num_replicas=1, route_prefix="/")
+@serve.ingress(app)
+class APIIngress:
+ def __init__(self, diffusion_model_handle) -> None:
+ self.handle = diffusion_model_handle
+
+ @app.get(
+ "/imagine",
+ responses={200: {"content": {"image/png": {}}}},
+ response_class=Response,
+ )
+ async def generate(self, prompt: str, img_size: int = 768):
+ assert len(prompt), "prompt parameter cannot be empty"
+
+ image_ref = await self.handle.generate.remote(prompt, img_size=img_size)
+ image = await image_ref
+ file_stream = BytesIO()
+ image.save(file_stream, "PNG")
+ return Response(content=file_stream.getvalue(), media_type="image/png")
+
+
+@serve.deployment(
+ ray_actor_options={"num_gpus": 1},
+ autoscaling_config={"min_replicas": 1, "max_replicas": 2},
+)
+class StableDiffusionV2:
+ def __init__(self):
+ from diffusers import EulerDiscreteScheduler, StableDiffusionPipeline
+
+ model_id = os.getenv('MODEL_ID')
+
+ scheduler = EulerDiscreteScheduler.from_pretrained(
+ model_id, subfolder="scheduler"
+ )
+ self.pipe = StableDiffusionPipeline.from_pretrained(
+ model_id, scheduler=scheduler
+ )
+ self.pipe = self.pipe.to("cuda")
+
+ def generate(self, prompt: str, img_size: int = 768):
+ assert len(prompt), "prompt parameter cannot be empty"
+
+ image = self.pipe(prompt, height=img_size, width=img_size).images[0]
+ return image
+
+
+entrypoint = APIIngress.bind(StableDiffusionV2.bind())
diff --git a/ai-ml/jark-stack/terraform/src/service/ray-service.yaml b/ai-ml/jark-stack/terraform/src/service/ray-service.yaml
new file mode 100644
index 000000000..a72a2c904
--- /dev/null
+++ b/ai-ml/jark-stack/terraform/src/service/ray-service.yaml
@@ -0,0 +1,99 @@
+---
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: dogbooth
+---
+apiVersion: ray.io/v1alpha1
+kind: RayService
+metadata:
+ name: dogbooth
+ namespace: dogbooth
+spec:
+ serviceUnhealthySecondThreshold: 600
+ deploymentUnhealthySecondThreshold: 600
+ serveConfig:
+ importPath: dogbooth:entrypoint
+ runtimeEnv: |
+ env_vars: {"MODEL_ID": "askulkarni2/dogbooth"}
+ rayClusterConfig:
+ rayVersion: '2.6.0'
+ headGroupSpec:
+ rayStartParams:
+ dashboard-host: '0.0.0.0'
+ template:
+ spec:
+ containers:
+ - name: ray-head
+ image: public.ecr.aws/h3o5n2r0/dogbooth:0.0.1-gpu
+ resources:
+ limits:
+ cpu: 2
+ memory: 16Gi
+ nvidia.com/gpu: 1
+ requests:
+ cpu: 2
+ memory: 16Gi
+ nvidia.com/gpu: 1
+ ports:
+ - containerPort: 6379
+ name: gcs-server
+ - containerPort: 8265
+ name: dashboard
+ - containerPort: 10001
+ name: client
+ - containerPort: 8000
+ name: serve
+ workerGroupSpecs:
+ - replicas: 1
+ minReplicas: 1
+ maxReplicas: 5
+ rayStartParams: {}
+ groupName: small-group
+ template:
+ spec:
+ containers:
+ - name: ray-worker
+ image: public.ecr.aws/h3o5n2r0/dogbooth:0.0.1-gpu
+ lifecycle:
+ preStop:
+ exec:
+ command: ["/bin/sh","-c","ray stop"]
+ resources:
+ limits:
+ cpu: "2"
+ memory: "16Gi"
+ nvidia.com/gpu: 1
+ requests:
+ cpu: "2"
+ memory: "16Gi"
+ nvidia.com/gpu: 1
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: dogbooth
+ namespace: dogbooth
+ annotations:
+ nginx.ingress.kubernetes.io/rewrite-target: "/$1"
+spec:
+ ingressClassName: nginx
+ rules:
+ - http:
+ paths:
+ # Ray Dashboard
+ - path: /dogbooth/(.*)
+ pathType: ImplementationSpecific
+ backend:
+ service:
+ name: dogbooth-head-svc
+ port:
+ number: 8265
+ # Ray Serve
+ - path: /dogbooth/serve/(.*)
+ pathType: ImplementationSpecific
+ backend:
+ service:
+ name: dogbooth-head-svc
+ port:
+ number: 8000
diff --git a/ai-ml/jark-stack/terraform/variables.tf b/ai-ml/jark-stack/terraform/variables.tf
new file mode 100644
index 000000000..b95ca832d
--- /dev/null
+++ b/ai-ml/jark-stack/terraform/variables.tf
@@ -0,0 +1,40 @@
+variable "name" {
+ description = "Name of the VPC and EKS Cluster"
+ default = "jark-stack"
+ type = string
+}
+
+# NOTE: Trainium and Inferentia are only available in us-west-2 and us-east-1 regions
+variable "region" {
+ description = "region"
+ default = "us-west-2"
+ type = string
+}
+
+variable "eks_cluster_version" {
+ description = "EKS Cluster version"
+ default = "1.27"
+ type = string
+}
+
+# VPC with 2046 IPs (10.1.0.0/21) and 2 AZs
+variable "vpc_cidr" {
+ description = "VPC CIDR. This should be a valid private (RFC 1918) CIDR range"
+ default = "10.1.0.0/21"
+ type = string
+}
+
+# RFC6598 range 100.64.0.0/10
+# Note you can only /16 range to VPC. You can add multiples of /16 if required
+variable "secondary_cidr_blocks" {
+ description = "Secondary CIDR blocks to be attached to VPC"
+ default = ["100.64.0.0/16"]
+ type = list(string)
+}
+
+variable "huggingface_token" {
+ description = "Hugging Face Secret Token"
+ type = string
+ default = "DUMMY_TOKEN_REPLACE_ME"
+ sensitive = true
+}
diff --git a/ai-ml/jark-stack/terraform/versions.tf b/ai-ml/jark-stack/terraform/versions.tf
new file mode 100644
index 000000000..bb085ae7a
--- /dev/null
+++ b/ai-ml/jark-stack/terraform/versions.tf
@@ -0,0 +1,37 @@
+terraform {
+ required_version = ">= 1.0.0"
+
+ required_providers {
+ aws = {
+ source = "hashicorp/aws"
+ version = ">= 3.72"
+ }
+ kubernetes = {
+ source = "hashicorp/kubernetes"
+ version = ">= 2.10"
+ }
+ helm = {
+ source = "hashicorp/helm"
+ version = ">= 2.4.1"
+ }
+ kubectl = {
+ source = "gavinbunney/kubectl"
+ version = ">= 1.14"
+ }
+ random = {
+ source = "hashicorp/random"
+ version = ">= 3.1"
+ }
+ http = {
+ source = "hashicorp/http"
+ version = ">= 3.3"
+ }
+ }
+
+ # ## Used for end-to-end testing on project; update to suit your needs
+ # backend "s3" {
+ # bucket = "doeks-github-actions-e2e-test-state"
+ # region = "us-west-2"
+ # key = "e2e/trainium-inferentia/terraform.tfstate"
+ # }
+}
diff --git a/ai-ml/jark-stack/terraform/vpc.tf b/ai-ml/jark-stack/terraform/vpc.tf
new file mode 100644
index 000000000..59c3da89c
--- /dev/null
+++ b/ai-ml/jark-stack/terraform/vpc.tf
@@ -0,0 +1,53 @@
+locals {
+ # Routable Private subnets only for Private NAT Gateway -> Transit Gateway -> Second VPC for overlapping CIDRs
+ # e.g., var.vpc_cidr = "10.1.0.0/21" => output: ["10.1.0.0/24", "10.1.1.0/24"] => 256-2 = 254 usable IPs per subnet/AZ
+ private_subnets = [for k, v in local.azs : cidrsubnet(var.vpc_cidr, 3, k)]
+ # Routable Public subnets with NAT Gateway and Internet Gateway
+ # e.g., var.vpc_cidr = "10.1.0.0/21" => output: ["10.1.2.0/26", "10.1.2.64/26"] => 64-2 = 62 usable IPs per subnet/AZ
+ public_subnets = [for k, v in local.azs : cidrsubnet(var.vpc_cidr, 5, k + 8)]
+ # RFC6598 range 100.64.0.0/16 for EKS Data Plane for two subnets(32768 IPs per Subnet) across two AZs for EKS Control Plane ENI + Nodes + Pods
+ # e.g., var.secondary_cidr_blocks = "100.64.0.0/16" => output: ["100.64.0.0/17", "100.64.128.0/17"] => 32768-2 = 32766 usable IPs per subnet/AZ
+ secondary_ip_range_private_subnets = [for k, v in local.azs : cidrsubnet(element(var.secondary_cidr_blocks, 0), 1, k)]
+}
+
+#---------------------------------------------------------------
+# VPC
+#---------------------------------------------------------------
+# WARNING: This VPC module includes the creation of an Internet Gateway and NAT Gateway, which simplifies cluster deployment and testing, primarily intended for sandbox accounts.
+# IMPORTANT: For preprod and prod use cases, it is crucial to consult with your security team and AWS architects to design a private infrastructure solution that aligns with your security requirements
+
+module "vpc" {
+ source = "terraform-aws-modules/vpc/aws"
+ version = "~> 5.0"
+
+ name = local.name
+ cidr = var.vpc_cidr
+ azs = local.azs
+
+ # Secondary CIDR block attached to VPC for EKS Control Plane ENI + Nodes + Pods
+ secondary_cidr_blocks = var.secondary_cidr_blocks
+
+ # 1/ EKS Data Plane secondary CIDR blocks for two subnets across two AZs for EKS Control Plane ENI + Nodes + Pods
+ # 2/ Two private Subnets with RFC1918 private IPv4 address range for Private NAT + NLB + Airflow + EC2 Jumphost etc.
+ private_subnets = concat(local.private_subnets, local.secondary_ip_range_private_subnets)
+
+ # ------------------------------
+ # Optional Public Subnets for NAT and IGW for PoC/Dev/Test environments
+ # Public Subnets can be disabled while deploying to Production and use Private NAT + TGW
+ public_subnets = local.public_subnets
+ enable_nat_gateway = true
+ single_nat_gateway = true
+ #-------------------------------
+
+ public_subnet_tags = {
+ "kubernetes.io/role/elb" = 1
+ }
+
+ private_subnet_tags = {
+ "kubernetes.io/role/internal-elb" = 1
+ # Tags subnets for Karpenter auto-discovery
+ "karpenter.sh/discovery" = local.name
+ }
+
+ tags = local.tags
+}
diff --git a/ai-ml/jupyterhub/addons.tf b/ai-ml/jupyterhub/addons.tf
index 1ca3f0f22..277cf7889 100755
--- a/ai-ml/jupyterhub/addons.tf
+++ b/ai-ml/jupyterhub/addons.tf
@@ -1,7 +1,3 @@
-data "aws_eks_cluster_auth" "this" {
- name = module.eks.cluster_name
-}
-
# Use this data source to get the ARN of a certificate in AWS Certificate Manager (ACM)
data "aws_acm_certificate" "issued" {
count = var.jupyter_hub_auth_mechanism == "cognito" ? 1 : 0
@@ -16,6 +12,7 @@ data "aws_ecrpublic_authorization_token" "token" {
locals {
cognito_custom_domain = var.cognito_custom_domain
}
+
#---------------------------------------------------------------
# IRSA for EBS CSI Driver
#---------------------------------------------------------------
@@ -69,7 +66,7 @@ module "eks_blueprints_addons" {
enable_cluster_proportional_autoscaler = true
cluster_proportional_autoscaler = {
timeout = "300"
- values = [templatefile("${path.module}/helm-values/coredns-autoscaler-values.yaml", {
+ values = [templatefile("${path.module}/helm/coredns-autoscaler/values.yaml", {
target = "deployment/coredns"
})]
description = "Cluster Proportional Autoscaler for CoreDNS Service"
@@ -81,7 +78,7 @@ module "eks_blueprints_addons" {
enable_metrics_server = true
metrics_server = {
timeout = "300"
- values = [templatefile("${path.module}/helm-values/metrics-server-values.yaml", {})]
+ values = [templatefile("${path.module}/helm/metrics-server/values.yaml", {})]
}
#---------------------------------------
@@ -91,7 +88,7 @@ module "eks_blueprints_addons" {
cluster_autoscaler = {
timeout = "300"
create_role = true
- values = [templatefile("${path.module}/helm-values/cluster-autoscaler-values.yaml", {
+ values = [templatefile("${path.module}/helm/cluster-autoscaler/values.yaml", {
aws_region = var.region,
eks_cluster_id = module.eks.cluster_name
})]
@@ -109,15 +106,158 @@ module "eks_blueprints_addons" {
}
#---------------------------------------
- # CloudWatch metrics for EKS
+ # AWS Load Balancer Controller
#---------------------------------------
- enable_aws_cloudwatch_metrics = true
- aws_cloudwatch_metrics = {
- timeout = "300"
- values = [templatefile("${path.module}/helm-values/aws-cloudwatch-metrics-values.yaml", {})]
+ enable_aws_load_balancer_controller = true
+
+ #---------------------------------------
+ # Prometheus and Grafana stack
+ #---------------------------------------
+ #---------------------------------------------------------------
+ # Install Monitoring Stack with Prometheus and Grafana
+ # 1- Grafana port-forward `kubectl port-forward svc/kube-prometheus-stack-grafana 8080:80 -n kube-prometheus-stack`
+ # 2- Grafana Admin user: admin
+ # 3- Get admin user password: `aws secretsmanager get-secret-value --secret-id --region $AWS_REGION --query "SecretString" --output text`
+ #---------------------------------------------------------------
+ enable_kube_prometheus_stack = true
+ kube_prometheus_stack = {
+ values = [templatefile("${path.module}/helm/kube-prometheus-stack/values.yaml", {})]
+ chart_version = "48.1.1"
+ set_sensitive = [
+ {
+ name = "grafana.adminPassword"
+ value = data.aws_secretsmanager_secret_version.admin_password_version.secret_string
+ }
+ ],
+ }
+ #---------------------------------------
+ # AWS for FluentBit
+ #---------------------------------------
+ enable_aws_for_fluentbit = true
+ aws_for_fluentbit_cw_log_group = {
+ use_name_prefix = false
+ name = "/${local.name}/aws-fluentbit-logs" # Add-on creates this log group
+ retention_in_days = 30
+ }
+ aws_for_fluentbit = {
+ values = [templatefile("${path.module}/helm/aws-for-fluentbit/values.yaml", {
+ region = local.region,
+ cloudwatch_log_group = "/${local.name}/aws-fluentbit-logs"
+ cluster_name = module.eks.cluster_name
+ })]
}
- enable_aws_load_balancer_controller = true
+ #---------------------------------------
+ # Additional Helm Charts
+ #---------------------------------------
+ helm_releases = {
+ storageclass = {
+ name = "storageclass"
+ description = "A Helm chart for storage configurations"
+ chart = "${path.module}/helm/storageclass"
+ }
+ karpenter-resources-cpu = {
+ name = "karpenter-resources-cpu"
+ description = "A Helm chart for karpenter CPU based resources"
+ chart = "${path.module}/helm/karpenter-resources"
+ values = [
+ <<-EOT
+ clusterName: ${module.eks.cluster_name}
+ karpenterRole: ${split("/", module.eks_blueprints_addons.karpenter.node_iam_role_arn)[1]}
+ EOT
+ ]
+ }
+ karpenter-resources-ts = {
+ name = "karpenter-resources-ts"
+ description = "A Helm chart for karpenter GPU based resources - compatible with GPU time slicing"
+ chart = "${path.module}/helm/karpenter-resources"
+ values = [
+ <<-EOT
+ name: gpu-ts
+ clusterName: ${module.eks.cluster_name}
+ karpenterRole: ${split("/", module.eks_blueprints_addons.karpenter.node_iam_role_arn)[1]}
+ instanceSizes: ["xlarge", "2xlarge", "4xlarge", "8xlarge", "16xlarge", "24xlarge"]
+ instanceFamilies: ["g5"]
+ taints:
+ - key: hub.jupyter.org/dedicated
+ value: "user"
+ effect: "NoSchedule"
+ - key: nvidia.com/gpu
+ effect: "NoSchedule"
+ amiFamily: Ubuntu
+ EOT
+ ]
+ }
+ karpenter-resources-mig = {
+ name = "karpenter-resources-gpu"
+ description = "A Helm chart for karpenter GPU based resources - compatible with P4d instances"
+ chart = "${path.module}/helm/karpenter-resources"
+ values = [
+ <<-EOT
+ name: gpu
+ clusterName: ${module.eks.cluster_name}
+ karpenterRole: ${split("/", module.eks_blueprints_addons.karpenter.node_iam_role_arn)[1]}
+ instanceSizes: ["24xlarge"]
+ instanceFamilies: ["p4d"]
+ taints:
+ - key: hub.jupyter.org/dedicated
+ value: "user"
+ effect: "NoSchedule"
+ - key: nvidia.com/gpu
+ effect: "NoSchedule"
+ amiFamily: Ubuntu
+ EOT
+ ]
+ }
+ karpenter-resources-inf = {
+ name = "karpenter-resources-inf"
+ description = "A Helm chart for karpenter Inferentia based resources"
+ chart = "${path.module}/helm/karpenter-resources"
+ values = [
+ <<-EOT
+ name: inferentia
+ clusterName: ${module.eks.cluster_name}
+ karpenterRole: ${split("/", module.eks_blueprints_addons.karpenter.node_iam_role_arn)[1]}
+ instanceSizes: ["8xlarge", "24xlarge"]
+ instanceFamilies: ["inf2"]
+ taints:
+ - key: aws.amazon.com/neuroncore
+ value: "true"
+ effect: "NoSchedule"
+ - key: aws.amazon.com/neuron
+ value: "true"
+ effect: "NoSchedule"
+ - key: hub.jupyter.org/dedicated
+ value: "user"
+ effect: "NoSchedule"
+ EOT
+ ]
+ }
+ karpenter-resources-trn = {
+ name = "karpenter-resources-trn"
+ description = "A Helm chart for karpenter Trainium based resources"
+ chart = "${path.module}/helm/karpenter-resources"
+ values = [
+ <<-EOT
+ name: trainium
+ clusterName: ${module.eks.cluster_name}
+ karpenterRole: ${split("/", module.eks_blueprints_addons.karpenter.node_iam_role_arn)[1]}
+ instanceSizes: ["32xlarge"]
+ instanceFamilies: ["trn1"]
+ taints:
+ - key: aws.amazon.com/neuroncore
+ value: "true"
+ effect: "NoSchedule"
+ - key: aws.amazon.com/neuron
+ value: "true"
+ effect: "NoSchedule"
+ - key: hub.jupyter.org/dedicated
+ value: "user"
+ effect: "NoSchedule"
+ EOT
+ ]
+ }
+ }
tags = local.tags
}
@@ -131,19 +271,25 @@ module "eks_data_addons" {
oidc_provider_arn = module.eks.oidc_provider_arn
+ #---------------------------------------------------------------
+ # Enable Neuron Device Plugin
+ #---------------------------------------------------------------
+ enable_aws_neuron_device_plugin = true
+
#---------------------------------------------------------------
# Enable GPU operator
#---------------------------------------------------------------
- enable_nvidia_gpu_operator = var.jupyter_notebook_support == "gpu" ? true : false
+ enable_nvidia_gpu_operator = true
nvidia_gpu_operator_helm_config = {
- values = [templatefile("${path.module}/helm-values/nvidia-values.yaml", {})]
+ values = [templatefile("${path.module}/helm/nvidia-gpu-operator/values.yaml", {})]
}
+
#---------------------------------------------------------------
# JupyterHub Add-on
#---------------------------------------------------------------
enable_jupyterhub = true
jupyterhub_helm_config = {
- values = [templatefile("${path.module}/helm-values/jupyterhub-values-${var.jupyter_hub_auth_mechanism}-${var.jupyter_notebook_support}.yaml", {
+ values = [templatefile("${path.module}/helm/jupyterhub/jupyterhub-values-${var.jupyter_hub_auth_mechanism}.yaml", {
ssl_cert_arn = try(data.aws_acm_certificate.issued[0].arn, "")
jupyterdomain = try("https://${var.jupyterhub_domain}/hub/oauth_callback", "")
authorize_url = try("https://${local.cognito_custom_domain}.auth.${local.region}.amazoncognito.com/oauth2/authorize", "")
@@ -151,7 +297,45 @@ module "eks_data_addons" {
userdata_url = try("https://${local.cognito_custom_domain}.auth.${local.region}.amazoncognito.com/oauth2/userInfo", "")
client_id = try(aws_cognito_user_pool_client.user_pool_client[0].id, "")
client_secret = try(aws_cognito_user_pool_client.user_pool_client[0].client_secret, "")
+ user_pool_id = try(aws_cognito_user_pool.pool[0].id, "")
+ identity_pool_id = try(aws_cognito_identity_pool.identity_pool[0].id, "")
jupyter_single_user_sa_name = kubernetes_service_account_v1.jupyterhub_single_user_sa.metadata[0].name
+ region = var.region
})]
}
+
+ #---------------------------------------------------------------
+ # Kubecost Add-on
+ #---------------------------------------------------------------
+ enable_kubecost = true
+ kubecost_helm_config = {
+ values = [templatefile("${path.module}/helm/kubecost/values.yaml", {})]
+ repository_username = data.aws_ecrpublic_authorization_token.token.user_name
+ repository_password = data.aws_ecrpublic_authorization_token.token.password
+ }
+}
+
+#---------------------------------------------------------------
+# Grafana Admin credentials resources
+#---------------------------------------------------------------
+data "aws_secretsmanager_secret_version" "admin_password_version" {
+ secret_id = aws_secretsmanager_secret.grafana.id
+ depends_on = [aws_secretsmanager_secret_version.grafana]
+}
+
+resource "random_password" "grafana" {
+ length = 16
+ special = true
+ override_special = "@_"
+}
+
+#tfsec:ignore:aws-ssm-secret-use-customer-key
+resource "aws_secretsmanager_secret" "grafana" {
+ name_prefix = "${local.name}-grafana-"
+ recovery_window_in_days = 0 # Set to zero for this example to force delete during Terraform destroy
+}
+
+resource "aws_secretsmanager_secret_version" "grafana" {
+ secret_id = aws_secretsmanager_secret.grafana.id
+ secret_string = random_password.grafana.result
}
diff --git a/ai-ml/jupyterhub/cleanup.sh b/ai-ml/jupyterhub/cleanup.sh
index 4412881ce..8438ddf84 100755
--- a/ai-ml/jupyterhub/cleanup.sh
+++ b/ai-ml/jupyterhub/cleanup.sh
@@ -2,13 +2,11 @@
set -o errexit
set -o pipefail
-read -p "Enter domain name with wildcard and ensure ACM certificate is created for this domain name, e.g. *.example.com :" acm_certificate_domain
-read -p "Enter sub-domain name for jupyterhub to be hosted, e.g. eks.example.com : " jupyterhub_domain
-
targets=(
"module.eks_data_addons"
"module.eks_blueprints_addons"
"module.eks"
+ "module.vpc"
)
#-------------------------------------------
@@ -32,7 +30,7 @@ done
#-------------------------------------------
for target in "${targets[@]}"
do
- destroy_output=$(terraform destroy -target="$target" -var="acm_certificate_domain=$acm_certificate_domain" -var="jupyterhub_domain=$jupyterhub_domain" -auto-approve | tee /dev/tty)
+ destroy_output=$(terraform destroy -target="$target" -auto-approve | tee /dev/tty)
if [[ ${PIPESTATUS[0]} -eq 0 && $destroy_output == *"Destroy complete!"* ]]; then
echo "SUCCESS: Terraform destroy of $target completed successfully"
else
@@ -44,7 +42,7 @@ done
#-------------------------------------------
# Terraform destroy full
#-------------------------------------------
-destroy_output=$(terraform destroy -target="$target" -var="acm_certificate_domain=$acm_certificate_domain" -var="jupyterhub_domain=$jupyterhub_domain" -auto-approve | tee /dev/tty)
+destroy_output=$(terraform destroy -target="$target" -auto-approve | tee /dev/tty)
if [[ ${PIPESTATUS[0]} -eq 0 && $destroy_output == *"Destroy complete!"* ]]; then
echo "SUCCESS: Terraform destroy of all targets completed successfully"
else
diff --git a/ai-ml/jupyterhub/cognito.tf b/ai-ml/jupyterhub/cognito.tf
new file mode 100644
index 000000000..57338986b
--- /dev/null
+++ b/ai-ml/jupyterhub/cognito.tf
@@ -0,0 +1,224 @@
+#---------------------------------------------------------------
+# Lambda function for pre token generation
+#----------------------------------------------------------------
+
+data "aws_iam_policy_document" "assume_role" {
+ statement {
+ effect = "Allow"
+ principals {
+ type = "Service"
+ identifiers = ["lambda.amazonaws.com", "cognito-idp.amazonaws.com"]
+ }
+ actions = ["sts:AssumeRole"]
+ }
+}
+
+data "aws_iam_policy" "lambda_execution_policy" {
+ arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
+}
+
+resource "aws_iam_role" "iam_for_lambda" {
+ count = var.jupyter_hub_auth_mechanism == "cognito" ? 1 : 0
+ name = "iam_for_lambda"
+ assume_role_policy = data.aws_iam_policy_document.assume_role.json
+}
+
+resource "aws_iam_role_policy_attachment" "lambda_policy_attachment" {
+ count = var.jupyter_hub_auth_mechanism == "cognito" ? 1 : 0
+ role = aws_iam_role.iam_for_lambda[0].name
+ policy_arn = data.aws_iam_policy.lambda_execution_policy.arn
+}
+
+data "archive_file" "lambda" {
+ type = "zip"
+ output_path = "/tmp/lambda.zip"
+ source {
+ filename = "index.mjs"
+ content = <<-EOF
+ export const handler = async (event) => {
+ event.response = {
+ claimsOverrideDetails: {
+ claimsToAddOrOverride: {
+ department: "engineering",
+ },
+ },
+ };
+
+ return event;
+ };
+
+ EOF
+ }
+}
+
+resource "aws_lambda_function" "pretoken_trigger" {
+ count = var.jupyter_hub_auth_mechanism == "cognito" ? 1 : 0
+ function_name = "pretoken-trigger-function"
+ filename = data.archive_file.lambda.output_path
+ source_code_hash = data.archive_file.lambda.output_base64sha256
+
+ runtime = "nodejs18.x"
+ handler = "index.handler"
+
+ role = aws_iam_role.iam_for_lambda[0].arn
+}
+
+#---------------------------------------------------------------
+# Cognito pool, domain and client creation.
+# This can be used
+# Auth integration later.
+#----------------------------------------------------------------
+resource "aws_cognito_user_pool" "pool" {
+ count = var.jupyter_hub_auth_mechanism == "cognito" ? 1 : 0
+ name = "jupyterhub-userpool"
+
+ username_attributes = ["email"]
+ auto_verified_attributes = ["email"]
+
+ password_policy {
+ minimum_length = 6
+ }
+
+ lambda_config {
+ pre_token_generation = aws_lambda_function.pretoken_trigger[0].arn
+ }
+}
+
+resource "aws_cognito_user_pool_domain" "domain" {
+ count = var.jupyter_hub_auth_mechanism == "cognito" ? 1 : 0
+ domain = local.cognito_custom_domain
+ user_pool_id = aws_cognito_user_pool.pool[0].id
+}
+
+resource "aws_cognito_user_pool_client" "user_pool_client" {
+ count = var.jupyter_hub_auth_mechanism == "cognito" ? 1 : 0
+ name = "jupyter-client"
+ access_token_validity = 1
+ token_validity_units {
+ access_token = "days"
+ }
+ callback_urls = ["https://${var.jupyterhub_domain}/hub/oauth_callback"]
+ user_pool_id = aws_cognito_user_pool.pool[0].id
+ allowed_oauth_flows_user_pool_client = true
+ allowed_oauth_flows = ["code"]
+ allowed_oauth_scopes = ["openid", "email"]
+ generate_secret = true
+ supported_identity_providers = [
+ "COGNITO"
+ ]
+
+ depends_on = [aws_cognito_user_pool_domain.domain]
+}
+
+#---------------------------------------------------------------
+# Cognito identity pool creation.
+#----------------------------------------------------------------
+resource "aws_cognito_identity_pool" "identity_pool" {
+ count = var.jupyter_hub_auth_mechanism == "cognito" ? 1 : 0
+ identity_pool_name = "jupyterhub-identity-pool"
+ allow_unauthenticated_identities = false
+ cognito_identity_providers {
+ client_id = aws_cognito_user_pool_client.user_pool_client[0].id
+ provider_name = aws_cognito_user_pool.pool[0].endpoint
+ server_side_token_check = true
+ }
+
+ depends_on = [aws_cognito_user_pool_client.user_pool_client]
+}
+
+resource "aws_s3_bucket" "jupyterhub_bucket" {
+ count = var.jupyter_hub_auth_mechanism == "cognito" ? 1 : 0
+ bucket_prefix = "jupyterhub-test-bucket-"
+}
+
+resource "aws_s3_object" "engineering_object" {
+ count = var.jupyter_hub_auth_mechanism == "cognito" ? 1 : 0
+ bucket = aws_s3_bucket.jupyterhub_bucket[0].id
+ key = "engineering/"
+}
+
+resource "aws_s3_object" "legal_object" {
+ count = var.jupyter_hub_auth_mechanism == "cognito" ? 1 : 0
+ bucket = aws_s3_bucket.jupyterhub_bucket[0].id
+ key = "legal/"
+}
+
+#---------------------------------------------------------------
+# IAM role for a team member from the engineering department
+# In theory there would be other departments such as "legal"
+#----------------------------------------------------------------
+resource "aws_iam_role" "cognito_authenticated_engineering_role" {
+ count = var.jupyter_hub_auth_mechanism == "cognito" ? 1 : 0
+
+ name = "EngineeringTeamRole"
+
+ assume_role_policy = jsonencode({
+ Version = "2012-10-17",
+ Statement = [
+ {
+ Action = ["sts:AssumeRoleWithWebIdentity", "sts:TagSession"],
+ Effect = "Allow",
+ Principal = {
+ Federated = "cognito-identity.amazonaws.com"
+ },
+ Condition = {
+ StringEquals = {
+ "cognito-identity.amazonaws.com:aud" = aws_cognito_identity_pool.identity_pool[0].id
+ },
+ "ForAnyValue:StringLike" : {
+ "cognito-identity.amazonaws.com:amr" : "authenticated"
+ }
+ }
+ }
+ ]
+ })
+}
+
+resource "aws_iam_role_policy" "s3_cognito_engineering_policy" {
+ count = var.jupyter_hub_auth_mechanism == "cognito" ? 1 : 0
+ name = "s3_cognito_engineering_policy"
+ role = aws_iam_role.cognito_authenticated_engineering_role[0].id
+
+ policy = <<-EOF
+{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Action": ["s3:List*"],
+ "Resource": "*",
+ "Condition": {
+ "StringEquals": {
+ "s3:prefix": "$${aws:PrincipalTag/department}"
+ }
+ }
+ }
+ ]
+}
+EOF
+}
+
+resource "aws_cognito_identity_pool_provider_principal_tag" "example" {
+ count = var.jupyter_hub_auth_mechanism == "cognito" ? 1 : 0
+ identity_pool_id = aws_cognito_identity_pool.identity_pool[0].id
+ identity_provider_name = aws_cognito_user_pool.pool[0].endpoint
+ use_defaults = false
+ principal_tags = {
+ department = "department"
+ }
+}
+
+resource "aws_iam_policy_attachment" "s3_readonly_policy_attachment" {
+ count = var.jupyter_hub_auth_mechanism == "cognito" ? 1 : 0
+ name = "S3ReadOnlyAccessAttachment"
+ policy_arn = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
+ roles = [aws_iam_role.cognito_authenticated_engineering_role[0].name]
+}
+
+resource "aws_cognito_identity_pool_roles_attachment" "identity_pool_roles" {
+ count = var.jupyter_hub_auth_mechanism == "cognito" ? 1 : 0
+ identity_pool_id = aws_cognito_identity_pool.identity_pool[0].id
+ roles = {
+ authenticated = aws_iam_role.cognito_authenticated_engineering_role[0].arn
+ }
+}
diff --git a/ai-ml/jupyterhub/examples/create_image.sh b/ai-ml/jupyterhub/examples/create_image.sh
new file mode 100755
index 000000000..ae33fcb7e
--- /dev/null
+++ b/ai-ml/jupyterhub/examples/create_image.sh
@@ -0,0 +1,36 @@
+#!/bin/bash
+
+# Set the AWS region and the name of the ECR repository
+
+REGION=us-west-2
+ECR_REPO_NAME=jupyterhub-pytorch-neuron-pytorch
+DOCKER_FILE=docker/jupyterhub-pytorch-neuron-pytorch.Dockerfile
+
+# Check if the ECR repository exists
+if aws ecr describe-repositories --repository-names "$ECR_REPO_NAME" --region "$REGION" >/dev/null 2>&1; then
+ echo "ECR repository '$ECR_REPO_NAME' already exists."
+
+ # Get the ECR_REPO_URI for the existing repository
+ ECR_REPO_URI=$(aws ecr describe-repositories --repository-name "$ECR_REPO_NAME" --query 'repositories[0].repositoryUri' --region "$REGION" --output text)
+ echo "Repository URL: $ECR_REPO_URI"
+else
+ # Create a new ECR repository with the specified name and region
+ aws ecr create-repository --repository-name "$ECR_REPO_NAME" --region "$REGION"
+
+ # Retrieve the URL of the newly created ECR repository
+ ECR_REPO_URI=$(aws ecr describe-repositories --repository-name "$ECR_REPO_NAME" --query 'repositories[0].repositoryUri' --region "$REGION" --output text)
+ echo "Repository URL: $ECR_REPO_URI"
+fi
+
+# Log in to Amazon ECR using docker
+echo -e "Logging in to Amazon ECR..."
+aws ecr get-login-password --region "$REGION" | docker login --username AWS --password-stdin "$ECR_REPO_URI"
+
+# Build the docker image using the provided jupyterhub-pytorch-neuron.Dockerfile and tag it with the ECR repository URI
+echo -e "Building, tagging and pushing docker image... $ECR_REPO_URI:latest"
+# docker build -f docker/jupyterhub-pytorch-neuron.Dockerfile-jupterhub-inferentia-pytorch -t "$ECR_REPO_URI:latest" .
+docker buildx build --push --tag "$ECR_REPO_URI:latest" -o type=image --platform=linux/amd64 -f $DOCKER_FILE .
+
+# Wait for 5 seconds
+sleep 5
+echo -e "Sleeping for 5 seconds..."
diff --git a/ai-ml/jupyterhub/examples/docker/jupyterhub-pytorch-neuron.Dockerfile b/ai-ml/jupyterhub/examples/docker/jupyterhub-pytorch-neuron.Dockerfile
new file mode 100644
index 000000000..687e7a52f
--- /dev/null
+++ b/ai-ml/jupyterhub/examples/docker/jupyterhub-pytorch-neuron.Dockerfile
@@ -0,0 +1,34 @@
+# Use the Jupyter base notebook with Python 3.10 as the base image
+FROM jupyter/base-notebook:python-3.10
+
+# Maintainer label
+LABEL maintainer="DoEKS"
+
+# Set environment variables to non-interactive (this prevents some prompts)
+ENV DEBIAN_FRONTEND=non-interactive
+
+# Switch to root to add Neuron repo and install necessary packages
+USER root
+
+# Install gnupg and other required packages
+RUN apt-get update -y && \
+ apt-get install -y gnupg git g++
+
+RUN \
+ . /etc/os-release && \
+ echo "deb https://apt.repos.neuron.amazonaws.com ${VERSION_CODENAME} main" > /etc/apt/sources.list.d/neuron.list && \
+ wget -qO - https://apt.repos.neuron.amazonaws.com/GPG-PUB-KEY-AMAZON-AWS-NEURON.PUB | apt-key add - && \
+ apt-get update -y && \
+ apt-get install aws-neuronx-collectives=2.* aws-neuronx-runtime-lib=2.* aws-neuronx-tools=2.* -y
+
+# Switch back to jovyan user for Python package installations
+USER jovyan
+
+# Set pip repository pointing to the Neuron repository and install required Python packages
+RUN pip config set global.extra-index-url https://pip.repos.neuron.amazonaws.com && \
+ pip install transformers-neuronx sentencepiece transformers wget awscli ipywidgets neuronx-cc==2.* torch-neuronx torchvision ipykernel environment_kernels && \
+ # Install new Jupyter Notebook kernel
+ python -m ipykernel install --user --name aws_neuron_venv_pytorch --display-name "Python (torch-neuronx)"
+
+# Add Neuron path to PATH
+ENV PATH /opt/aws/neuron/bin:$PATH
diff --git a/ai-ml/jupyterhub/examples/docker/jupyterhub-tensorflow-neuron.Dockerfile b/ai-ml/jupyterhub/examples/docker/jupyterhub-tensorflow-neuron.Dockerfile
new file mode 100644
index 000000000..6f167444d
--- /dev/null
+++ b/ai-ml/jupyterhub/examples/docker/jupyterhub-tensorflow-neuron.Dockerfile
@@ -0,0 +1,34 @@
+# Use the Jupyter base notebook with Python 3.10 as the base image
+FROM jupyter/base-notebook:python-3.10
+
+# Maintainer label
+LABEL maintainer="DoEKS"
+
+# Set environment variables to non-interactive (this prevents some prompts)
+ENV DEBIAN_FRONTEND=non-interactive
+
+# Switch to root to add Neuron repo and install necessary packages
+USER root
+
+# Install gnupg and other required packages
+RUN apt-get update -y && \
+ apt-get install -y gnupg git g++
+
+RUN \
+ . /etc/os-release && \
+ echo "deb https://apt.repos.neuron.amazonaws.com ${VERSION_CODENAME} main" > /etc/apt/sources.list.d/neuron.list && \
+ wget -qO - https://apt.repos.neuron.amazonaws.com/GPG-PUB-KEY-AMAZON-AWS-NEURON.PUB | apt-key add - && \
+ apt-get update -y && \
+ apt-get install aws-neuronx-collectives=2.* aws-neuronx-runtime-lib=2.* aws-neuronx-tools=2.* -y
+
+# Switch back to jovyan user for Python package installations
+USER jovyan
+
+# Set pip repository pointing to the Neuron repository and install required Python packages
+RUN pip config set global.extra-index-url https://pip.repos.neuron.amazonaws.com && \
+ pip install transformers-neuronx sentencepiece transformers wget awscli ipywidgets neuronx-cc==2.* tensorflow-neuronx ipykernel environment_kernels && \
+ # Install new Jupyter Notebook kernel
+ python -m ipykernel install --user --name aws_neuron_venv_tensorflow --display-name "Python (tensorflow-neuronx)"
+
+# Add Neuron path to PATH
+ENV PATH /opt/aws/neuron/bin:$PATH
diff --git a/ai-ml/jupyterhub/examples/notebook-examples/gpu-timeslice-test-tensorflow.ipynb b/ai-ml/jupyterhub/examples/notebook-examples/gpu-timeslice-test-tensorflow.ipynb
new file mode 100644
index 000000000..75b7e55e3
--- /dev/null
+++ b/ai-ml/jupyterhub/examples/notebook-examples/gpu-timeslice-test-tensorflow.ipynb
@@ -0,0 +1,333 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "851f73c7-068a-4e44-861b-511a3d2caf16",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Collecting tensorrt\n",
+ " Downloading tensorrt-8.6.1.post1.tar.gz (18 kB)\n",
+ " Preparing metadata (setup.py) ... \u001b[?25ldone\n",
+ "\u001b[?25hBuilding wheels for collected packages: tensorrt\n",
+ " Building wheel for tensorrt (setup.py) ... \u001b[?25ldone\n",
+ "\u001b[?25h Created wheel for tensorrt: filename=tensorrt-8.6.1.post1-py2.py3-none-any.whl size=17281 sha256=055cb554c81337084ecbeece43281f85702addd8902e31b0004f43a2fb65f518\n",
+ " Stored in directory: /home/jovyan/.cache/pip/wheels/f4/c8/0e/b79b08e45752491b9acfdbd69e8a609e8b2ed7640dda5a3e59\n",
+ "Successfully built tensorrt\n",
+ "Installing collected packages: tensorrt\n",
+ "Successfully installed tensorrt-8.6.1.post1\n",
+ "Requirement already satisfied: matplotlib in /opt/conda/lib/python3.10/site-packages (3.7.1)\n",
+ "Requirement already satisfied: cycler>=0.10 in /opt/conda/lib/python3.10/site-packages (from matplotlib) (0.11.0)\n",
+ "Requirement already satisfied: pyparsing>=2.3.1 in /opt/conda/lib/python3.10/site-packages (from matplotlib) (3.0.9)\n",
+ "Requirement already satisfied: packaging>=20.0 in /opt/conda/lib/python3.10/site-packages (from matplotlib) (23.0)\n",
+ "Requirement already satisfied: python-dateutil>=2.7 in /opt/conda/lib/python3.10/site-packages (from matplotlib) (2.8.2)\n",
+ "Requirement already satisfied: contourpy>=1.0.1 in /opt/conda/lib/python3.10/site-packages (from matplotlib) (1.0.7)\n",
+ "Requirement already satisfied: pillow>=6.2.0 in /opt/conda/lib/python3.10/site-packages (from matplotlib) (9.4.0)\n",
+ "Requirement already satisfied: kiwisolver>=1.0.1 in /opt/conda/lib/python3.10/site-packages (from matplotlib) (1.4.4)\n",
+ "Requirement already satisfied: numpy>=1.20 in /opt/conda/lib/python3.10/site-packages (from matplotlib) (1.23.5)\n",
+ "Requirement already satisfied: fonttools>=4.22.0 in /opt/conda/lib/python3.10/site-packages (from matplotlib) (4.39.3)\n",
+ "Requirement already satisfied: six>=1.5 in /opt/conda/lib/python3.10/site-packages (from python-dateutil>=2.7->matplotlib) (1.16.0)\n"
+ ]
+ }
+ ],
+ "source": [
+ "! python3 -m pip install --upgrade tensorrt\n",
+ "! pip3 install matplotlib"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "id": "5c0d4d12-6c12-4cdc-bb79-6ad2a808e7a1",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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RomTSBkUHBzMbbfdxhdffJG97bvvvsPDw4NBgwbl+ZgDBw4wcuRIpk+fzsyZMxkwYAD/+te/ePXVV7P3mTVrFjVr1qRFixasWrWKVatWMWvWrBzHee655zh06BATJ05k7ty5VKpUKVdbFSpUYNq0aaxbt45nn30WgOTkZG6//XaqVavGxIkTnXaeRVWQc89L//79qV27Nj/++CMvv/wys2fP5qabbsqRMBbE0aNHadOmDbNmzeKpp55iwYIFTJgwgZCQEE6fPg1AWloap06dYtSoUcyePZvvvvuOjh07MmDAACZPnpx9rFWrVuHn50evXr2yz+Xjjz/Ot+1vv/2Wvn37EhwczHfffcekSZM4ffo0Xbp04ffff8+1/6233krdunWZMWMGY8aM4dtvv+XJJ58s1Ple7O+//wagYsWKJCcn07lzZ77++msee+wxFixYwLPPPstXX33FLbfckiPRB/PN+yeffMJLL73EokWLaN++PatWrSIiIoJrr702+/wjIyOJj4+nQ4cOLF68mFdffZU5c+Zw4403MmrUKB599NFccf33v//lt99+491332XBggXUr18/+2cDBw6kWbNmzJgxg/vvv5///Oc/PPnkk/Tr14/evXsza9Ysrr/+ep599llmzpyZ/biCvoZZfv75Zz788ENeeeUVZsyYQWhoKP379+eff/7J3mfz5s20adOG1atX88orr7BgwQLGjx9PWloa6enpgJk0tW3blkWLFvHSSy+xYMEC7r33XsaPH8/999+fo82suWhZyWZ+Fi9eDHDVvXoXv/6OMmLECA4dOpQ9LDTLrl27WLt2LcOHDwfM35VBgwZRs2ZNpk2bxs8//8xLL71UpGRy/fr1JCQk0KdPH4ecw759+/D09KR8+fIOOZ6IAIaIlBlffvmlARjr1q0zli5dagDGtm3bDMMwjDZt2hj33HOPYRiG0ahRI6Nz5875HsdmsxkZGRnGK6+8YoSFhRl2uz37Z/k9Nqu9Tp065fuzpUuX5tj+1ltvGYAxa9Ys4+677zb8/PyMLVu2OO0884tj//79BmB8+eWX2dvGjh1rXPonNL9zv9zjn3zyyRz7fvPNNwZgTJ06NXtb586dcx0XMMaOHZt9f8SIEYaXl5exY8eOfJ6V3DIzM42MjAzj3nvvNVq0aJHjZwEBAcbdd9+d6zGXPkc2m82IiooymjRp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ZVdA6tGjB3v37i3yMffu3UtUVBTR0dHccccdOSboihS3ypUrM3r0aPr06cPdd9+d735Zi01ardbsbSkpKUyZMiXXvhd/mn01bDYbgwcPxmKxZE9W/+CDD3JMlC+ogp5nVhW8LVu25Ng+Z86cArVTlHPPWnsly/Tp08nMzCz0BzQ9e/Zk6dKl7N69O999LBYL3t7eOd40xcbG5lmRraDnUq9ePSpXrsy3336bo/DCuXPnmDFjRnalveJy8803s2/fPsLCwmjdunWur6tZ+PSGG25gx44dORZlBbMHxGKx0LVr16uM/soK8xoWhJ+fH507d+aHH3647AcEN998M9u2baNWrVp5Pq9FSZz69u1LkyZNGD9+fL4JwaJFi3IN9bySa665hsDAwDzX9dqxYwfbt2/nxhtvLNCxhg8fzpo1a/j999+ZO3cud999d46/gxfz8fGhc+fO2esmbdy4sVBxe3t7M3r0aLZt2+aUtZduvPFGkpKSWLhwYa6fTZ8+HQ8PD66//nqHtytSmpTpHqfL2bdvH9999x1HjhzJ/ocwatQoFi5cyJdffpnj06CCateuHZMnT6Zu3bocP36c1157jQ4dOrB9+/ZCf7os4ihvvvnmFffp3bs377//PkOGDOGBBx7g5MmTvPvuu3muSdOkSROmTZvG999/T82aNfH19S30vCSAsWPHsnLlShYvXkxERARPP/00y5cv595776VFixZER0cX6ngFOc+IiAhuvPFGxo8fT/ny5alevTq//vprgZO1opz7zJkz8fT0pFu3bmzfvp1///vfNGvWjIEDBxaozSxZ1dA6derE888/T5MmTUhISGDhwoU89dRT1K9fn5tvvpmZM2fy8MMPc9ttt3H48GFeffVVIiMjc30g1KRJE5YtW8bcuXOJjIwkKCiIevXq5WrXw8ODt99+mzvvvJObb76ZkSNHkpaWxjvvvENCQkKBnndHeuKJJ5gxYwadOnXiySefpGnTptjtdg4dOsTixYt5+umnadeuXZGO/eSTTzJ58mR69+7NK6+8QvXq1fn555/5+OOPeeihh6hbt66Dzya3wryGBfX+++/TsWNH2rVrx5gxY6hduzbHjx9nzpw5fPrppwQFBfHKK6+wZMkSOnTowGOPPUa9evVITU3lwIEDzJ8/n4kTJ2YP97v33nv5+uuv2bdv32XnOVmtVmbNmkX3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X2RkJB9++CEffvhh9jpOwcHBeHiYA+AOHDhwxWNERETwxRdfODnS/GmonoiIiJRYPRpH0rNxRK7tXlaL04cfiUjZosRJRERESiy73WDrUbPS1r+ur824W8wJ8Jk2g3bRqqQnIo6jxElERERKrPUHT3PkdAqBPp483KU2d3eIpl54EAawYm+8q8MTkVJEiZOIiIiUWLM2HgGgR+MI/LzNRTm71K8IwPLdSpxExHGUOImIiEiJlJphY96WGAAGtKicvb1L3UoALN8Tj92eV709EZHCU+IkIiIiJdLSXXGcSc0kMsSXa2pemM/UukZ5An08OXkunW3HEl0YoYiUJkqcREREpESaufEoALc0j8LjopLjXlYPrq1tJlLLNFxPRBxEiZOIiIiUOKfPpbNsdxwAA1pUyfXzLvXM4XpZ+4iIXC0lTiIiIlLizNsaQ4bNoGFkMPUignL9vEs9s0DEpsMJnD6XXtzhiUgppMRJRERESpxZG8xqev0vKgpxscgQP+qFB2E3VJZcJAe7DfavhK0/mt/tNldHVGIocRIREZES5eDJc2w4lICHBfo2j8p3P5UlF7nEjjkwoTF8fTPMuNf8PqGxud3JYmNjefzxx6lduzb+/v7UrVuXTp06MXHiRJKTkwGoUaMGFosFi8WCv78/jRs35tNPP80+xssvv0zz5s1zHTshIQGLxcKyZcuceg5KnERERKREmXW+KMS1tStQKdg33/1UllzkIjvmwPRhkHQs5/akGHO7E5Onf/75hxYtWrB48WLeeOMN/vrrL2bNmsXjjz/O3Llz+eWXX7L3feWVV4iJiWHLli3069ePBx98kO+//95psRWGp6sDEBERESkowzCyE6f8hullubQsedMq5YohQpFiZBiQkXzl/ew2WPAMkNcHCAZggYXPQs0u4GG98vG8/MFiufJ+5z388MN4enqyfv16AgICsNvtJCUl0b59e26//XYM40JcQUFBREREAPDaa68xffp0Zs+ezaBBgwrcnrMocRIREZESY+PhBA6eTMbPy8pNjSIuu29WWfJF24+zbHe8EicpfTKS4Y38h6sWnGH2RL1ZtWC7P38MvAMKtOvJkyeze5oCAvJ+jOUySZivry8ZGRkFi8vJNFRPRERESoxZG8zeph6NIwjwufLnvypLLuJaf//9N4ZhUK9evRzba9WqRXBwMIGBgTz77LO5HpeZmclXX33F1q1bueGGG4or3MtSj5OIiIiUCOmZduZuMedn9LvCML0sWWXJN54vS14+wNtp8YkUOy9/s/fnSg7+Cd/cduX97vwRqncoWLuFdGmv0q+//oq/vz933XUXaWlp2dufffZZXnzxRdLS0vD29mb06NGMHDmy0O05gxInERERKRGW74knITmDikE+XFsrrECPiQzxo35EELtiz7Bibzx9mxcs4RIpESyWgg2Zq3U9BEeZhSDynOdkMX9e6/qCzXEqhNq1a2OxWNi1a1eO7TVq1CA4OBg/P78c20ePHs0999yDv78/kZGRORKu4OBgEhMTc7WRkJAAQEhIiENjv5SG6omIiEiJMGujuXZT32ZReFoL/hamcz2VJZcyzsMKPd46f+fS+UTn7/d40+FJE0BYWBjdunXjww8/5Ny5c1fcv0KFCtSuXZuoqKhcvVT169fnyJEjxMbG5ti+bt06PDw8qF27tkNjv5QSJxEREXF7iSkZ/LLTnKdU0GF6WVSWXARoeAsMnAzBkTm3B0eZ2xve4rSmP/74YzIzM2ndujXff/89O3fuZO/evUydOpVdu3ZhtRYsYevevTsNGjTgjjvu4I8//mD//v389NNPjBo1igcffJCgoCCnnQNoqJ6IiIiUAAu2xpCeaadueCCNooIL9ViVJRc5r+EtUL+3Oefp7HEIDDfnNDmhp+litWrVYuPGjbzxxhs899xzHDlyBB8fHxo2bMioUaN4+OGHC3QcT09PFi9ezPPPP8+dd95JXFwc1atX57777uOZZ55x6jmAEicREREpAWZmr91U5bKli/OisuQiF/GwQvR1xd5sZGQkH3zwAR988EH2Ok7BwcF4eFwYAHfgwIErHiciIoIvvvjCiZHmT0P1RERExK0dPpXM2v2nsFigb/OirVmjsuQicrWUOImIiIhbm7PZLLd8TXQYUeX8rrB33i4tSy4iUlhKnERERMRtGYbBzA1mNb3+LYteSjyrLLlhwIq9qq4nIoWnxElERETc1tajieyLP4ePpwc9G0dc1bFUllxEroYSJxEREXFbs84XhejWMJwgX6+rOpbKkktpYBi6dgvLUc+ZEicRERFxS5k2O3PPz28acBXD9LJcWpZcpCTx8jI/OEhOTnZxJCVPero5r7Gg60XlR+XIRURExC2t3HuCE2fTCQvw5ro6Fa/6eCpLLiWZ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IH1vkYMOaNlPU/RERERERLyMEic7bJoNn48ETl/414LZYyE0EhKH2BCYiIiIiIiURtc4eZor34w0lUiaikh62LQTERERERGvoMTJ05KXnTY973SWueYpeZnHQhIRERERkbNT4uRpx/dXbDsREREREal0Spw8rUY5y4yXt52IiIiIiFQ6JU6e1rArhMcDZ6qc54DweqadiIiIiIh4BSVOnuYMgH4vnLpxevJ06na/SaadiIiIiIh4BSVOdkgcAjd+BOFxxY+Hx5vjKkUuIiIiIuJVtI6TXRKHQKuBpnre8f3mmqaGXTXSJCIiIiLihZQ42ckZAI272x2FiIiIiIiUQVP1REREREREyqDESUREREREpAxKnERERERERMqgxElERERERKQMSpxERERERETKoMRJRERERESkDEqcREREREREyqDESUREREREpAxKnERERERERMqgxElERERERKQMSpxERERERETKoMRJRERERESkDEqcREREREREyhBodwCeZlkWAOnp6TZHIhciNzeXzMxM0tPTCQoKsjsc8XPqb+Jp6nPiaepz4kne1N8KcoKCHOFsqlzilJGRAUBCQoLNkYiIiIiIiDfIyMggIiLirG0cVnnSKz/icrnYu3cvNWvWxOFw2B2OnKf09HQSEhLYvXs34eHhdocjfk79TTxNfU48TX1OPMmb+ptlWWRkZBAfH4/TefarmKrciJPT6aR+/fp2hyEVJDw83PYfOKk61N/E09TnxNPU58STvKW/lTXSVEDFIURERERERMqgxElERERERKQMSpzEJ4WEhPDEE08QEhJidyhSBai/iaepz4mnqc+JJ/lqf6tyxSFERERERETOlUacREREREREyqDESUREREREpAxKnERERERERMqgxElERERERKQMSpzEa0ycOJFLL72UmjVrEhMTw7XXXsvWrVuLtbEsiyeffJL4+HiqVatGr1692LhxY7E22dnZ3HvvvURHR1O9enWGDBnC77//7smXIj5o4sSJOBwOxo0b5z6m/iYVbc+ePdx6661ERUURFhZG+/btWb16tfu8+pxUlLy8PB577DEaN25MtWrVaNKkCU8//TQul8vdRv1NLsQPP/zA4MGDiY+Px+FwMGvWrGLnK6p/HT16lBEjRhAREUFERAQjRozg2LFjlfzqSqfESbzG4sWLGTNmDCtWrGD+/Pnk5eXRp08fTpw44W7z4osv8sorr/DGG2+watUqYmNj6d27NxkZGe4248aN44svvmD69OksXbqU48ePM2jQIPLz8+14WeIDVq1axTvvvMNFF11U7Lj6m1Sko0ePcsUVVxAUFMTXX3/Npk2bePnll4mMjHS3UZ+TivLCCy8wefJk3njjDTZv3syLL77ISy+9xOuvv+5uo/4mF+LEiRNcfPHFvPHGG6Wer6j+NXz4cNauXUtSUhJJSUmsXbuWESNGVPrrK5Ul4qUOHDhgAdbixYsty7Isl8tlxcbGWpMmTXK3ycrKsiIiIqzJkydblmVZx44ds4KCgqzp06e72+zZs8dyOp1WUlKSZ1+A+ISMjAyrefPm1vz5862ePXta9913n2VZ6m9S8SZMmGB169btjOfV56QiDRw40LrjjjuKHbv++uutW2+91bIs9TepWID1xRdfuG9XVP/atGmTBVgrVqxwt1m+fLkFWFu2bKnkV1WSRpzEa6WlpQFQu3ZtAHbu3Mm+ffvo06ePu01ISAg9e/Zk2bJlAKxevZrc3NxibeLj42nbtq27jUhRY8aMYeDAgVxzzTXFjqu/SUWbPXs2nTp1YtiwYcTExNChQwemTJniPq8+JxWpW7duLFy4kG3btgGwbt06li5dyoABAwD1N6lcFdW/li9fTkREBJdffrm7TefOnYmIiLClDwZ6/BlFysGyLMaPH0+3bt1o27YtAPv27QOgbt26xdrWrVuX5ORkd5vg4GBq1apVok3B/UUKTJ8+nZ9//plVq1aVOKf+JhXtt99+46233mL8+PE8+uijrFy5krFjxxISEsLIkSPV56RCTZgwgbS0NFq1akVAQAD5+fk899xz3HzzzYB+x0nlqqj+tW/fPmJiYko8fkxMjC19UImTeKV77rmHX375haVLl5Y453A4it22LKvEsdOVp41ULbt37+a+++7j22+/JTQ09Izt1N+korhcLjp16sTzzz8PQIcOHdi4cSNvvfUWI0eOdLdTn5OK8NlnnzFt2jQ++eQT2rRpw9q1axk3bhzx8fGMGjXK3U79TSpTRfSv0trb1Qc1VU+8zr333svs2bP5/vvvqV+/vvt4bGwsQIlvGA4cOOD+RiM2NpacnByOHj16xjYiYKYIHDhwgI4dOxIYGEhgYCCLFy/mtddeIzAw0N1f1N+kosTFxZGYmFjsWOvWrUlJSQH0O04q1oMPPsjDDz/MTTfdRLt27RgxYgT3338/EydOBNTfpHJVVP+KjY1l//79JR7/4MGDtvRBJU7iNSzL4p577mHmzJl89913NG7cuNj5xo0bExsby/z5893HcnJyWLx4MV27dgWgY8eOBAUFFWuTmprKhg0b3G1EAK6++mrWr1/P2rVr3VunTp245ZZbWLt2LU2aNFF/kwp1xRVXlFhiYdu2bTRs2BDQ7zipWJmZmTidxT/mBQQEuMuRq79JZaqo/tWlSxfS0tJYuXKlu82PP/5IWlqaPX3Q4+UoRM7g7rvvtiIiIqxFixZZqamp7i0zM9PdZtKkSVZERIQ1c+ZMa/369dbNN99sxcXFWenp6e42d911l1W/fn1rwYIF1s8//2xdddVV1sUXX2zl5eXZ8bLEhxStqmdZ6m9SsVauXGkFBgZazz33nLV9+3br3//+txUWFmZNmzbN3UZ9TirKqFGjrHr16llz5861du7cac2cOdOKjo62HnroIXcb9Te5EBkZGdaaNWusNWvWWID1yiuvWGvWrLGSk5Mty6q4/tWvXz/roosuspYvX24tX77cateunTVo0CCPv17LsiwlTuI1gFK3qVOnutu4XC7riSeesGJjY62QkBCrR48e1vr164s9zsmTJ6177rnHql27tlWtWjVr0KBBVkpKiodfjfii0xMn9TepaHPmzLHatm1rhYSEWK1atbLeeeedYufV56SipKenW/fdd5/VoEEDKzQ01GrSpIn1t7/9zcrOzna3UX+TC/H999+X+rlt1KhRlmVVXP86fPiwdcstt1g1a9a0atasad1yyy3W0aNHPfQqi3NYlmV5fpxLRERERETEd+gaJxERERERkTIocRIRERERESmDEicREREREZEyKHESEREREREpgxInERERERGRMihxEhERERERKYMSJxERERERkTIocRIRERERESmDEicREfF7jRo14tVXX63U57jtttu49tprK/U5RETEPkqcRESk0t122204HA7uuuuuEudGjx6Nw+HgtttuK/fj7dq1C4fDwdq1a8vVftWqVfz5z38u9+OX5u233+biiy+mevXqREZG0qFDB1544QX3+X/+85988MEHF/QcIiLivZQ4iYiIRyQkJDB9+nROnjzpPpaVlcWnn35KgwYNKuU5c3JyAKhTpw5hYWHn/Tjvvfce48ePZ+zYsaxbt47//ve/PPTQQxw/ftzdJiIigsjIyAsNWUREvJQSJxER8YhLLrmEBg0aMHPmTPexmTNnkpCQQIcOHYq1TUpKolu3bkRGRhIVFcWgQYPYsWOH+3zjxo0B6NChAw6Hg169egGF0+UmTpxIfHw8LVq0AIpP1Vu0aBHBwcEsWbLE/Xgvv/wy0dHRpKamlhr7nDlzuPHGG7nzzjtp1qwZbdq04eabb+aZZ55xtyk6Va9gROz0rSBOgGXLltGjRw+qVatGQkICY8eO5cSJE+f2poqIiMcocRIREY+5/fbbmTp1qvv2+++/zx133FGi3YkTJxg/fjyrVq1i4cKFOJ1OrrvuOlwuFwArV64EYMGCBaSmphZLxhYuXMjmzZuZP38+c+fOLfHYvXr1Yty4cYwYMYK0tDTWrVvH3/72N6ZMmUJcXFypccfGxrJixQqSk5PL9ToTEhJITU11b2vWrCEqKooePXoAsH79evr27cv111/PL7/8wmeffcbSpUu55557yvX4IiLieQ7Lsiy7gxAREf922223cezYMd59913q16/Pli1bcDgctGrVit27d/PHP/6RyMjIM14jdPDgQWJiYli/fj1t27Zl165dNG7cmDVr1tC+fftiz5OUlERKSgrBwcHu440aNWLcuHGMGzcOMFP4OnfuTPPmzdm4cSNdunRhypQpZ4w/NTWV66+/nhUrVtCiRQu6dOnCgAED+MMf/oDT6Sz2GmfNmlXsvllZWfTq1Ys6derw5Zdf4nQ6GTlyJNWqVePtt992t1u6dCk9e/bkxIkThIaGntsbLCIilU4jTiIi4jHR0dEMHDiQDz/8kKlTpzJw4ECio6NLtNuxYwfDhw+nSZMmhIeHu6fmpaSklPkc7dq1K5Y0lSY4OJhp06YxY8YMTp48WWbFvbi4OJYvX8769esZO3Ysubm5jBo1in79+rlHwc7kzjvvJCMjg08++cSdZK1evZoPPviAGjVquLe+ffvicrnYuXNnma9RREQ8L9DuAEREpGq544473FPS3nzzzVLbDB48mISEBKZMmUJ8fDwul4u2bdu6iz2cTfXq1csVx7JlywA4cuQIR44cKdf92rZtS9u2bRkzZgxLly6le/fuLF68mCuvvLLU9s8++yxJSUmsXLmSmjVruo+7XC7+8pe/MHbs2BL3qaxCGSIicmGUOImIiEf169fPnQD17du3xPnDhw+zefNm3n77bbp37w6YaWxFFYwo5efnn1cMO3bs4P7772fKlCl8/vnnjBw50n0tVXklJiYCnLGgw4wZM3j66af5+uuvadq0abFzl1xyCRs3bqRZs2bnFb+IiHieEicREfGogIAANm/e7N4/Xa1atYiKiuKdd94hLi6OlJQUHn744WJtYmJiqFatGklJSdSvX5/Q0FAiIiLK9fz5+fmMGDGCPn36cPvtt9O/f3/atWvHyy+/zIMPPljqfe6++27i4+O56qqrqF+/PqmpqTz77LPUqVOHLl26lGi/YcMGRo4cyYQJE2jTpg379u0DTMJXu3ZtJkyYQOfOnRkzZgx/+tOfqF69urugxeuvv16u1yEiIp6la5xERMTjwsPDCQ8PL/Wc0+lk+vTprF69mrZt23L//ffz0ksvFWsTGBjIa6+9xttvv018fDxDhw4t93M/99xz7Nq1i3feeQcwFfPeffddHnvssTMuqHvNNdewYsUKhg0bRosWLbjhhhsIDQ1l4cKFREVFlWj/008/kZmZybPPPktcXJx7u/766wG46KKLWLx4Mdu3b6d79+506NCBxx9//IxV/URExH6qqiciIiIiIlIGjTiJiIiIiIiUQYmTiIiIiIhIGZQ4iYiIiIiIlEGJk4iIiIiISBmUOImIiIiIiJRBiZOIiIiIiEgZlDiJiIiIiIiUQYmTiIiIiIhIGZQ4iYiIiIiIlEGJk4iIiIiISBmUOImIiIiIiJTh/wEdIQTjLDE0GAAAAABJRU5ErkJggg==",
+ "text/plain": [
+ "