JupyterHub搭建 + OAuth2认证 + Anaconda

最近因为公司项目,还得利用空余时间学习NLP。首先得搭建炼丹环境,Python下自然首选Jupyter。不过之前一直在使用单纯的JupyterLab/Notebook,正巧现在单点登录系统早已经搭建完成,正愁接入应用太少,现在不妨升级一下,使用支持多人同时使用的JupyterHub。(才不是怕公司监听了我的管理员帐号密码(尽管启用了两步认证),才另开一个普通账户呢!)

目这篇文章目标如下:要求JupyterHub能使用已有认证系统进行认证登录、为登录用户自动创建相关用户文件夹、用户文件夹相互隔离、每个用户可使用不同Anaconda环境、且每个用户管可理自己的Anaconda环境。

这篇文章仅适用于Linux。(没钱买Windows Server)

0 准备工作

准备工作就不多赘述了,系统需要装好pip、anaconda等软件包并可用。需要注意的是,需要使用npm安装模块,因此你可能也需要装个nodejs。

当然了,如果你想公开访问自己的网站,还需要有域名,HTTPS证书,等等

1 安装JupyterHub

1.1 安装

直接使用pip安装即可,推荐安装到系统。如果你想安装到已有的anaconda环境也可以,只不过稍后的配置过程会有亿点点不同,当然了,我也会说明的。

若安装到系统:

# 你可以先搜索软件仓库里有没有jupyterhub包,如果有,建议直接从软件仓库安装:
apt search jupyterhub 还是 jupyter-hub jupyter_hub什么的
apt install juyterhub

# 如果你发现软件仓库里找不到jupyterhub包,那就老老实实通过pip安装吧:
sudo pip install jupyterhub

若安装到anaconda环境:

conda activate env_name
conda install jupyterhub

1.2 基本配置

安装完成后,生成配置文件,并将其放置到/etc/文件夹下(其实放哪里随你):

jupyterhub --generate-config -f /etc/jupyterhub/jupyterhub_config.py

然后进行基本的配置,如设置监听IP、端口等等

官方文档已经写了很多了,这里就不再赘述了:https://jupyterhub.readthedocs.io/en/stable/

1.3 尝试运行一下

注意!运行前需要先安装configurable-http-proxy,这是一个nods.js的模块。(貌似pip也有这个模块的开源实现,但没测试过,不知其可用性)

sudo npm install -g configurable-http-proxy
# 嫌太慢的话,使用淘宝源安装:
sudo npm --registry https://registry.npm.taobao.org install -g configurable-http-proxy

安装完成后,就可以尝试运行一下了:

jupyterhub -f /etc/jupyterhub/jupyterhub_config.py
首次打开JupyterHub登录页面

其实此时JupyterHub已经可用,使用系统用户和密码登录即可。只需要使用系统用户的同学可以直接跳转到第5节配置Anaconda了。但我们这篇文章目标是星辰大海,不使用这种传统但简单的登录方法,而是使用更加便捷、可拓展性更好的单点登录系统,我们还有很长的路要走——

2 为JupyterHub启用单点登录

参考:https://github.com/jupyterhub/oauthenticator

OAuthenticator文档:https://oauthenticator.readthedocs.io/en/latest/getting-started.html

2.1 单点登录系统基本信息配置

打开配置文件jupyterhub_config.py,编辑添加以下配置:

c.JupyterHub.authenticator_class = 'generic-oauth'
c.GenericOAuthenticator.oauth_callback_url = 'https://你的hub地址/hub/oauth_callback'
c.GenericOAuthenticator.client_id = 'client_id'
c.GenericOAuthenticator.client_secret = 'client_secret'
c.GenericOAuthenticator.login_service = '显示名称'
c.GenericOAuthenticator.authorize_url = "https://单点登录系统/oauth/authorize"
c.GenericOAuthenticator.userdata_url = 'https://单点登录系统/user'
c.GenericOAuthenticator.token_url = 'https://单点登录系统/token'
# 配置从哪里获取用户名信息
# 这取决于你的认证系统如何返回用户数据(POST https://认证系统/user)
# 比如认证系统返回{"email": "foo@bar.com", "username": "foobar"}
# 你可以配置成'email'使用邮箱作为用户名
c.GenericOAuthenticator.username_key = 'email'

