CUBE SUGAR CONTAINER

技術系のこと書きます。

OpenAI の Web API を使い始める

最近は OpenAI 互換の Web API が、LLM の機能を提供する上でデファクトに近い方法となっているように思う。 そこで、今回は本家の OpenAI の Web API を使い始めるまでの内容を自分用のメモを兼ねて残しておく。

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

$ sw_vers         
ProductName:        macOS
ProductVersion:     15.5
BuildVersion:       24F74
$ uname -srm
Darwin 24.5.0 arm64
$ curl --version
curl 8.7.1 (x86_64-apple-darwin24.0) libcurl/8.7.1 (SecureTransport) LibreSSL/3.3.6 zlib/1.2.12 nghttp2/1.64.0
Release-Date: 2024-03-27
Protocols: dict file ftp ftps gopher gophers http https imap imaps ipfs ipns ldap ldaps mqtt pop3 pop3s rtsp smb smbs smtp smtps telnet tftp
Features: alt-svc AsynchDNS GSS-API HSTS HTTP2 HTTPS-proxy IPv6 Kerberos Largefile libz MultiSSL NTLM SPNEGO SSL threadsafe UnixSockets
$ python -V
Python 3.12.11
$ pip list | grep -i openai               
openai            1.90.0

もくじ

OpenAI Platform について

OpenAI の Web API を利用する際の料金体系は、ChatGPT のサブスクリプションとは分離されている。 また、Web API に関する管理は OpenAI Platform というサービスを使うことになる。

platform.openai.com

Web API を利用するには、上記のサービスにあらかじめ前払いでクレジットを登録しておく必要がある。 Web API を利用することで生じた料金は、前払いしたクレジットの中から支払われる。

Organization を作る

OpenAI Platform を利用するには、まず Organization を作成する必要があるようだ。 現時点 (2025-06-21) の UI であれば、右上にある「Start Bulding」のボタンを押下する。 特に何も入力しないでウィザードを進めると Organization name が「Personal」になる。 個人による利用であれば、それで特に問題ないはず。 また、Organization の配下にデフォルトのプロジェクトとして「Default」も同時に作られるようだ。

クレジットを追加する

前述した通り Web API はクレジットが無いと使えない。 そこで、まずは「Settings > Billing」の画面でクレジットを追加する。

platform.openai.com

「Add payment details」ボタンを押下するとクレジットカードの登録画面が出てくる。 自身のカードの情報を入力したら、追加するクレジットをドルで指定する。 クレジットが少なくなったときに自動でチャージする設定は不要であればオフにする。

API トークンを作る

Web API を呼び出す際には OpenAI Platform のアカウントとの紐づけが必要になる。 そのために、API トークンを作成する。

platform.openai.com

現時点の UI であれば右上の「Create new secret key」を押下して作成する。 API トークンは作成したタイミングで一度しか表示されない。 忘れずにパスワードマネージャなどに記録しておこう。

curl(1) から Web API を呼び出す

まずは curl(1) を使って呼び出してみる。

先ほど発行したトークンを、シェルのセッション変数に取り込む。 パスワードなどのセンシティブな情報を扱うときは read コマンドと -s オプションを用いると良い。 こうすれば入力した内容がコマンドの履歴に残らない。

$ read -s OPENAI_API_KEY

上記を実行したら、先ほど記録したトークンをターミナルにペーストする。

現時点で最もベーシックな Web API として Chat Completions を試してみよう。 Chat Completions API のリファレンスは以下にある。

platform.openai.com

なお、今だと状態 (ステート) を API 側で管理してもらえる Chat Completions with Responses という API もあるようだ。 従来の API は状態を持たないステートレスだったので、クライアント側ですべて管理する必要があった。 Responses の API では、その点が改善されているようだ。 ただし、今回は使わない。

リファレンスの内容を元に curl(1) で HTTP リクエストを作る。 先ほどセッション変数に入れたトークンは Authorization ヘッダの Bearer 以降に指定する。 model キーにはモデルの名称を入力する。 messages にはロール毎の指示を入力する。 たとえば "role": "developer" の指示は、"role": "user" の入力に関係なく従うべき内容と解釈される。

$ curl -X POST https://api.openai.com/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -d '{
    "model": "gpt-4.1",
    "messages": [
      {
        "role": "developer",
        "content": "あなたは親切なアシスタントです"
      },
      {
        "role": "user",
        "content": "こんにちは!"
      }
    ]
  }'

結果は次のような JSON として得られる。

