CUBE SUGAR CONTAINER

技術系のこと書きます。

Linux でスワップファイルを使ってスワップ領域のサイズを柔軟に変更する

一昔前だと、スワップ領域といえば専用のパーティションを用意して作るものというイメージがあった。 しかし、どうやら最近はファイルシステム上に作成したファイルを使ったスワップファイルの利用も盛んなようだ。 スワップファイルには、サイズを柔軟に変更できるメリットがある。 今回はスワップファイルを使ってスワップ領域のサイズを変更する方法について見ていく。

使った環境は次のとおり。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 24.04.2 LTS
Release:    24.04
Codename:   noble
$ uname -srm
Linux 6.8.0-59-generic x86_64
$ mkswap --version
mkswap from util-linux 2.39.3
$ swapon --version
swapon from util-linux 2.39.3
$ fallocate --version
fallocate from util-linux 2.39.3

もくじ

スワップ領域の状況を確認する

スワップ領域の状況は procfs から確認できる。 具体的には /proc/swaps から使用しているスワップパーティションやスワップファイルの情報が得られる。

$ cat /proc/swaps 
Filename                Type        Size        Used    Priority
/swap.img                               file        3043324        0  -2

今回使用した環境では /swap.img というスワップファイルがスワップ領域に使われている。 サイズは 3GB ほどのようだ。

スワップファイルを使ってスワップ領域を増やす

現状を確認したところで、試しにスワップ領域を 4GB に増やしてみよう。

まずは fallocate(1) や dd(1) を使って特定のサイズを持ったファイルを作る。

$ sudo fallocate -l 4G /swapfile
$ ls -alF /swapfile 
-rw-r--r-- 1 root root 4294967296 May 13 10:46 /swapfile

上記では /swapfile というファイルパスに 4GB のサイズでファイルを作っている。

スーパーユーザ以外に読み書きできないようにパーミッションを変更する。

$ sudo chmod 600 /swapfile

mkswap(1) でスワップ領域として使用できるように初期化する。

$ sudo mkswap /swapfile
Setting up swapspace version 1, size = 4 GiB (4294963200 bytes)
no label, UUID=b724dc3f-02ec-4b2d-b137-8072132179e7

新しく作ったスワップファイルを swapon(1) でスワップ領域として有効化する。

$ sudo swapon /swapfile

古いスワップファイルは swapoff(1) でスワップ領域として無効化しておく。

$ sudo swapoff /swap.img

これでスワップ領域が 4GB になる。

$ cat /proc/swaps 
Filename                Type        Size        Used        Priority
/swapfile                               file        4194300        0      -2

スワップ領域の設定を fstab(5) で永続化する

先ほどのやり方では変更したスワップ領域の設定が永続化されない。 そのためマシンを再起動すると状況が元に戻ってしまう。

試しに再起動してみよう。

$ sudo shutdown -r now

もう一度ログインして確認すると /swap.img が使われている。

$ cat /proc/swaps 
Filename                Type        Size        Used        Priority
/swap.img                               file        3043324        0      -2

これは fstab(5) に設定を書き込んでいなかったために生じる。

$ grep "/swap.img" /etc/fstab 
/swap.img   none    swap    sw  0  0

そこで fstab(5) を書き換えてスワップファイルとして /swapfile が使われるようにしてみよう。

$ sudo sed -i.bak s:swap.img:swapfile: /etc/fstab

次のように /swapfile を使う形になる。

$ grep "/swapfile" /etc/fstab
/swapfile   none    swap    sw  0  0

以前のファイルは /etc/fstab.bak に残る。 何かあったときはこちらから元に戻そう。

$ grep "/swap.img" /etc/fstab.bak 
/swap.img   none    swap    sw  0  0

この状態でマシンを再起動する。

$ sudo shutdown -r now

もう一度ログインして確認すると、ちゃんとスワップ領域に /swapfile が使われている。

$ cat /proc/swaps 
Filename                Type        Size        Used        Priority
/swapfile                               file        4194300        0      -2

ばっちり。

まとめ

今回はスワップファイルを使ってスワップ領域のサイズを柔軟に変更する方法について確認した。 スワップファイルであれば、たとえば後から増設した高速な SSD を使ってスワップ領域を構築するといったこともやりやすい。


textlint を使って日本語の文章を校正する

textlint 1 は自然言語向けの Linter のひとつ。 対象とする文章を静的解析して、特定のルールに抵触していないか確認できる。 今回は macOS で textlint を使い始めるまでについてメモしておく。

使った環境は次のとおり。

$ sw_vers
ProductName:        macOS
ProductVersion:     15.4.1
BuildVersion:       24E263
$ uname -srm
Darwin 24.4.0 arm64
$ node --version  
v23.11.0
$ npm --version 
10.9.2
$ npx textlint --version    
v14.7.1

もくじ

下準備

textlint は npm で配布されている。 そこで、まずは Homebrew で Node.js をインストールする。

$ brew install node

textlint をインストールする

チェックしたい文章のある場所で npm を使って textlint をインストールする。 このとき --save-dev オプションをつけると package.json ファイルが作られる。

$ npm install --save-dev textlint

package.json には依存関係が書かれている。

$ cat package.json 
{
  "devDependencies": {
    "textlint": "^14.7.1"
  }
}

また、node_modules というディレクトリに textlint と依存パッケージがインストールされる。

$ ls -1 node_modules | head
@azu
@isaacs
@keyv
@pkgjs
@textlint
@types
ajv
ansi-regex
ansi-styles
argparse

これで textlint の本体がインストールできた。

校正用のプリセットルールをインストールする

次にチェックする具体的な内容の書かれたプリセットルールをインストールする。

ここでは例として日本語の技術文章向けのプリセットの textlint-rule-preset-ja-technical-writing を入れる。 プリセットも npm でインストールできる。

$ npm install --save-dev textlint-rule-preset-ja-technical-writing

その他にも textlint-ja というコミュニティのリポジトリを見ると色々なプリセットがある。

github.com

textlint の設定ファイルを用意する

次に、先ほどインストールしたプリセットを使う textlint の設定ファイルを用意する。 設定ファイルの名前は .textlintrc で、フォーマットは JSON になっている。

cat << 'EOF' > .textlintrc
{
    "rules": {
        "preset-ja-technical-writing": true
    }
}
EOF

文章をチェックする

サンプルとなる文章を用意する。

$ cat << 'EOF' > helloworld.md
吾輩は猫である。名前はまだ無い。
EOF

npx コマンドを使って textlint を呼び出して上記のファイルをチェックする。 すると ja-technical-writing/no-mix-dearu-desumasu というルールに抵触する箇所が見つかる。

$ npx textlint helloworld.md                                      

/Users/amedama/Documents/temporary/helloworld.md
  1:5  error  本文: "ですます"調 でなければなりません
=> "ですます"調 であるべき箇所に、次の "である"調 の箇所があります: "である。"
Total:
である  : 1
ですます: 0
  ja-technical-writing/no-mix-dearu-desumasu

✖ 1 problem (1 error, 0 warnings)

なお、npx コマンドを使わないパターンとして、コマンドに PATH を通してしまうやり方もある。 インストール先の bin ディレクトリは $(npm root) 以下の .bin になる。 つまり、以下のようにすれば良い。

$ PATH=$(npm root)/.bin:$PATH textlint helloworld.md

/Users/amedama/Documents/temporary/helloworld.md
  1:5  error  本文: "ですます"調 でなければなりません
=> "ですます"調 であるべき箇所に、次の "である"調 の箇所があります: "である。"
Total:
である  : 1
ですます: 0
  ja-technical-writing/no-mix-dearu-desumasu

✖ 1 problem (1 error, 0 warnings)

いじょう。


Homebrew のパッケージの情報を調べる

今回は Homebrew のパッケージについて諸々を調べる方法について。 毎回調べている気がするのでメモしておく。

使った環境は次のとおり。

$ sw_vers
ProductName:        macOS
ProductVersion:     15.4.1
BuildVersion:       24E263
$ uname -srm  
Darwin 24.4.0 arm64
$ brew --version                            
Homebrew 4.5.2

もくじ

下準備

あらかじめ Homebrew をインストールしておく。 やり方は公式の Web サイト 1 を参照のこと。

基本的な情報を確認する

まずはパッケージの基本的な情報について知りたいときは brew info <package> を使う。 バージョンやライセンス、Webサイト、インストール用の Ruby スクリプトの場所など色々と確認できる。

$ brew info jq
==> jq: stable 1.7.1 (bottled), HEAD
Lightweight and flexible command-line JSON processor
https://jqlang.github.io/jq/
Not installed
Bottle Size: 525.0KB
Installed Size: 1.4MB
From: https://github.com/Homebrew/homebrew-core/blob/HEAD/Formula/j/jq.rb
License: MIT
==> Dependencies
Required: oniguruma ✘
==> Options
--HEAD
    Install HEAD version
