標籤:Linux

在 Ubuntu 裡面啓用 cgroups v2

因為最近跳了 Podman[1] 的坑,所以就想說順便玩玩 rootless container 好了,結果發現如果要用 rootless container 的話,Podman 在 cgroups v2 裡面才可以做各種資源限制,但 Ubuntu 預設只使用了 cgroups v1,所以下面就來看看要怎麼把它切到 v2 囉。

TL;DR

#!/bin/bash

echo 'GRUB_CMDLINE_LINUX_DEFAULT="${GRUB_CMDLINE_LINUX_DEFAULT} systemd.unified_cgroup_hierarchy=1"' | sudo tee /etc/default/grub.d/70-cgroup-unified.cfg
sudo update-grub

Kernel parameters

想要在 Systemd enabled Linux 裡面啓用 cgroups v2 的話可以直接在 kernel parameters 裡面加上 systemd.unified_cgroup_hierarchy=1[2] 即可在開機後使用 cgroups v2。

Grub

雖然在開機的時候可以手動修改 parameters,但總不可能每次開機的時候都手動調整,所以我們要靠修改 grub 開機選單來幫我們解決這個問題。

我們可以在 /etc/default/grub 裡面直接添加 GRUB_CMDLINE_LINUX_DEFAULT="systemd.unified_cgroup_hierarchy=1" 或是在 /etc/default/grub.d/ 下面新增一個檔案來載入設定,我自己會選擇後者[3],避免被其他設定檔覆蓋(例如: 50-curtin-settings.cfg 會覆寫 GRUB_CMDLINE_LINUX_DEFAULT):

# /etc/default/grub.d/70-cgroup-unified.cfg
GRUB_CMDLINE_LINUX_DEFAULT="${GRUB_CMDLINE_LINUX_DEFAULT} systemd.unified_cgroup_hierarchy=1"

修改完畢後執行 sudo update-grub 即可套用我們剛剛新增的設定,亦可從 /boot/grub/grub.cfg 檢查設定是否有誤,都沒問題後就可以直接重開機了!

檢查結果

我們可以在重開機之後看看系統是不是真的有把 cgroups v2 啓用,可以從 mount list 檢查:

$ mount | grep cgroup2
cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime)

也可以手動 mount 看看 cgroups2:

$ sudo mkdir -p /test-cgroups2
$ sudo mount -t cgroup2 none /test-cgroups2
$ ls /test-cgroups2
cgroup.controllers  cgroup.max.descendants  cgroup.stat             cgroup.threads
cgroup.max.depth    cgroup.procs            cgroup.subtree_control  ...
$ sudo umount /test-cgroups2
$ sudo rmdir /test-cgroups2

如果可以成功存取 cgroups2 的資源,基本上就是成功了!


  1. Podman, a Pod Manager tool. ↩︎

  2. cgroups - ArchWiki ↩︎

  3. 關於 /etc/default/grub.d 是如何被載入的,可以參考 /usr/sbin/grub-mkconfig 裡面的實作 ↩︎

在 WSL2 中使用 Ubuntu 桌面環境

雖然微軟在 BUILD 2020 上已經宣佈,未來會讓 WSL2 可以執行 GUI 應用程式[1],但不知道什麼時候才會正式支援這個功能,對於想體驗看看效果到底如何的我呢,就打算先在 Windows 端啓動一個 X Window Server 來嚐鮮看看。

事前準備

首先,對於一個習慣 Ubuntu 的我來說,如果可以體驗到完整的 Ubuntu 桌面是再好不過了,換句話說,我們需要在上面可以跑一個完整的 Gnome Shell 環境,並加上 Ubuntu 的 Extension 們。

取得完整 systemd 環境

Gnome Shell 從 3.34 版開始,就已經跟 systemd 整合[2]了,也就是說,我們必須要先有一個完整的 systemd 環境才能順利的執行 Gnome Shell。由於 WSL2 的實作機制其實是啓動一個 Tiny Linux VM,然後利用 Linux Namespace 機制來執行並隔離各個發行版,並且由微軟實作了一個 init (PID 1) 來辦到快速啓動以及作為與 windows 溝通的橋樑[3]

也因此,在 WSL2 的系統裡面,其實是沒有啓用 systemd 的,我們可以簡單的透過下面的方式來檢查看看:

$ systemctl
System has not been booted with systemd as init system (PID 1). Can't operate.
Failed to connect to bus: Host is down
$ ps u -q 1
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root           1  0.0  0.0    908   592 ?        Sl   10:31   0:00 /init

可以很明顯的看到,當我們執行 systemctl 的時候,會顯示出我們的 init system (PID 1) 並非 systemd,而是微軟提供的 /init

那我們如果想要擁有一個 systemd 環境的話,該怎麼辦呢?

由於 systemd 必須以 PID 1 的方式執行,所以直接執行 systemd 是沒有用的,但多虧了 Linux Namespace 我們可以在 WSL2 中建立新的 Namespace 並把 systemd 作為 PID 1 來執行,也就是在 WSL2 中再多加一層 PID Namespace,使得我們可以建築一個 systemd 的環境並跳進這個新的 Namespace 中。

所幸,我們不需要自己來研究這部分該如何操作,GitHub 上已經有幾個專案可以直接拿來參考並使用:

我們下面會使用 DamionGans/ubuntu-wsl2-systemd-script 來當作範例,只要按照說明操作就可以了:

