【话在前头】
用 Docker 部署 JupyterLab 感觉是部署 JupyterLab 最方便的方式了,官方提供了很多可选的镜像,也可以自己从 jupyter/base-notebook 中继续打包,镜像启动命令加上“--NotebookApp.password”就可以直接用密码登录用了。虽然只是自己一个人用,但是如果放在互联网上访问的话,总感觉不是那么安全,还是希望能像其他服务一样,能独立管理用户信息,能设置二次验证(2FA)。不过搜了下网上关于 JupyterHub 的资料比较少,甚至于官方的说明文档写的也不是很详细,有些配置和参数只能去源码里扒。
【文章索引】
【一、打包 JupyterHub 镜像】
JupyterHub 架构的介绍和原理官方文档中描述的非常清楚了,这里不再赘述了,简单说就是 JupyterHub 把 认证 和 单用户 JupyterLab 的管理 分别拆成了 Authenticator 和 Spawner 模块,可以根据不同的需要配置不同的认证方式或管理方式。不过官方的 JupyterHub 镜像只包含了 JupyterHub 项目 本身,只有最基本的认证和管理(如通过 Linux 下 PAM 进行认证、通过本地进程运行 JupyterLab 等)。如果想通过自定义账号密码、并且开启 2FA 的话,JupyterHub 其实也已经实现了一个官方的 NativeAuthenticator 模块,官方文档还是比较详细的,默认用户信息存储在 JupyterHub 的 sqlite 数据库中,可以通过数据源配置改成 Mysql,如果需要连接 Mysql 的话,官方的镜像也不包含相关模块,也需要自行安装。
除此之外,如果 JupyterHub 管理的 JupyterLab 也想在 docker 中运行的话,还需要使用官方提供的 DockerSpawner 进行管理,不过官方文档不是特别详细,好在代码不多,扒扒代码也能看明白具体应该怎么配置。
所以,如果我们需要实现能独立管理的用户信息、支持2FA、使用Mysql数据库存储用户数据,用户的 JuyterLab 也通过 docker 镜像进行运行和管理的话,我们可以通过如下的 Dockerfile 在官方镜像之上打一个更完整的镜像。
1 ARG BASE_IMAGE=jupyterhub/jupyterhub:1.2 2 FROM $BASE_IMAGE 3 4 LABEL maintainer="MaysWind " 5 6 # Install Dependencies 7 RUN apt-get update \ 8 && apt-get install -y --no-install-recommends unzip \ 9 && rm -rf /var/lib/apt/lists/* \ 10 && rm -rf /tmp/* 11 12 # Install Mysql 13 RUN pip3 --no-cache-dir install mysql-connector \ 14 && rm -rf /tmp/* 15 16 # Install NativeAuthenticator 17 RUN curl "https://github.com/jupyterhub/nativeauthenticator/archive/master.zip" -L -o /tmp/nativeauthenticator.zip \ 18 && unzip /tmp/nativeauthenticator.zip -d /tmp \ 19 && mv /tmp/nativeauthenticator-master /usr/local/bin/nativeauthenticator \ 20 && pip --no-cache-dir install -e /usr/local/bin/nativeauthenticator \ 21 && rm -rf /tmp/* 22 23 # Install DockerSpawner 24 RUN pip --no-cache-dir install dockerspawner \ 25 && rm -rf /tmp/*
注:写这篇博客的时候,JupyterHub 的最新 Release 版本是 1.1.0,但是 1.1.0 的 docker 镜像存在问题(静态资源没有编译等),所以这里使用的是还在开发中的镜像(1.2 tag 目前与 1.2.0dev tag 一致)。
【二、配置 JupyterHub 启动参数】
打完镜像后后其实就可以启动了,不过通常还有些配置需要调整下。我通过 docker-compose 启动 JupyterHub 容器,所有配置参数都通过参数或环境变量进行配置,同时由于 JupyterHub 在 docker 容器中,还需要把宿主机的 docker.sock 挂载到容器内,以便 JupyterHub 能够管理 JupyterLab 容器。并且为 JupyterHub 和之后的 JupyterLab 建了一个单独的网络,方便之后对 JupyterLab 的请求进行隔离,如果没有需求的话实际上按默认的网络配置也是可以的,相关的 yml 示例配置如下
1 version: "2" 2 networks: 3 jupyter-network: 4 driver: bridge 5 ipam: 6 config: 7 - subnet: 192.168.254.0/24 8 gateway: 192.168.254.1 9 services: 10 jupyterhub: 11 image: 你的 JupyterHub 镜像名称 12 container_name: jupyterhub 13 hostname: "jupyterhub" 14 networks: 15 - "jupyter-network" 16 command: 17 - "jupyterhub" 18 - "--JupyterHub.hub_bind_url='http://:8081'" # JupyterHub 默认绑定 127.0.0.1,需要改成绑定所有 IP 使 JupyterLab 能跨容器访问 19 - "--JupyterHub.db_url='mysql+mysqlconnector://Mysql用户名:Mysql密码@数据库地址/数据库名称'" # 设置 Mysql 数据库,如果使用默认 Sqlite,可以挂载目录到 /srv/jupyterhub 实现数据库持久化 20 - "--JupyterHub.authenticator_class='nativeauthenticator.NativeAuthenticator'" # 使用 NativeAuthenticator 21 - "--JupyterHub.spawner_class='dockerspawner.DockerSpawner'" # 使用 DockerSpawner 22 - "--JupyterHub.admin_access=True" # 启用管理员功能 23 - "--Authenticator.admin_users={'管理员账户名称'}" # 管理员名称 24 - "--Authenticator.allow_2fa=True" # 开启 2FA 功能 25 - "--DockerSpawner.remove_containers=True" # 每次启动 JuypyterLab 容器时都删除之前的容器,如果通过 docker-compose 设置的网络,docker-compose 重新配置网络后一定要重新创建容器才能启动 26 - "--DockerSpawner.notebook_dir='/home/jovyan/work'" # 设置笔记本默认目录(默认是 ~) 27 - "--DockerSpawner.image='你的 JupyterLab 镜像名称'" 28 - "--DockerSpawner.network_name='JupyterLab 网络名称'" # 如果是通过 docker-compose 设置的网络,与第3行可能不一致,需要通过 docker network ls 查看 29 - "--DockerSpawner.args=['--Application.log_level=WARN']" # 设置日志默认输出级别 30 - "--DockerSpawner.environment={\ 31 'JUPYTER_ENABLE_LAB': 'yes'\ # 开启 JupyterLab 32 }" 33 - "--DockerSpawner.volumes={\ 34 '/etc/localtime': {'bind': '/etc/localtime', 'mode': 'ro'},\ 35 '本机 Jupyter 笔记存储路径': '/home/jovyan/work'\ # 可以使用 “{username}” 占位,表示用户名,如 '/mnt/data1/jupyter/{username}/work': '/home/jovyan/work' 36 }" 37 volumes: 38 - "/etc/localtime:/etc/localtime:ro" 39 - "/var/run/docker.sock:/var/run/docker.sock" 40 restart: on-failure
其中,JupyterHub 配置文件中的配置都可以通过启动参数的方式进行配置,如上述配置中 command 中的配置项,所有 JupyterHub 配置可以参考官方文档。对于 NativeAuthenticator,也额外提供了一些其他参数,如自己注册完账号,可以设置“Authenticator.open_signup”参数为 False,关闭开放注册功能,“Authenticator.ask_email_on_signup” 注册时需要提供邮箱账号等,这些参数可以如上附到启动参数中,或者也可写入到配置文件中,更多参数和用法可以参考官方文档。对于 DockerSpawner,有些参数是实现了基础类 Spawner 中定义的,可以查阅 Spawner 的定义文档 进行配置,也有部分是其本身单独实现的,可以查阅其源代码,例如其支持限制内存 “DockerSpawner.mem_limit”、限制CPU “DockerSpawner.cpu_limit”等参数,都是实现基础类 Spawner 中定义的,Docker 网络名称 “DockerSpawner.network_name ”、启动容器前删除已有容器的参数 “DockerSpawner.remove_containers” 等都是其本身自己实现的。
如果之前也是通过 docker 部署的 JupyterLab,可能下述几个参数能迁移大部分之前的个性化配置,
【三、启动 JupyterHub】
根据第二步的配置,就可以通过 docker-compose 或者其他方式启动 JupyterHub 的 docker 镜像了,只不过很有可能会失败,主要是由于 NativeAuthenticator 对 Mysql 的兼容性问题,用于管理注册用户信息的那张表没有自动创建成功,不过我们可以帮他完成这个任务,即编写类似如下的SQL(具体存储引擎、编码可以根据自己实际情况调整)。
CREATE TABLE `users_info` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(255) NOT NULL, `password` blob NOT NULL, `is_authorized` bit(1) DEFAULT NULL, `email` varchar(255) DEFAULT NULL, `has_2fa` bit(1) DEFAULT NULL, `otp_secret` varchar(16) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
创建完 users_info 表后 JupyterHub 应该就能正常启动了,接下来就可以自己进行注册账号了,如果没有关闭开放注册功能或者注册的账号名在配置中的管理员用户名中的话,账号直接就可以登录,否则需要自行去数据库中找到自己注册的记录,并将 “is_authorized” 字段设置为1。
登录后应该会默认启动 JupyterLab,或者也可以自行选择启动,启动成功后会自动跳转到 JupyterLab,下次访问时直接就会访问 JupyterLab,而不会再显示 JupyterHub 的界面了。如果启动失败,也可以通过 docker 查看 JupyterLab 的容器情况。
【四、隔离 JupyterHub/JupyterLab 网络】
JupyterLab 里什么都能干,能执行代码,能运行脚本,总觉得部署了 JupyterLab 后,直接把内网环境对外打开了,所以还想再对 JupyterHub/JupyterLab 的网络进行隔离,不允许其访问内网。这块通过 iptables 就可以实现,比如上述我定义了 jupyter-network 网络,IP 是 192.168.254.0/24,我内网 IP 是 192.168.1.0/24,路由(网关)是 192.168.1.1,所以我在宿主机上定义如下 iptables,禁止来自 jupyter-network 的 IP 请求内网 IP(但允许通过路由访问互联网)。当然,如果 Mysql 服务器不与 JupyterHub/JupyterLab 在一台宿主机上的话,别忘了允许 JupyterHub 的 IP 地址访问 Mysql 端口。
iptables -I DOCKER-USER -s 192.168.254.0/24 -d 192.168.1.0/24 -j DROP iptables -I DOCKER-USER -s 192.168.254.0/24 -d 192.168.1.1 -j ACCEPT
此外,如果宿主机上还有其他服务或 docker 实例,如果需要禁止 JupyterHub/JupyterLab 访问他们,还需要再定义一条
iptables -I INPUT -s 192.168.254.0/24 -p tcp -j DROP
这样,应该就相对安全了一些吧。