==> Analytics
install: 34,169 (30 days), 103,697 (90 days), 519,279 (365 days)
install-on-request: 33,728 (30 days), 102,033 (90 days), 511,906 (365 days)
build-error: 0 (30 days)

インストール先のディレクトリを確認する

インストールされるディレクトリを確認するには brew --cellar <package> を使う。

$ brew --cellar jq
/opt/homebrew/Cellar/jq

このコマンドはインストールした後にパッケージに含まれるファイルを使って作業するときにも使うことがある。

パッケージに含まれるファイルを確認する

続いてはインストールした後にパッケージに含まれるファイルを確認する方法について。 まずは調査したいパッケージはインストールしておく。

$ brew install jq

そして brew list -v <package を実行する。

$ brew list -v jq
/opt/homebrew/Cellar/jq/1.7.1/INSTALL_RECEIPT.json
/opt/homebrew/Cellar/jq/1.7.1/bin/jq
/opt/homebrew/Cellar/jq/1.7.1/.brew/jq.rb
/opt/homebrew/Cellar/jq/1.7.1/ChangeLog
/opt/homebrew/Cellar/jq/1.7.1/AUTHORS
/opt/homebrew/Cellar/jq/1.7.1/NEWS.md
/opt/homebrew/Cellar/jq/1.7.1/include/jv.h
/opt/homebrew/Cellar/jq/1.7.1/include/jq.h
/opt/homebrew/Cellar/jq/1.7.1/sbom.spdx.json
/opt/homebrew/Cellar/jq/1.7.1/README.md
/opt/homebrew/Cellar/jq/1.7.1/COPYING
/opt/homebrew/Cellar/jq/1.7.1/lib/libjq.a
/opt/homebrew/Cellar/jq/1.7.1/lib/pkgconfig/libjq.pc
/opt/homebrew/Cellar/jq/1.7.1/lib/libjq.dylib
/opt/homebrew/Cellar/jq/1.7.1/lib/libjq.1.dylib
/opt/homebrew/Cellar/jq/1.7.1/share/man/man1/jq.1
/opt/homebrew/Cellar/jq/1.7.1/share/doc/jq/AUTHORS
/opt/homebrew/Cellar/jq/1.7.1/share/doc/jq/NEWS.md
/opt/homebrew/Cellar/jq/1.7.1/share/doc/jq/README.md
/opt/homebrew/Cellar/jq/1.7.1/share/doc/jq/COPYING

インストールに使われたスクリプトの内容を確認する

インストールしたパッケージは brew cat <package> でスクリプトの内容を確認できる。 もちろん、先ほど brew info にあったファイルを確認しても良い。

$ brew cat jq | head      
class Jq < Formula
  desc "Lightweight and flexible command-line JSON processor"
  homepage "https://jqlang.github.io/jq/"
  url "https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-1.7.1.tar.gz"
  sha256 "478c9ca129fd2e3443fe27314b455e211e0d8c60bc8ff7df703873deeee580c2"
  license "MIT"

  livecheck do
    url :stable
    regex(/^(?:jq[._-])?v?(\d+(?:\.\d+)+)$/i)

いじょう。


nftablesは同じフックで優先度が後ろのルールがあるとaccept済みのパケットが再び評価される

nftables の公式 Wiki を眺めていたところ、気になる記述があった。 どうやら、nftables は同じフックポイントで、優先度が異なるチェーンがあるときに注意を要する振る舞いを示すようだ。

nftables の公式 Wiki の記述 1 を以下に引用する。

NOTE: If a packet is accepted and there is another chain, bearing the same hook type and with a later priority, then the packet will subsequently traverse this other chain. Hence, an accept verdict - be it by way of a rule or the default chain policy - isn't necessarily final. However, the same is not true of packets that are subjected to a drop verdict. Instead, drops take immediate effect, with no further rules or chains being evaluated.

以下に拙訳する。

注意: もしパケットが accept されても、他に同じフックタイプでより後ろの優先度のチェーンがあると、パケットはその別のチェーンを通過します。したがって、accept 判定はルールによるものであっても、デフォルトのチェーンポリシーであっても、それは必ずしも最終的なものではありません。ただし、drop 判定のパケットは同じことが当てはまりません。代わりに drop は即座に影響し、さらなるルールやチェインでは評価されません。

上記を見ると、一旦 accept されたパケットが異なるチェーンで再び評価されるらしい。 ただし、drop されたパケットについては再び評価されることはないようだ。 今回は、この振る舞いについて実際に動かして検証してみる。