{
  "id": "chatcmpl-XXX...",
  "object": "chat.completion",
  "created": 1750480953,
  "model": "gpt-4.1-2025-04-14",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "こんにちは!😊  \nどうぞ、なんでもお気軽にご相談ください。",
        "refusal": null,
        "annotations": []
      },
      "logprobs": null,
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 23,
    "completion_tokens": 16,
    "total_tokens": 39,
    "prompt_tokens_details": {
      "cached_tokens": 0,
      "audio_tokens": 0
    },
    "completion_tokens_details": {
      "reasoning_tokens": 0,
      "audio_tokens": 0,
      "accepted_prediction_tokens": 0,
      "rejected_prediction_tokens": 0
    }
  },
  "service_tier": "default",
  "system_fingerprint": "fp_51e1070cf2"
}

なお、クレジットが無い状態で呼び出すと次のようなエラーになる。

{
    "error": {
        "message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.",
        "type": "insufficient_quota",
        "param": null,
        "code": "insufficient_quota"
    }
}

Python から Web API を呼び出す

次は公式の Python パッケージを使って Web API を呼んでみよう。

まずは openai パッケージをインストールする。

$ pip install openai

また、先ほどトークンをセッション変数 OPENAI_API_KEY に格納してあった。 これを、プロセスが fork した後の Python プロセスからも使えるように環境変数にする。

$ export OPENAI_API_KEY

openai パッケージは環境変数 OPENAI_API_KEY が定義されていると自動的に読んでくれる。

そして、Python インタプリタを起動する。

$ python

openai.OpenAI クラスをインスタンス化する。

>>> from openai import OpenAI
>>> client = OpenAI()

openai.OpenAI#chat.completions.create() メソッドを使って Chat Completions API を呼び出す。

>>> completion = client.chat.completions.create(
...   model="gpt-4.1",
...   messages=[
...     {"role": "developer", "content": "あなたは親切なアシスタントです"},
...     {"role": "user", "content": "こんにちは!"}
...   ]
... )

レスポンスを確認すると、ちゃんと結果が得られていることが確認できる。

>>> completion
ChatCompletion(id='chatcmpl-XXX...', choices=[Choice(finish_reason='stop', index=0, logprobs=None, message=ChatCompletionMessage(content='こんにちは!😊  \n何かお手伝いできることはありますか?', refusal=None, role='assistant', annotations=[], audio=None, function_call=None, tool_calls=None))], created=1750477841, model='gpt-4.1-2025-04-14', object='chat.completion', service_tier='default', system_fingerprint='fp_51e1070cf2', usage=CompletionUsage(completion_tokens=17, prompt_tokens=23, total_tokens=40, completion_tokens_details=CompletionTokensDetails(accepted_prediction_tokens=0, audio_tokens=0, reasoning_tokens=0, rejected_prediction_tokens=0), prompt_tokens_details=PromptTokensDetails(audio_tokens=0, cached_tokens=0)))

いじょう。