$ git clone https://github.com/DamionGans/ubuntu-wsl2-systemd-script.git
Cloning into 'ubuntu-wsl2-systemd-script'...
remote: Enumerating objects: 76, done.
remote: Counting objects: 100% (76/76), done.
remote: Compressing objects: 100% (55/55), done.
remote: Total 76 (delta 40), reused 41 (delta 21), pack-reused 0
Unpacking objects: 100% (76/76), 19.46 KiB | 996.00 KiB/s, done.
$ cd ubuntu-wsl2-systemd-script/
ubuntu-wsl2-systemd-script $ bash ubuntu-wsl2-systemd-script.sh
[sudo] password for davy:
Hit:1 http://security.ubuntu.com/ubuntu focal-security InRelease
Hit:2 http://archive.ubuntu.com/ubuntu focal InRelease
Hit:3 http://archive.ubuntu.com/ubuntu focal-updates InRelease
Hit:4 http://archive.ubuntu.com/ubuntu focal-backports InRelease
Reading package lists... Done
(Reading database ... 31836 files and directories currently installed.)
Preparing to unpack .../0-dbus-user-session_1.12.16-2ubuntu2.1_amd64.deb ...
Unpacking dbus-user-session (1.12.16-2ubuntu2.1) over (1.12.16-2ubuntu2) ...
Preparing to unpack .../1-dbus-x11_1.12.16-2ubuntu2.1_amd64.deb ...
Unpacking dbus-x11 (1.12.16-2ubuntu2.1) over (1.12.16-2ubuntu2) ...
Preparing to unpack .../2-dbus_1.12.16-2ubuntu2.1_amd64.deb ...
Unpacking dbus (1.12.16-2ubuntu2.1) over (1.12.16-2ubuntu2) ...
Preparing to unpack .../3-libdbus-1-3_1.12.16-2ubuntu2.1_amd64.deb ...
Unpacking libdbus-1-3:amd64 (1.12.16-2ubuntu2.1) over (1.12.16-2ubuntu2) ...
Selecting previously unselected package daemonize.
Preparing to unpack .../4-daemonize_1.7.8-1_amd64.deb ...
Unpacking daemonize (1.7.8-1) ...
Selecting previously unselected package fontconfig.
Preparing to unpack .../5-fontconfig_2.13.1-2ubuntu3_amd64.deb ...
Unpacking fontconfig (2.13.1-2ubuntu3) ...
Setting up fontconfig (2.13.1-2ubuntu3) ...
Regenerating fonts cache... done.
Setting up libdbus-1-3:amd64 (1.12.16-2ubuntu2.1) ...
Setting up dbus (1.12.16-2ubuntu2.1) ...
Setting up daemonize (1.7.8-1) ...
Setting up dbus-x11 (1.12.16-2ubuntu2.1) ...
Setting up dbus-user-session (1.12.16-2ubuntu2.1) ...
Processing triggers for systemd (245.4-4ubuntu3) ...
Processing triggers for man-db (2.9.1-1) ...
Processing triggers for libc-bin (2.31-0ubuntu9) ...
rm: cannot remove '/lib/systemd/system/sysinit.target.wants/proc-sys-fs-binfmt_misc.mount': No such file or directory
'\\wsl$\Ubuntu-20.04\home\davy\ubuntu-wsl2-systemd-script'
是目前用來啟動 CMD.EXE 的目錄路徑。不支援 UNC 路徑。
預設目錄是 Windows 目錄。

成功: 已經儲存指定的值。
'\\wsl$\Ubuntu-20.04\home\davy\ubuntu-wsl2-systemd-script'
是目前用來啟動 CMD.EXE 的目錄路徑。不支援 UNC 路徑。
預設目錄是 Windows 目錄。

成功: 已經儲存指定的值。
ubuntu-wsl2-systemd-script $

安裝完畢後我們需要重啓整個 WSL2,假定我們的 WSL 名稱是 ubuntu-20.04,在命令提示字元(CMD)中執行下列指令以關閉 WSL2:

> wsl.exe -t ubuntu-20.04

接者使用一般使用者啓動 WSL2,就可以發現有不一樣的地方了(多了一個啓動 systemd 的提示),透過檢查 PID 1 也可以發現整個環境已經是由 systemd 掌握了:

Sleeping for 1 second to let systemd settle
Welcome to Ubuntu 20.04 LTS (GNU/Linux 4.19.104-microsoft-standard x86_64)

$ ps u -q 1
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root           1  2.8  0.0 175224 12920 ?        Ss   21:48   0:06 /lib/systemd/systemd --system-unit=basic.target
$

移除用不到的 snap(可選)

由於 Ubuntu 安裝 systemd 時,會連 snap 也一併啓用,如果大家用不到的話可以把 snap 從系統中移除,這麼一來也可以提升啓動速度,我們可以看到系統啓動時也連著一些 snap 的元件一起啓動了(而且還不少 Processes):