使った環境は次のとおり。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 24.04.2 LTS
Release:    24.04
Codename:   noble
$ uname -srm
Linux 6.8.0-58-generic x86_64
$ nft --version
nftables v1.0.9 (Old Doc Yak #3)

もくじ

下準備

あらかじめ、必要なパッケージをインストールしておく。

$ sudo apt-get -y install nftables iproute2 iputils-ping

実験用の Network Namespace を用意する

ホストを直接使って nftables の実験をすると不都合が多い。 そこで、Network Namespace を使って隔離されたネットワークスタックを用意する。 今回は 2 つの Network Namespace を用意して、それぞれを veth でつなぐ。

まずは Network Namespace を用意する。

$ sudo ip netns add ns1
$ sudo ip netns add ns2

両者をつなぐための veth を作る。

$ sudo ip link add ns1-veth0 type veth peer name ns2-veth0

veth の両端を Network Namespace に所属させる。

$ sudo ip link set ns1-veth0 netns ns1
$ sudo ip link set ns2-veth0 netns ns2

veth デバイスの MAC アドレスをドキュメンテーションアドレスに変更しておく。

$ sudo ip netns exec ns1 ip link set dev ns1-veth0 address 00:00:5E:00:53:01
$ sudo ip netns exec ns2 ip link set dev ns2-veth0 address 00:00:5E:00:53:02

インターフェイスの状態を UP にする。

$ sudo ip netns exec ns1 ip link set ns1-veth0 up
$ sudo ip netns exec ns2 ip link set ns2-veth0 up

インターフェイスにドキュメンテーションアドレスの IP アドレスを付与する。

$ sudo ip netns exec ns1 ip address add 192.0.2.1/24 dev ns1-veth0
$ sudo ip netns exec ns2 ip address add 192.0.2.2/24 dev ns2-veth0

この状態で、一旦 ping による疎通があるかを確認しておく。

$ sudo ip netns exec ns1 ping -c 3 192.0.2.2 -I 192.0.2.1
PING 192.0.2.2 (192.0.2.2) from 192.0.2.1 : 56(84) bytes of data.
64 bytes from 192.0.2.2: icmp_seq=1 ttl=64 time=0.034 ms
64 bytes from 192.0.2.2: icmp_seq=2 ttl=64 time=0.017 ms
64 bytes from 192.0.2.2: icmp_seq=3 ttl=64 time=0.030 ms

--- 192.0.2.2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2074ms
rtt min/avg/max/mdev = 0.017/0.027/0.034/0.007 ms

以降は Network Namespace の ns1 に nftables の設定を投入して実験していく。

同じフックで優先度が異なるルール (accept -> drop) を用意する

以下のコマンドでは Network Namespace の ns1 に nftables の設定を入れている。 まず、prerouting チェーンでは、prerouting フックでデバッグ用に ICMPv4 のパケットにトレース用のフラグを付与している。 これにより nft monitor trace でパケットを追跡できる。 input_pri0 チェーンでは、input フックの priority 0 で ICMPv4 の Echo-Request を accept している。 そして input_pri1 チェーンでは、同じ input フックの priority 1 で ICMPv4 の Echo-Request を drop している。 優先度では input_pri0 の方が input_pri1 よりも前になる。 つまり、ドキュメントの記述通りであれば input_pri0 で accept されたパケットは input_pri1 で再び評価されて drop されるはず。

$ cat << 'EOF' | sudo ip netns exec ns1 nft -f -
#!/usr/sbin/nft -f

flush ruleset

table inet filter {

    chain prerouting {
        type filter hook prerouting priority 0; policy accept
        # すべての ICMPv4 にトレース機能を有効にするメタ情報を付与する
        ip protocol icmp meta nftrace set 1
    }

   chain input_pri0 {
       # hook が input で priority 0   
       type filter hook input priority 0; policy accept;
       # ICMP Echo-Request を通す
       ip protocol icmp icmp type echo-request accept
   }

   chain input_pri1 {
       # hook が input で priority 1
       type filter hook input priority 1; policy accept;
       # ICMP Echo-Request を落とす
       ip protocol icmp icmp type echo-request drop
   }
}
EOF

設定が入ったことを確認する。

$ sudo ip netns exec ns1 nft -y list ruleset
table inet filter {
        chain prerouting {
                type filter hook prerouting priority 0; policy accept;
                ip protocol icmp meta nftrace set 1
        }

        chain input_pri0 {
                type filter hook input priority 0; policy accept;
                ip protocol icmp icmp type echo-request accept
        }

        chain input_pri1 {
                type filter hook input priority 1; policy accept;
                ip protocol icmp icmp type echo-request drop
        }
}

次に、パケットを追跡するために nft monitor trace コマンドを実行する。

$ sudo ip netns exec ns1 nft monitor trace

そして、ns2 から ns1 に向けて ping を打つ。

$ sudo ip netns exec ns2 ping -c 1 192.0.2.1
PING 192.0.2.1 (192.0.2.1) 56(84) bytes of data.

--- 192.0.2.1 ping statistics ---
1 packets transmitted, 0 received, 100% packet loss, time 0ms

すると、先ほど実行した nft monitor trace に出力が得られる。

$ sudo ip netns exec ns1 nft monitor trace
trace id 7a7d7959 inet filter prerouting packet: iif "ns1-veth0" ether saddr 00:00:5e:00:53:02 ether daddr 00:00:5e:00:53:01 ip saddr 192.0.2.2 ip daddr 192.0.2.1 ip dscp cs0 ip ecn not-ect ip ttl 64 ip id 398 ip protocol icmp ip length 84 icmp type echo-request icmp code net-unreachable icmp id 2566 icmp sequence 1 @th,64,96 0xd893146800000000c2f80200
trace id 7a7d7959 inet filter prerouting rule ip protocol icmp meta nftrace set 1 (verdict continue)
trace id 7a7d7959 inet filter prerouting policy accept
trace id 7a7d7959 inet filter input_pri0 packet: iif "ns1-veth0" ether saddr 00:00:5e:00:53:02 ether daddr 00:00:5e:00:53:01 ip saddr 192.0.2.2 ip daddr 192.0.2.1 ip dscp cs0 ip ecn not-ect ip ttl 64 ip id 398 ip protocol icmp ip length 84 icmp type echo-request icmp code net-unreachable icmp id 2566 icmp sequence 1 @th,64,96 0xd893146800000000c2f80200
trace id 7a7d7959 inet filter input_pri0 rule ip protocol icmp icmp type echo-request accept (verdict accept)
trace id 7a7d7959 inet filter input_pri1 packet: iif "ns1-veth0" ether saddr 00:00:5e:00:53:02 ether daddr 00:00:5e:00:53:01 ip saddr 192.0.2.2 ip daddr 192.0.2.1 ip dscp cs0 ip ecn not-ect ip ttl 64 ip id 398 ip protocol icmp ip length 84 icmp type echo-request icmp code net-unreachable icmp id 2566 icmp sequence 1 @th,64,96 0xd893146800000000c2f80200
trace id 7a7d7959 inet filter input_pri1 rule ip protocol icmp icmp type echo-request drop (verdict drop)

上記で、以下はトレース機能のメタ情報を付与している様子 (meta nftrace set 1) を表している。

trace id 7a7d7959 inet filter prerouting packet: iif "ns1-veth0" ether saddr 00:00:5e:00:53:02 ether daddr 00:00:5e:00:53:01 ip saddr 192.0.2.2 ip daddr 192.0.2.1 ip dscp cs0 ip ecn not-ect ip ttl 64 ip id 398 ip protocol icmp ip length 84 icmp type echo-request icmp code net-unreachable icmp id 2566 icmp sequence 1 @th,64,96 0xd893146800000000c2f80200
trace id 7a7d7959 inet filter prerouting rule ip protocol icmp meta nftrace set 1 (verdict continue)
trace id 7a7d7959 inet filter prerouting policy accept

そして、以下で input_pri0 でパケットが accept されている。

trace id 7a7d7959 inet filter input_pri0 packet: iif "ns1-veth0" ether saddr 00:00:5e:00:53:02 ether daddr 00:00:5e:00:53:01 ip saddr 192.0.2.2 ip daddr 192.0.2.1 ip dscp cs0 ip ecn not-ect ip ttl 64 ip id 398 ip protocol icmp ip length 84 icmp type echo-request icmp code net-unreachable icmp id 2566 icmp sequence 1 @th,64,96 0xd893146800000000c2f80200
trace id 7a7d7959 inet filter input_pri0 rule ip protocol icmp icmp type echo-request accept (verdict accept)

しかし、同じ input フックで、より優先度が後ろの input_pri1 があるためパケットが再び評価される。 以下では input_pri1 で ICMPv4 の Echo Request が drop されている。

trace id 7a7d7959 inet filter input_pri1 packet: iif "ns1-veth0" ether saddr 00:00:5e:00:53:02 ether daddr 00:00:5e:00:53:01 ip saddr 192.0.2.2 ip daddr 192.0.2.1 ip dscp cs0 ip ecn not-ect ip ttl 64 ip id 398 ip protocol icmp ip length 84 icmp type echo-request icmp code net-unreachable icmp id 2566 icmp sequence 1 @th,64,96 0xd893146800000000c2f80200
trace id 7a7d7959 inet filter input_pri1 rule ip protocol icmp icmp type echo-request drop (verdict drop)

ドキュメントにある通りの振る舞いを示すことが上記から確認できた。

同じフックで優先度が異なるルール (drop -> accept) を用意する

念の為、逆のパターンも確認しておこう。 同じフックで、より優先度が前のチェーンで drop されると、後ろのチェーンでは評価されないはず。

以下のように、先ほどと accept と drop するチェーンを入れ替えた設定を投入する。 今回は input_pri0 で drop して、input_pri1 で accept している。

$ cat << 'EOF' | sudo ip netns exec ns1 nft -f -
#!/usr/sbin/nft -f

flush ruleset

table inet filter {

    chain prerouting {
        type filter hook prerouting priority 0; policy accept
        # すべての ICMPv4 にトレース機能を有効にするメタ情報を付与する
        ip protocol icmp meta nftrace set 1
    }

   chain input_pri0 {
       # hook が input で priority 0   
       type filter hook input priority 0; policy accept;
       # ICMP Echo-Request を落とす
       ip protocol icmp icmp type echo-request drop
   }

   chain input_pri1 {
       # hook が input で priority 1
       type filter hook input priority 1; policy accept;
       # ICMP Echo-Request を通す
       ip protocol icmp icmp type echo-request accept
   }
}
EOF

投入した設定を確認する。

$ sudo ip netns exec ns1 nft -y list ruleset
table inet filter {
        chain prerouting {
                type filter hook prerouting priority 0; policy accept;
                ip protocol icmp meta nftrace set 1
        }

        chain input_pri0 {
                type filter hook input priority 0; policy accept;
                ip protocol icmp icmp type echo-request drop
        }

        chain input_pri1 {
                type filter hook input priority 1; policy accept;
                ip protocol icmp icmp type echo-request accept
        }
}

再び nft monitor torace コマンドを実行しておく。

$ sudo ip netns exec ns1 nft monitor trace

ns2 から ns1 に向けて ping を打つ。

$ sudo ip netns exec ns2 ping -c 1 192.0.2.1
PING 192.0.2.1 (192.0.2.1) 56(84) bytes of data.

--- 192.0.2.1 ping statistics ---
1 packets transmitted, 0 received, 100% packet loss, time 0ms

nft monitor trace に次のような出力が得られる。

$ sudo ip netns exec ns1 nft monitor trace
trace id ea548659 inet filter prerouting packet: iif "ns1-veth0" ether saddr 00:00:5e:00:53:02 ether daddr 00:00:5e:00:53:01 ip saddr 192.0.2.2 ip daddr 192.0.2.1 ip dscp cs0 ip ecn not-ect ip ttl 64 ip id 52494 ip protocol icmp ip length 84 icmp type echo-request icmp code net-unreachable icmp id 10208 icmp sequence 1 @th,64,96 0x9fb71968000000002fb80000
trace id ea548659 inet filter prerouting rule ip protocol icmp meta nftrace set 1 (verdict continue)
trace id ea548659 inet filter prerouting policy accept
trace id ea548659 inet filter input_pri0 packet: iif "ns1-veth0" ether saddr 00:00:5e:00:53:02 ether daddr 00:00:5e:00:53:01 ip saddr 192.0.2.2 ip daddr 192.0.2.1 ip dscp cs0 ip ecn not-ect ip ttl 64 ip id 52494 ip protocol icmp ip length 84 icmp type echo-request icmp code net-unreachable icmp id 10208 icmp sequence 1 @th,64,96 0x9fb71968000000002fb80000
trace id ea548659 inet filter input_pri0 rule ip protocol icmp icmp type echo-request drop (verdict drop)

以下では先ほどと同じようにトレース機能のメタ情報をパケットに付与している。

trace id ea548659 inet filter prerouting packet: iif "ns1-veth0" ether saddr 00:00:5e:00:53:02 ether daddr 00:00:5e:00:53:01 ip saddr 192.0.2.2 ip daddr 192.0.2.1 ip dscp cs0 ip ecn not-ect ip ttl 64 ip id 52494 ip protocol icmp ip length 84 icmp type echo-request icmp code net-unreachable icmp id 10208 icmp sequence 1 @th,64,96 0x9fb71968000000002fb80000
trace id ea548659 inet filter prerouting rule ip protocol icmp meta nftrace set 1 (verdict continue)
trace id ea548659 inet filter prerouting policy accept

次に以下では input_pri0 でパケットが drop されている。

trace id ea548659 inet filter input_pri0 packet: iif "ns1-veth0" ether saddr 00:00:5e:00:53:02 ether daddr 00:00:5e:00:53:01 ip saddr 192.0.2.2 ip daddr 192.0.2.1 ip dscp cs0 ip ecn not-ect ip ttl 64 ip id 52494 ip protocol icmp ip length 84 icmp type echo-request icmp code net-unreachable icmp id 10208 icmp sequence 1 @th,64,96 0x9fb71968000000002fb80000
trace id ea548659 inet filter input_pri0 rule ip protocol icmp icmp type echo-request drop (verdict drop)

そして、以降はトレース情報の出力がない。 したがって、drop された後は別のチェーンで処理されていない。 ドキュメント通りの振る舞いが確認できた。

いじょう。


systemd で nftables の設定を永続化する

今回は nftables のスクリプトを systemd から読み込むことで設定を永続化する方法について。 結論から述べると systemctl cat nftables で読み込んでいるファイルの場所を確認したら、そこにルールを書けば良い。

使った環境は次のとおり。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 24.04.2 LTS
Release:    24.04
Codename:   noble
$ uname -srm
Linux 6.8.0-59-generic x86_64
$ nft --version
nftables v1.0.9 (Old Doc Yak #3)

もくじ

下準備

まずは nftables をインストールする。 一般的な環境であれば最初から入っているはず。

$ sudo apt-get -y install nftables

UFW (Uncomplicated Firewall) は、nftables と同時に利用すると競合しやすい。 そのため、もし使っている場合には無効にする。

$ sudo systemctl stop ufw
$ sudo systemctl disable ufw

そして、systemd で nftables のサービスを動かす。

$ sudo systemctl start nftables
$ sudo systemctl enable nftables

設定ファイルの場所を確認する

まずは nftables のサービスが、どこの設定ファイルを読むのか確認する。 systemctl cat でユニットファイルの内容を見るのが手っ取り早い。

$ systemctl cat nftables | grep -i ^exec
ExecStart=/usr/sbin/nft -f /etc/nftables.conf
ExecReload=/usr/sbin/nft -f /etc/nftables.conf
ExecStop=/usr/sbin/nft flush ruleset

上記から /etc/nftables.conf を読んでいることが確認できる。

デフォルトの設定を確認する

先ほど確認した設定ファイルの内容を見てみよう。 すると input, forward, output hook に base チェインが設定されている。 単なる入れ物が用意されているだけで、すべての通信が accept される状態になっている。

$ cat /etc/nftables.conf 
#!/usr/sbin/nft -f

flush ruleset

table inet filter {
    chain input {
        type filter hook input priority filter;
    }
    chain forward {
        type filter hook forward priority filter;
    }
    chain output {
        type filter hook output priority filter;
    }
}

上記の設定ファイルの内容がシステムに反映されているか確認してみよう。

nft list ruleset コマンドを実行すると、先ほどの設定ファイルと同じ内容が確認できる。

$ sudo nft list ruleset
table inet filter {
    chain input {
        type filter hook input priority filter; policy accept;
    }

    chain forward {
        type filter hook forward priority filter; policy accept;
    }

    chain output {
        type filter hook output priority filter; policy accept;
    }
}

これは、先ほど systemd の nftables サービスを開始したため設定が読み込まれている。

nftables の設定ファイルを編集してみる

続いては systemd のサービスが読んでいる nftables の設定ファイルを編集してみよう。

以下では /etc/nftables にディレクトリを作って、そこに nftables のスクリプトを用意している。 内容は基本的な設定を入れたファイアウォールになっている。

$ sudo mkdir -p /etc/nftables
$ cat << 'EOF' | sudo tee /etc/nftables/simple-firewall.nft
#!/usr/sbin/nft -f

flush ruleset

table inet filter {
   chain input {
      type filter hook input priority 0; policy drop;
      # 関連・確立済みのコネクションは通す
      ct state established,related accept
      # 不正なコネクションは落とす
      ct state invalid drop
      # ループバックは通す
      iif lo accept
      # ICMPv4 の特定タイプは通す
      ip protocol icmp icmp type { destination-unreachable, router-advertisement, time-exceeded, parameter-problem } accept
      # ICMPv6 の特定タイプは通す
      ip6 nexthdr icmpv6 icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert } accept
      # SSH (TCP/22) はレートリミットつきで通す
      tcp dport ssh limit rate 10/minute accept
   }

   chain forward {
      type filter hook forward priority 0; policy drop;
   }

   chain output {
      type filter hook output priority 0; policy accept;
   }

}
EOF

上記の設定を /etc/nftables.conf から include する。

$ cat << 'EOF' | sudo tee /etc/nftables.conf >/dev/null
#!/usr/sbin/nft -f

flush ruleset

include "/etc/nftables/simple-firewall.nft"
EOF

この状態で systemd の nftables サービスをリロードする。

$ sudo systemctl reload nftables

すると、先ほどのファイルに書いた内容が動作に反映される。

$ sudo nft list ruleset
table inet filter {
    chain input {
        type filter hook input priority filter; policy drop;
        ct state established,related accept
        ct state invalid drop
        iif "lo" accept
        ip protocol icmp icmp type { destination-unreachable, router-advertisement, time-exceeded, parameter-problem } accept
        ip6 nexthdr ipv6-icmp icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert } accept
        tcp dport 22 limit rate 10/minute burst 5 packets accept
    }

    chain forward {
        type filter hook forward priority filter; policy drop;
    }

    chain output {
        type filter hook output priority filter; policy accept;
    }
}

