Add a Jupyter App on a Kubernetes Cluster¶
This tutorial will walk you through creating an interactive Jupyter app that your users will use to launch a Jupyter Notebook Server in a kubernetes cluster.
It assumes you have a working understanding of app development already. The purpose of this document is to describe how to write apps specifically for a kubernetes cluster, so it skips a lot of important details about app development that may be found in other tutorials like Add a Jupyter App.
We’re going to be looking at the bc k8s jupyter app which you can fork, clone and modify for your site. This page also holds the submit yml in full for reference.
Refer to interactive K8s Jupyter using HPC-like containers for details on running Jupyter on Kubernetes with containers that behave more like traditional HPC jobs.
container spec¶
Let’s look at this section first. Here you must specify the name
, image
and command
. The name determines the Pod Id (the job name in HPC parlance).
working_dir
is the working directory of the container. This is optional as
a container may specify this. restart_policy
is also optional and is Never
by default.
Next you can specify additional environment variables in env
.
container:
name: "jupyter"
image: "docker.io/jupyter/scipy-notebook:python-3.9.7"
command: "/usr/local/bin/start.sh /opt/conda/bin/jupyter notebook --config=/ood/ondemand_config.py"
working_dir: "<%= Etc.getpwnam(ENV['USER']).dir %>"
restart_policy: 'OnFailure'
env:
NB_UID: "<%= user.uid %>"
NB_USER: "<%= user.name %>"
NB_GID: "<%= user.group.id %>"
HOME: "<%= user.home %>"
Here is the default environment. You can use a null
here to unset any of these.
USER: username,
UID: run_as_user,
HOME: home_dir,
GROUP: group,
GID: run_as_group,
KUBECONFIG: '/dev/null'
resource requests¶
port
is is the port the container is going to listen on. cpu
and memory
are the cpu and memory request. Note that memory here has Gi
which is the unit.
container:
# ...
port: "8080"
cpu: "<%= cpu %>"
memory: "<%= memory %>Gi"
Kubernetes has some flexibility in requests. One can make _requests_ and _limits_ which are like hard and soft limits. In the example above, they’re both the same.
Here’s an example utilizing requests and limits for both memory and cpu. Note that
we’re using millicores in cpu_request
.
container:
# ...
port: "8080"
cpu_request: "0.200"
cpu_limit: "4"
memory_request: "500Mi"
memory_limit: "4Gi"
See kubernetes pod memory and kubernetes pod cpu for more details.
configmap¶
A Kubernetes configmap is a way to apply configurations to a container. In this example, we’re using a configmap to generate the config file for Jupyter. We’ll see later how we use init containers to update it, but let’s see how we initialize it.
You need to specify filename
which is the name of the file. data
is
the contents of the file. mount_path
is the directory in the container
the file will be mounted to. files
here is an array so you can add many
files to a single configmap.
configmap:
files:
- filename: "<%= configmap_filename %>"
data: |
c.NotebookApp.port = 8080
c.NotebookApp.ip = '0.0.0.0'
c.NotebookApp.disable_check_xsrf = True
c.NotebookApp.allow_origin = '*'
c.Application.log_level = 'DEBUG'
mount_path: '/ood'
mounts¶
This example mounts the host’s directory into the container. Even though these are containers, users often want to persist the files they work on. This example mounts the home directory, but could mount any project or scratch space just the same.
When mounting a host directory host_type
must alwasy be Directory.
This example shows how to mount host directories and nfs storage locations.
mounts:
- type: host
name: home
host_type: Directory
path: <%= user.home %>
destination_path: <%= user.home %>
- type: nfs
name: cold-storage
host: some.nfs.host:3333
path: /some/location
destination_path: /some/container/location
init containers¶
If you’re app needs some work to be done before the app itself (the container) starts up, we provide a way to specify init containers.
We provide docker.io/ohiosupercomputer/ood-k8s-utils
for some simple
reusable functionality.
You must specify a name
, an image
and the command
to be run.
init_containers:
- name: "init-secret"
image: "<%= utility_img %>"
command:
- "/bin/save_passwd_as_secret"
- "user-<%= user.name %>"
Tip
If you’re mounting a users $HOME
directory into the container, you
likely don’t need init containers. They’re provided for sites & use cases
where you’re not mounting the users $HOME
directory. This example
does both because it is just an example.
Let’s walk through these init containers and what they’re doing.
init-secret
does just that. It initialzies a kubernetes secret.
add-passwd-to-cfg
then reads that secret and creates a salt and
sha1 of this secret (these are needed specifically for Jupyter). Lastly
it adds a single line to our configmap, which is the c.NotebookApp.password
.
add-hostport-to-cfg
does something similar, reading the host and port
of the pod and sets the c.NotebookApp.base_url
of the same configmap.
submit yml in full¶
# submit.yml.erb
<%
pwd_cfg = "c.NotebookApp.password=u\'sha1:${SALT}:${PASSWORD_SHA1}\'"
host_port_cfg = "c.NotebookApp.base_url=\'/node/${HOST_CFG}/${PORT_CFG}/\'"
configmap_filename = "ondemand_config.py"
configmap_data = "c.NotebookApp.port = 8080"
utility_img = "docker.io/ohiosupercomputer/ood-k8s-utils:v1.0.0"
user = OodSupport::User.new
%>
---
script:
accounting_id: "<%= account %>"
wall_time: "<%= wall_time.to_i * 3600 %>"
native:
# here's the bulk of setting up the container. You'll likely need to specify all of these.
container:
name: "jupyter"
image: "docker.io/jupyter/scipy-notebook:python-3.9.7"
command: "/usr/local/bin/start.sh /opt/conda/bin/jupyter notebook --config=/ood/ondemand_config.py"
working_dir: "<%= Etc.getpwnam(ENV['USER']).dir %>"
restart_policy: 'OnFailure'
env:
NB_UID: "<%= user.uid %>"
NB_USER: "<%= user.name %>"
NB_GID: "<%= user.group.id %>"
HOME: "<%= user.home %>"
port: "8080"
cpu: "<%= cpu %>"
memory: "<%= memory %>Gi"
configmap:
files:
- filename: "<%= configmap_filename %>"
data: |
c.NotebookApp.port = 8080
c.NotebookApp.ip = '0.0.0.0'
c.NotebookApp.disable_check_xsrf = True
c.NotebookApp.allow_origin = '*'
c.Application.log_level = 'DEBUG'
mount_path: '/ood'
mounts:
- type: host
name: home
host_type: Directory
path: <%= user.home %>
destination_path: <%= user.home %>
init_containers:
- name: "init-secret"
image: "<%= utility_img %>"
command:
- "/bin/save_passwd_as_secret"
- "user-<%= user.name %>"
- name: "add-passwd-to-cfg"
image: "<%= utility_img %>"
command:
- "/bin/bash"
- "-c"
- "source /bin/passwd_from_secret; source /bin/create_salt_and_sha1; /bin/add_line_to_configmap \\\"<%= pwd_cfg %>\\\" <%= configmap_filename %>"
- name: "add-hostport-to-cfg"
image: "<%= utility_img %>"
command:
- "/bin/bash"
- "-c"
- "source /bin/find_host_port; /bin/add_line_to_configmap \\\"<%= host_port_cfg %>\\\" <%= configmap_filename %>"