注:以上为针对普通OAuth2.0协议服务器的配置。事实上,OAuthenticator已经内置提供了许多认证服务器的支持,如使用GitHub、Google登录等等,参考上方的OAuthenticator文档即可轻松完成相关的接入。

2.2 配置白名单

我的私人服务器虽然也计划加一张4090,但毕竟不是V100,没有矿场那样的算力。所以有必要限制谁可以使用。继续在配置文件jupyterhub_config.py中添加以下配置:

c.GenericOAuthenticator.allowed_users = [
    "foo@bar.com",
    "bar@baz.com",
]

2.3 配置用户映射(可选)

有些时候可能你不需要自动创建用户功能,因此需要将单点登录系统返回的邮箱和你本地账户关联起来,此时添加映射可以方便地完成这一自动转换。

如果你打算配置自动创建用户,建议不要配置。

继续在配置文件jupyterhub_config.py中添加以下配置:

c.GenericOAuthenticator.username_map = {
    "foo@bar.com": "foo-bar",
    "bar@baz.com": "my-local-name",
}

你也可以通过重载GenericOAuthenticator类来配置用户映射:

from oauthenticator.generic import GenericOAuthenticator as OAuthenticator
class GenericOAuthenticator(OAuthenticator):
    def normalize_username(self,username):
        username = username.lower()
        username = username.split('@')[0]
        return username

c.JupyterHub.authenticator_class = GenericOAuthenticator

完成后,保存配置文件,再尝试启动一下:

jupyterhub -f /etc/jupyterhub/jupyterhub_config.py

打开网页,发现已经可以使用单点登录系统登录了,点击按钮,能跳转到认证系统:

配置单点登录后的登录页面

3 JupyterHub自动创建用户

实现了使用单点登录系统登录后,当我们访问JupyterHub,其认证工作流程如下:

  • 用户打开一个页面,发现未登录,于是重定向到登录页面,这个重定向携带了c.GenericOAuthenticator.oauth_callback_url、c.GenericOAuthenticator.client_id信息;
  • 用户点击“Sign in with foobar”,跳转到认证系统进行登录;
  • 用户在认证系统登录完成,跳转回c.GenericOAuthenticator.oauth_callback_url,同时携带授权码;
  • JupyterHub收到授权码,利用这个授权码对c.GenericOAuthenticator.token_url进行请求,获取到访问令牌;
  • JupyterHub利用这个访问令牌访问c.GenericOAuthenticator.userdata_url,获取到用户信息,一般包括邮箱、用户名等信息;
  • JupyterHub通过c.GenericOAuthenticator.username_key确定哪个是用户名,于是获取到了用户名;
  • JupyterHub确定这个用户名对应的用户空间是否存在,将调用Spawner来创建用户空间。

所以,现在我们来到了最后一步,JupyterHub获取到了用户名后,应该如何创建用户空间。

3.1 方法一:直接创建系统用户

这种方法简单粗暴,认证到了什么用户,就调用系统命令来创建相应的用户就可以了,用户的空间就位于默认的/home/user_name文件夹,这个过程直接由GenericOAuthenticator完成。无需额外配置。

当然了,如果你想限制用户在系统的其他信息,可以添加如下配置,本质上是输入一条adduser命令:

c.LocalGenericOAuthenticator.add_user_cmd = [‘adduser’, ‘-q’, ‘–gecos’, ‘””’, ‘–home’, ‘/customhome/USERNAME’, ‘–disabled-password’]

3.2 方法二:使用SystemdSpawner创建

方法一虽然简单,但这些用户一旦添加到了系统,就意味着他们也有了访问这个系统的完整权限,虽然可以通过adduser的一系列参数+更加严格的系统策略等一套组合拳来限制用户,但毕竟太麻烦。此时就可以使用Systemd创建动态用户的方式来管理了。

不多说,先上配置:

c.JupyterHub.spawner_class = 'systemdspawner.SystemdSpawner'
# 是否以动态用户的方式创建用户
c.SystemdSpawner.dynamic_users = True
# 默认Shell
c.SystemdSpawner.default_shell = '/bin/zsh'
# 临时systemd service名称
c.SystemdSpawner.unit_name_template = 'jupyter-{USERNAME}-singleuser'
# 是否隔离/tmp文件夹
c.SystemdSpawner.isolate_tmp = True
# 是否隔离/dev下设备
c.SystemdSpawner.isolate_devices = False
# 禁止sudo
c.SystemdSpawner.disable_user_sudo = True
# 只读文件夹
c.SystemdSpawner.readonly_paths = ['/']

更加详细的说明可参考官方仓库:https://github.com/jupyterhub/systemdspawner

这样一来,JupyterHub会将用户文件夹存储到/var/lib/username,比如/var/lib/foo@bar.com,且uid,gid是按需动态的,权限也可被进一步限制。

uid和gid信息

试着运行一下,登录,然后享受吧:

jupyterhub -f /etc/jupyterhub/jupyterhub_config.py

4 完成部署

现在来稍微收一下尾,让我们的服务持续运行。

4.1 编写systemd service

编写一个systemd service来让我们的JupyterHub服务开机自动运行:

vi /etc/systemd/system/jupyterhub.service
=====================
[Unit]
Description=Jupyter Hub

[Service]
Type=simple
User=root
Group=root
ExecStart=/usr/bin/jupyterhub -f jupyterhub_config.py
WorkingDirectory=/etc/jupyter
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target

如果你将jupyterhub安装在anaconda环境中,可以将命令封装到一个脚本中,再使用systemd执行:

#!/bin/bash

. /anaconda安装路径/anaconda/bin/activate env_name
/usr/bin/jupyterhub -f jupyterhub_config.py

===================

ExecStart=/foo/bar/start.sh

===================

sudo chmod +x /foo/bar/start.sh
sudo chown root:root /foo/bar/start.sh

5 配置Anaconda

历经千难万险,我们的JupyterHub终于起来了,现在还差最后一步——在Jupyter中利用好Anaconda。

我们完全可以通过已经搭建好的JupyterHub来完成这一工作,让我们先进入Terminal。是不是成就感满满?

进入Terminal

如果有需要,我们可以先创建一个环境,并切换进来

conda create -n "new_env"
conda activate new_env

然后,安装ipython和ipykernel(网上的教程都漏了ipython!不过其实也可有可无,具体的原因稍后讲解)

conda install ipython ipykernel

重新激活环境(需要更新环境变量),然后检查ipykernel是不是当前环境的:

conda activate new_env
which ipython

输出的ipython路径应该位于环境中:

检查ipython路径

最后,安装当前kernel为用户全局:

ipython kernel install --user --name="new_env_kernel"

关闭Terminal,刷新页面,新建一个Notebook,可以看到新的Kernel了:

新Kernel已可供选择

最后说明一下为什么需要安装ipython:

  • 如果你使用pip将JupyterHub安装到系统,那么因为依赖关系,同时也会安装ipython和ipykernel。
  • 但是,当你创建一个新的、空白的anaconda环境时,环境内是没有ipython和ipykernel的,因此在环境内执行ipython来安装kernel到用户全局时,使用的是系统的ipython。
  • 系统的ipython当然只能注册系统的ipykernel,毕竟它完全没有头绪,你要注册的ipykernel位于哪里。因此你需要首先在环境中安装好ipython,执行环境内的ipython来注册ipykernel。
  • 同理,如果你是在anaconda环境中安装的JupyterHub,切换环境后就无法访问ipython,执行安装命令会直接报错。就算你找到了这个文件,执行的还是其他环境的ipython,自然无法注册当前环境的ipykerel。
  • 再深入一点:执行ipython kernel install时,本质上是将ipykernel相关信息存储到.local/share/jupyter/kernels/kernel_name中。其中这个目录下的kernel.json文件描述了kernel所在位置。Jupyter对kernel的选择,实际上就是来源于这个文件夹内的配置。你可以打开这个文件夹,尤其是kernel.json看看。

大功告成!TF、PyTorch、Paddle装起来,开始愉(tong)快(ku)的Coding (tiao can)吧!

发表评论