[asin:B085BG8CH5:detai

Mac: LM Studio に Hugging Face Hub からダウンロードしたモデルを CLI で読み込む

LM Studio は、ローカル LLM を動かすためのソフトウェアのひとつ。

lmstudio.ai

基本的に LM Studio では、モデルをダウンロードする際には Discover タブから GUI で検索してダウンロードできる。 しかし、Hugging Face Hub 1 などから手動でダウンロードしたモデルをインポートする方法も用意されている。 今回はそのやり方について書く。

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

$ sw_vers
ProductName:        macOS
ProductVersion:     15.5
BuildVersion:       24F74
$ uname -srm
Darwin 24.5.0 arm64
$ sysctl machdep.cpu.brand_string
machdep.cpu.brand_string: Apple M2 Pro
$ huggingface-cli version
huggingface_hub version: 0.33.0
$ lms version  
   __   __  ___  ______          ___        _______   ____
  / /  /  |/  / / __/ /___ _____/ (_)__    / ___/ /  /  _/
 / /__/ /|_/ / _\ \/ __/ // / _  / / _ \  / /__/ /___/ /  
/____/_/  /_/ /___/\__/\_,_/\_,_/_/\___/  \___/____/___/  

lms - LM Studio CLI - v0.0.41
GitHub: https://github.com/lmstudio-ai/lmstudio-cli

もくじ

下準備

まずは Homebrew から LM Studio をインストールする。

$ brew install --cask lm-studio

インストールすると $HOME/.lmstudio 以下にディレクトリができる。 この中の bin ディレクトリには LM Studio を CLI で操作するためのバイナリが入っている。 そこで、ここにパスを通す。

$ export PATH="$PATH:$HOME/.lmstudio/bin"

必要に応じてシェルの設定ファイルなどに永続化すると良い。

パスを通すと lms コマンドが使えるようになる。

$ lms version  
   __   __  ___  ______          ___        _______   ____
  / /  /  |/  / / __/ /___ _____/ (_)__    / ___/ /  /  _/
 / /__/ /|_/ / _\ \/ __/ // / _  / / _ \  / /__/ /___/ /  
/____/_/  /_/ /___/\__/\_,_/\_,_/_/\___/  \___/____/___/  

lms - LM Studio CLI - v0.0.41
GitHub: https://github.com/lmstudio-ai/lmstudio-cli

続いて、Hugging Face Hub を操作するためのパッケージをPyPI からダウンロードする。

$ pip install "huggingface_hub[cli]"

これで huggingface-cli コマンドが使えるようになる。

$ huggingface-cli version
huggingface_hub version: 0.33.0

モデルをダウンロードする

次に Hugging Face Hub からモデルをダウンロードする。 LM Studio はデフォルトの実行ランタイムとして llama.cpp を使用する。 そのため、GGUF のモデルをダウンロードする。

$ huggingface-cli download "lmstudio-community/gemma-3-1B-it-qat-GGUF" --local-dir .

モデルをインポートする

ダウンロードしたファイルを lms import コマンドでインポートする。 このときインポートのやり方を対話的に確認される。

$ lms import gemma-3-1B-it-QAT-Q4_0.gguf

無事にインポートできると LM Studio の画面でモデルが確認できるようになるはず。

LM Studio でインポートしたモデルが確認できる

モデルの動作を確認する

サーバを起動するとモデルが WebAPI 経由で使えるようになる。

試しにモデル一覧を curl(1) で確認してみよう。

$ curl http://localhost:1234/v1/models
{
  "data": [
    {
      "id": "gemma-3-1b-it-qat",
      "object": "model",
      "owned_by": "organization_owner"
    },
    {
      "id": "text-embedding-nomic-embed-text-v1.5",
      "object": "model",
      "owned_by": "organization_owner"
    }
  ],
  "object": "list"
}

インポートした gemma-3-1b-it-qat が確認できる。

同様に completions の API も使ってみよう。

$ curl -X POST -s http://localhost:1234/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
  "model": "gemma-3-1b-it-qat",
  "messages": [
    { "role": "user", "content": "こんにちは!" }
  ]
}'       
{
  "id": "chatcmpl-kfep0etrq6dz9fbcth25uh",
  "object": "chat.completion",
  "created": 1749874667,
  "model": "gemma-3-1b-it-qat",
  "choices": [
    {
      "index": 0,
      "logprobs": null,
      "finish_reason": "stop",
      "message": {
        "role": "assistant",
        "content": "こんにちは!何かお手伝いできることはありますか? 😊 どんなことでもお気軽にご質問ください。"
      }
    }
  ],
  "usage": {
    "prompt_tokens": 11,
    "completion_tokens": 22,
    "total_tokens": 33
  },
  "stats": {},
  "system_fingerprint": "gemma-3-1b-it-qat"
}

問題ないようだ。


Mac: Ollama でローカル LLM を動かす

Ollama 1 はローカル LLM を動かすためのソフトウェアのひとつ。 一般的なラップトップやデスクトップマシンで動かすようなユースケースが主に想定されているように思う。 LLM の機能を不特定多数に提供するというよりは、マシンを操作しているユーザ自身が使用するイメージだろう。 似たようなユースケースで用いられるソフトウェアには、他にも LM Studio 2 などがある。 今回は、そんな Ollama を Mac から使う方法についてメモしておく。

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

$ sw_vers
ProductName:        macOS
ProductVersion:     15.5
BuildVersion:       24F74
$ uname -srm                
Darwin 24.5.0 arm64
$ sysctl machdep.cpu.brand_string
machdep.cpu.brand_string: Apple M2 Pro
$ ollama --version
ollama version is 0.9.0

もくじ

下準備

まず、Ollama は Homebrew を使ってインストールできる。

$ brew install --formula ollama

サーバを起動する

Ollama を利用するには、何をするにもまずはサーバのインスタンスを立ち上げる必要がある。

そのためには、Homebrew でインストールした場合には Ollama をサービスとして起動すれば良い。

$ brew services start ollama

あるいは、フォアグラウンドで実行したいときは単に ollama serve コマンドを叩いても良い。

$ ollama serve