$ ps aux
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root           1  1.3  0.0 108788 11360 ?        Ss   22:03   0:00 /lib/systemd/systemd --system-unit=basic.targ
root          43  2.1  0.1  45248 14600 ?        S<s  22:03   0:00 /lib/systemd/systemd-journald
root          61  0.4  0.0  21592  8496 ?        Ss   22:03   0:00 /lib/systemd/systemd-udevd
systemd+      63  1.2  0.0  18548  7896 ?        Ss   22:03   0:00 /lib/systemd/systemd-networkd
root         152  0.0  0.0  10572  4588 pts/0    S    22:03   0:00 /bin/login -p -f      'HOSTTYPE=x86_64' 'PWD=
root         219  1.2  0.0   3608  1792 ?        Ss   22:03   0:00 snapfuse /var/lib/snapd/snaps/core18_1705.sna
root         220  0.3  0.0   3660  1476 ?        Ss   22:03   0:00 snapfuse /var/lib/snapd/snaps/lxd_14804.snap
root         221  0.1  0.0   3488  1536 ?        Ss   22:03   0:00 snapfuse /var/lib/snapd/snaps/snapd_7264.snap
systemd+     228  1.1  0.0  24116 12592 ?        Ss   22:03   0:00 /lib/systemd/systemd-resolved
root         231  0.1  0.0 241020  9280 ?        Ssl  22:03   0:00 /usr/lib/accountsservice/accounts-daemon
message+     232  0.2  0.0   7428  4640 ?        Ss   22:03   0:00 /usr/bin/dbus-daemon --system --address=syste
root         235  0.3  0.1  29216 17788 ?        Ss   22:03   0:00 /usr/bin/python3 /usr/bin/networkd-dispatcher
syslog       236  0.1  0.0 224328  4296 ?        Ssl  22:03   0:00 /usr/sbin/rsyslogd -n -iNONE
root         238  2.1  0.2 1457156 35432 ?       Ssl  22:03   0:00 /usr/lib/snapd/snapd
root         240  1.1  0.0  16852  7728 ?        Ss   22:03   0:00 /lib/systemd/systemd-logind
root         261  0.0  0.0 236408  9084 ?        Ssl  22:03   0:00 /usr/lib/policykit-1/polkitd --no-debug
root         302  0.0  0.0   8540  2764 ?        Ss   22:03   0:00 /usr/sbin/cron -f
root         307  0.4  0.1 108036 20512 ?        Ssl  22:03   0:00 /usr/bin/python3 /usr/share/unattended-upgrad
daemon       308  0.0  0.0   3796  2200 ?        Ss   22:03   0:00 /usr/sbin/atd -f
root         318  0.0  0.0   7356  2172 tty1     Ss+  22:03   0:00 /sbin/agetty -o -p -- \u --noclear --keep-bau
root         326  0.0  0.0   5832  1744 ?        Ss   22:03   0:00 /sbin/agetty -o -p -- \u --noclear tty1 linux
davy         376  0.2  0.0  18444  9612 ?        Ss   22:03   0:00 /lib/systemd/systemd --user
davy         377  0.0  0.0 110132  3188 ?        S    22:03   0:00 (sd-pam)
davy         387  0.2  0.0  10048  4992 pts/0    S    22:03   0:00 -bash
davy         431  0.0  0.0  10604  3224 pts/0    R+   22:04   0:00 ps aux
$

首先我們先將所有 snap 都列出後,一個一個移除:

$ snap list
Name    Version   Rev    Tracking         Publisher   Notes
core18  20200311  1705   latest/stable    canonical✓  base
lxd     4.0.1     14804  latest/stable/…  canonical✓  -
snapd   2.44.3    7264   latest/stable    canonical✓  snapd
# snap remove lxd
# snap remove core18
# snap remove snapd
$ snap list
No snaps are installed yet. Try 'snap install hello-world'.
# apt purge snapd
$ ps aux
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root           1  0.6  0.0 109692 12692 ?        Ss   22:03   0:03 /lib/systemd/systemd --system-unit=basic.targ
root          43  0.1  0.1  53444 15456 ?        S<s  22:03   0:00 /lib/systemd/systemd-journald
root          61  0.0  0.0  21592  8496 ?        Ss   22:03   0:00 /lib/systemd/systemd-udevd
systemd+      63  0.1  0.0  18548  7896 ?        Ss   22:03   0:00 /lib/systemd/systemd-networkd
root         152  0.0  0.0  10572  4588 pts/0    S    22:03   0:00 /bin/login -p -f      'HOSTTYPE=x86_64' 'PWD=
systemd+     228  0.0  0.0  24116 12592 ?        Ss   22:03   0:00 /lib/systemd/systemd-resolved
root         231  0.0  0.0 241020  9280 ?        Ssl  22:03   0:00 /usr/lib/accountsservice/accounts-daemon
message+     232  0.0  0.0   7428  4640 ?        Ss   22:03   0:00 /usr/bin/dbus-daemon --system --address=syste
root         235  0.0  0.1  29216 17788 ?        Ss   22:03   0:00 /usr/bin/python3 /usr/bin/networkd-dispatcher
syslog       236  0.0  0.0 224328  4296 ?        Ssl  22:03   0:00 /usr/sbin/rsyslogd -n -iNONE
root         240  0.1  0.0  16852  7728 ?        Ss   22:03   0:00 /lib/systemd/systemd-logind
root         261  0.0  0.0 236408  9084 ?        Ssl  22:03   0:00 /usr/lib/policykit-1/polkitd --no-debug
root         302  0.0  0.0   8540  2764 ?        Ss   22:03   0:00 /usr/sbin/cron -f
root         307  0.0  0.1 108036 20512 ?        Ssl  22:03   0:00 /usr/bin/python3 /usr/share/unattended-upgrad
daemon       308  0.0  0.0   3796  2200 ?        Ss   22:03   0:00 /usr/sbin/atd -f
root         318  0.0  0.0   7356  2172 tty1     Ss+  22:03   0:00 /sbin/agetty -o -p -- \u --noclear --keep-bau
root         326  0.0  0.0   5832  1744 ?        Ss   22:03   0:00 /sbin/agetty -o -p -- \u --noclear tty1 linux
davy         376  0.0  0.0  18444  9708 ?        Ss   22:03   0:00 /lib/systemd/systemd --user
davy         377  0.0  0.0 110132  3188 ?        S    22:03   0:00 (sd-pam)
davy         387  0.0  0.0  10180  5252 pts/0    S    22:03   0:00 -bash
root        1168  0.0  0.1 283780 15888 ?        Ssl  22:07   0:00 /usr/lib/packagekit/packagekitd
davy        1273  0.0  0.0  10604  3316 pts/0    R+   22:11   0:00 ps aux

結束後我們就可以發現 Processes 數量減少了許多,讓我們的環境又稍微輕量了些。

在 Windows 準備 X Window Server

