Posts for: #Linux

nginx の設定調査

開発合宿の準備をしていて今日のバドミントン練習はお休み。

nginx のリバースプロキシ

compose に起動している2つのサービスを同じポート番号で共有したい。nginx を tls 終端にしていてリバースプロキシとして構築している。ルーティングするには2つの方法がある。次のような compose.yml を作ってローカルのファイルシステムをマウントして設定変更しながら検証する。

services:
  proxy:
    container_name: proxy
    image: docker.io/library/nginx:stable
    ports:
      - 8443:8443
    network_mode: "host"
    restart: unless-stopped
    volumes:
      - ./nginx:/etc/nginx

sub-domain based routing

クラウドでは普通のやり方がサブドメインのホスト名でルーティングを行う。設定もシンプルでファイルも管理しやすいように分割できてよいと思える。システム変更時の移行も名前を切り替えればよいので移行しやすい。

  • nginx.conf
http {
    sendfile on;

    upstream web-api1 {
        server localhost:8801;
    }

    upstream web-api2 {
        server localhost:8802;
    }

    ssl_certificate /etc/nginx/ssl/sample.crt;
    ssl_certificate_key /etc/nginx/ssl/sample.key;

    include /etc/nginx/sites-enabled/*;
}
  • nginx/sites-enabled/www.sub1.example.com
server {
    listen 8443 ssl;
    server_name sub1.example.com;
    location / {
        include     /etc/nginx/common_proxy.conf;
        proxy_pass  http://web-api1;
    }
}

path based routing

サブドメインの方が私は好みではあるが、サブドメインをネームサーバーに登録したり、tls の証明書にも複数ホストの考慮が必要になってくる。パスでルーティングするなら1台のマシンのように仮想的にみせられるというメリットはある。

http {
    sendfile on;

    upstream web-api1 {
        server localhost:8801;
    }

    upstream web-api2 {
        server localhost:8802;
    }

    ssl_certificate /etc/nginx/ssl/sample.crt;
    ssl_certificate_key /etc/nginx/ssl/sample.key;

    server {
        listen 8443 ssl;

        location /app1/ {
            include     /etc/nginx/common_proxy.conf;
            proxy_pass  http://web-api1;
        }

        location /app2/ {
            include     /etc/nginx/common_proxy.conf;
            proxy_pass  http://web-api2;
        }
    }
}

コンテナの capability

docker compose を rootless mode で使っていて 443 ポートを使いたいと言われてどうしたらいいのだろう?といままで考えていないことに気付いた。調べたらすぐにやり方が書いてあった。

$ sudo setcap cap_net_bind_service=ep $(which rootlesskit)
$ systemctl --user restart docker

これだけで compose サービスの1つにポート設定できた。めちゃ簡単だった。いままで capability を設定したことがなかったのと、権限周りはややこしいという先入観もあって触る機会がなかった。いろいろ洗練されて抽象化されているのだと推測する。バックエンドの場合は任意のポート番号を使えばよいから capability の設定をして 1024 以下のポート番号を使わないといけない理由はあまりない気はするが、そういう設定もできるようにはみえる。そういう話題すら聞いたことがなかった。

コンテナー間のデータ通信はやはり http プロトコル

過去に コンテナー間のデータ通信に named pipe を使って実装 した。機能的には問題なく運用できていたが、環境構築時に named pipe の設定がわかりにくいという課題があった。また docker compose は volumes に設定したパスが存在しないときに歴史的経緯でディレクトリを作成してしまう。初期設定時に named pipe を期待するリソースがディレクトリになってしまっているところの、エラー制御のわかりにくさもあった。またこの運用はオンプレミスの環境でしか利用できず、将来的にクラウド対応 (k8s) で運用することを考慮すると、http 通信できる方がよいと考えを改め、データ通信の仕組みを再実装した。http サーバーと比べて named pipe の方が優れているのはセキュリティとして物理的にその OS 上からしかアクセスできないところ。

go でフレームワークを使わず、シンプルな http サーバーを実装してみた。セキュリティの制約さえなければ、このぐらいの手間を惜しんで named pipe を実装することもなかったかと、いまとなっての学びとなった。

func newMux(cfg config.MyConfig) *http.ServeMux {
	mux := http.NewServeMux()
	mux.HandleFunc("/version", handler.HandleVersion(cfg))
	return mux
}

func main() {
	ctx, stop := signal.NotifyContext(context.Background(),
		syscall.SIGINT,
		syscall.SIGTERM,
	)
	defer stop()

	cfg := config.MyConfig{}
	server := &http.Server{
		Addr:    ":7099",
		Handler: newMux(cfg),
	}
	go func() {
		if err := server.ListenAndServe(); err != http.ErrServerClosed {
			slog.Error("got an error providing http service", "err", err)
		}
		slog.Info("stopped http server normally")
	}()

	<-ctx.Done()
	slog.Info("caught the stop signal")
	ctxTimeout, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	if err := server.Shutdown(ctxTimeout); err != nil {
		slog.Error("failed to shutdown normally", "err", err)
		return
	}
	slog.Info("done graceful shutdown")
}

初めて lvm を操作してみた

0時に寝て何度か起きて7時に起きた。最近は寝る前にはてブのアプリで適当に記事を読みながら寝ることが多い。

隔週の雑談

顧問のはらさんと隔週の打ち合わせ。今日の議題はこれら。

ここ1-2週間ぼーっとしていて、忙しくもなく、なにかやっているわけでもないけど、のんびり過ごしている。軽いバーンアウトだと思う。先週末は休みも取った。「hugo のテンプレート作り」のような新規開発を、どこかのもくもく会やイベントに行って、その場で集中してやったらいいんじゃないか?というアドバイスをいただいて、確かにそういうやり方もよいように思えた。今週末は課題管理のコンテンツを考えて、できればブログに書いてみようと思う。

lvm の論理ボリュームの結合

新規に almalinux 8 で仮想マシンを作った。デフォルト設定でインストールしたら //home でパーティション分割されていて、これは使い勝手が悪いなと思ってパーティションを結合することにした。

$ df -h
ファイルシス               サイズ  使用  残り 使用% マウント位置
devtmpfs                     1.9G     0  1.9G    0% /dev
tmpfs                        2.0G     0  2.0G    0% /dev/shm
tmpfs                        2.0G  8.6M  2.0G    1% /run
tmpfs                        2.0G     0  2.0G    0% /sys/fs/cgroup
/dev/mapper/almalinux-root    70G  6.5G   64G   10% /
/dev/mapper/almalinux-home   437G  5.0G  432G    2% /home
/dev/vda1                   1014M  221M  794M   22% /boot
tmpfs                        393M   12K  393M    1% /run/user/1000

基本的には次の記事をみてやったらうまくいった。

/home をバックアップする

# mkdir /home-bkup
# cp -a /home/ /home-bkup/

emergency モードに入る?

# systemctl emergency

結合したい領域を unmount して、home の論理ボリュームを削除する。

# umount /dev/mapper/almalinux-home
# lvremove /dev/mapper/almalinux-home
Do you really want to remove active logical volume almalinux/home? [y/n]: y
  Logical volume "home" successfully removed.

バックアップからデータを戻す。

# cp -a /home-bkup/ /home/

/etc/fstab から不要なパーティション設定を削除する。

# vi /etc/fstab
...
/dev/mapper/almalinux-home  /home  xfs  defaults  0  0  # <- この行を削除
...

root の論理ボリュームに余っている領域を拡張する。

# lvextend -l +100%FREE -r /dev/mapper/almalinux-root
  Size of logical volume almalinux/root changed from 70.00 GiB (17920 extents) to <507.04 GiB (129802 extents).
  Logical volume almalinux/root successfully resized.
meta-data=/dev/mapper/almalinux-root isize=512    agcount=4, agsize=4587520 blks
         =                       sectsz=512   attr=2, projid32bit=1
         =                       crc=1        finobt=1, sparse=1, rmapbt=0
         =                       reflink=1    bigtime=0 inobtcount=0
data     =                       bsize=4096   blocks=18350080, imaxpct=25
         =                       sunit=0      swidth=0 blks
naming   =version 2              bsize=4096   ascii-ci=0, ftype=1
log      =internal log           bsize=4096   blocks=8960, version=2
         =                       sectsz=512   sunit=0 blks, lazy-count=1
realtime =none                   extsz=4096   blocks=0, rtextents=0
data blocks changed from 18350080 to 132917248

この時点で1つの領域に結合されたことがわかる。

# df -h
ファイルシス               サイズ  使用  残り 使用% マウント位置
devtmpfs                     1.9G     0  1.9G    0% /dev
tmpfs                        2.0G     0  2.0G    0% /dev/shm
tmpfs                        2.0G   78M  1.9G    4% /run
tmpfs                        2.0G     0  2.0G    0% /sys/fs/cgroup
/dev/mapper/almalinux-root   508G   14G  494G    3% /
/dev/vda1                   1014M  221M  794M   22% /boot

バックアップを削除する。

# rm -rf /home-bkup/

マシンを再起動する。

# reboot

これで問題なければ完了。

コンテナー間のデータ通信と named pipe

22時頃から寝ていて2回起きて3時に起き出して、4時までネットみたりしていて、また寝て6時に起きた。生活のリズムがおかしい。

コンテナー間のデータのやり取り

昨日 モジュール分割 したことにより、いままで1つのモジュールで管理していたが、モジュールを分割したのでそれぞれのバージョンを取得できるとよいという話題が出た。コンテナー内にアプリケーションのバイナリがあり、バイナリを実行するとバージョン情報を取得できる。それぞれのモジュールは独立したコンテナで動いてるため、コンテナー間でその情報を受け渡す方法が必要になる。ググってみると次の so がヒットして named pipe がプラクティスだという。

ホスト os 上の named pipe をコンテナーの volumes でマウントして、それぞれのコンテナーが読み書きすればよい。構築時に named pipe さえ作ったらコンテナー内での読み書きでデータ通信を実現できるため、シンプルでよいんじゃないかと思えた。

mypipe という named pipe を作る。

$ mkfifo mypipe

読み込み用コンテナーのための read-Dockerfile を作る。tail コマンドで named pipe を読む。

$ vi read-Dockerfile
From bash:latest

ENTRYPOINT [ "tail", "-f", "/app/mypipe" ]
$ docker build -t mypipe-read:latest -f read-Dockerfile .

named pipe をマウントして読み込み用コンテナーを起動する。

$ docker run --rm --mount type=bind,source="$(pwd)"/mypipe,target=/app/mypipe,readonly mypipe-read

書き込み用のエントリーポイントのスクリプトはちょっと工夫がいる。おそらく Dockerfile 内で直接リダイレクトの操作ができない (やり方がわからなかった) 。シェルスクリプトを呼び出す形にして、シェルスクリプト内部でリダイレクトにより、named pipe に書き込みする。

エントリーポイントのスクリプトは次のような感じ。eval "$@" で任意のコード実行できるようにちょっと細工。

$ vi myentrypoint.sh
#!/bin/sh

cleanup() {
    echo "cleanup ..."
    exit 0
}

trap cleanup INT TERM

while true
do
    echo "$(date)"
    eval "$@"
    sleep 1
done

書き込み用コンテナーのための write-Dockerfile を作る。先の myentrypoint.sh を ENTRYPOINT として起動させる。

$ vi write-Dockerfile
From bash:latest

COPY myentrypoint.sh /app/
RUN chmod +x /app/myentrypoint.sh

ENTRYPOINT [ "/app/myentrypoint.sh" ]
$ docker build -t mypipe-write:latest -f write-Dockerfile .

適当に乱数を生成する cli を eval 実行させる。

$ docker run --rm --mount type=bind,source="$(pwd)"/mypipe,target=/app/mypipe mypipe-write "tr -dc 0-9 < /dev/urandom | fold -w 8 | head -1  > /app/mypipe"

読み込み用コンテナーで乱数を表示できるはず。

なにも難しくなく、linux の標準の機能を使ってコンテナー間のデータ通信を実現できたことにちょっと驚いた。

go で named pipe を読むときは linux ならば syscall.O_NONBLOCK を指定することで書き込みしていなくてもブロックせずに読める。値を取得できない可能性はあるけど、それが許される要件ならこれで済む。またテックブログにまとめたい。

func readNamedPipe(path string) ([]byte, error) {
	flag := os.O_RDONLY | syscall.O_NONBLOCK
	pipe, err := os.OpenFile(path, flag, os.ModeNamedPipe)
	if err != nil {
		return nil, fmt.Errorf("failed to open path: %w", err)
	}
	defer pipe.Close()
	reader := bufio.NewReader(pipe)
	b, _, err := reader.ReadLine()
	if err != nil {
		return nil, fmt.Errorf("failed to read line: %w", err)
	}
	return b, err
}

rpm のパッケージングを作り直す

1時に寝て何度か起きてあまり眠れなかった。昨日は遅くに帰ってきて晩ご飯を遅くに食べたので寝ていて吐き気がしてうまく眠れなかった。寝る前に食べることはできないみたい。

rpm パッケージング再び

ビルドができるようになったモジュール を rpm でパッケージングする。rpm でのビルドもできる状態で渡してもらえたので私が開発したモジュールを追加してパッケージングを修正する。rpm のパッケージングを行うのも5年ぶりといったところ。コンテナに慣れてしまって rpm を使うことはもうないと思っていたけれど、まだまだ現役であることを実感する。spec ファイルは普通に読めるので既存の設定や、他の rpm パッケージの spec ファイルの記述などもみながら、自分のモジュールで必要な設定を追加していく。久しぶりだったわりには順調に作業が進捗して2-3時間もやっていて追加の修正をして、実際にインストールして動作確認もできた。

rpm のマクロを確認する。

$ rpm --eval "%{_libdir}"
/usr/lib64

あるサーバーサービスを systemd 経由で実行させる。内部的に環境変数を使っている。systemd の EnvironmentFile で環境変数を設定したファイルへのパスを指定できる。例えば、次のように EnvironmentFile にパスを設定する。

[Service]
Type=simple
EnvironmentFile=/opt/path/to/my.env
ExecStart=/opt/path/to/bin/my-service
KillMode=process
StartLimitBurst=2
Restart=on-abnormal
User=ldap
Group=ldap

この環境変数にはパスワードのような機密情報も含むので rpm の %files で root 権限でのみ読めるようにアクセス制限を設定する。systemd 自体は root 権限で動くので環境変数の設定は root が行って my-service は ldap のユーザー/グループ権限で動く。

%attr(600,root,root) %config(noreplace) %{_sysconfdir}/path/to/my.env

ubuntu の環境構築

0時に寝て8時に起きた。昨日は出張から遅くに帰ってきたのでバテた。

ストレッチ

昨日はもくもく会で遅くに帰ってきたので毎週のストレッチを土曜日から日曜日に変更していた。ここ1-2ヶ月ほどでは最も調子がよかった。今週は出張していて普段より健康的な生活をしていた。というのは、お手伝い先のオフィスでは原則として私は9-18時でしか働けないため、業務の都合でずっと座りっぱなしにならない。例えば、自社のオフィスだとだいたい8時から始業して遅いと23時ぐらいまでずっと座りっぱなしになってしまう。たまたまホテルもいつもよりも離れた場所にとり、10分ほど歩く距離だったのでちょうどよかった。帰ってからホテルで2-3時間作業していたりもするのだが、それでも同じ姿勢でずっと座り続けている方が体にとってはよくないことがわかる。今日の開脚幅は開始前152cmで、ストレッチ後156cmだった。座っている時間が短かったために腰の張りはほとんどなかった。

ubuntu 22.04 lts の環境構築

先日 マシンに ubuntu をインストールした 続きの話し。東京出張があったり、開発の終盤でいろいろやっている中で開発環境を作り直すインセンティブが低かったので環境構築が遅くなった。過去の環境構築の issue が残っているのでそれをみながらやれば5-6時間もあれば作り直せた。結論から言って、マシンスペックが上がって、os も新しいものに変わったので開発環境としてはめちゃくちゃ快適になった。好みだが、私はリソースの潤沢なデスクトップマシンに linux をインストールして開発するのが一番しっくりくる。主観的に感情的にその構成が好きだというもの。

realforce の bluetooth キーボードの設定をするのにもう1つキーボードが必要なことに気付いた。私は bluez というツールを使って bluetooth キーボードのペアリングをしている。ペアリングするためには bluez の bluetoothctl という cli から操作してペアリングを行う。realforce のキーボードは有線/bluetooth 接続を両方同時につなぐことはできないため、bluetooth 接続するためには別のキーボードを接続して cli 操作しないといけない。

$ bluetoothctl
[bluetooth]# power on
[bluetooth]# agent on
[bluetooth]# scan on

bluetooth キーボードから接続要求を送る。

...
[NEW] Device F6:9D:A5:41:B7:1F REALFORCE_2
...
[bluetooth]# info F6:9D:A5:41:B7:1F
Device F6:9D:A5:41:B7:1F (random)
	Name: REALFORCE_2
	Alias: REALFORCE_2
	Appearance: 0x03c1
	Icon: input-keyboard
	Paired: no
	Trusted: no
	Blocked: no
	Connected: no
	LegacyPairing: no
	UUID: Human Interface Device    (00001812-0000-1000-8000-00805f9b34fb)
	RSSI: -51

マシンからデバイスを検出したらデバイス情報を確認してペアリングを実行する。

[bluetooth]# pair F6:9D:A5:41:B7:1F
Attempting to pair with F6:9D:A5:41:B7:1F
[CHG] Device F6:9D:A5:41:B7:1F Connected: yes
[agent] Passkey: ***

これでペアリングが実行されて bluetooth 接続できるようになる。ubuntu 20.04 lts の頃より進化しているのは接続/切断時に電池の残量も表示されるようになった。キーボードには充電池を使っているので地味に嬉しい。残量が少なくなってきたら帰るときに充電を仕掛けて帰れる。

今回の環境構築で1つはまったものがある。vagrant の仮想環境として virtualbox を使う。secure boot のために virtualbox をインストールするときにパスワードを mok (Machine-Owner Key) に登録しないといけない。この作業手順が分からなくて右往左往していた。インストール時に mok のダイアログが表示されてパスワードを登録するが、この後に os を再起動して bios 起動時に特別なダイアログが表示されて mok に登録したいパスワードを入力する。アプリケーションのインストール時に bios の設定が必要という概念が私の頭の中になくて分からなかった。これをやらないと virtualbox サービスが正常に起動しない。調べていると ubuntu 標準の virtualbox は壊れていて 3rd party の contrib なパッケージを使えという古い記事も出てくる。22.04 なら ubuntu 標準の virtualbox でも問題なく、mok の登録後に systemd から virtualbox サービスが起動することを確認した。再起動後の mok のダイアログは1度しか出てこないのでうっかり見逃してしまったら再設定しないといけない。次の cli で再設定できる。

sudo dpkg-reconfigure virtualbox-dkms

ubuntu のインストール

1時に寝て5時に起きて7時に起きた。今週も開発に集中していたのでバテ気味。時間がなくて2ヶ月以上行ってなかった散髪に行ってきた。前髪が目にかかって鬱陶しかったのでいつもよりも短くしてもらった。頭が軽いと日々の生活のストレスも軽減しそう。

ストレッチ

今日の開脚幅は開始前153cmで、ストレッチ後153cmだった。ストレッチ後の開脚幅はいつもとは計り方を変えたので数値は低くなっているが普段と同じぐらいのものだろう。すねの外側の筋は気にならないぐらいになったので復調したと言っていいだろう。今日は右股関節から太ももにかけての張りと右腰の張りが大きかった。今週も根を詰めて長時間オフィスで作業していたので座っているとかかる部位の負荷が高くなっていると想定される。納期が4月末なのでこの生活はもうしばらく続くといったところ。

dell マシンに ubuntu をインストール

いつも通りオフィスで会社の事務手続きをしながら、並行して 購入した dell マシン に ubuntu をインストールする。いまお仕事で開発しているアプリケーションの成果物は amd64 をターゲットにしているのでローカルに amd64 の開発環境がないとインフラ周りやコンテナ周りの検証に一手間かかる。この状況を解消するために早くローカルの dell マシンを置き換えて開発環境を構築したい。dvd-r から ubuntu 22.04.01 (サイズ超過で dvd-r におさまらなくて 22.04.02 は断念) のインストールを行う。bios で一時的に secure boot の機能を無効にしないと署名チェックで起動しなかった。インストール作業中に予期せぬ不具合が発生しました的なポップアップが2-3回発生したけど、無視して継続できた。インストールして普通に起動すればいいやと緩い感覚で進めた。いつもはパーティションも自分で設定したりしているけれど、今回は windows 領域を残してデュアルブートな環境にしてみた。ディスク領域もインストーラーから windows 領域を 160GiB、残りをすべて ubuntu 領域にするといった設定ができた。すごい簡単。余裕があるときに windows の wsl を使うのも試してみれればと思う。

rocky linux も悪くない

2時に寝て7時に起きた。夜うまく眠れなくて夜更かしした。3月になってしまったか。2月の記憶がほとんどない。

rocky linux をベースイメージとして使う

コンテナを作るときのデフォルトの os は、私の中では alpine だが、依存ライブラリの制約があって、いま作っている docker image は rockylinux をベースイメージに使う。rockylinux を docker pull した感覚は速いのでそんな大きなイメージでもない。minimal だと40数 MiB 程度のサイズ。すでに成果物として rpm パッケージがあり、その rpm が依存解決できないといけないため、rhel ベースの os をベースイメージにしないといけない。rpm パッケージに systemd の起動スクリプトなども入っているのでそのまま使えばいいかとも思ったのだけど普通には systemd は動かない。rockylinux のドキュメントにも systemd はこうやったら動くよと書いてあるものの、実際に起動してみるとエラーになる。細かくは追いかけていないけれど、cgroup v2 で未解決な問題があるらしく、cgroup v2 を使うカーネルでは systemd が動かないらしい。

コンテナを動かすプラットフォームがハイパーバイザーも兼ねるので systemd を動かす必要性はないけれど、サードパーティの起動スクリプトを自分たちで保守するのも嫌だなという印象はあるのでもしかしたら悩ましい問題なのかもしれない。

go 1.20.1 を使い始めた

0時に寝て何度か起きて7時半に起きた。あまりうまく眠れなかった。

go 1.20.1 へのアップグレード

リファクタリングの区切りがついたらやろうと思っていて遅れた。すでに 1.20.1 がリリースされている。先日 Go 1.20 リリースパーティ に参加して、いろいろ聞いていると改善されているところや新しい機能を試してみたいものがいくつかみつかった。単純にアップグレードするだけでも最大でビルド時間が10%短縮される可能性がある。1.18 と 1.19 で generics 対応でビルド時間が遅くなっていたのが 1.17 相当に改善されたらしい。やっぱり generics はコンパイラを複雑にするものなのでコンパイル時間が長くなる傾向があるんやなと話しを聞いていて思ったりもした。ついでに依存ライブラリなどのバージョンアップもまとめてやった。単体テストと結合テストが普通には揃ってきたのでバージョンアップなども安心して実行できる。

rocky linux のネットワーク設定

テスト環境に使っている rocky linux の ip アドレスを変更する必要があったので調べてみた。ちゃんとした公式のガイドが出てきてすごいなと感心した。

ip アドレスを確認するコマンドが ifconfig から ip に変わっていたり、ネットワーク設定のためのツールも私が知っているものとは全然変わってしまっている。

$ ip addr

Network Manager のサービスで設定されているようでそのための設定ファイルは次の場所に保存されていた。

/etc/NetworkManager/system-connections/my-device.nmconnection 

これを書き換えるツールとして nmtui という tui ツールと nmcli という cli ツールの2つがある。tui とか懐かしいなとか思いながら操作していた。ssh 経由で設定していたので cli でいきなり設定を反映するよりも tui で設定ファイルを書き換えて os を再起動するのがよいだろうと考えてやってみた。ドキュメントに書いてある通りに操作したら意図した ip アドレスを設定して変更できた。