設定を変更する

Ollama は、OLLAMA_ から始まる名前のシェル変数・環境変数を使って動作を変更できる。

たとえば、Homebrew のサービスからサーバを起動する場合には launchctl setenv で設定を変更する。 以下ではグローバルなコンテキスト長を 32768 トークンに変更している。

$ launchctl setenv OLLAMA_CONTEXT_LENGTH 32768

あるいは ollama serve コマンドで起動しているときは、単純にセッション変数として指定すれば良い。

$ OLLAMA_CONTEXT_LENGTH=32768 ollama serve

モデルをダウンロードする

続いてモデルをダウンロードするには ollama pull コマンドを使う。

以下では例として Gemma3 の 1B モデルを指定している。

$ ollama pull gemma3:1b
pulling manifest 
pulling 7cd4618c1faf: 100% ▕██████████████████▏ 815 MB                         
pulling e0a42594d802: 100% ▕██████████████████▏  358 B                         
pulling dd084c7d92a3: 100% ▕██████████████████▏ 8.4 KB                         
pulling 3116c5225075: 100% ▕██████████████████▏   77 B                         
pulling 120007c81bf8: 100% ▕██████████████████▏  492 B                         
verifying sha256 digest 
writing manifest 
success

Ollama が公式が提供しているモデルについては以下の Web ページで検索できる。

ollama.com

ダウンロードしたモデルは ollama ls コマンドで確認できる。

$ ollama ls           
NAME         ID              SIZE      MODIFIED    
gemma3:1b    8648f39daa8f    815 MB    6 hours ago

モデルの情報を得る

モデルの詳しい情報は ollama show コマンドで確認できる。

$ ollama show gemma3:1b
  Model
    architecture        gemma3     
    parameters          999.89M    
    context length      32768      
    embedding length    1152       
    quantization        Q4_K_M     

  Capabilities
    completion    

  Parameters
    stop           "<end_of_turn>"    
    temperature    1                  
    top_k          64                 
    top_p          0.95               

  License
    Gemma Terms of Use                  
    Last modified: February 21, 2024    
    ...                                 

ターミナルでモデルと対話する

ollama run コマンドを使うと、ターミナルを使ってモデルと対話できる。

$ ollama run gemma3:1b
>>> こんにちは!
こんにちは!何かお手伝いできることはありますか?😊 

どんなことでも構いません。例えば:

*   質問に答える
*   文章を作成する
*   アイデアを出す
*   情報検索をする

など、お気軽にお申し付けください。

なお、ollama ps コマンドを使うと、現在どのモデルがメモリ上で動作しているか確認できる。

$ ollama ps                     
NAME         ID              SIZE      PROCESSOR    UNTIL              
gemma3:1b    8648f39daa8f    2.2 GB    100% GPU     4 minutes from now

OpenAI-like な WebAPI を利用する

Ollama は OpenAI-like な Web API を提供している。 この Web API を通してモデルの機能を利用できる。

たとえば completions の API を curl(1) で叩いてみよう。 API はループバックアドレスの 11434 ポートで提供されている。

$ curl -s http://localhost:11434/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
  "model": "gemma3:1b",
  "messages": [
    { "role": "user", "content": "ご機嫌いかがですか?" }
  ]
}' | jq .

すると JSON で結果が得られる。

$ curl -s http://localhost:11434/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
  "model": "gemma3:1b",
  "messages": [
    { "role": "user", "content": "ご機嫌いかがですか?" }
  ]
}' | jq .
{
  "id": "chatcmpl-578",
  "object": "chat.completion",
  "created": 1749812263,
  "model": "gemma3:1b",
  "system_fingerprint": "fp_ollama",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "元気です、ありがとう! 😊 あなたはいかがですか? \n\n何かお手伝いできることはありますか?\n"
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 16,
    "completion_tokens": 24,
    "total_tokens": 40
  }
}

すべての API のドキュメントは以下の Web ページで確認できる。

github.com

そして、API を通して様々なツールと連携できる。 連携できるツールは以下のページにまとめられている。

github.com

必要に応じて、先ほど叩いた API のエンドポイントをツールに設定することで連携が可能になる。

不要になったモデルを削除する

LLM のファイルはサイズが大きいので、不要になったときは消したくなることもある。

そんなときは ollama rm コマンドを使って削除できる。

$ ollama rm gemma3:1b 
deleted 'gemma3:1b'

いじょう。


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 された後は別のチェーンで処理されていない。 ドキュメント通りの振る舞いが確認できた。

いじょう。