R2 虽然说是对标 AWS S3,但是并没有完整实现所有 API 接口,所以目前 Github 上发布的 uPic 版本无法兼容。尽管可以自行通过修改 uPic 的开源代码来实现兼容,但毕竟修改别人的代码的成本还是有一点高的。
自从博客从 WordPress 转到静态博客(先 Hexo 后 Jekyll)之后,文章的图片处理、图片上传就成了一个不大顺畅的事情。最先是使用了 vgy.me 提供的免费图床,支持直接从剪切板上传,操作上相对比较简单,也不需要任何本地存储。不过后来 vgy.me 进行了升级改版,原先的剪切板上传功能也不再支持了,偶尔还出现图片像素被降低、丢失的问题。同时,考虑到 WebP 格式可能会适合博客使用,而 vgy.me 还不支持该格式。于是开始切换到 “对象存储 + CDN” 的方案。本地准备好的 PNG 格式图片,先通过 cwebp 命令行转成 WebP 格式图片,再通过 uPic 工具修改文件名后上传到对象存储。由于此前采用的是腾讯云的 COS 对象存储和 CDN,经常面临着 SSL 证书更新等琐碎的事情。这样一来,整体的效率实际上并不高,只能说勉强接受。
为此,也对其他工具和对象存储进行了考察。
PicGo 是一款集客户端 UI 和命令行于一体的图片上传工具,虽然可以利用命令串联的方式简单将图片格式转换和上传两步变成一步,但还是有那么点不舒服的地方,比如 PicGo 不提供文件名修改(为固定长度随机字符串)的特性。而 uPic 本身关注于客户端桌面交互,不提供命令行接口。
至于对象存储,国内各家云服务厂商提供的都需要自行手动更新 SSL 证书,且收费。尽管腾讯云 CDN 目前老用户可以每月领券免费使用,但一旦忘记就开始被收费了。实际上也有逐步转向收费的趋势。国外各家云服务厂商基本上都需要收费使用对象存储或者 CDN,大差不差。
当然,网上一直有一种 “Backblaze B2 + Cloudflare” 的解决方案。由于 B2 本身存储和读写操作都有免费额度,流量需要收费,且无法自定义域名,而 B2 和 Cloudflare 同属于宽带联盟(内部流量免费),Cloudflare 还提供 URL 重写功能,这种解决方案一时成为了潮流。不过,B2 的访问链接是中间带着一串参数,重写之后仍然还是有部分参数,最终的 URL 并不是那么朴素。总的来说,还是有那么点别扭。
因此观望了许久,直到 Cloudflare 推出了 R2。R2 是一款对标 AWS S3 的产品,基于 Cloudflare 对宽带联盟的承诺而构建,为存储对象提供零成本出口,实际上就是免费 CDN。由于 Cloudflare 本身就是一家 CDN 服务商,自定义对象存储访问域名、自动生成部署 SSL 证书这些事情就变得轻而易举了。R2 提供 10GB 的免费存储,读操作每月免费 1000 万次,写、更新和删除操作每月免费 100 万次。这对于一般的静态博客来说,应该完全足够了,即使超出了免费额度,超出部分收费也相较其他云服务厂商便宜一些。
对笔者而言,R2 产品将对象存储和 CDN 两款产品有机地结合起来,解决了静态博客图片对外访问前的“最后一公里”。于是想从 COS 迁移到 R2,无奈 uPic 这个时候卖了个“破绽”。uPic 似乎在开始转向商业收费,Github 上不再发布新版本,而仅在 Appstore 上继续更新对 R2 的支持。当然,Appstore 上的 uPic 是收费的(4.99 美元,其实也不贵)。
R2 虽然说是对标 AWS S3,但是并没有完整实现所有 API 接口,所以目前 Github 上发布的 uPic 版本无法兼容。尽管可以自行通过修改 uPic 的开源代码来实现兼容,但毕竟修改别人的代码的成本还是有一点高的。
在充分梳理个人使用需求之后,还是决定开个新项目——开发一款简单易用的命令行工具 PICTL(全称 Picture Control)。该工具须具备以下特性:
根据所列出的特性,这款命令行工具主要的模块就是:图片处理模块和上传模块,架构如下图所示。
图片处理模块包括图片压缩、图片格式转换、图片水印、尺寸调整等功能,可以进行无缝横向扩展。上传模块主要包括对于三种存储方式的支持:第一类是最为广泛的类 S3 对象存储,如 R2、AWS S3 及国内外云服务厂商各种对象存储等,第二类是目前仍然在博客中广泛流行的第三方图床,如 SM.MS、vgy.me、chevereto 类型图床等,第三类是自托管 Git 平台和 FTP 平台。
鉴于 Github、Gitlab、Gitee 等公共代码托管平台均禁止把 Git 仓库作为图床的做法,本工具仅支持自建 Git 平台,如自建 Gitlab 和 Gitlab Pages。如试图上传到公共代码托管平台,本工具会自行中断上传。
本工具由于仅支持命令行,所以计划用 Python 和 Click 进行开发。虽然借助 Google Fire 也可以快速开发命令行工具,但其使用方式上与原生 *nix 的命令行工具有所不同。相比之下,用 Click 开发可能会麻烦一点,但能够开发出更类原生的 Python 命令行工具。当然,目前开发上还是比较喜欢使用 Rust 或者 Go 语言来开发高性能的命令行工具。不过,本工具只是做一些非常简单的工作,没有性能上的瓶颈,用 Python 开发也足够了。
请移步 Github Project 了解更多。
目前,PICTL 已经在 Github 和 PyPI 同步发布了 v0.1.0 版本。该版本实现了以上架构图中的基本功能,即特性中的前三点必要项。接下来介绍一下如何安装和使用本工具。
目前支持两种安装方式:源码安装和 PIP 安装,后续将增加对于 brew 的支持。
安装前,请务必确保已满足 Python 版本高于 3.10 的条件。
git clone https://github.com/zhonger/pictl
cd pictl
pip3 install .
pip3 install pictl
╰─$ pictl -V
PICTL 0.1.0
目前 v0.1.0 版本包含四个子命令:config(配置管理),compress(图片处理),upload(上传文件)和 cup(一键式压缩、转换、上传)。
╰─$ pictl
Usage: pictl [OPTIONS] COMMAND [ARGS]...
A command line tool for image processing and uploading (ex. S3-type).
Now it supports:
- transformation from other image types to `webp` image as well as
image compression.
- image file uploading to AWS S3 or Cloudflare R2.
Options:
-V, --version Show the pictl version.
-h, --help Show this message and exit.
Commands:
compress Compress any image into `webp` image.
config Operations for the config file `~/.pictlrc`.
cup Compress image and upload to remote storage (compress and...
upload Upload the file to remote storage
config 子命令包含 add(添加配置)、delete(删除配置)、info(查看所有配置)和 init(初始化配置)四种操作。使用本工具需要先执行初始行配置操作,再执行其他配置操作。
╰─$ pictl config
Usage: pictl config [OPTIONS] COMMAND [ARGS]...
Operations for the config file `~/.pictlrc`.
Options:
-h, --help Show this message and exit.
Commands:
add Add configs to the config file.
delete Delete config group from the config file.
info Check the configs.
init Initialize config file with default configs.
如下所示,使用 pictl config init
命令初始化配置。第二次执行时,由于已存在配置文件,会提示已经初始化过了。
╰─$ pictl config init
The settings has been initilized in /home/ubuntu/.pictlrc.
╰─$ pictl config init
/home/ubuntu/.pictlrc already exists.
Please add settings or change it manaully.
使用 pictl config info
以 JSON 格式打印所有配置信息。配置文件默认采用 TOML 格式。
╰─$ pictl config info
{
'basic': {'length': 6, 'ntype': 'random', 'algorithm': 'sha1'},
}
╰─$ cat ~/.pictlrc
[basic]
length = 6
ntype = "random"
algorithm = "sha1"
使用 pictl config add
以命令行交互方式添加配置(以下是 R2 的例子)。添加完成后可以再次查看新增后的所有配置。
╰─$ pictl config add
****** Please input these information ******
Group Name (default 'blog'): test
[?] Type: : R2
S3
> R2
Account ID: testid
Bucket Name: test
Key: testkey
Secret: testsecret
Prefix (Default is None):
Access Url (like `https://i.example.com`): https://i.example.com
New group 'test' has been saved in /Users/zhonger/.pictlrc.
╰─$ pictl config info
{
'basic': {'length': 6, 'ntype': 'random', 'algorithm': 'sha1'},
'test': {
'endpoint': 'https://testid.r2.cloudflarestorage.com/test',
'bucket': 'test',
'prefix': '',
'key': 'testkey',
'secret': 'testsecret',
'url': 'https://i.example.com',
'type': 'R2'
}
}
使用 pictl config delete
命令可以选择删除远程配置组(此处为了展示,除 test 外还添加了其他配置组)。
╰─$ pictl config delete
[?] Please select one group: : test
blog
cover
> test
test has been deleted.
本子命令输入参数为图片文件名,可以带路径。输出图片会被保存在命令执行位置,而非原图片所在目录。执行该操作后,图片还不会被上传,需要继续使用 upload 命令完成上传。
╰─$ pictl compress -h
Usage: pictl compress [OPTIONS] FILENAME
Compress any image into `webp` image.
FILENAME is the name of the file to compress.
Options:
-h, --help Show this message and exit.
╰─$ pictl compress ../../pictl/fig01.png
The output file is EVyP2J.webp
如下所示,使用 pictl upload
命令加上指定上传文件名和远程配置组即可完成上传。
╰─$ pictl upload -h
Usage: pictl upload [OPTIONS] FILENAME GROUP
Upload the file to remote storage.
FILENAME is the name of the file to upload.
GROUP is the group in the config file you want to use.
Options:
-h, --help Show this message and exit.
╰─$ pictl upload EVyP2J.webp test
Direct URL: https://i.example.com/EVyP2J.webp
Markdown: ![EVyP2J.webp](https://i.example.com/EVyP2J.webp)
HTML Code: <img src="https://i.example.com/EVyP2J.webp" alt="EVyP2J.webp" />
cup 子命令 = compress 子命令 + upload 子命令,仅需要指定初始图片文件和远程配置组,即可一步完成图片压缩、格式转换、修改名称、上传。
╰─$ pictl cup -h
Usage: pictl cup [OPTIONS] FILENAME GROUP
Compress image and upload to remote storage (compress and upload).
FILENAME is the name of the file to compress.
GROUP is the group in the config file you want to use.
Options:
-h, --help Show this message and exit.
╰─$ pictl cup ../../pictl/fig01.png test
The output file is Gl8qhI.webp
Direct URL: https://i.example.com/Gl8qhI.webp
Markdown: ![Gl8qhI.webp](https://i.example.com/Gl8qhI.webp)
HTML Code: <img src="https://i.example.com/Gl8qhI.webp" alt="Gl8qhI.webp" />
目前的 v0.1.0 版本还比较简陋,不过已经能满足笔者的必要需求,有效提升效率。后续将继续完善、新增功能:
之前介绍了高性能集群中常用的运行环境和软件版本管理工具 Modules,今天打算介绍一款适合个人或团队开发使用的通用运行环境版本管理神器 ASDF。与高性能计算任务不同,个人或团队开发项目一般来说都是使用独立的设备或环境,然后通过代码版本跟踪 git 等来进行异步协作。所以说,在每个人的单个或多个设备上都安装配置 Modules 显得有点不太现实和高效。但是项目开发所需的代码环境确实有的时候可能比较复杂,比如说同时需要 Ruby、NodeJS、Java、Python 四种环境,而且可能对于每种环境还有版本的限制。这样一来,光配置这一堆环境就要花上大半天时间了。
ASDF 提供了全平台通用的环境配置方案,使用单个命令行工具和交互界面就可以管理超复杂的运行环境。以往针对不同运行环境,需要使用不同的配置文件来进行版本的声明。对于 ASDF,只需要一个可共享的 .tool-versions
配置文件即可。ASDF 涵盖了包括 Ruby、NodeJS、Java、PHP、.Net 在内的几百种运行环境,具体可以查看 ASDF 插件列表 了解更多。
另外,ASDF 完全支持包括 Bash、Zsh、Fish 和 Elvish 在内的常用 shell 类型,并提供补全功能。在类似 Github Actions 等的 CI/CD 工作流中,也可以轻松使用 ASDF。值得一提的是,笔者翻译了 ASDF 文档的中文版本并被官方采纳,现在 ASDF 官网支持英语、巴西语和中文三种语言。
很显然,如果大家的设备上都有 Docker 环境且 CPU 架构相同的话,Docker 无疑是最省心的方案。Docker 镜像的确可以轻松涵盖所有开发环境和实际运行环境(Apache 等 HTTP 服务器、数据库以及其他)。相比笨重的 VMware 或 Virtualbox 虚拟机镜像而言,Docker 镜像也更加小巧、便捷。而且团队可以通过在内部搭建自己的 Docker 镜像仓库,来分享这些镜像给所有参与项目的开发者。唯一可能会有问题的是,设备 CPU 架构和操作系统的多样性可能会给实际操作带来了不小的问题。实际上可能会有 Windows 系统、Linux 系统、MacOS 系统以及 Intel 架构、AMD 架构、ARM 架构(如 M1、M2 等)。可行的解决方法是,尽可能地构建更多架构的镜像。
Anaconda 现在可能已经完全超出了一个 Python 环境管理工具,有的时候也可以当成通用软件或环境管理器来用。但是毕竟还是以科学计算为主要目的,如果项目仅仅是 Python、R 语言可能还是比较合适的,对于实际编程所需的其他运行环境来说可能还是支持不够的。
据笔者所知,在 conda-forge
频道里的确有 PHP 等编程语言的支持。除此之外,也有一些热心开发者在个人频道提供了 java-jdk、golang 等编程环境支持。
云开发主要是指基于云基础设施的在线代码开发环境,主要的代表有:
除了以上列举的云开发之外,也有一些比较传统的小型云开发实践,比如说知名的 JSFiddle、CodePen、Replit 等。虽然说这些云开发主要是适合较小代码库,但在实际学习过程中用处也是很大的。
云开发不仅兼顾了传统开发过程中的协同与流程,又将资源与环境整合在云里面,自然而然是最好的解决方案。随着 VS Code 在开发者之间的流行和云服务提供商的努力,基于 VS Code 的云开发环境层出不穷。即使云开发环境本身免费,云也还是要按量按时计费的。对于还没有足够支持上云的团队或个人来说,ASDF 依然是个不错的选择。
# 下载源码到 ~/.asdf 目录
git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.11.3
# 在 ~/.zshrc 文件中加入内容
tee -a ~/.zshrc << EOF
. "$HOME/.asdf/asdf.sh"
EOF
# 激活配置
source ~/.zshrc
# 验证
╰─$ asdf version
v0.11.3-0adc6c1
由于 ASDF 支持插件较多,这里以 Python 环境为例介绍 ASDF 安装插件。
# 添加插件
asdf plugin add python
# 查看已安装插件
╰─$ asdf plugin list
python
# 查看最新 Python 版本
╰─$ asdf latest python
3.11.2
# ASDF 安装 Python 3.11.2 (latest)
╰─$ asdf install python latest
python-build 3.11.2 /home/ubuntu/.asdf/installs/python/3.11.2
Downloading Python-3.11.2.tar.xz...
-> https://www.python.org/ftp/python/3.11.2/Python-3.11.2.tar.xz
Installing Python-3.11.2...
Installed Python-3.11.2 to /home/ubuntu/.asdf/installs/python/3.11.2
# 查看已安装 Python 版本列表
╰─$ asdf list
python
3.11.2
ASDF 提供全局版本(Global)和本地版本(Local)两种方式定义运行环境版本。全局版本是系统级别的,类似于 PATH 变量中定义的;本地版本则是为了某个代码库或者部分代码准备的,通常在目录中的 .tool-versions
文件里定义。为了区分全局和本地的效果差别,这里再安装一个指定 Python 版本。
# 查询 Python 插件支持的所有版本
╰─$ asdf list all python
2.1.3
2.2.3
......
stackless-3.7.5
# 安装 Python 3.9.0
╰─$ asdf install python 3.9.0
python-build 3.9.0 /home/zhonger/.asdf/installs/python/3.9.0
Downloading Python-3.9.0.tar.xz...
-> https://www.python.org/ftp/python/3.9.0/Python-3.9.0.tar.xz
Installing Python-3.9.0...
patching file Misc/NEWS.d/next/Build/2021-10-11-16-27-38.bpo-45405.iSfdW5.rst
patching file configure
patching file configure.ac
Installed Python-3.9.0 to /home/zhonger/.asdf/installs/python/3.9.0
# 查看已安装 Python 版本列表
╰─$ asdf list
python
3.11.2
3.9.0
# 查看当前系统 Python 及 Python3 版本
╰─$ python -V
No python executable found for python system
╰─$ python3 -V
Python 3.10.6
# 更改全局版本为 3.11.2 并查看
╰─$ asdf list
python
*3.11.2
3.9.0
╰─$ python -V
Python 3.11.2
# 创建子目录指定本地版本并查看
mkdir py && cd py
asdf local python 3.9.0
╰─$ python -V
Python 3.9.0
# 返回父目录查看 Python 版本
╰─$ cd .. && python -V
Python 3.11.2
# 查看当前系统 Python3 版本
╰─$ python3 -V
Python 3.10.6
这里有一点比较有趣的是:由于 ASDF 接管的 python
命令而非 python3
命令,所以 python3
命令输出的版本依然还是系统安装版本。
如果想要恢复到系统指定版本,可以很容易使用 asdf global python system
命令。当然,对于本地版本,可以使用 asdf local python system
来实现。除此之外,还有一些常规操作如下。
# 查看 Python 指定版本安装位置
╰─$ asdf where python 3.11.2
/home/zhonger/.asdf/installs/python/3.11.2
# 查看命令所在位置
╰─$ asdf which python
/home/zhonger/.asdf/installs/python/3.11.2/bin/python
# 查看当前 ASDF 管理的运行环境
╰─$ asdf current
python 3.11.2 /home/ubuntu/.tool-versions
# 查看全局和本地版本配置文件
╰─$ cat ~/.tool-versions
python 3.11.2
╰─$ cat ~/py/.tool-versions
python 3.9.0
近年来得益于其轻量、易学易用、第三方支持依赖库多的特点,Python 语言大量被用于机器学习相关的研究、项目开发。在学术界,有以 Scikit-Learn 为代表的全能机器学习库;在产业界,有以 TensorFlow、PyTorch 为代表的生产级机器学习模型计算框架。(当然,学术界实际上构建大规模深度学习模型时也会用到 PyTorch 等计算框架。)但对于大多数人来说,学习这些库、框架或者借助它们从事某些研究、项目开发时,可能还是在用自己的笔记本、台式机。哪怕是在高校的实验室里,这种事情也是屡见不鲜。因此,有交互界面、相对容易上手的 Anaconda 可能会作为大家管理 Python 环境的首选。
当我们在用 Python 编写一些代码,而代码一次运行不可能在短短几秒、几分钟内得到结果时,将任务提交给高性能工作站或者集群作业系统就显得格外有效。尤其是当应用规模较大、计算迭代次数较多时,非交互式的作业提交方式会变得更加有利。毕竟如果是用自己的笔记本运行着这么大的计算,资源基本上都被计算占用了,根本没办法用笔记本去干点别的事情。甚至说,计算还会使得 CPU 等核心部件温度上升,从而影响计算性能。这样比较下来,不得不说提交任务给高性能工作站或者集群作业系统是多么明智的选择。
其实,Anaconda 在没有交互界面的服务器操作系统上也还是可以使用的,我们可以使用其免费的精简版 —— miniconda。虽然 miniconda 已经是精简版了,但和原生 Python 环境比起来还是要多不少东西的。从高性能计算环境的角度来看,使用 Modules 直接管理 Python 环境实际上更加贴近原生,也更加有利于用户与其他环境搭配使用。比如说安装 Python 的 MPI 支持库 —— MPI4PY,仅需要通过 Modules 管理工具加载 Python 和 MPI 两个基础环境,使用 pip3 install mpi4py
命令即可安装。
将 Python 环境纳入 Modules 管理的步骤就是两步:第一步,编译源代码及安装;第二步,添加 Modules 配置文件。当然,最开始还是需要确认一下编译环境是否完备以及文件夹是否准备好。
# 安装编译环境
sudo apt install -y build-essential libbz2-dev libdb-dev \
libreadline-dev libffi-dev libgdbm-dev liblzma-dev \
libncursesw5-dev libsqlite3-dev libssl-dev \
zlib1g-dev uuid-dev tk-dev wget
# 准备文件夹
sudo mkdir -p /opt/python/3.10.6
# 下载源代码
cd /tmp
wget -c https://www.python.org/ftp/python/3.10.6/Python-3.10.6.tar.xz
# 解压源代码
tar xf Python-3.10.6.tar.xz
# 配置安装路径及编译选项
cd Python-3.10.6
./configure --prefix=/opt/python/3.10.6 --enable-optimizations --with-lto
# 编译及安装
make && sudo make install
sudo mkdir -p /opt/modules/modulefiles/py
sudo vim /opt/modules/modulefiles/py/3.10.6
首先如上命令准备文件夹,并新建 module 配置文件,内容如下:
#%Module
proc ModulesHelp { } {
puts stderr \tThis module file will load Python 3.10.6"
}
module-whatis "Enable Python 3.10.6"
eval set [ array get env HOME ]
set basedir /opt/python/3.10.6
prepend-path PATH "${basedir}/bin"
prepend-path LIBRARY_PATH "${basedir}/lib"
prepend-path LD_LIBRARY_PATH "${basedir}/lib"
prepend-path INCLUDE_PATH "${basedir}/include"
prepend-path LD_INCLUDE_PATH "${basedir}/include"
# 查看所有可用模块
╰─$ module ava
-------------------------- /opt/modules/modulefiles ---------------------------
dot module-git module-info modules null py/3.10.6 use.own
Key:
modulepath
# 加载 python 3.10.6 环境,并确认已加载模块
╰─$ module load py/3.10.6
╰─$ module list
Currently Loaded Modulefiles:
1) py/3.10.6
# 确认目前 python 版本
╰─$ python3 -V
Python 3.10.6
由于以上操作将 Python 3.10.6 安装到了一个系统文件夹中,编译完成后会出现如下警告提示。不过无须担心,普通用户可以通过 venv 虚拟环境工具正常使用。
Installing collected packages: setuptools, pip
WARNING: The scripts pip3 and pip3.10 are installed in '/opt/python/3.10.6/bin' which is not on PATH.
Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location.
Successfully installed pip-22.2.1 setuptools-63.2.0
WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv
如下所示,当已经如验证部分加载好 python 3.10.6 模块后,使用以下命令新建虚拟环境、激活后即可使用。
新建虚拟环境时最后的参数 env 是指虚拟环境的名字,我们可以取任意符合 python 规则的字符串作为虚拟环境名字。值得注意的是,python 虚拟环境有关的文件将会被安装在命令执行的当前目录下的同名文件夹中。为了便于管理和使用,建议将所有的 python 虚拟环境都放置在同一目录下。
# 新建 env 虚拟环境
╰─$ python3 -m venv env
# 激活 env 虚拟环境
╰─$ source env/bin/activate
# 可以看到 <env> 的环境提示
# 尝试升级 pip,可以看到成功升级
╭─zhonger@lep-u ~ ‹env›
╰─$ pip3 install -U pip
Requirement already satisfied: pip in ./env/lib/python3.10/site-packages (22.2.1)
Collecting pip
Using cached pip-23.0.1-py3-none-any.whl (2.1 MB)
Installing collected packages: pip
Attempting uninstall: pip
Found existing installation: pip 22.2.1
Uninstalling pip-22.2.1:
Successfully uninstalled pip-22.2.1
Successfully installed pip-23.0.1
# 取消激活 env 虚拟环境
╰─$ deactivate
如果使用 module 提供的 python 模块创建了虚拟环境后,实际运行虚拟环境时还需要使用 module 加载 python 模块吗?答案是不需要。虚拟环境的本质是拷贝运行相同命令所需的必要文件,如下对比查看一下 python 模块和 env 虚拟环境的顶级目录。可以发现,两者的差别不是很大。env 虚拟环境少了 share 目录,多了 pyvenv.cfg 文件。查看该文件可知,存在与 python 模块之间的关系的声明。再查看 bin 目录,可以看到 python 可执行命令用了链接的方式,pip 命令则是直接从原来的 python 模块复制过来的。于是,这就能允许普通用户自行管理 pip 命令和 python 库了。
╰─$ ls /opt/python/3.10.6
bin include lib share
╰─$ ls env
bin include lib lib64 pyvenv.cfg
╰─$ cat env/pyvenv.cfg
home = /opt/python/3.10.6/bin
include-system-site-packages = false
version = 3.10.6
╰─$ ll env/bin
total 36K
-rw-r--r-- 1 zhonger zhonger 8.9K Mar 20 15:20 Activate.ps1
-rw-r--r-- 1 zhonger zhonger 2.0K Mar 20 15:20 activate
-rw-r--r-- 1 zhonger zhonger 908 Mar 20 15:20 activate.csh
-rw-r--r-- 1 zhonger zhonger 2.1K Mar 20 15:20 activate.fish
-rwxrwxr-x 1 zhonger zhonger 234 Mar 20 15:20 pip
-rwxrwxr-x 1 zhonger zhonger 234 Mar 20 15:20 pip3
-rwxrwxr-x 1 zhonger zhonger 234 Mar 20 15:20 pip3.10
lrwxrwxrwx 1 zhonger zhonger 7 Mar 20 15:20 python -> python3
lrwxrwxrwx 1 zhonger zhonger 30 Mar 20 15:20 python3 -> /opt/python/3.10.6/bin/python3
lrwxrwxrwx 1 zhonger zhonger 7 Mar 20 15:20 python3.10 -> python3
Modules 包是一款简化 shell 初始化的工具,允许用户在会话期间使用模块文件轻松切换环境。
在高性能集群或者超算中,我们可以经常看到 Modules 的身影。它可以帮助我们轻松加载运行程序所需的各类环境,比如说笔者之前写过的第一性原理计算软件 CONQUEST 的运行,就需要包括 Intel OneAPI HPCKit、FFTW3、LibXC 在内的多种环境;又比如说运行大规模的机器学习模型时,可能需要 Python、GPU 环境和 PyTorch 等依赖库。
由于集群往往是面向很多人服务的,如果管理员将软件安装到全局环境,则只能安装某个软件的某个特定版本,而不能同时为不同的用户提供所需的同款软件的不同版本。举个例子,笔者编写的代码只能在 Python 3.10 环境下运行,而其他用户编写的代码所支持的 Python 版本是 3.0,那么可能最简单的解决方法是笔者自行编译一个 Python 3.10 的环境,然后利用 PATH 等变量的配置来提升优先级。或许之后有一天,另外一个用户也要使用 Python 3.10 环境,那么是不是让他再自行编译配置一遍呢?哈哈哈,听起来有点无奈,但是千万不要高估集群的使用用户,可能自行编译并配置 Python 3.10 对他们来说也有点困难。因此,Modules 成为了集群动态管理软件环境的最佳选择。集群管理员可以预先编译和配置好各种环境的各种版本,用户使用时只需要执行类似 module load py/3.10.6
的命令就可以轻松加载 Python 3.10.6 环境。
Modules 所支持的模块不仅仅可以是编程语言的多版本,还可以是其他任意的基础环境,比如说 gcc、openmpi 等编译环境。这样一来,不仅可以为用户提供足够的编程环境直接使用,还可以为一些想要自行编译运行环境的用户提供了便利。
Modules 可以在 Linux、Windows、MacOS 任一操作系统上安装运行,这里仅介绍类 Unix 操作系统下的编译安装过程。由于 Modules 需要使用 tcl 工具来解析 modulefiles,所以系统必须预先安装 tcl 及其开发者库。
# 根据实际情况三者选其一
# Debian/Ubuntu 等 APT 系列操作系统上安装 tcl
sudo apt install -y tcl tcl-dev
# CentOS 等 YUM 系列操作系统上安装 tcl
sudo yum install -y tcl tcl-devel
# MacOS 操作系统上安装 tcl (需有 brew 工具)
brew install tcl-tk
# 下载源代码并解压
wget -c https://github.com/cea-hpc/modules/releases/download/v5.1.1/modules-5.1.1.tar.gz
tar xfz modules-5.1.1.tar.gz
# 创建安装目录
sudo mkdir -p /opt/modules
# 进入目录并配置安装目录
cd modules-5.1.1
./configure --prefix=/opt/modules
# MacOS 可能需要如下另外指定 tclConfig.sh 文件所在的位置
./configure --prefix=/opt/modules --with-tcl=/opt/homebrew/opt/tcl-tk/lib
# 编译并安装到 /opt/modules 目录
make && sudo make install
并非是编译完了之后就可以直接使用 Modules,还有至关重要的一步–添加生效命令。首先需要确认当前使用的 Shell 是什么,一般来说默认是 Bash,当然也有 Zsh 等等。然后,在对应的 Shell 配置文件中增加一行生效命令。操作如下所示:
# 确认当前使用的 Shell
╰─$ echo $SHELL
/bin/zsh
# 查看 Modules 支持的 Shell
╰─$ ls /opt/modules/init
bash cmake fish ksh lisp profile.csh python.py ruby.rb tcl tcsh_completion zsh-functions
bash_completion csh fish_completion ksh-functions perl.pm profile.sh r.R sh tcsh zsh
# 如上所示,大部分流行的 Shell 都在支持列表中
# 使 Modules 生效
vim ~/.zshrc
# 添加以下内容
source /opt/modules/init/zsh
Modules 的编译安装是不是很简单?不过如果 tcl 工具无法使用命令安装的话,就只能从源码编译安装,也比较方便,如下所示:
# 下载源码并解压
wget -c https://prdownloads.sourceforge.net/tcl/tcl8.6.12-src.tar.gz
tar xfz tcl8.6.12-src.tar.gz
# 创建安装目录
sudo mkdir /opt/tcl
# 进入目录并配置安装目录
cd tcl8.6.12/unix
./configure --prefix=/opt/tcl
# 编译并安装
make && sudo make install
# 查看所有可用模块
module avail / module ava
# 加载指定模块(支持同时加载多个模块)
module load py/3.10.6 mpi
# 查看已加载模块
module list
# 查看指定模块
module show py/3.10.6
# 添加自定义模块配置目录
module use --apend ~/opt/modulefiles
Modules 编译安装后默认会有一些环境,它们的配置文件都被存储在安装目录的 modulefiles 文件夹中,如下所示:
╰─$ module ava
--------------------------- /opt/modules/modulefiles ---------------------------
dot module-git module-info modules null use.own
Key:
modulepath
╰─$ ls /opt/modules/modulefiles
dot module-git module-info modules null use.own
一般来说,打算提供给所有用户的环境配置都可以放在这个目录里,这样任何用户都可以查看到。
这里我们可以把 use.own
文件作为模板来学习一下如何编写 Modulefiles 文件。
#%Module1.0#####################################################################
##
## use.own modulefile
##
proc ModulesHelp { } {
puts stderr "\tThis module file will add \$HOME/privatemodules to the"
puts stderr "\tlist of directories that the module command will search"
puts stderr "\tfor modules. Place your own module files here."
puts stderr "\tThis module, when loaded, will create this directory"
puts stderr "\tif necessary."
}
module-whatis "adds your own modulefiles directory to MODULEPATH"
eval set [ array get env HOME ]
set ownmoddir $HOME/privatemodules
# create directory if necessary
if [ module-info mode load ] {
if { ! [ file exists $ownmoddir ] } {
file mkdir $ownmoddir
set null [open $ownmoddir/null w]
puts $null "#%Module########################################################################"
puts $null "##"
puts $null "## null modulefile"
puts $null "##"
puts $null "proc ModulesHelp { } {"
puts $null " puts stderr \"\tThis module does absolutely nothing.\""
puts $null " puts stderr \"\tIt's meant simply as a place holder in your\""
puts $null " puts stderr \"\tdot file initialization.\""
puts $null "}"
puts $null ""
puts $null "module-whatis \"does absolutely nothing\""
}
}
module use --append $ownmoddir
Modulefiles 文件一般符合以下规则:
#%Module1.0
开头;proc ModulesHelp {}
函数来添加模块详细描述;module-whatis
字段来添加一句话简短描述;eval set [ array get env HOME]
来获取系统变量 $HOME
;set ownmoddir
来定义变量 ownmoddir
;这里先开个坑,后续打算补充 Modules 配置系列文章:
公司、学校、云服务等一般需要将内外网进行分离,如果想要从外部网络访问某些内部应用,通常需要使用公司、学校、云服务提供的专用网络接入服务。国内公司、学校比较常用的是由深信服开发的 Easy Connect,一种 SSL VPN 技术的实现。虽然每年需要支付一定的费用来维护、升级 Easy Connect 服务,但是毕竟它能够提供比较细粒度的权限控制,比如说对目标 IP、目标端口的特别指定,能够有效保护内网服务器只有 Web 应用本身能被用户接入,而类似于 SSH 等服务及端口则可以通过单独申请和配置来实现。总而言之,除了需要付费,似乎没有什么不好的地方。
实际上如果是在大公司或者学校的话,可能在内网里面还会有更深的内网存在。举个例子,正常的内网是日常的办公或开发网络,服务器所处的内网是独立的网络,即使是已经连接了办公网络,还是需要通过专用网络接入服务器内网才能进行服务器的维护。如果是以数据中心的模式运营的话,甚至说每一次访问服务器都是需要经过临时审批和登录密码发放的。一旦过了有效时间或者完成了任务,访问都将会被拒绝。
虽然 Easy Connect 可以用于上述的场景,但是似乎显得有些大材小用了,毕竟还是要支付一定费用的。为了尽量降低成本,开源的 OpenVPN 或许是一种不错的选择。据笔者所知,Easy Connect 根据购买的许可不同允许的同时在线人数可能也会不同,实际上可能存在“需大于供”的问题。为了缓解这一可能存在的问题,还是会搭建一套 OpenVPN 来作为冗余接入方式。其实 OpenVPN 的商业版本许可也是会有人数限制的,只不过因为只是备份方式也没有太大关系。
OpenVPN 除了开源免费之外,还支持大部分主流的认证方式,比如说 LDAP 认证、微软的商业级目录服务 Active Directory(简称 AD)认证等。近年来,基于 Identify Provider(简称 IdP)、Single Sign On(简称 SSO)、Central Authentication Service(简称 CAS)等的国产化的一站式登录服务解决方案也在逐渐替换原来的 LDAP 或 AD 直接认证,LDAP 或 AD 将作为底层的基础认证方式存在。所以说,开源免费的 LDAP 目录服务在一般的团队中还是足够的,作为 OpenVPN 的认证方式也是完全能满足要求的。
在公网上搭建专用网络接入服务是需要有工信部颁发的专门资格许可的,一般公司、学校、云服务都是有该类资格许可,所以可以对外提供该项服务。而个人是无法获得这类许可,除非注册公司并申请该类许可。如果个人在云服务上搭建该类服务,将会面临被云服务提供商警告甚至单方面停止服务的风险。
在实践前请务必保证具备以下环境:
为了方便部署和测试,这里采用 wheelybird/openvpn-ldap-otp 提供的 Docker 镜像。这个镜像比较小,同时也支持 x64 和 arm 两种体系架构,能满足大部分主流服务器平台。
# docker-compose.yml
version: '3'
services:
openvpn:
cap_add:
- NET_ADMIN
image: wheelybird/openvpn-ldap-otp
container_name: openvpn
ports:
- "1194:1194/udp"
restart: always
volumes:
- /etc/localtime:/etc/localtime:ro
- /etc/timezone:/etc/timezone:ro
- ./openvpn-data:/etc/openvpn
environment:
- OVPN_SERVER_CN=oc-vpn.example.com
- LDAP_URI=ldap://ldap.example.com
- LDAP_BASE_DN=ou=users,dc=example,dc=com
- LDAP_BIND_USER_DN=cn=admin,dc=example,dc=com
- LDAP_BIND_USER_PASS=password
- LDAP_LOGIN_ATTRIBUTE=uid
- LOG_TO_STDOUT=false
- OVPN_DNS_SEARCH_DOMAIN=example.com
使用以下 docker-compose.yml 文件和 docker-compose up -d
命令启动实例。为了能够避免实例在重新创建后证书发生改变,将 Docker 实例中 /etc/openvpn 的目录持久化(与本地目录绑定)是非常重要的。在这里给出的环境变量(environment)中,前三项 OVPN_SERVER_CN、LDAP_URI、LDAP_BASE_DN 是必须要有的。如果 LDAP 目录服务默认是不能被匿名查找的,也必须包含 LDAP_BIND_USER_DN 和 LDAP_BIND_USER_PASS 变量的(即管理员账户名和密码)。当然,如果你想要指定匹配登录用户名字段,则需要新增 LDAP_LOGIN_ATTRIBUTE 变量。该变量默认是 uid 字段,也可以指定为其他 LDAP 目录服务中包含的字段,比如 email。这个镜像默认是会将服务的实时输出打印在终端,如果想要以日志文件的形式保存下来,则将变量 LOG_TO_STDOUT 置为 false 即可。
一般来说,服务器内网为了管理方便,会根据服务器的 ip 和编号来配置对应的域名解析及反向域名解析,形如 ec2-1-1-1-1.aws.com
,也有可能就是简单的 c1.sever.aws.com
。所以当接入服务器内网后,我们可能会期望用 c1 来作为这台服务器的标签,而在终端我们也可能通过 ping c1
来测试通路。实际上只要在启动实例时新增变量 OVPN_DNS_SEARCH_DOMAIN 就可以实现,当然这里变量对应的值也应该变成 server.aws.com
。相当于,有了这个配置后,本地 DNS 解析没有记录时会自动尝试加入后缀来解析。这样一来,是不是方便了很多呢?
除此之外,该镜像还支持其他一些特性,比如 OTP,请访问 wheelybird/openvpn-ldap-otp 了解更多。
在生成配置文件上,wheelybird/openvpn-ldap-otp 要比 kylemanna/docker-openvpn 更复杂一些,可以手动从 Docker 实例的日志文件或终端输出内容中看到内容,大致内容形式如下所示:
#---------- Start of client.ovpn ----------
client
tls-client
dev tun
persist-key
persist-tun
remote-cert-tls server
key-direction 1
auth SHA512
proto tcp
reneg-sec 0
comp-lzo
redirect-gateway def1
auth-user-pass
# Set log file verbosity
verb 3
<connection>
remote oc-vpn.example.com 1194 udp
float
nobind
</connection>
<ca>
-----BEGIN CERTIFICATE-----
.........
.........
-----END CERTIFICATE-----
</ca>
<tls-auth>
#
# 2048 bit OpenVPN static key
#
-----BEGIN OpenVPN Static key V1-----
.........
.........
-----END OpenVPN Static key V1-----
</tls-auth>
key-direction 1
#---------- End of client.ovpn ----------
将以上内容复制保存在 oc-vpn.example.com.ovpn 文件中即可。
通常来说使用 OpenVPN 专用或者兼容客户端来加载配置文件 oc-vpn.example.com.ovpn,当然也可以用终端命令连接,如下所示:
sudo openvpn --config oc-vpn.example.com.ovpn
执行上述命令后会提示输入用户名和密码进行认证,认证通过后会建立连接。默认分配的是 10.50.50.0/24 段中的某个 IP,网关为 10.50.50.254,当然这个也可以在启动实例时自行设置。
以上内容比较适用于团队办公或开发网络与服务器网络独立分离的情况(内网环境)。请勿在未获得工信部的资质许可的情况下在公网部署类似服务,一旦被云服务提供商监测到,云服务提供商有权进行警告、断网、关停等操作,并且无法申诉。
我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:加入链接。
Squid cache,简称 Squid,是一款高性能的代理缓存软件。Squid 本身支持包括 HTTP、HTTPS、FTP、SSH 在内的多种协议,且采用一个单独的、非模块化的、I/O 驱动的进程来处理所有的客户端请求,从而提供主动缓存加速的功能。除此之外,Squid 还可以提供应用层过滤控制的功能,也可与其他的防病毒软件一起使用。在一些大公司、学校、研究机构内部,一般采用 Squid 代理上网的方式,可以过滤危险内容或操作、节省网络带宽、提升访问速度。
但实际上 Squid 代理也可用于正向代理,即为外来用户访问内网应用提速。这也是 CDN(内容分发网络)的加速原理,利用位于全球网络边缘的节点提供服务,而实际上的应用内容则通过边缘节点间的内网来缓存提速。
虽然 Squid 一般部署在团队或公司网络内部,但是由于用户的权限不同可能需要应用不同的规则,所以能够与 LDAP 认证服务结合就变得非常重要了。
在进行正式的实践之前,务必确保已有以下环境:
笔者已经编译并公开了在多种体系架构上可用的 Docker 镜像 zhonger/squid。这里直接使用以下 docker-compose.yml 配置文件和 docker-compose up -d
命令启动实例。
# docker-compose.yml
version: '3.2'
services:
squid:
image: zhonger/squid
container_name: squid
hostname: squid
ports:
- "3128:3128"
volumes:
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
environment:
- SQUID_SSH=true
- SQUID_RSYNC=false
- LDAP_ENABLE=true
- LDAP_HOST=yourldap.domain.com
- LDAP_PORT=389
- LDAP_BindDN="cn=admin,dc=yourdomain,dc=com"
- LDAP_BindPass="********"
- LDAP_DN="ou=Users,dc=yourdomain,dc=com"
- LDAP_ATTRIBUT="uid=%s"
- PROXY_NAME="Proxy Display Name"
restart: always
上面有关的配置变量及其含义可以访问 zhonger/squid 了解更多。
通过浏览器访问 http://ip:3128 并输入对应的用户名和密码可以看到类似下面的内容。
由于我们未指定实际需要访问的地址,Squid 会直接报错。一般使用时,需要使用操作系统的网络设置中的代理来配置好 Squid。如下图所示,勾选“网页代理(HTTP)”并填写相关的 IP、端口、用户名及密码。如果想要同样应用在访问 HTTPS 站点,则还需要勾选“安全网页代理(HTTPS)”及填写相关信息。然后点击保存生效。
这样一来就可以将访问的流量完全交给 Squid 来控制了。当然,如果不确定客户端连接是否成功,可以通过访问 ip.sb 来确认当前客户端流量出口 IP 是否为 Squid 服务器 IP。
大家可能知道,在网络被发明出来之后一段时间,大家采用 IP + Port
的方式一起共享资源。后来随着资源越来越多,这样一种方式显得非常不友好。比如说,现在有 254 个 IP,每个 IP 上有 20 个 Web 应用,那么我们就必须记住 5080 个 IP + Port
的组合,简直太折磨人了。于是在 1983 年,保罗・莫卡派乔斯发明了域名解析服务和域名服务(DNS,Domain Name System)。从此以后,大家开始用域名来访问各种各样的应用服务。显然,相比原来 IP + Port
的方式,域名的含义更加具象、更容易被人记住。
域名解析实际上就是把 IP 和一串有意义的字符对应,这是一对多的关系,即一个 IP 可能对应多个域名。域名的管理单位我们称之为域名注册管理机构,他们掌握着顶级域名的管理权利。比如说,.net、.com、.org 就是顶级域名,域名注册管理机构对顶级域名具有完整的权利。就像上次讲到的 SSL 根证书一样,光有域名注册管理机构还是不够的,他们没有办法直接把域名卖给全世界所有的公司或者个人,而是需要域名注册商承担这部分工作。像我们比较熟知的国内的万网(现在阿里云域名)、DNSPod(现在腾讯云域名)等,国外的 Gandi、Godaddy 等,都是域名注册商。顶级域名根据用途不同可以分为,国别域名后缀和通用域名后缀。国别域名后缀就是指定给各个国家或地区使用的,通用域名后缀就是其余的。我们可以在某域名注册商处购买某个域名后缀(也称顶级域名)的子域名(也称二级域名),比如说我们可以购买域名后缀为 .com 的域名 baidu.com(当然 baidu.com 早就被注册了,我们只能购买还未被注册的域名)。由于一些品牌效应,大部分域名后缀都会保留一些子域名,我们一般称之为溢价域名。一般来说,溢价域名也会比普通域名价格更贵。当我们购买了域名之后,域名注册商会免费提供域名解析服务。当然,我们也可以要求使用其他厂商提供的免费或收费域名解析服务,甚至也可自行搭建域名解析服务。
国内需具备一定资质才能在公网上搭建域名解析服务,否则会面临警告和阻断的风险。
为了简便,我们一般把域名解析服务称为 DNS 服务。在操作系统中,53 端口被视为 DNS 服务的标准端口(TCP/UDP 服务),853 端口被视为 DNS over TLS 的标准端口(TCP 服务)。除此之外,DNS over HTTPS(TCP 服务)和 DNS over Quic(新协议,UDP 服务)的标准端口是 443。所以现有能够公开使用的 DNS 服务都采用了标准端口,比如国内著名的 114 DNS、阿里云 DNS、腾讯云 DNS、百度云 DNS 都是如此。如果想要了解更多开放可用的 DNS 服务可以查看 这里。
DNS 按照功能上的不同可以分为 权威 DNS 和 递归 DNS。权威 DNS 负责对某个或多个子域名进行管理,注册商提供的域名解析服务就是这一类。递归 DNS 负责接收客户端的请求并将查询到的域名对应记录返回给客户端,也就是说它本身不对任何子域名进行管理,只转述别人告诉它的结果。
当我们使用 DNS 时,它(这里指递归 DNS)会如下图所示按照域名系统的构成逐级进行查询。比如说,现在我们想访问 www.baidu.com:
那么问题来了,一个 DNS 能否同时是权威 DNS 和递归 DNS 呢?实际上是不可能的,但是可以实现这种效果。如果我们把权威 DNS 隐藏在递归 DNS 的后面,那么对于顶级域名 DNS 来说,你指定的递归 DNS 就是它理解的权威 DNS。这里的隐藏指的是,当有请求询问递归 DNS 的权威解析记录时,递归 DNS 根据规则将请求转发给背后真正的权威 DNS。等待权威 DNS 返回解析记录后,递归 DNS 再把结果返回给请求方(其他递归 DNS 或者客户端)。
以上谈到的都是公有域名的相关事情,为什么又要考虑私有域名解析呢?首先,公有域名和私有域名本质上可以没有区别,都是在域名注册商处购买的域名,也可以有所区别,即私有域名是未购买的或者是非 ICANN 支持的域名(比如 .lisz 后缀)。这样一来,我们将不再受域名是否注册的限制而应用在内部网络中。
当然,我们应当避免使用 ICANN 支持的域名后缀且可注册的域名,毕竟可能以后会有人购买这个域名。
其次,解析记录的内外分离需求。随着基础服务架构和应用架构的不断发展,越来越多像云计算、Kubernetes 这样需要采用域名解析来连通内部服务。如果我们直接采用一个权威 DNS 来解析这些记录,那就意味着所有人都可以通过查询来知道这些解析记录,甚至有些人就能大致猜出服务架构,这并不是很安全。出于安全上的考量,将同一个域名的解析记录进行内外分离能够在一定程度上提升安全。
如参考资料中《内网域名系统的安全保密风险研究》所说,“随着内网规模的不断扩大,特别是国家电子政务内网的建成、扩展,在电子政务内网中构建国家级可信内网域名体系的需求越来越迫切,内网域名系统会成为内网的核心基础设施,其安全问题将会越来越被关注。”私有域名解析,即内网域名系统在内网安全中发挥着举足轻重的作用。
笔者在实际使用 AWS、Azure、Oracle、阿里云、腾讯云、Ucloud 的过程中,也发现了私有域名解析的应用。举个例子,Oracle 云创建的每一台 VPS 都会有一个内部 FQDN(以 oraclecv.com 为后缀的多级域名)。当你在 VPS 上使用 dig 命令查询这个 FQDN 对应的 A 记录时,VPS 的内网 IP 会被返回。当你在自己的设备上使用 dig 命令查询时,返回为空。再举个例子,云计算厂商的 VPS 默认配置的是他们自家的镜像源,比如腾讯云 VPS 的默认镜像源域名为 mirrors.tencentyun.com,而这个域名在公网上是查不到解析记录的。可见,云计算厂商的架构上也在使用私有域名解析。
当然,云计算厂商们也向用户提供依托于 VPC(私有网络)的私有域名解析服务,即该私有域名解析服务只有在同一 VPC 内的 VPS 可以使用。而我们又知道,VPC 是属于用户个人的,也就是说不同用户之间的私有域名解析服务完全互不干扰。
在参考资料一中,阿里云列出了实际云服务中私有域名解析的四大应用场景:
域名解析分为正向解析和反向解析,我们一般见过的“域名 → IP”的域名解析就是正向解析,而反向解析则是“IP → 域名”。一般来说,反向解析多用于邮件服务器的可信认证。将 IP 与 邮件服务器域名的正反向解析都绑定后,能够增强邮件服务器的可信度,降低被收信服务器判定为 垃圾 IP 发信的概率。
内网私有域名解析实际上就是要在内网中搭建一台权威 DNS 和递归 DNS:权威 DNS 用于管理私有域名,递归 DNS 用于解析权威 DNS 记录以及正常的公网解析记录。当然,在公司网络或者机房集群网络中,一般会搭建权威 DNS 集群和递归 DNS 集群来提升可靠性和可用性。权威 DNS 集群通常是主从架构,主节点作为接受域名解析操作的主要接收方,从节点将实时同步主节点记录。当主节点发现故障时,从节点自动升级为主节点。并非所有的集群都是这样,但是这样更加能避免因主节点宕机后无法进行域名解析的更改操作。由于这与采用架设 DNS 服务的软件相关,这里就不作过多探讨。
与权威 DNS 集群不同的是,内网递归 DNS 集群实际上一般会有两个节点暴露出来。这一点与公网中提供公网递归 DNS 服务是一样的。比如 114 DNS 会告诉大家要设置两个 DNS IP 地址,即 114.114.114.114 和 114.114.115.115。为什么要这样呢?主要还是因为通过冗余来提高可靠性和可用性。我们可能会简单地以为 114 DNS 就只有这两个 IP,但实际上它们的背后还有很多台递归 DNS 服务器。这两台服务器的作用并不是直接处理解析请求,而是为想要域名解析的客户端提供一个更加快和高效的方式来使用递归 DNS。类似于复杂 Web 系统中首先会在交换机上用网关进行优化,然后在服务器上利用 LVS 来负载均衡,之后再利用多个服务后台来分别处理相同的业务,最后再返回信息给用户。这两台递归 DNS 节点也起到了负载均衡的作用。
当然,这两个 IP 也不是一般的 IP,而是使用了 Anycast 技术的 IP。也就是说,在互联网上会有多台服务器使用了这两个 IP,而当我们向这两个 IP 发起请求时,BGP 会根据客户端所处的地理位置和网络情况将 IP 定位到离客户端最近的两台服务器上。有一个比较客观的体验就是,当我们在全国不同地方 ping 这两个 IP 时,发现似乎延迟差不多且都很短。但是无论我们的骨干网建得多么好,因地理位置和跨网(电信、联通、移动、教育网)所带来的延迟也是无法避免的。唯一一个延迟都很短的可能解释就是响应请求的机器实际上并非同一位置的同两台,而是位于不同位置的不同两台。
上面的方案考虑的问题比较多,也比较适合在大规模集群或内网中进行实践,但是在小集群中可能就有点过于庞大了,显得没有必要。其实,小集群或者小团队内网可以采用“合二为一”的方案,即递归 DNS 与权威 DNS 由一台服务器来同时提供。由于小集群内网私有域名解析和公有域名解析不需要接近于零的宕机率,所以完全可以最简化。当前最流行的免费解决方案可能就是 AdGuard Home 了。
AdGuard Home 是一款全网广告拦截与反跟踪软件。在您将其安装完毕后,它将保护您所有家用设备,同时您不再需要安装任何客户端软件。随着物联网与连接设备的兴起,掌控您自己的整个网络环境变得越来越重要。
AdGuard Home 之所以这么受到欢迎,主要是因为其丰富的功能和简单的可视化操作,对于管理员来说非常友好。而且,AdGuard Home 的部署也非常简单,支持多平台架构、多方式一键部署,比如 AdGuard Home 也能在 ARM 芯片上用 Docker 容器的方式一键部署。虽然 AdGuard Home 自带简单的解析记录重写,可以满足大部分常用的内网私有域名解析需求,但是像一些比较高级的解析记录可能就无法做到,比如 TXT 记录。尽管 TXT 记录在 IP 和域名的相互映射中并不起到作用,但是 TXT 记录可以填写比较长的内容,非常适合用来验证对于域名的管理权限,像 HTTPS 证书的申请一般就是采用新增 TXT 记录的方式验证,还有 Gitlab Page 的自定义域名绑定也是如此。所以为了提供比较完整的域名解析服务,这里还是建议增加一个权威 DNS,可以采用 PowerDNS + PowerDNS Admin(交互界面)或者 Bind9 等。
考虑到友好的交互界面更容易上手使用,这里只介绍 AdGuard Home + PowerDNS 的方案。需要提前准备的环境有:
由于 AdGuard Home 官方已经提供了多平台架构的 Docker 镜像,我们直接使用即可,docker-compose.yml 文件如下所示:
version: "3"
services:
adgurad:
image: adguard/adguardhome
ports:
- 53:53/tcp
- 53:53/udp
- 80:80/tcp
- 443:443/tcp
- 3000:3000/tcp
volumes:
- ./work:/opt/adguardhome/work
- ./conf:/opt/adguardhome/conf
使用 docker-compose up -d
命令启动 AdGuard Home 实例。
使用浏览器访问 http://localhost:3000 进行实例初始化设置,如下所示根据页面提示设置好用户名和密码。
初始化成功后,页面会自动跳转到登录界面 http://localhost(80 端口)。
由于在实际环境中,我们不一定是在本机启动该实例,所以可能需要使用服务器的 IP 来替代 localhost 访问。另外,如果原来就有 Nginx 或其他服务占用了 80 端口,我们在配置端口映射的时候可能就会设置到另外一个端口,因此自动跳转到的页面并非是 AdGuard Home 的首页。我们需要使用 IP + 映射 80 的端口来定位到首页。
由于接下来我们将要用 PowerDNS 来管理权威域名解析,所以需要设置私有域名规则,即当 AdGuard Home 收到关于内网自定义权威域名的请求时,就会把请求转给 PowerDNS。这在 AdGuard Home 中也是比较容易就能设置好的,如下图所示,添加一行规则使得匹配的所有二级域名请求转发给 PowerDNS。
虽然 PowerDNS 和 PowerDNS-Admin 官方都提供了 Docker 镜像,但是搭配起来用还是有点莫名其妙的问题。为了更加简单,笔者参考官方自行构建了 zhonger/pdns
和 zhonger/powerdns-admin
两个 Docker 镜像,搭配使用更加便捷可用。如果想要了解更多,可以查看 《Docker 镜像构建之 PowerDNS 篇》。
version: "3"
services:
pdns:
image: zhonger/pdns:latest
restart: always
ports:
- "753:53/tcp"
- "753:53/udp"
# - "8081:8081"
environment:
- PDNS_launch=gsqlite3
- PDNS_gsqlite3_database=/var/lib/powerdns/pdns.sqlite3
- PDNS_webserver_address=0.0.0.0
- PDNS_webserver_allow_from=127.0.0.1,10.0.0.0/8,172.0.0.0/8,192.168.0.0/16
- PDNS_api=yes
- PDNS_api_key={Random Long String}
volumes:
- /etc/localtime:/etc/localtime:ro
- ./powerdns:/var/lib/powerdns
db:
image: mysql:latest
environment:
- MYSQL_ALLOW_EMPTY_PASSWORD=yes
- MYSQL_DATABASE=powerdnsadmin
- MYSQL_USER=pdns
- MYSQL_PASSWORD=mypdns
restart: always
volumes:
- /etc/localtime:/etc/localtime:ro
- ./pda-mysql:/var/lib/mysql
app:
image: zhonger/powerdns-admin:latest
restart: always
depends_on:
- db
- pdns
ports:
- "8080:80"
logging:
driver: json-file
options:
max-size: 50m
volumes:
- /etc/localtime:/etc/localtime:ro
environment:
- SQLALCHEMY_DATABASE_URI=mysql://pdns:mypdns@db/powerdnsadmin
- GUNICORN_TIMEOUT=60
- GUNICORN_WORKERS=2
- GUNICORN_LOGLEVEL=DEBUG
- OFFLINE_MODE=False # True for offline, False for external resources
使用 docker-compose up -d
命令启动 PowerDNS 和 PowerDNS-Admin 实例。
PowerDNS-Admin 本身不会自动初始化管理员用户,而是将注册的第一个用户认定为管理员用户。使用浏览器访问 PowerDNS-Admin 登录页面 http://localhost:8080,如下图所示点击 Create an account 链接跳转到注册页。
如下图所示,填写姓名、邮箱、用户名和密码,点击 Register 按钮即可完成注册。这里,PowerDNS-Admin 默认采用邮箱的 Gavatar 头像作为用户头像。
注册和登录后,会跳转到 PDNS 配置页面。这里由于 PDNS 和 PowerDNS-Admin 实例是在同一个网络中,可以直接使用 pdns 来代替 PDNS 实例的 IP 地址。PDNS API KEY 则是刚才启动时设置的那一长串字符(PDNS_api_key)。PDNS VERSION 最好是与 PDNS 实际使用的一致,不过不一致也不会有什么事。zhonger/pdns:latest
目前实际是 4.6 版本,这里默认填的 4.1.1 也可以。然后点击 Update 按钮保存配置。
保存配置成功后,如果填写信息无误,点击侧边导航中的 PDNS 就可以看到 PDNS 的各项配置信息。如果填写有误,则没有任何信息。
接下来就可以点击侧边导航栏中的 New Domain 来新增私有域名 home.lisz。如下图所示,我们需要填写的是域名,需要选择的是域名模板,一般 basic_template_1 即可。之后点击 Submit 按钮提交。
新增域名成功后,我们就可以在 Dashboard 里面的域名列表看到 home.lisz 了。点击即可进入域名解析。
这里我们以一个 CNAME 和 A 记录为例,来尝试新增解析记录。如下图所示,点击左上角的 Add Record 添加记录,完成后点击右上角的 Apply Changes 来提交解析记录到 PDNS。
实际域名解析时我们一般会采用 CNAME 和 A 记录联合使用的方式,这样相当于在 DNS 解析层面就有负载均衡了。A 记录是域名与 IP 的关系,这就意味着同一个域名可以有多个 A 记录。CNAME 记录是域名与域名的关系,而两个域名分别是为了不同的目的,前一个是为了给大家使用的,后一个是为了运维人员使用的。当存在 CNAME → A 时,客户端会根据网络情况来判断使用哪一条 A 记录对应的 IP,从而提升用户体验。
如下所示向 AdGuard Home 询问私有域名解析记录,解析正常。
─$ dig @127.0.0.1 -p 53 www.home.lisz
; <<>> DiG 9.18.1-1ubuntu1.1-Ubuntu <<>> @127.0.0.1 -p 53 www.home.lisz
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 47193
;; flags: qr aa rd; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;www.home.lisz. IN A
;; ANSWER SECTION:
www.home.lisz. 60 IN CNAME www101.home.lisz.
www101.home.lisz. 60 IN A 192.168.1.1
;; Query time: 20 msec
;; SERVER: 127.0.0.1#53(127.0.0.1) (UDP)
;; WHEN: Tue Aug 23 17:05:15 JST 2022
;; MSG SIZE rcvd: 79
如下所示,向 AdGuard Home 询问公有域名解析记录,解析正常。
-$ dig @127.0.0.1 -p 53 www.baidu.com
; <<>> DiG 9.18.1-1ubuntu1.1-Ubuntu <<>> @127.0.0.1 -p 53 www.baidu.com
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 8988
;; flags: qr rd ra; QUERY: 1, ANSWER: 4, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;www.baidu.com. IN A
;; ANSWER SECTION:
www.baidu.com. 831 IN CNAME www.a.shifen.com.
www.a.shifen.com. 28 IN CNAME www.wshifen.com.
www.wshifen.com. 192 IN A 45.113.192.102
www.wshifen.com. 192 IN A 45.113.192.101
;; Query time: 244 msec
;; SERVER: 127.0.0.1#53(127.0.0.1) (UDP)
;; WHEN: Tue Aug 23 17:06:03 JST 2022
;; MSG SIZE rcvd: 127
开发团队或者公司内部一般会采用内外网隔离、上网行为过滤等措施,比较可靠地保证了内部设备无法被外部网络所侦测,从而可能认为 HTTP 内网站点是一个相对安全的存在。即使在 HTTPS 证书如此盛行的今天,也还暂时不考虑内部站点的 HTTPS 化。IP + Port
或者 http://本地域名
的访问方式依旧是座上宾。当然,如果考虑到购买 HTTPS 证书的成本或者团队内网站点采用 Letsencrypt 等免费证书过于麻烦(只能采用 DNS 验证的方式每三个月申请一次新证书),那么自签名 SSL 证书则成为首选了。不过,如果为每一个内网站点都生成一个 SSL 证书,然后让大家都手动把 HTTPS 标为可信,那么当面临大量内网站点时,大家可能要被搞崩溃。更为可行的办法是,生成一个内网用的根证书,只标记该根证书可信。
与其相信别人根证书生成的 SSL 证书,不如相信自己根证书生成的。我们的目的毕竟不是要任何一个人都把我们自签名的证书标为可信,只要在内网内使用内网站点的设备能够信任即可。而且成为一个受到公众信任的根证书是非常困难的一件事,即使经过几十年可能也没有办法做到。如今现有的根证书实际上有限,像我们平常熟悉的 Letsencrypt、ZeroSSL、Cloudflare 等等并不是根证书而是中间证书。有点类似总代理和分代理的感觉,根证书在业界具有广泛的公信力,但是让根证书去给个人或者企业生成证书可能有点忙不过来。于是根证书生成若干个中间证书,再由中间证书来为个人或者企业生成实际的证书。
一般来说,操作系统或浏览器的产商会预置国际上认可的根证书。如下所示,为 Mac OS 上预置的根证书列表。
话不多说,让我们来实践一下如何生成自己的根证书和签发 SSL 证书吧。
此处只考虑 Mac OS 和 Ubuntu,其他环境如何安装可以自行搜索。
# Mac OS
brew install openssl
# Ubuntu
sudo apt install -y openssl
使用以下命令创建根密钥 zhonger-key.pem
。
openssl genrsa -out zhonger-key.pem 4096
使用刚创建好的根密钥 zhonger-key.pem
生成根证书,并输入相关信息。
openssl req -new -x509 -days 3600 -key zhonger-key.pem -out zhongerca.pem
╰─$ openssl req -new -x509 -days 3600 -key zhonger-key.pem -out zhongerca.pem
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:CN
State or Province Name (full name) [Some-State]:Shanghai
Locality Name (eg, city) []:Shanghai
Organization Name (eg, company) [Internet Widgits Pty Ltd]:zhonger
Organizational Unit Name (eg, section) []:zhonger
Common Name (e.g. server FQDN or YOUR name) []:lisz.me
Email Address []:contact@lisz.me
─$ openssl x509 -text -in zhongerca.pem -noout
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
44:48:03:56:ff:15:57:03:00:34:1f:85:61:ca:f7:7a:1e:4f:38:8f
Signature Algorithm: sha256WithRSAEncryption
Issuer: C = CN, ST = Shanghai, L = Shanghai, O = zhonger, OU = zhonger, CN = lisz.me, emailAddress = contact@lisz.me
Validity
Not Before: Aug 3 05:25:47 2022 GMT
Not After : Jun 11 05:25:47 2032 GMT
Subject: C = CN, ST = Shanghai, L = Shanghai, O = zhonger, OU = zhonger, CN = lisz.me, emailAddress = contact@lisz.me
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
RSA Public-Key: (4096 bit)
Modulus:
00:b2:d1:47:73:8a:83:48:e3:47:1a:41:01:f6:63:
69:43:39:71:eb:2b:74:be:dc:63:f3:df:79:66:ee:
00:30:65:b3:4f:7e:58:88:00:13:09:e6:4f:74:57:
fa:a3:56:24:cd:b6:1f:53:25:77:98:bf:9f:45:64:
7c:6c:04:23:c4:8f:0f:bf:2e:b3:d1:2e:4c:05:4d:
4c:e6:65:54:ad:0c:35:b7:d9:c8:74:97:19:c7:a5:
cd:9a:a4:73:37:13:71:80:34:7c:bc:b3:41:5a:34:
bb:16:82:44:18:a1:0a:a5:f5:f1:07:ca:8d:b3:9a:
ef:74:fb:a0:6c:72:4a:53:5c:59:74:6f:aa:c7:bc:
48:26:af:1b:70:f3:5f:7f:c7:df:8d:e5:da:e4:f4:
d2:fa:90:d3:e2:67:e1:9a:df:c7:c4:c7:53:6f:62:
25:ed:ff:0a:17:cf:8d:4d:84:6b:38:cb:49:e7:3d:
c5:2b:15:76:e6:eb:cc:17:94:40:20:7d:ee:8c:36:
6d:cf:9c:d7:1f:a6:41:20:9d:45:cd:57:8f:a8:61:
f8:8b:e9:31:6a:a9:96:c1:db:57:64:0b:09:da:ca:
b3:07:d9:55:ed:fe:69:a0:9c:78:5b:59:a5:7b:a1:
2b:4d:68:22:b4:7f:db:c6:c1:12:ee:eb:9b:29:38:
ae:7b:4c:0d:2a:ab:33:3f:af:a8:7b:ca:89:2c:62:
0f:a8:ef:89:60:9e:fd:a2:df:36:6d:70:82:8b:fa:
b3:ee:79:7e:fd:3f:e7:90:84:58:85:7e:7e:69:07:
1e:50:05:0b:87:4d:66:e4:17:6b:c2:97:03:48:e4:
7d:08:b4:81:a6:05:80:60:5c:eb:8d:53:db:7c:62:
a8:6d:a7:75:f1:56:b6:d9:0d:6b:3b:be:8b:72:39:
8d:e7:2d:77:74:e3:4d:a1:fd:8b:44:f9:ee:fd:0d:
04:ec:6a:fc:f3:d2:15:fc:18:ff:7d:33:44:2b:6d:
7f:3c:33:21:e1:d8:5f:08:fa:53:fd:26:fb:6e:74:
d7:4b:51:62:d3:15:1b:3b:44:78:78:9b:91:c7:ba:
82:2b:12:d7:b2:83:0a:39:ec:5e:a9:a9:c1:04:a6:
2e:64:a5:ea:15:c3:85:e9:ac:38:6b:22:eb:3b:08:
b8:0a:31:10:df:45:1d:76:81:e0:0f:88:e4:00:ef:
6e:90:59:8c:d8:36:e9:77:bf:4a:0e:3d:03:02:4d:
5d:a7:90:16:81:11:e0:81:bb:e0:18:a3:bb:dc:8d:
7d:c6:cf:c6:0b:d2:80:53:ea:d0:27:e6:6a:cc:8e:
2b:b3:72:e4:ab:84:88:e2:e9:a5:bb:72:9a:c6:a2:
0e:5a:cb
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Subject Key Identifier:
EE:EF:AE:DB:73:45:9A:6E:82:00:3C:A7:05:0D:60:E4:20:81:3B:02
X509v3 Authority Key Identifier:
keyid:EE:EF:AE:DB:73:45:9A:6E:82:00:3C:A7:05:0D:60:E4:20:81:3B:02
X509v3 Basic Constraints: critical
CA:TRUE
Signature Algorithm: sha256WithRSAEncryption
98:cf:f4:23:61:d2:2a:64:ce:51:57:1d:fb:61:2f:34:68:86:
c9:02:5a:c8:97:80:58:c1:7f:04:e1:97:f5:0b:35:d5:c4:91:
fa:98:8c:73:16:43:b3:af:63:af:2c:30:cf:6a:8e:10:99:bc:
fd:3d:84:c7:3d:01:e0:8d:8d:d8:76:74:12:69:1a:f5:e5:ec:
ef:eb:dc:f8:08:0c:c7:03:19:de:c5:e8:c7:4e:b4:5c:67:39:
9f:33:11:6f:29:e1:03:d8:4e:70:09:7a:69:bd:3a:db:96:71:
2b:38:c4:46:87:f6:59:34:f9:dc:5c:6d:34:9a:ba:ea:36:13:
d8:e3:e3:91:ea:70:3b:ea:39:cb:fc:fd:08:0f:73:e5:16:c3:
0d:9a:62:20:3f:5a:28:90:e6:b2:65:23:a1:ba:d0:77:c0:8e:
16:51:55:44:f6:4b:16:b9:a1:97:bc:f8:95:70:af:a6:d4:07:
27:21:96:78:0b:58:18:51:45:a6:ea:07:c8:09:1b:ad:f3:e1:
16:be:64:bf:8f:b7:4c:d1:e6:d0:c6:c1:db:cd:3d:e9:88:ec:
e2:87:ff:bd:c3:7b:31:23:00:c3:71:53:90:68:46:99:7d:1d:
e1:78:26:76:6a:41:8d:9e:9a:55:97:63:a5:df:86:fc:03:9b:
28:13:55:ff:74:f2:56:d9:20:02:e8:c9:90:4f:b1:5d:1b:66:
57:4e:f7:c6:50:4f:c9:8b:ff:39:a1:9e:b4:ee:2b:8a:bf:46:
b4:3e:65:cb:34:12:73:bc:ae:ba:a5:41:20:d4:b9:c5:c4:da:
89:bd:50:83:27:71:7a:9f:2c:3e:cf:de:db:13:b1:39:cf:4a:
39:62:68:b3:f5:dc:49:44:3e:c1:cf:0c:a4:9a:4b:cb:5e:ec:
aa:33:a5:57:ae:c6:f3:4f:69:01:d1:6a:a7:12:90:88:05:e9:
18:d8:3a:a7:89:70:55:ab:18:ba:4f:28:74:5b:5f:21:8e:66:
bc:ae:ff:1b:c7:ed:42:73:c1:1c:a4:97:f2:e6:c7:5a:8f:a8:
44:a5:ed:b7:76:ac:cf:40:f0:a4:4f:22:03:d0:db:db:6e:18:
32:33:4a:79:c2:bb:98:20:71:03:a7:9c:ea:4e:7e:0a:28:79:
30:f3:3f:ef:03:b2:e0:00:b0:2b:71:27:8b:fc:f9:a0:e5:b9:
a0:9e:6f:93:3a:f3:d3:1c:87:8a:b7:2d:5c:38:ab:f9:ff:39:
8b:52:a5:9a:95:2f:a0:82:b9:b6:f8:9a:c3:e3:55:dd:4b:b5:
e4:e3:fb:f8:8b:10:50:f8:42:7d:03:fe:72:40:c1:d3:f7:26:
a7:f9:de:b9:9d:30:26:94
首次打开刚刚生成的根证书 zhongerca.pem
会像下面这样显示“此根证书不被信任”,我们可以将下面的使用此证书时的使用系统默认改成始终信任,然后输入操作系统用户密码即可保存修改。改完之后再次打开如下下图所示,显示“此证书已标记为受此账户信任”。这样一来,由该根证书签发的证书就都会被信任了。
这里我们打算采用 jsha/minica 来辅助快速签发证书。
# Mac OS
brew install minica
# Other OS
go install github.com/jsha/minica@latest
# 给域名签发 SSL 证书
minica -ca-cert zhongerca.pem -ca-key zhonger-key.pem --domains "sni.lisz.me,zhonger.io,*.zhonger.io"
# 给 IP 签发 SSL 证书
minica -ca-cert zhongerca.pem -ca-key zhonger-key.pem --ip-addresses "127.0.0.1"
minica 提供了非常简单的方式来签发 SSL 证书,比如说指定根证书和根密钥、指定单个或多个域名、通配符域名以及 IP。minica 签发的证书默认时效为 2年30天(相信可能是考虑到 30天 的缓冲期所以多了一个月)。这里,我们模仿了 Cloudflare 的 SSL 证书生成方式,第一个域名是 sni.根证书域名
,第二个开始才是真正想要签发的域名。由于 minica 默认会将第一个域名作为文件夹的名字生成 SSL 证书 cert.pem
和 key.pem
文件,如果采取这种方式在同一目录执行以上签发命令势必会使得旧文件被覆盖,因此推荐像 Certonly 或者 acme.sh 那样修改目录名来区分。
除了 minica 之外,FiloSottile/mkcert 提供的 mkcert 工具也非常简单方便。mkcert 会自行生成根证书,然后签发证书。个人觉得,相比 minica 而言,mkcert 更适合个人本地开发 HTTPS 化,而非团队内网 HTTPS 化。
自生成根证书比较令人担心的地方可能就是任何人都可以用同样的方法伪造出相似的根证书。实际上,根证书是独一无二的,即使所有的信息都设置成一样,还是两个根证书。我们需要做的是:
如下所示,是上面生成的根证书和签发的 SSL 证书的密钥 ID 对比。可以看到,两者完全一样,即可信任的 SSL 证书。
上面已经提到了在 Mac OS 中如何安装根证书,其他平台比如 Windows、Linux、Android、IOS 等也是可以按照类似的方式,略微有些差别。考虑到 Windows 和 Linux 桌面版安装根证书的步骤几乎与 Mac OS 一样就不再赘述,这里主要讲一下 Linux Server、Android、IOS 平台的安装方法。
sudo cp zhongerca.pem /usr/local/share/ca-certificates/zhongerca.pem
sudo update-ca-certificates
以华为鸿蒙系统(HarmonyOS)为例,首先下载根证书到设备上,然后在 设置 > 安全 > 更多安全设置 > 加密和凭据 > 从存储设备上安装 中选择已下载的根证书完成安装。安装完成之后可以在同级别的 受信任的凭据 > 用户 下面看到安装好的根证书。
首先下载根证书到设备上,在文件中点击打开(会自动跳转到 设置 > 通用 > VPN与设备管理)。可以在 配置描述文件 列表中看到根证书,点击进去输入密码并验证即可。
Nextcloud 是一款非常适合个人或者团队使用的开源网盘软件,也有一款和其名字非常相似的 ownCloud。实际上这两款开源网盘是出自一个人之手,只不过在发展的过程中(2016年),ownCloud 团队内部产生了一些意见分歧,造成了核心开发人员出走创建了 Nextcloud。与 ownCloud 相比,Nextcloud 更加在乎宽广的功能多样性以及安全性,比如说视频会议、在线协作、提供对密码暴力破解的保护、限制密码错误登录的次数等。ownCloud 中可能也有这些功能,但并不包含在开源版本中,而是需要企业订阅版本才能享受这些功能。从这些看来,Nextcloud 似乎更加适合个人或团队使用。
据笔者所知,国内 F 搜团队所提供的网盘文档-F 文档 实际上就是用 Nextcloud 搭建的。其提供的在线文档编辑采用的是开源的 OnlyOffice。
除了 Nextcloud,国内也有一款也比较好用的开源网盘 Seafile。Nextcloud 与 Seafile 虽然都是网盘,但是关注点不大一样。正如上面讲到的 Nextcloud 更在乎协作、功能多样化,而 Seafile 则更在乎稳定和安全。举个例子,Nextcloud 如果不设置服务端加密,所有的文件都会直接存在文件系统中。如果可以访问服务器的文件系统,那么意味着无须任何用户自身的许可就可以查看所有文件。Seafile 默认就将所有文件都分成小块存储,这样一来你是无法直接通过服务器的文件系统读取文件内容的。当然,这样分块存储也有一个好处,在客户端设置同步时会分块进行增量同步,提升了同步的速度和可靠性。不至于一个几个 G 的大文件传到一半中断后又要从头开始上传。不过,这样也有一个比较明显的坏处,如果不借助 Seafile 或其支持团队的帮助,无法自行从分块数据恢复原始文件内容。
从 Nextcloud、ownCloud、Seafile 的官网来看,三者都支持 LDAP/AD 认证集成,但实际上 Seafile 是需要专业版订阅才能有这个功能的,包括 Office 文件预览和编辑、全文检索、断点续传等功能也是需要专业版订阅的。Nextcloud 和 ownCloud 都在开源版本提供了 LDAP 认证集成功能。所以这里也不考虑 Seafile,只以 Nextcloud 为例介绍 LDAP 集成到网盘中。
由于之前笔者也曾写过《Nextcloud 搭建自己的云盘》和《Nextcloud 升级那些事儿》两篇文章介绍如何安装和升级 Nextcloud ,所以这里就不再对此进行赘述了。当前开始的环境即是已正常运行的 Nextcloud 实例。
在 Nextcloud 的应用捆绑包的企业捆绑包里就有我们想要用的 LDAP 认证集成插件,点击右侧启用按钮即可正常启用。
在启用 LDAP 插件后,我们就可以在管理员的设置中看见相应的选项。如下图所示,点击用户头像弹出菜单中的设置链接。
由于这个设置会同时包含个人用户设置和管理员设置,可以将左侧的导航栏往下拉,看到管理中的 LDAP/AD 集成 点击进入。
下面是 LDAP/AD 集成的基本设置,主要填写四个信息:LDAP 服务器地址、LDAP 管理员 DN、LDAP 管理员密码、查询基础 DN。由于此处填写的是非真实信息,所以下面会显示配置错误的提示。如果填写的 LDAP 信息无误,会自动变成配置成功的提示。然后点击继续按钮,后面的设置可以保留默认选项即可。当然如果 LDAP 服务与一般的设置有些不同,也要根据实际情况对后面的用户、登录属性、群组信息进行调整,这里就不一一介绍了。
以上配置均完成之后,即可退出登录即可使用 LDAP 账户和密码登录验证是否配置成功(登录界面不会有任何改变)。
可能和 Gitlab 的情况有点类似,Nextcloud 本身就有用户体系,然后才接入的 LDAP 认证。其实,我们还是希望 LDAP 用户和原有用户能够自动识别成同一用户,无感完成合并。这里 Nextcloud 和 Gitlab 一样都是凭借着邮箱来判断的。当 LDAP 用户邮箱与 Nextcloud 原有用户邮箱一致时,自动合并成一个用户,并且不再拥有修改用户密码的权利。虽然已经集成了 LDAP 认证,但是我们依然可以用 Nextcloud 自身的用户体系去创建新用户,这其实也是两个用户体系、一个软件系统。
Gitlab 是一款对标 Github 的开源 Git 管理软件,能够为用户提供非常丰富的功能。因为之前写过的《私有代码托管平台的搭建与运维》和《Gitlab 升级那些事儿》已经对 Gitlab 作了比较详细的介绍,这里就不多赘述了。
Gitlab 提供了对于多种认证方式的支持,包括自带的用户体系、LDAP、CAS、OAuth 2.0 及其他第三方认证方式。这也为我们实现不同的用户需求提供了可能。国内高校中,中科大和南京大学都为本校生提供了基于 Gitlab 的代码托管服务,访问地址如下。唯一不同的是,中科大采用的社区版本(CE),南京大学采用的是由 Gitlab 在中国的子公司极狐支持的企业版本(EE),据说有一些更先进的功能。当然,考虑到使用 Gitlab 的主要需求是代码托管,而非 Gitlab Pages 功能,两家都没有提供该功能。
学校 | Git URL |
---|---|
中科大 | https://git.lug.ustc.edu.cn |
南京大学 | https://git.nju.edu.cn |
言归正传,中科大和南京大学都可以自助注册,只需要是使用学校邮箱即可。中科大同时也提供了 Github、Gitlab、学校一卡通认证。自助注册的好处是,即使已经离校无法使用学校邮箱,实际上也还是可以保留账户继续使用(除非专门对毕业生进行封禁)。其实,对于内部团队使用的 Gitlab 来说,由管理员手动创建用户也是没有什么问题的,毕竟人数不会太多。但是这样一来,可能会出现维护多个账号和密码的烦恼。因此,采用 LDAP 来接入认证是比较合适的。对于非团队用户不打算放在 LDAP 目录里也可以手动在 Gitlab 创建用户,当用户不再使用时就可以封禁。
也可能存在先有 Gitlab 账户、然后才有的 LDAP 目录的情况,这也不要紧,因为 Gitlab 支持 LDAP 认证方式的用户与现有用户进行合并。举个例子,如果我已经在 Gitlab 中创建了用户名为 zhonger、邮箱为 zhonger@example.com 的用户,那么我在 LDAP 目录中只需要把 mail 字段也写成 zhonger@example.com 即可被 Gitlab 识别成同一用户。或者说,我们可以在 Gitlab 中增加 LDAP 目录中的 mail 字段的邮箱(Gitlab 支持同一用户绑定多个邮箱),这样在 Gitlab 中使用 LDAP 认证的时候也会被视为同一用户。
由于 LDAP 用户的账户名和密码不会被 Gitlab 接管,所以当你使用 LDAP 认证登录后,原有的同邮箱的 Gitlab 用户就会自动丧失修改密码的权利。除此之外,一切照旧。
为了更加简便实践并且容易复现,这里采用的是常用的 sameersbn/docker-gitlab Docker 镜像。可以在镜像 Github 页面下载到提供的对应 docker-compose.yml 文件。由于 LDAP 并非是默认配置,所以默认是没有 LDAP 配置段的。需要在 docker-compose.yml 文件的 environment 中增加以下环境变量。其中,请根据实际情况更改成相应的信息。
...
environment:
...
- LDAP_ENABLED=true
- LDAP_LABEL=LDAP
- LDAP_HOST=ldap.example.com
- LDAP_BIND_DN=cn=admin,dc=example,dc=com
- LDAP_PASS=xxxxxxxxxxx
- LDAP_UID=uid
- LDAP_BASE=ou=people,dc=example,dc=com
...
使用命令 docker-compose up -d
运行一个 Gitlab 实例。由于初始运行需要执行数据库初始化等操作,可能需要几分钟,之后打开指定的端口即可看到如下类似的登录页面。可以看到,这里默认就是使用 LDAP 登录,次要登录方式才是标准登录。
这里 Gitlab 默认使用 uid 作为 username,而非常见 LDAP 登录定义里的 cn 字段,所以只需要最简单的 uid 和 password 即可登录成功。