再起動して設定が永続化されていることを確認する

念の為、システムを再起動しても設定が反映され直すことを確認しておこう。

$ sudo shutdown -r now

再起動が終わったら、もう一度ログインして nftables の設定を確認する。

$ sudo nft list ruleset
table inet filter {
    chain input {
        type filter hook input priority filter; policy drop;
        ct state established,related accept
        ct state invalid drop
        iif "lo" accept
        ip protocol icmp icmp type { destination-unreachable, router-advertisement, time-exceeded, parameter-problem } accept
        ip6 nexthdr ipv6-icmp icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert } accept
        tcp dport 22 limit rate 10/minute burst 5 packets accept
    }

    chain forward {
        type filter hook forward priority filter; policy drop;
    }

    chain output {
        type filter hook output priority filter; policy accept;
    }
}

ちゃんと、先ほどと同じ設定が読み込まれていることが確認できる。

めでたしめでたし。

nftables の処理をトレース機能 (meta nftrace) で追跡する

nftables は、Linux の Netfilter サブシステムをバックエンドに実装されたフレームワークのひとつ。 nftables を使うことで、パケットフィルタリングや NAT、パケット分類などを統一的に管理できる。 nftables は、xtables (iptables, ip6tables など) を置き換える後継として開発された。

今回は、その nftables が管理しているルールがどのように動いているかをトレース機能 (meta nftrace) を使って調べる方法について。 nftables のルールをデバッグする際、素朴なやり方ではログやカウンタを用いるやり方がある。 それに比べてトレース機能を使うと、より詳細は情報が得られる。