這邊有很多選擇,我選擇了 X410[4](付費),大家也可以選擇 VcXsrv[5] 之類的解決方案,這邊就不多贅述了,在這裡的範例中會需要將 X Window Server 開在 Windows 的 6000 port 上。

安裝 Ubuntu 桌面

接著我們就可以來安裝 Ubuntu 的預設桌面了,這裡會需要比較多的硬碟空間:

# apt install ubuntu-desktop
...
10 upgraded, 1077 newly installed, 0 to remove and 64 not upgraded.
Need to get 605 MB of archives.
After this operation, 2260 MB of additional disk space will be used.
Do you want to continue? [Y/n] y
...

安裝結束後,我們需要先取得 Windows 的 IP 位置[6],並嘗試對 X Window Server 連線看看:

$ cat /etc/resolv.conf
# This file was automatically generated by WSL. To stop automatic generation of this file, add the following entry to /etc/wsl.conf:
# [network]
# generateResolvConf = false
nameserver 172.17.160.1
$ nc -v 172.17.160.1 6000
Connection to 172.17.160.1 6000 port [tcp/x11] succeeded!
^C
$

成功連線後,我們可以撰寫一個啓動腳本,將 DISPLAY 指向 Windows 上的 X Window Server 中並且加上一些 Ubuntu 桌面的設定後執行 Gnome Shell:

$ cat - > gnome.sh <<'EOF'
#!/bin/bash
export DISPLAY=$(cat /etc/resolv.conf | grep nameserver | awk '{print $2}'):0.0;
export XDG_SESSION_TYPE="x11"
export XDG_RUNTIME_DIR=~/.cache/xdg
export XDG_SESSION_CLASS="user"
export XDG_SESSION_DESKTOP=ubuntu
export XDG_CURRENT_DESKTOP=ubuntu:GNOME
export DESKTOP_SESSION=ubuntu
export GDMSESSION=ubuntu
export GNOME_SHELL_SESSION_MODE=ubuntu

gnome-session "$@"
EOF
$ chmod +x gnome.sh
$ ./gnome.sh

Ubuntu Desktop (Gnome Shell) in WSL2

當然,如果想要在這個桌面中執行 Windows CMD 也是沒有問題的,而系統也可以正確的偵測到 Virtualization 是 WSL 呢。

Run CMD in Desktop

關閉 systemd-resolved

這裡是一個可選的項目,由於筆者在透過 /etc/resolv.conf 取得 Windows IP 時有遇到 resolv.conf 被 systemd-resolved 替換成 127.0.0.53 的問題,這邊提供一個方式將這個東西停用,操作完重啓 WSL2 即可:

# systemctl stop systemd-resolved
# systemctl disable systemd-resolved
Removed /etc/systemd/system/multi-user.target.wants/systemd-resolved.service.
Removed /etc/systemd/system/dbus-org.freedesktop.resolve1.service.

後記

由於目前的 WSL2 還沒有 GPGPU 加速,所以拿來執行 GUI 程式的話可能還是會有點 lag,微軟提出的最終方案是透過 RDP 來執行 GUI 應用程式也許會透過 RemoteApp 來將顯卡加速做在 Windows 端,但作為嚐鮮一下,目前的結果已經算是還可以的了XD


  1. 微軟在 BUILD 2020 上宣佈,將會讓 WSL2 可以執行 GUI 應用程式,並使用 Wayland 配合 RDP Protocol 來實作這個功能,詳見微軟部落格〈The Windows Subsystem for Linux BUILD 2020 Summary〉一文 ↩︎

  2. 關於 Gnome Shell 的這個變化,可以參考 Gnome Blog 的〈GNOME 3.34 is now managed using systemd〉一文。 ↩︎

  3. 對於 WSL2 的詳細架構,可以參考 BUILD 2019 的〈The new Windows subsystem for Linux architecture: a deep dive - BRK3068〉議程。 ↩︎

  4. https://x410.dev/ ↩︎

  5. https://sourceforge.net/projects/vcxsrv/ ↩︎

  6. https://docs.microsoft.com/zh-tw/windows/wsl/compare-versions#accessing-network-applications ↩︎

在 WSL2 裡面使用 GPU 加速機器學習

微軟在 Microsoft Build 2020 時曾表示,將在 WSL2 中新增支援 NVIDIA CUDA 以及 DirectML 來讓 Linux 中的機器學習應用可以無痛直接放到 WSL2 中使用。

沒想到才過一個月,微軟就在 Windows 10 Insider (20150+, WSL2 Kernel 4.19.121+) 釋出了支援 NVIDIA CUDA 以及 DirectML 的功能,目前 NVIDIA CUDA 看起來是直接跟 NVIDIA 合作並釋出驅動程式來支援在 WSL2 中的 GPU 虛擬化;而 DirectML 則是跟 NVIDIA/AMD/Intel 合作釋出驅動程式[1],並在 TensorFlow 上面實作以 DirectML 為後端的版本,並向上游提交了 Pull Request[2]

沒想到微軟動作這麼快,看來是真的有在 WSL2 上面投注很多資源想要鞏固整個生態系,對我們這些開發者來說其實也是挺好的,選擇也變多了。XD

ref: https://blogs.windows.com/windowsdeveloper/2020/06/17/gpu-accelerated-ml-training-inside-the-windows-subsystem-for-linux/
ref: https://docs.microsoft.com/zh-tw/windows/win32/direct3d12/gpu-accelerated-training


  1. 截至本文發文時間為止,NVIDIA 僅釋出了可使用 CUDA 的驅動,而 DirectML 的版本則尚未釋出。 ↩︎

  2. https://github.com/tensorflow/community/pull/243 ↩︎

使用 systemd 管理 Docker container

因為把部落格重建[1]的關係,順便把所有的服務都 container[2] 化了,這次使用 systemd[3] 來幫忙管理 container deamonlize 的部分,下面來介紹一下我用了什麼工具跟方法。