使った環境は次のとおり。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 24.04.2 LTS
Release:    24.04
Codename:   noble
$ uname -srm
Linux 6.8.0-58-generic x86_64
$ nft --version
nftables v1.0.9 (Old Doc Yak #3)

もくじ

下準備

あらかじめ、必要なパッケージをインストールしておく。

$ sudo apt-get -y install nftables iproute2 iputils-ping

実験用の Network Namespace を用意する

ホストを直接使って nftables の実験をすると不都合が多い。 そこで、Network Namespace を使って隔離されたネットワークスタックを用意する。 今回は 2 つの Network Namespace を用意して、それぞれを veth でつなぐ。

まずは Network Namespace を用意する。

$ sudo ip netns add ns1
$ sudo ip netns add ns2

両者をつなぐための veth を作る。

$ sudo ip link add ns1-veth0 type veth peer name ns2-veth0

veth の両端を Network Namespace に所属させる。

$ sudo ip link set ns1-veth0 netns ns1
$ sudo ip link set ns2-veth0 netns ns2

veth デバイスの MAC アドレスをドキュメンテーションアドレスに変更しておく。

$ sudo ip netns exec ns1 ip link set dev ns1-veth0 address 00:00:5E:00:53:01
$ sudo ip netns exec ns2 ip link set dev ns2-veth0 address 00:00:5E:00:53:02

インターフェイスの状態を UP にする。

$ sudo ip netns exec ns1 ip link set ns1-veth0 up
$ sudo ip netns exec ns2 ip link set ns2-veth0 up

インターフェイスにドキュメンテーションアドレスの IP アドレスを付与する。

$ sudo ip netns exec ns1 ip address add 192.0.2.1/24 dev ns1-veth0
$ sudo ip netns exec ns2 ip address add 192.0.2.2/24 dev ns2-veth0

この状態で、一旦 ping による疎通があるかを確認しておく。

$ sudo ip netns exec ns1 ping -c 3 192.0.2.2 -I 192.0.2.1
PING 192.0.2.2 (192.0.2.2) from 192.0.2.1 : 56(84) bytes of data.
64 bytes from 192.0.2.2: icmp_seq=1 ttl=64 time=0.034 ms
64 bytes from 192.0.2.2: icmp_seq=2 ttl=64 time=0.017 ms
64 bytes from 192.0.2.2: icmp_seq=3 ttl=64 time=0.030 ms

--- 192.0.2.2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2074ms
rtt min/avg/max/mdev = 0.017/0.027/0.034/0.007 ms

以降は Network Namespace の ns1 に nftables の設定を投入して実験していく。

トレース対象のルールを追加する

まず、初期状態では nftables に何も設定が入っていない。 nftables に設定されているルールは nft list ruleset コマンドで確認できる。

$ sudo ip netns exec ns1 nft list ruleset

ここに nft -f で設定を投入する。 引数にハイフンを指定することで、標準入力から設定を読み込むことができる。

以下の設定では、inet アドレスファミリの filter テーブルの中に、input チェインがある。 input チェインは type filter hook input なので、Netfilter の input hook 経由でパケットが入ってくる。 そして、input チェインには ip protocol icmp icmp type echo-request というルールが含まれる。 これは ICMPv4 の Echo Request、つまりは Ping の往路と一致するルールになっている。

$ cat << 'EOF' | sudo ip netns exec ns1 nft -f -
table inet filter {
    chain input {
        type filter hook input priority 0; policy accept
        ip protocol icmp icmp type echo-request
    }
}
EOF

上記を実行すると、Network Namespace の ns1 に nftables の設定が入る。 なお、このルールは一致したとしても何もしない。 また、チェインのデフォルトのポリシーが accept なので、パケットはそのまま通過する。

$ sudo ip netns exec ns1 nft list ruleset
table inet filter {
    chain input {
        type filter hook input priority filter; policy accept;
        ip protocol icmp icmp type echo-request
    }
}

上記のルールによってパケットが処理される様子をトレース機能で追跡したい。

トレース用のルールを追加する

続いては、トレース機能を使うためのルールを追加する。 nftables のトレース機能 (meta nftrace) を使うには、パケットにトレースを有効にするメタ情報のフラグをつける必要がある。 トレース機能のメタ情報のフラグが有効になったパケットは、以降の処理でモニター用の機能を使って追跡できるようになる。 そこで、メタ情報のフラグを付与するためのルールが必要になる。

今回は、input hook よりも早く処理される prerouting hook に、トレース用のルールを追加しよう。 以下の設定では、inet アドレスファミリの filter テーブルの中に、prerouting チェインを作っている。 prerouting チェインは type filter hook prerouting なので、Netfilter の prerouting hook 経由でパケットが入ってくる。 そして、prerouting チェインには ip protocol icmp meta nftrace set 1 というルールが含まれる。 これは ICMPv4 のパケットにトレース機能のフラグを付与している。

$ cat << 'EOF' | sudo ip netns exec ns1 nft -f -
table inet filter {
    chain prerouting {
        type filter hook prerouting priority 0; policy accept
        ip protocol icmp meta nftrace set 1
    }
}
EOF

設定を投入すると、ルールセットは次のようになる。

$ sudo ip netns exec ns1 nft list ruleset
table inet filter {
    chain input {
        type filter hook input priority filter; policy accept;
        ip protocol icmp icmp type echo-request
    }

    chain prerouting {
        type filter hook prerouting priority filter; policy accept;
        ip protocol icmp meta nftrace set 1
    }
}

処理の流れをモニターする

この状態で nft monitor trace コマンドを実行しよう。 実行すると出力を待ち受けた状態になる。

$ sudo ip netns exec ns1 nft monitor trace

ここで、別のターミナルを開いて ns2 から ns1 に向けて Ping を打ってみよう。

$ sudo ip netns exec ns2 ping -c 1 192.0.2.1
PING 192.0.2.1 (192.0.2.1) 56(84) bytes of data.
64 bytes from 192.0.2.1: icmp_seq=1 ttl=64 time=0.051 ms

--- 192.0.2.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.051/0.051/0.051/0.000 ms

すると、先ほど実行した nft monitor trace コマンドに出力が得られる。

$ sudo ip netns exec ns1 nft monitor trace
trace id 2d55095a inet filter prerouting packet: iif "ns1-veth0" ether saddr 00:00:5e:00:53:02 ether daddr 00:00:5e:00:53:01 ip saddr 192.0.2.2 ip daddr 192.0.2.1 ip dscp cs0 ip ecn not-ect ip ttl 64 ip id 23062 ip protocol icmp ip length 84 icmp type echo-request icmp code net-unreachable icmp id 3186 icmp sequence 1 @th,64,96 0x455136800000000f21b0100 
trace id 2d55095a inet filter prerouting rule ip protocol icmp meta nftrace set 1 (verdict continue)
trace id 2d55095a inet filter prerouting policy accept 
trace id 2d55095a inet filter input packet: iif "ns1-veth0" ether saddr 00:00:5e:00:53:02 ether daddr 00:00:5e:00:53:01 ip saddr 192.0.2.2 ip daddr 192.0.2.1 ip dscp cs0 ip ecn not-ect ip ttl 64 ip id 23062 ip protocol icmp ip length 84 icmp type echo-request icmp code net-unreachable icmp id 3186 icmp sequence 1 @th,64,96 0x455136800000000f21b0100 
trace id 2d55095a inet filter input rule ip protocol icmp icmp type echo-request (verdict continue)
trace id 2d55095a inet filter input policy accept 

上記から、以下の部分で ICMP の Echo Request のパケットに prerouting のルールでメタ情報が付与され、そのまま通過してチェインのデフォルトのポリシーで accept された様子が確認できる。

trace id 2d55095a inet filter prerouting packet: iif "ns1-veth0" ether saddr 00:00:5e:00:53:02 ether daddr 00:00:5e:00:53:01 ip saddr 192.0.2.2 ip daddr 192.0.2.1 ip dscp cs0 ip ecn not-ect ip ttl 64 ip id 23062 ip protocol icmp ip length 84 icmp type echo-request icmp code net-unreachable icmp id 3186 icmp sequence 1 @th,64,96 0x455136800000000f21b0100 
trace id 2d55095a inet filter prerouting rule ip protocol icmp meta nftrace set 1 (verdict continue)
trace id 2d55095a inet filter prerouting policy accept 

同様に、以下の部分では input のルールに一致した後に、そのまま通過してチェインのデフォルトのポリシーで accept されたことが確認できる。

trace id 2d55095a inet filter input packet: iif "ns1-veth0" ether saddr 00:00:5e:00:53:02 ether daddr 00:00:5e:00:53:01 ip saddr 192.0.2.2 ip daddr 192.0.2.1 ip dscp cs0 ip ecn not-ect ip ttl 64 ip id 23062 ip protocol icmp ip length 84 icmp type echo-request icmp code net-unreachable icmp id 3186 icmp sequence 1 @th,64,96 0x455136800000000f21b0100 
trace id 2d55095a inet filter input rule ip protocol icmp icmp type echo-request (verdict continue)
trace id 2d55095a inet filter input policy accept 

このように、ルールでトレース機能を有効化して、それをモニターすることで nftables の処理の流れを把握できる。

まとめ

今回は nftables で処理の流れを追跡してルールのデバッグに活かすことのできるトレース機能 (meta nftrace) について扱った。

今回の例では、メタ情報を付与するための専用のチェインとルールを追加していた。 しかし、必要な区間でピンポイントにメタ情報を付与して、必要なくなったらメタ情報を取り除くといったことも考えられる。

また、nft monitor trace の出力は場合によっては大量になることから grep(1) などを用いて必要な内容だけに絞り込むのも良いようだ。

参考

wiki.nftables.org


Python: パッケージ・プロジェクトマネージャの uv を使ってみる

今回は Rust で書かれた Python のパッケージ・プロジェクトマネージャの uv を使ってみる。

これまで Python では複数のツールを組み合わせて開発のワークフローを構築するのが一般的だった。 そのような構成では、それぞれのツールは目的に特化しているので、単独ではシンプルに扱える。 一方で、目的ごとにツールをどのように組み合わせるかユーザ自身で考えなければいけない点は敷居が高かった。 そのような状況で、uv は以下のツール群を単独で置き換えることができるとしている。

  • pip
  • pip-tools
  • pipx
  • poetry
  • pyenv
  • twine
  • virtualenv

上記のツールを見ると、要するに Python の実行環境、仮想環境、パッケージ、プロジェクトの管理をひとつのツールで代替できそうなことがうかがえる。

使った環境は次のとおり。

$ sw_vers
ProductName:        macOS
ProductVersion:     15.4.1
BuildVersion:       24E263
$ uv -V
uv 0.6.14 (Homebrew 2025-04-09)

もくじ

下準備

uv をインストールする方法はいくつかある。 インストーラのシェルスクリプトを実行したり、pip でインストールしたり。 今回は macOS なので Homebrew を使うのが一番楽だろう。

$ brew install uv

インストールすると uv コマンドが使えるようになる。

$ uv -V
uv 0.6.14 (Homebrew 2025-04-09)

初期設定

使い始める前にやっておくと良さそうな設定について。

uv generate-shell-completion コマンドを使うと、シェルごとの補完 (Completion) 用の設定が得られる。

$ uv generate-shell-completion <shell>

たとえば zsh を使っているのであれば、設定ファイル (~/.zshrc など) に次のようなコードを追加しておけば良い。 uv コマンドが使える環境で補完用の設定が入るようになる。

if which uv >/dev/null 2>&1; then
  eval "$(uv generate-shell-completion zsh)"
fi

Scripts 機能

uv はいくつかの機能を提供している。 一番シンプルな機能として、まずは Scripts という機能を紹介する。 この機能を使うと、Python のスクリプトに実行環境や依存関係に関するメタデータを付与できる。 メタデータを付与したスクリプトは uv を使ってメタデータを解決した形で実行できる。

たとえば、次のようにサードパーティー製のパッケージである requests を使うスクリプトを用意する。

$ cat << 'EOF' > fetch.py
import requests


def main():
    response = requests.get("https://example.org")
    print(f"status code: {response.status_code}")


if __name__ == "__main__":
    main()
EOF

この状態でシステムの Python を使って実行してみる。 このとき、requests がインストールされていない環境であればエラーになる。

$ python3 fetch.py 
Traceback (most recent call last):
  File "/Users/amedama/Documents/temporary/uv/fetch.py", line 1, in <module>
    import requests
ModuleNotFoundError: No module named 'requests'

スクリプトに依存パッケージのメタデータを登録する

この状態で、uv add サブコマンドを使う。 このサブコマンドは uv が管理するプロジェクトやスクリプトに依存関係のパッケージを登録するのに使う。 ここでは、先ほど用意したスクリプトの依存パッケージに requests を登録する。 実行する際にはオプションとして --script をつけて、スクリプトと依存パッケージを続ける。

$ uv add --script fetch.py requests
Updated `fetch.py`

すると、スクリプトの先頭部分にコメントでメタデータが付与される。

$ head fetch.py                    
# /// script
# requires-python = ">=3.12"
# dependencies = [
#     "requests",
# ]
# ///
import requests


def main():

登録されたメタデータを解決してスクリプトを実行する

この状態で uv run サブコマンドを使ってスクリプトを実行する。 このとき、オプションとして --script をつける。

$ uv run --script fetch.py
Installed 5 packages in 5ms
status code: 200

すると、メタデータを読み取って依存パッケージをインストールした仮想環境上でスクリプトが実行される。

内部的にどんなことをやっているかは -v オプションをつけて実行すると分かりやすい。

$ uv run -v --script fetch.py
DEBUG uv 0.6.14 (Homebrew 2025-04-09)
DEBUG Reading inline script metadata from `fetch.py`
DEBUG Acquired lock for `/Users/amedama/Documents/temporary/uv/fetch.py`
DEBUG Using Python request Python >=3.12 from `requires-python` metadata
DEBUG Checking for Python environment at `/Users/amedama/.cache/uv/environments-v2/fetch-e04e193531c37c15`
DEBUG The script environment's Python version satisfies `>=3.12`
DEBUG Released lock at `/var/folders/p2/g5ntz9vn46bbdvrl0pfkfpnm0000gn/T/uv-e04e193531c37c15.lock`
DEBUG All requirements satisfied: certifi>=2017.4.17 | charset-normalizer>=2, <4 | idna>=2.5, <4 | requests | urllib3>=1.21.1, <3
DEBUG Using Python 3.12.10 interpreter at: /Users/amedama/.cache/uv/environments-v2/fetch-e04e193531c37c15/bin/python3
DEBUG Running `python fetch.py`
DEBUG Spawned child 75859 in process group 75858
status code: 200
DEBUG Command exited with code: 0

上記から、以下の仮想環境上で実行されていることが確認できる。

$ /Users/amedama/.cache/uv/environments-v2/fetch-e04e193531c37c15/bin/python3 -V
Python 3.12.10

スクリプトに登録されている依存パッケージを確認する

スクリプトに登録されている依存関係は uv tree サブコマンドで確認できる。

$ uv tree --script fetch.py 
Resolved 5 packages in 2ms
requests v2.32.3
├── certifi v2025.1.31
├── charset-normalizer v3.4.1
├── idna v3.10
└── urllib3 v2.4.0

スクリプトに登録されている依存パッケージを削除する

なお、依存パッケージを削除したいときは uv remove サブコマンドを使う。

$ uv remove --script fetch.py requests
Updated `fetch.py`

Python versions 機能

続いては Python versions という機能について。 これは pyenv をイメージすると分かりやすい。 要するに異なるバージョンや実装の Python をインストールする機能になる。

インストール済みの Python 実行環境を確認する

まず、現在インストールされている Python は uv python list サブコマンドで確認できる。 --only-installed オプションをつけることでインストール済みのバージョンだけに絞られる。 以下を見て分かる通り、uv 以外でインストールした Python も PATH が通っていれば検出されるようだ。

$ uv python list 
cpython-3.12.10-macos-aarch64-none    /opt/homebrew/bin/python3.12 -> ../Cellar/python@3.12/3.12.10/bin/python3.12
cpython-3.9.6-macos-aarch64-none      /usr/bin/python3

--only-installed をつけずに実行すると、主要なマイナーバージョンについて最新パッチバージョンの結果が得られる。 また、--all-versions をつけると過去のすべてのバージョンを含んだ結果が得られる。

$ uv python list --all-versions | head
cpython-3.14.0a6-macos-aarch64-none                 <download available>
cpython-3.14.0a6+freethreaded-macos-aarch64-none    <download available>
cpython-3.13.3-macos-aarch64-none                   <download available>
cpython-3.13.3+freethreaded-macos-aarch64-none      <download available>
cpython-3.13.2-macos-aarch64-none                   <download available>
cpython-3.13.2+freethreaded-macos-aarch64-none      <download available>
cpython-3.13.1-macos-aarch64-none                   <download available>
cpython-3.13.1+freethreaded-macos-aarch64-none      <download available>
cpython-3.13.0-macos-aarch64-none                   <download available>
cpython-3.13.0+freethreaded-macos-aarch64-none      <download available>

Python の実行環境をインストールする

特定のバージョンをインストールしたいときは uv python install サブコマンドを使う。 ためしに Python 3.13 をインストールしてみよう。

$ uv python install 3.13
Installed Python 3.13.3 in 3.34s
 + cpython-3.13.3-macos-aarch64-none

以下のように Python 3.13 がインストールされた。

$ uv python list | grep 3.13
cpython-3.13.3-macos-aarch64-none                   /Users/amedama/.local/share/uv/python/cpython-3.13.3-macos-aarch64-none/bin/python3.13
cpython-3.13.3+freethreaded-macos-aarch64-none      <download available>

CPython だけでなく PyPy など異なる実装もインストールできる。

$ uv python list --all-platforms | grep ^pypy | head -n 5
pypy-3.11.11-windows-x86_64-none                       <download available>
pypy-3.11.11-macos-x86_64-none                         <download available>
pypy-3.11.11-macos-aarch64-none                        <download available>
pypy-3.11.11-linux-x86_64-gnu                          <download available>
pypy-3.11.11-linux-x86-gnu                             <download available>

たとえば PyPy のバージョン 3.11 を入れたいときは次のようにする。

$ uv python install pypy@3.11
Installed Python 3.11.11 in 4.61s
 + pypy-3.11.11-macos-aarch64-none

Python の実行環境がインストールされるディレクトリを確認する

インストール先のディレクトリは uv python dir サブコマンドで確認できる。

$ uv python dir
/Users/amedama/.local/share/uv/python

使用する Python のバージョンを指定する

デフォルトで使用したい Python のバージョンを指定したいときは uv python pin サブコマンドを使う。

$ uv python pin 3.13
Pinned `.python-version` to `3.13`

とはいえ、これは単に .python-version というファイルをカレントワーキングディレクトリに作成するだけ。

$ cat .python-version    
3.13

uv は、ディレクトリのファイルを読んでデフォルトの Python を選択するようだ。 つまり、特定のディレクトリで使う Python を指定していることになる。

$ uv run python -V
Python 3.13.3
$ uv python pin 3.12
Updated `.python-version` from `3.13` -> `3.12`
$ uv run python -V  
Python 3.12.10

ディレクトリ単位ではなく、ユーザが使用するデフォルトを指定したいときは --global オプションをつける。 すると、~/.config/uv 以下にファイルが作られる。

$ uv python pin 3.12 --global
Pinned `/Users/amedama/.config/uv/.python-version` to `3.12`

上記はユーザのデフォルトなので、もしディレクトリに .python-version があるときはそちらが優先される。

$ uv python pin 3.13         
Updated `.python-version` from `3.12` -> `3.13`
$ uv run python -V  
Python 3.13.3

インストール済みの Python インタプリタへのパスを取得する

uv が認識している各バージョンの Python インタプリタへのパスは uv python find サブコマンドで得られる。

$ uv python find 3.13
/Users/amedama/.local/share/uv/python/cpython-3.13.3-macos-aarch64-none/bin/python3.13
$ uv python find 3.12
/opt/homebrew/opt/python@3.12/bin/python3.12

Python の実行環境をアンインストールする

Python の実行環境をアンインストールしたいときは uv python uninstall サブコマンドを使う。

$ uv python uninstall 3.13
Searching for Python versions matching: Python 3.13
Uninstalled Python 3.13.3 in 74ms
 - cpython-3.13.3-macos-aarch64-none
$ rm .python-version

Projects 機能

続いては Projects 機能について。 uv のユースケースとしては、この機能を使う場面が一番多そう。 この機能は poetry をイメージすると分かりやすい。 プロジェクトは、Python を使って開発する何らかのアプリケーションやパッケージになる。

プロジェクトを作成する

まずは uv init サブコマンドでプロジェクトを作成する。 以下では helloworld という名前でプロジェクトを作っている。 --vcs none をつけているのは、デフォルトではプロジェクトに空の Git リポジトリを作成するため。 今回はバージョン管理の部分が不要なので明示している。

$ uv init helloworld --vcs none
Initialized project `helloworld` at `/Users/amedama/Documents/temporary/uv/helloworld`

なお、上記ではコマンドの後ろにプロジェクト名を指定している。 この場合はプロジェクト名でサブディレクトリが作られる。 これ以外に、自分でディレクトリを作った上で、そこで uv init するやり方もある。

$ mkdir helloworld
$ cd helloworld
$ uv init  --vcs none

uv init を実行すると、次のようにプロジェクトに必要な最低限のファイルが用意される。

$ cd helloworld
$ ls -1a
.
..
.python-version
main.py
pyproject.toml
README.md

デフォルトで用意された Python のモジュール main.py は uv run コマンドで実行できる。

$ uv run main.py    
Using CPython 3.12.10 interpreter at: /opt/homebrew/opt/python@3.12/bin/python3.12
Creating virtual environment at: .venv
Hello from helloworld!

実行するとプロジェクト用の仮想環境がディレクトリの配下に作られる。

$ ls -1a .venv
.
..
.gitignore
bin
CACHEDIR.TAG
lib
pyvenv.cfg

また、依存パッケージなどのバージョンやハッシュの情報を記載する uv.lock というファイルも用意される。

$ cat uv.lock        
version = 1
revision = 1
requires-python = ">=3.12"

[[package]]
name = "helloworld"
version = "0.1.0"
source = { virtual = "." }

.python-version は先述したとおり uv が読み取って実行する際のバージョンに使われる。

$ cat .python-version 
3.12

pyproject.toml は言わずとしれた Python でパッケージやプロジェクトを管理するメタデータを記載するファイルになっている。

$ cat pyproject.toml 
[project]
name = "helloworld"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []

プロジェクトに依存パッケージを追加する

プロジェクトに依存パッケージを追加するときは uv add サブコマンドを使う。 この点は Scripts 機能を試したときと同じで、違いは --script <module> オプションの指定がないだけ。

$ uv add requests                  
Resolved 6 packages in 86ms
Prepared 5 packages in 162ms
Installed 5 packages in 5ms
 + certifi==2025.1.31
 + charset-normalizer==3.4.1
 + idna==3.10
 + requests==2.32.3
 + urllib3==2.4.0

パッケージの使用時ではなく開発のタイミングで必要になるものを追加するときは --dev をつける。

$ uv add --dev pytest    
Resolved 11 packages in 169ms
Prepared 4 packages in 117ms
Installed 4 packages in 5ms
 + iniconfig==2.1.0
 + packaging==24.2
 + pluggy==1.5.0
 + pytest==8.3.5

なお、--dev--group dev のエイリアスに過ぎない。 別の名称を使いたいときは --group オプションを使えば良い。 たとえば以下では testing というグループ名を使っている。

$ uv add --group testing pytest
Resolved 11 packages in 3ms
Audited 9 packages in 0.01ms

プロジェクトの依存パッケージを削除する

依存パッケージを削除したいときは uv remove を使う。 この点も Scripts 機能を試したときと変わらない。

$ uv remove --group testing pytest
Resolved 11 packages in 2ms
Audited 9 packages in 0.01ms

コマンドを実行すると pyproject.toml や uv.lock が更新される。 必要に応じて手動で編集すると良い。

$ cat pyproject.toml 
[project]
name = "helloworld"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
    "requests>=2.32.3",
]

[dependency-groups]
dev = [
    "pytest>=8.3.5",
]
testing = []

プロジェクトの環境を同期する

プロジェクトのリポジトリをチェックアウトした直後など、環境が整っていないときは uv sync サブコマンドを実行する。 試しに仮想環境を削除した上で実行してみよう。 すると、改めて仮想環境が作成されてパッケージがインストールされる。

$ rm -rf .venv 
$ uv sync                         
Using CPython 3.12.10 interpreter at: /opt/homebrew/opt/python@3.12/bin/python3.12
Creating virtual environment at: .venv
Resolved 11 packages in 0.51ms
Installed 9 packages in 6ms
 + certifi==2025.1.31
 + charset-normalizer==3.4.1
 + idna==3.10
 + iniconfig==2.1.0
 + packaging==24.2
 + pluggy==1.5.0
 + pytest==8.3.5
 + requests==2.32.3
 + urllib3==2.4.0

プロジェクトの依存パッケージを確認する

プロジェクトが依存しているパッケージは uv tree サブコマンドで確認できる。 この点も Scripts 機能と変わらない。

$ uv tree                   
Resolved 11 packages in 0.56ms
helloworld v0.1.0
├── requests v2.32.3
│   ├── certifi v2025.1.31
│   ├── charset-normalizer v3.4.1
│   ├── idna v3.10
│   └── urllib3 v2.4.0
└── pytest v8.3.5 (group: dev)
    ├── iniconfig v2.1.0
    ├── packaging v24.2
    └── pluggy v1.5.0