前言

其實管理 Docker container 的工具已經有很多了,最常見的就是 Docker Compose[4]或是廣義的來說用 K8s[5] 等工具也是可以管理容器,但為什麼我還要自己搞呢?明明就有現成的工具方便我管理了。

但,我必須得說,我就是沒那麼喜歡 Docker Compose,畢竟你還要為這些 docker-compose.yml 找一個家,雖然可以跟要掛載的 volume 放一起,但還是很亂,既然我都要管理這些 Container 了,那我是不是可以直接掛到 systemd 上面幫我管理呢?

systemd service unit

好,既然已經確定要用 systemd 了,那麼就先來寫個 Unit file[6] 來描述這個 Docker container 的啟動方式吧,現在很簡單的假設一下我們現在要啓動一個很簡單的服務吧,這裡使用 busybox:musl 這個 image 並在裡面跑一個簡單的腳本令它每分鐘輸出一句 hello world

那麼 Unit file 如下: docker-hello-world.service

[Unit]
Description=Docker container - hello world
After=docker.service
Requires=docker.service

[Service]
TimeoutStartSec=0
Restart=always
ExecStart=/usr/bin/docker run --rm --name hello-world busybox:musl /bin/sh -c 'while true; do echo hello world; sleep 60; done'

[Install]
WantedBy=multi-user.target

如此一來我們就可以進行初步的管理了,例如使用 systemctl enable docker-hello-world.service 讓這個 container 可以開機自動執行,並且有加上 After=docker.serviceRequires=docker.service 的緣故,確保這個 service 會接在 docker.service 執行之後才開始

systemd-docker

在成功的啓動服務之後呢,還不要太心急。雖然現在 systemd 的確可以成功的幫我們把 container 開起來,但當我們想透過 systemd 觀察 process 的時候只能看到如下的內容:

$ sudo systemctl status docker-hello-world.service
● docker-hello-world.service - Docker container - hello world
   Loaded: loaded (/etc/systemd/system/docker-hello-world.service; disabled; vendor preset: enabled)
   Active: active (running) since Mon 2019-08-19 08:49:09 UTC; 16ms ago
 Main PID: 21293 (docker)
    Tasks: 1 (limit: 1152)
   CGroup: /system.slice/docker-hello-world.service
           └─21293 /usr/bin/docker run --rm --name hello-world busybox:musl /bin/sh -c while true; do echo hello world; sleep 60; done

Aug 19 08:49:09 did systemd[1]: Started Docker container - hello world.

這是為什麼呢?
原來 Docker 的設計分為 Server 跟 Client,我們平常呼叫的 docker 指令就(Docker CLI)是所謂的 Client,而 Server 又稱 Docker Engine[7],其任務則是負責處理 container 管理、網路管理等任務。當我們操作 Docker CLI 時,其實只是藉由 Client 去向 Server 溝通,實際處理請求的還是 Docker Engine。

也是因為這樣,所以 Container 的 Process 其實是從 Docker Engine 延伸出來[8]的,且 Docker 還會將 Container 的 Process 隔離進新的 CGroup[9],因此從 systemd 上面是無法從執行 docker 這個指令的 docker-hello-world.service unit 查詢到。

那怎麼辦呢?
所幸有人在理解背後的原理之後作出了一個工具 systemd-docker[10],這個工具的運作原理十分簡單,他會將吃到的參數直接轉送給 docker,如果這個操作將會建立 container 的話,systemd-docker 就會把建立好的 container process 的 cgroup 移動回 systemd 下面讓 systemd 可以管理,並將 stdout/stderr[11] 導向到 systemd-journal[12],而且支援 systemd-notify[13]

正所謂,有了 systemd-docker 考試都考一百分呢(不要瞎掰好嗎)!
那麼我們把 Unit file 改一下: docker-hello-world.service

[Unit]
Description=Docker container - hello world
After=docker.service
Requires=docker.service

[Service]
TimeoutStartSec=0
Restart=always
Type=notify
NotifyAccess=all
ExecStart=/usr/local/bin/systemd-docker --cgroups name=systemd run --rm --name hello-world busybox:musl /bin/sh -c 'while true; do echo hello world; sleep 60; done'

[Install]
WantedBy=multi-user.target

可以注意到,我們同時加上了與 systemd-notify 有關的參數,並且在 systemd-docker 中指定 cgroup 到 systemd 上,如果想要指定其他 cgroup 也可以自行修改參數。

那麼就來看看結果吧:

$ systemctl daemon-reload
$ systemctl start docker-hello-world.service
$ systemctl status docker-hello-world.service
● docker-hello-world.service - Docker container - hello world
   Loaded: loaded (/etc/systemd/system/docker-hello-world.service; disabled; vendor preset: enabled)
   Active: active (running) since Mon 2019-08-26 07:30:48 UTC; 21s ago
 Main PID: 3742 (sh)
    Tasks: 4 (limit: 1152)
   CGroup: /system.slice/docker-hello-world.service
           └─3661 /usr/local/bin/systemd-docker --cgroups name=systemd run --rm --name hello-world busybox:musl /bin/sh -c while true; do echo hello world; sleep 60;
           ‣ 3742 /bin/sh -c while true; do echo hello world; sleep 60; done

Aug 26 07:30:48 did systemd[1]: Started Docker container - hello world.
Aug 26 07:30:48 did systemd-docker[3661]: hello world
Aug 26 07:31:48 did systemd-docker[3661]: hello world
Aug 26 07:32:48 did systemd-docker[3661]: hello world