コマンドの入るパッケージの扱いについて

なお、パッケージによっては仮想環境の bin ディレクトリにコマンドがインストールされることがある。 たとえば Ruff であれば ruff コマンドが入る。

$ uv add --dev ruff
Resolved 12 packages in 455ms
Prepared 1 package in 1.66s
Installed 1 package in 8ms
 + ruff==0.11.5

インストールされたコマンドを呼びたいときは uv run 経由で実行できる。

$ uv run -- ruff -V               
ruff 0.11.5

あるいは、単純に仮想環境をアクティベートすることで PATH を通してしまっても良い。

$ source .venv/bin/activate
(helloworld) $ ruff -V         
ruff 0.11.5

仮想環境から抜けるときは deactivate する。

(helloworld) $ deactivate

パッケージの配布物をビルドして公開する

プロジェクトの pyproject.toml を元にパッケージの配布物をビルドして公開できる。 配布物を公開する部分は twine をイメージすると分かりやすい。

まずは uv build サブコマンドを使って配布物をビルドする。

$ uv build

これで、ソース配布物 (sdist) や Wheel ファイルができる。

$ ls -1 dist
helloworld-0.1.0-py3-none-any.whl
helloworld-0.1.0.tar.gz

そして、uv publish すると PyPI に公開できる。 デフォルトではインタラクティブにアカウントの情報を入力する。

$ uv publish

とはいえ、ここらへんは手作業でやるよりも CI/CD の仕組みを作り込むのが一般的だろう。

Tools 機能

先ほどの Projects 機能では、プロジェクトに紐づいた依存パッケージの管理について学んだ。 とはいえ、プロジェクトに紐づかない普段遣いのツールをグローバルに入れたい場合もあるはず。 そのようなときは Tools 機能を用いる。

ツールをインストールする

たとえば black が使いたいなーと思ったときは uv tool install サブコマンドを使う。 すると、Tools 機能用の仮想環境にパッケージがインストールされる。