要注意的是,由於我們移動了 cgroup,所以 docker run--cpuset/-m` 參數就會不起作用了(取而代之的是,我們可以從 systemd 來為這些 cgroup 設定限制)。

systemd template unit

在經過一番忙碌之後,我們終於把 Docker container 與 systemd 結合了,但要是每次建立新 container 都要複製貼上一個 unit file,豈不太搞剛(太麻煩)?

於是我們應該要將我們的 service unit 改良成一個 template unit,如此一來我們就可以不需要重複撰寫有關於 Docker container service 的重疊部分,把焦點放置在差異的部分就好了。

首先,我們來把 docker-hello-world.service 改一下名字以及內容,取作 [email protected],注意名稱中的 @ 是必要的,systemd 會把 @ 結尾的 unit file 視為一個 template unit,內容如下:

[Unit]
Description=Docker container - %i
After=docker.service
Requires=docker.service

[Service]
TimeoutStartSec=0
Restart=always
Type=notify
NotifyAccess=all
EnvironmentFile=/etc/systemd/docker/%i.conf
ExecStart=/usr/local/bin/systemd-docker --cgroups name=systemd run --rm --name systemd-%i $ARGS $IMAGE $CMD

[Install]
WantedBy=multi-user.target

我們在這裡定義了一個 template file,其執行時會讀取 /etc/systemd/docker/%i.conf 的檔案來做 unit 的環境變數設定,然後我們再將環境變數中的 $IMAGE $ARGS $CMD 丟給 systemd-docker 執行,其中 %i[14] 指的就是這個 template unit 的 instance name。

覺得太過複雜的話,可以換個角度這樣理解,當 systemd 要執行一個 template service unit 的時候,會要求以 [email protected] 來執行,systemd 會去讀取 [email protected] 這個 template unit,並且把 %i 取代為 @ 後方的內容(此例為 instance-name)。以上面的例子來說,如果我們執行 [email protected],我們可以想成是在執行如下的 unit file:

[Unit]
Description=Docker container - hello-world
After=docker.service
Requires=docker.service

[Service]
TimeoutStartSec=0
Restart=always
Type=notify
NotifyAccess=all
EnvironmentFile=/etc/systemd/docker/hello-world.conf
ExecStart=/usr/local/bin/systemd-docker --cgroups name=systemd run --rm --name systemd-hello-world $ARGS $IMAGE $CMD

[Install]
WantedBy=multi-user.target

如此一來就清晰多了,關鍵的問題是 /etc/systemd/docker/hello-world.conf 的內容是什麼呢?其實也很簡單,內容如下:

IMAGE=busybox:musl
ARGS=
CMD=sh -c 'while true; do echo hello world; sleep 60; done'

而 systemd 在讀取這個檔案之後會把裡面的內容設成環境變數,並填入 ExecStart 中,整個 unit file 最後就會變成這樣:

[Unit]
Description=Docker container - hello-world
After=docker.service
Requires=docker.service

[Service]
TimeoutStartSec=0
Restart=always
Type=notify
NotifyAccess=all
EnvironmentFile=/etc/systemd/docker/hello-world.conf
ExecStart=/usr/local/bin/systemd-docker --cgroups name=systemd run --rm --name systemd-hello-world busybox:musl sh -c 'while true; do echo hello world; sleep 60; done'

[Install]
WantedBy=multi-user.target

這樣子就跟原本的一樣了,那麼我們如果要建立新的 container service 的時候,我們只需要進行下列的步驟:

  1. 決定一個 instance name,例如 nginx
  2. /etc/systemd/docker/ 下面建立 nginx.conf,並填入 IMAGE, ARGS, CMD
  3. 執行 systemctl start [email protected]
  4. (可選) systemctl enable [email protected] 設為開機自動執行

後記

經過了上面的研究之後,也去看了 systemd-docker 的原始碼瞭解這個工具實際上做的事情以及 Docker 與 systemd 背後做的事情有哪些,雖然還只是冰山一角,但至少又更理解自己平時使用的工具到底在幹嘛,而不是單純一味的使用,知其然而不知所以然,希望大家也有學到一些技巧。


  1. 有關重建部落格的資訊,請見《部落格復活》一文 ↩︎

  2. 這邊專指 Linux container,是一種作業系統層級的虛擬化技術,參見 Docker 的《What is a Container?》一文或維基百科上的《OS-level virtualisation》條目 ↩︎

  3. systemd 是一套用於 Linux 的管理框架,其實作包含 Daemon、Library 及眾多 Application,並通常用來取代 System V 作為 init,並使用 cgroup 來追從 process,請見維基百科《systemd》條目及 ArchWiki 《systemd》條目 ↩︎

  4. Docker Compose 是 Docker 所推薦的一種同時管理多個容器的方式,相關用法可以參見官網說明 ↩︎

  5. Kubernetes,是一個用於自動部署、擴展、容器化管理的工具,並不是專為 Docker 而設計,但支援使用 Docker 作為容器化的引擎,相關資訊可見其網站介紹 ↩︎

  6. systemd 裡面所有與 service、socket、device、mountpoint、等有關的描述都被稱為 Unit,有關 Unit 的說明可以參考 man systemd.unit (link)或 ArchWiki 〈[systemd - 編寫單元檔案(https://wiki.archlinux.org/index.php/Systemd_(正體中文)#編寫單元檔案)]〉一節 ↩︎

  7. Docker Engine 的介紹可見《About Docker Engine》一文 ↩︎

  8. 在預設的環境中,其實 Docker Engine 只是 containerd 的前端界面,實際建立 Process 的其實是 containerd,有關 containerd 的介紹請見網站說明 ↩︎

  9. Control Groups,是 Linux 核心的一個功能,用來限制、控制與分離一個行程群組的資源,常被用來做 Container 的基礎,systemd 也以此為基礎來管理 service。 ↩︎

  10. 提供 Systemd 與 Docker 互動的界面,其原始版本只支援舊 Docker API,筆者這裡使用的是由 @DonTseTse fork 出來的版本: github.com/DonTseTse/systemd-docker ↩︎

  11. stdout 表示為標準輸出、stderr 表示為標準錯誤輸出,兩者預設皆會直接輸出至終端 ↩︎

  12. Systemd 中提供的日誌系統,用以管理系統 log 等記錄,更多資訊可見ArchWiki《Systemd/Journal》條目 ↩︎

  13. Systemd-notify 可向 systemd 發送一個提示以表示 service 的狀態(例如:執行中),以便 systemd 處理 unit 之間的相依性 ↩︎

  14. %i 等變數我們稱為 specifier,其他可用的 specifier 可見 man systemd.unit 中的 〈specifiers〉一節 ↩︎

使用 Cmder 直入 WSL

因為工作有 SSH 連線到遠端機器操作的習慣,在換了 Surface Book 2[1] 之後,也還是想要儘量保持這個習慣,一般來說我都是使用 PieTTY[2] 在進行 SSH 連線,不過對於行動裝置我通常會再搭配上 Mosh[3] 使用,可惜在 Windows 上面沒有原生的 Client 支援,於是我打算在 WSL[4] 裡面安裝 Mosh 來使用!

初始規劃

原本我是在找尋可用的 Mosh client,發現 MobaXterm 有支援 mosh,不過試用了一下之後發現他對於鍵盤的 ASCII Keycode 支援沒有很好,一些按鍵是送不出去的,於是就放棄了,打算改轉我在桌機上面的做法——開 Linux 虛擬機再用 PieTTY 連線進入。

不過虛擬機這個念頭很快就被 WSL 取代了(我 Hyper-V 都打開準備要裝系統了XD),因為筆電還是會想要以電量為優先考量,如果裝 VM 的話,電池很快就不夠力了……

於是這邊有兩個想法:

  1. 在 WSL 裡面啓用 SSH Server,用 PieTTY 搭配自動登入[5]來作為 mosh 的執行器
  2. 找一個 PieTTY 的替代品直接接入 WSL Shell 來執行 mosh

由於 WSL 的 lifecycle 只會到所有 shell 關閉就結束,所以如果要用方案 1 的話還必須在 Windows 裡面建立一個 service 來掛著 WSL sshd 的 Deamon(怎麼有種 Docker 的感覺XD),有點小麻煩,雖然我很喜歡,因為 PieTTY 已經用習慣了,其實我沒有很想換 Terminal…… 如果之後用的不順再來研究這個方法XD

使用 Cmder

不過介於我懶惰設定的關係,所以我姑且選擇了方案 2,我找了一個 Windows Terminal Emulator —— Cmder[6],他其實是把 ConEmu[7] 做了一些包裝,使用起來還算是上手,設定一下快捷鍵之後覺得應該還能接受

不過設定 WSL 的地方就需要照著說明來做了,原先我以為進去 cmder 之後,下 ubuntu 啓用 WSL 就完美了,結果在我 ssh/mosh 到遠端機器的時候發現 256 color 的設定不見了,整個顏色都變得很奇怪。

查了一下資料之後發現好像需要 wslbridge 來幫忙轉換一些畫面繪製的指令,這部份其實也不需要自己安裝額外的東西,cmder 包裝的 ConEmu 版本夠新,需要的元件其實本來就有包含了,我們只需要自己加上 Task config。

依照這篇的說明,首先建立一個 Task(名稱隨便取,像我就使用 wsl::bridge),在指令的部分填上如下內容:

set "PATH=%ConEmuBaseDirShort%\wsl;%PATH%" & %ConEmuBaseDirShort%\conemu-cyg-64.exe --wsl -cur_console:pnm:/mnt -t bash -l

然後在 cmder 建立新 Tab 的時候選擇這個 task,就可以得到一個有 256color support 的 Terminal 了!

看看全彩的 lolcat 就覺得……好空虛XDDD

預設進入 mosh

如果想要預設建立某個 mosh 連線的話,也可以複製剛剛的 Task,並在後面加上:-c "mosh [email protected]" 讓 bash 會自動執行 mosh 之後離開。


  1. Surface Book 2 開箱一文 ↩︎

  2. PieTTY ↩︎

  3. Mosh: the mobile shell ↩︎

  4. WSL: Windows Subsystem for Linux ↩︎

  5. 修改 PAM 讓特定 IP 透過 SSH 登入時不需要輸入密碼一文 ↩︎

  6. cmder | Console Emulator ↩︎

  7. ConEmu - Handy Windows Terminal ↩︎

修改 PAM 讓特定 IP 透過 SSH 登入時不需要輸入密碼

我有兩臺電腦,一臺是外出用的 MacBook Air (13" later)、另一臺是自己組裝的 Windows 桌機,但平時工作都是在 Windows 上面開 Linux(當然是我最愛的 Ubuntu)的 VM 再從兩臺電腦 SSH 進去 VM 裡面。但從 Windows SSH 進去 VM 的時候就很想跳過輸入密碼的步驟,當然用 SSH Key 也可以辦到,但我覺得他們根本就是同一個電腦啊,還要加上 SSH Key 也太累了吧!要如何僅依靠來源 IP 就決定要不要輸入密碼呢?

!!警告!! 跳過密碼就可以登入是非常危險的,看看蘋果今天才緊急更新的免密碼登入 root 漏洞,即使是本機也是非常危險的呢!
https://support.apple.com/en-us/HT208315

PAM (Pluggable Authentication Modules)

首先,想跟各位介紹一個叫做 PAM (Pluggable Authentication Modules) 的模組,PAM 提供了一套驗證的 API 讓應用程式可以透過 PAM 做使用者驗證。

我們即是要對 PAM 來下手,而 PAM 的設定檔們都存放在 /etc/pam.d/ 下,sshd 會有自己專屬的設定檔可以使用就叫做 /etc/pam.d/sshd

我們把設定檔 cat 出來看一下會看到類似以下的內容:

# PAM configuration for the Secure Shell service

# Standard Un*x authentication.
@include common-auth

# Disallow non-root logins when /etc/nologin exists.
account    required     pam_nologin.so

# Uncomment and edit /etc/security/access.conf if you need to set complex
# access limits that are hard to express in sshd_config.
# account  required     pam_access.so

# Standard Un*x authorization.
@include common-account

# SELinux needs to be the first session rule.  This ensures that any
# lingering context has been cleared.  Without this it is possible that a
# module could execute code in the wrong domain.
session [success=ok ignore=ignore module_unknown=ignore default=bad]        pam_selinux.so close

# Set the loginuid process attribute.
session    required     pam_loginuid.so

# Create a new session keyring.
session    optional     pam_keyinit.so force revoke

# Standard Un*x session setup and teardown.
@include common-session

# Print the message of the day upon successful login.
# This includes a dynamically generated part from /run/motd.dynamic
# and a static (admin-editable) part from /etc/motd.
session    optional     pam_motd.so  motd=/run/motd.dynamic
session    optional     pam_motd.so noupdate

# ......

# Standard Un*x password updating.
@include common-password

我們可以很清楚的看到,每一條項目都有以下格式:

[驗證類別] [控制標記] [檔名] [參數]

驗證類別 (Type)

驗證類別分成四種:account(帳號)、auth(驗證)、session(執行期間)、password(更新驗證資訊)。

舉例來說,我們可以清楚的看到 account 中有一個用來阻擋 nologin 的模組:

# Disallow non-root logins when /etc/nologin exists.
account    required     pam_nologin.so

表示這個模組會在使用者登入時,輸入帳號後,檢查是否為 nologin,來決定這個帳號是否合法。

控制標記 (Control Flag)

常見的控制標記有 4 種:

  • required
    若驗證成功則帶有 success 的標誌;
    若驗證失敗則帶有 failure 的標誌。
    但不論成功或失敗都會繼續後續的驗證流程。

  • requisite
    若驗證失敗則立刻回報 failure,並終止後續的驗證流程;
    若驗證成功則帶有 success 的標誌並繼續後續的驗證流程。
    required 最大的差異就在於失敗時就會終止驗證流程。

  • sufficient
    若驗證成功則立刻回報 success,並終止後續的驗證流程;
    若驗證失敗則帶有 failure 標誌並繼續後續的驗證流程。
    requisits 剛好相反。

  • optional
    不論成功或失敗都不回報且不影響後續的流程。

但這些都只是常見的預設標記,PAM 還提供了自己撰寫驗證流程規則的標記,可以自己寫要不要回報,以及要不要中斷流程。可以看看 pam.d(5) 的 man page。

觀察可用的 PAM 模組

我們可以注意到預設的 sshd 裡面有一個令人有興趣的設定是:

# Uncomment and edit /etc/security/access.conf if you need to set complex
# access limits that are hard to express in sshd_config.
# account  required     pam_access.so

如果我把註解拿掉的話,他就會在帳號檢查階段(account),去檢查這個帳號是不是可以被存取,這個不是跟我們想要的東西很像嗎?
只是我們要檢查的是「這個密碼需不需要被輸入」而已,很像嘛! (迷之音:真的很像嗎?)

實做

PAM 設定

以下是我使用的設定,插在 common-auth 之前,來保持我的語句比其他人早驗證:

# PAM configuration for the Secure Shell service

auth [success=done default=ignore] pam_access.so accessfile=/etc/security/sshd-access-local.conf # <<<<<<<
# Standard Un*x authentication.
@include common-auth

# ...

我在這裡使用了 auth,讓我需要被驗證的時候執行檢查;
而在控制標記的部分我則是自己撰寫了規則 [success=done default=ignore],表示在 success 的時候就直接回報 success 並結束驗證流程,但在非 success 的情況下就不回報也不結束流程(ignore)。
而使用的當然是剛剛觀察到的 pam_access.so 模組,他可以接受一個名為 accessfile 的參數,來提供他判斷的條件,這裡我先命名為 /etc/security/access-local.conf
其他參數大家可以參考 pam_access(8) 的 man page。

pam_access.so accessfile

接下來我們來看看這個 accessfile 裡面到底長怎樣,一樣大家可以參考 access.conf(5) 的 man page,先列出我自己的 config:

+ : davy : 192.168.3.0/24
+ : davy : 10.8.0.0/24
- : ALL : ALL

相對於 PAM 的設定,access.conf 的格式更加地簡單了:

permission : users/groups : origins

其中 permission 只有兩種值: +- 分別對應到 granteddenied,表示符合條件的狀況會不會通過測驗;
user/groups 則是指定套用規則的使用者或群組,可以用 ALL 表示全部使用者皆套用;
origins 指定的是來源,可以是 TTY 名稱、IP address、domain name 等,其中有兩個特別的值是 ALLLOCAL,分別表示全部適用以及套用到本機服務或本機 TTY。

設定檔前兩條規則,帳號都限定在我個人的帳號(davy);
而第一條是只有我才連得上的 VPN subnet、第二條是 VM 網路的 subnet。
最後還要加上一條 deny 全部其他的規則,到這裡我們已經大功告成了。

測試

只要我從符合上述條件的網路登入指定帳號,就可以不需要密碼就能夠登入了!

以下是從 Windows 用 pietty 登入的測試結果,可以看到我輸入完帳號之後就進入系統了:

最後我在這裡還是要在提醒大家一次:跳過密碼就可以登入是非常危險的,除非你的環境十分值得信賴,不然千萬不要忽略驗證!