$ uv tool install black
Resolved 6 packages in 174ms
Prepared 5 packages in 282ms
Installed 6 packages in 6ms
 + black==25.1.0
 + click==8.1.8
 + mypy-extensions==1.0.0
 + packaging==24.2
 + pathspec==0.12.1
 + platformdirs==4.3.7
Installed 2 executables: black, blackd
warning: `/Users/amedama/.local/bin` is not on your PATH. To use installed tools, run `export PATH="/Users/amedama/.local/bin:$PATH"` or `uv tool update-shell`.

コマンドは $HOME/local/bin にデフォルトでインストールされる。 もし、ここに PATH が通っていない場合には上記のように警告が出る。

警告にあるとおり、手動でシェルの設定を投入するか、あるいは uv tool update-shell サブコマンドを使う。

$ uv tool update-shell
Created configuration file: /Users/amedama/.zshenv
Restart your shell to apply changes

uv tool update-shell サブコマンドを使うとシェルにあわせて設定ファイルを作ってくれる。 普段使いする環境なら、手動で設定を投入する方が挙動をコントロールできて良いかな。

$ cat ~/.zshenv 
# uv
export PATH="/Users/amedama/.local/bin:$PATH"

ツールを実行する

インストールしたツールは uv tool run サブコマンドで実行できる。

$ uv tool run black main.py 
All done! ✨ 🍰 ✨
1 file left unchanged.

あるいは uvx というエイリアスもあるようだ。

$ uvx black main.py    
All done! ✨ 🍰 ✨
1 file left unchanged.

もちろんコマンドに PATH が通っていれば、シェルでそのまま実行しても構わない。

$ which black 
/Users/amedama/.local/bin/black
$ black --version
black, 25.1.0 (compiled: yes)
Python (CPython) 3.12.10

インストール済みのツールを確認する

インストール済みのパッケージは uv tool list サブコマンドで確認できる。

$ uv tool list        
black v25.1.0
- black
- blackd

実行時の Python のバージョンを指定したいときは --python オプションを使うと良い。

$ uv tool run --python 3.11 black main.py

インストール済みのツールを更新する

インストール済みのパッケージのバージョンを上げるときは uv tool upgrade を使う。 --all オプションをつけるとすべてのパッケージを更新できる。

$ uv tool upgrade --all

ツールをアンインストールする

パッケージをアンインストールするときは uv tool uninstall サブコマンドを使う。

$ uv tool uninstall black
Uninstalled 2 executables: black, blackd

pip Interface 機能

uv には pip Interface という機能がある。 この機能を使うことで、既存の pip や venv / virtualenv を使った開発の中でも uv の恩恵を得ることができる。

仮想環境を作成する

まず、uv venv サブコマンドを使うと仮想環境を作成できる。 デフォルトではカレントワーキングディレクトリに .venv という名前で仮想環境ができる。

$ cd ../
$ uv venv                         
Using CPython 3.12.10 interpreter at: /opt/homebrew/opt/python@3.12/bin/python3.12
Creating virtual environment at: .venv
Activate with: source .venv/bin/activate

もし名前を指定して作成したいときはサブコマンドに続けて仮想環境の名前を指定する。

$ uv venv myvenv
Using CPython 3.12.10 interpreter at: /opt/homebrew/opt/python@3.12/bin/python3.12
Creating virtual environment at: myvenv
Activate with: source myvenv/bin/activate

作成した仮想環境は、一般的な venv / virtualenv を使った仮想環境と同様に source でアクティベートする。

$ source .venv/bin/activate

uv の pip Interface は、仮想環境を次の優先順位で探索する。

  • VIRTUAL_ENV 環境変数
    • uv / venv / virtualenv で作成した環境をアクティベートすると自動で設定される
  • CONDA_PREFIX 環境変数
    • Conda で作成した環境をアクティベートすると自動で設定される
  • カレントワーキングディレクトリの .venv ディレクトリ

今回は明示的にアクティベートしたので一番上のルールに該当する。

$ echo $VIRTUAL_ENV
/Users/amedama/Documents/temporary/uv/.venv

uv の pip Interface は、pip の操作方法の一部を移植している。 内部的に pip を利用しているわけではないらしい。

パッケージをインストールする

たとえば uv pip install を使って環境にパッケージをインストールできる。

$ uv pip install pylint
Resolved 7 packages in 209ms
Prepared 6 packages in 196ms
Installed 7 packages in 6ms
 + astroid==3.3.9
 + dill==0.4.0
 + isort==6.0.1
 + mccabe==0.7.0
 + platformdirs==4.3.7
 + pylint==3.3.6
 + tomlkit==0.13.2

インストール済みのパッケージを確認する

uv pip list サブコマンドでインストール済みのパッケージ一覧を確認できる。

$ uv pip list          
Package      Version
------------ -------
astroid      3.3.9
dill         0.4.0
isort        6.0.1
mccabe       0.7.0
platformdirs 4.3.7
pylint       3.3.6
tomlkit      0.13.2

パッケージの依存関係を確認する

それぞれのパッケージの依存関係は uv pip tree の方が見やすい。

$ uv pip tree       
pylint v3.3.6
├── astroid v3.3.9
├── dill v0.4.0
├── dill v0.4.0
├── isort v6.0.1
├── mccabe v0.7.0
├── platformdirs v4.3.7
└── tomlkit v0.13.2

パッケージのバージョンに矛盾がないか確認する

インストールしたパッケージが要求するバージョンに矛盾がないか uv pip check で確認できる。

$ uv pip check
Checked 7 packages in 0.39ms
All installed packages are compatible

インストール済みのパッケージのバージョンを確認する

インストールしたパッケージのバージョンは uv pip freeze で確認する。

$ uv pip freeze
astroid==3.3.9
dill==0.4.0
isort==6.0.1
mccabe==0.7.0
platformdirs==4.3.7
pylint==3.3.6
tomlkit==0.13.2

インストールしたパッケージの詳細を確認する

uv pip show サブコマンドでインストールしたパッケージの詳細を確認できる。

$ uv pip show pylint
Name: pylint
Version: 3.3.6
Location: /Users/amedama/Documents/temporary/uv/.venv/lib/python3.12/site-packages
Requires: astroid, dill, isort, mccabe, platformdirs, tomlkit
Required-by:

パッケージをアンインストールする

パッケージをアンインストールしたいときは uv pip uninstall を使う。

$ uv pip uninstall pylint
Uninstalled 1 package in 11ms
 - pylint==3.3.6

pyproject.toml でインストールされるパッケージの詳細を確認する

uv pip compile を使うと pyproject.toml から入るパッケージとバージョンを確認できる。 これは pip にはない機能かな。 似たようなことをするならまっさらな環境に pip install . した上で pip freeze するのかな。

$ cd helloworld
$ uv pip compile pyproject.toml 
Resolved 5 packages in 65ms
# This file was autogenerated by uv via the following command:
#    uv pip compile pyproject.toml
certifi==2025.1.31
    # via requests
charset-normalizer==3.4.1
    # via requests
idna==3.10
    # via requests
requests==2.32.3
    # via helloworld (pyproject.toml)
urllib3==2.4.0
    # via requests

pyproject.toml からパッケージをインストールする

uv pip sync を使うことで、pyproject.toml などから仮想環境にパッケージをインストールできる。 こちらも本来の pip にはない機能で、似たようなことがしたいときは pip install -U . とかするかな。

$ uv pip sync pyproject.toml 
Using Python 3.12.10 environment at: /Users/amedama/Documents/temporary/uv/.venv
Resolved 1 package in 1ms
Uninstalled 6 packages in 14ms
Installed 1 package in 2ms
 - astroid==3.3.9
 - dill==0.4.0
 - isort==6.0.1
 - mccabe==0.7.0
 - platformdirs==4.3.7
 + requests==2.32.3
 - tomlkit==0.13.2

その他

uv のキャッシュしているデータを削除したいときは uv cache cleanuv cache prune が使える。

$ uv cache clean            
Clearing cache at: /Users/amedama/.cache/uv
Removed 1527 files (56.1MiB)

キャッシュのあるディレクトリ自体は uv cache dir サブコマンドで得られる。

$ uv cache dir  
/Users/amedama/.cache/uv

uv をインストール用のスクリプトを使って入れた場合には uv self update サブコマンドで uv 自体を更新できる。 今回のように Homebrew などのパッケージマネージャ経由で入れたときはエラーになっておわり。

$ uv self update
error: uv was installed through an external package manager, and self-update is not available. Please use your package manager to update uv.

まとめ

今回は Python のパッケージおよびプロジェクトマネージャの uv を使ってみた。 様々なツールを uv だけで代替できるところは便利に感じられる。 一方で、動作する際に内部的な振る舞いが予想しにくく感じられる場面もあった。 今後は Projects 機能を中心に、適材適所で使っていきたい。