🕒

Go な Web Server を Systemd で良い感じに動かすメモ

2022/08/16に公開2

ざっと設定するタイミングがあったのでメモがわりに残しておく。
環境は以下の通り。

lib version
Ubuntu 22.04
Go 1.19
systemd 249

Go で Web Server 書く

動作確認するだけのコードなのでなるだけシンプルに書く。んで、 Port 80 を Listen したいけどプロセスは root で走らせたくないので、 systemd でポート開いてそれをプログラム側で使うようにする。
ディスクリプタごにょる部分は coreos/go-systemd がドンピシャなのでこれを使う。

package main

import (
	"errors"
	"fmt"
	"log"
	"net"
	"net/http"

	"github.com/coreos/go-systemd/activation"
)

func main() {
	// Mux を設定する
	m := http.NewServeMux()
	m.HandleFunc("/", handler)
	// Server を設定する
	s := &http.Server{
		Handler: m,
	}
	// systemd で開いたディスクリプタを取得する
	l, err := newListener()
	if err != nil {
		log.Fatal(err)
	}
	//
	log.Println("starting server.")
	log.Fatal(s.Serve(l))
}

// リクエストが来るたびログに書き込む
func handler(w http.ResponseWriter, r *http.Request) {
	// stderr に書き込む
	log.Printf("stderr: method=%v url=%v", r.Method, r.URL)
	// stdout に書き込む
	fmt.Printf("stdout: method=%v url=%v\n", r.Method, r.URL)
	//
	fmt.Fprintln(w, "ok")
}

// Systemd からソケットをひとつも受け取れなかった場合のエラー
var ErrNoListenerFound = errors.New("can't find any listeners.")

// Systemd からソケットを受け取る。
// 複数受け取った場合は先頭のものを返す。
func newListener() (net.Listener, error) {
	ls, err := activation.Listeners()
	if err != nil {
		return nil, err
	}
	// ひとつも受け取れなかった場合でもエラーにならないため
	// len で別途確認する必要がある
	if len(ls) == 0 {
		return nil, ErrNoListenerFound
	}
	return ls[0], nil
}

これをビルドして適当な場所に置く。今回は /usr/local/bin/test-server に置くことにしてみた。

$ go mod init example.com
$ go mod tidy
$ go build -o test-server
$ sudo mv ./test-server /usr/local/bin/

systemd の Unit 書く

今回は .socket.service のふたつの Unit ファイルを /etc/systemd/system に配置する。
それぞれ test-server.sockettest-server.service という名前で作成する。今後 systemctl で操作するときにこの名前で行うので分かりやすに名前にしておくほうが良い。

# test-server.socket

[Unit]
Description=Socket for test server.

[Socket]
ListenStream=0.0.0.0:80
NoDelay=true

[Install]
WantedBy=sockets.target
# test-server.service

[Unit]
Description=test server.
After=network.target
Requires=test-server.socket

[Service]
ExecStart=/usr/local/bin/test-server
User=www-data
Group=www-data
Restart=on-failure

[Install]
WantedBy=multi-user.target

いろいろ書いてあるけど、そこまで意味不明な項目はないし分からなくてもググれば速攻で答え出てくるから問題ないと思う。
.service 側で UserGroup を指定することで root 以外で走らせることができる。が、その状態で Go 側で Port 80 を Listen しようとすると Permission denied で怒られてしまうのでそこを .socket で担ってる。

ファイルが配置できたら起動・停止をテストしてみる。

$ sudo systemctl start test-server
$ ps aux | grep test-server
www-data    2081  0.0  0.2 1085020 4692 ?        Ssl  11:35   0:00 /usr/local/bin/test-server

ちゃんと起動していたら停止も。

$ sudo systemctl stop test-server
Warning: Stopping test-server.service, but it can still be activated by:
  test-server.socket

なんか Warning でた。今止めたのは test-server.service だけで、 test-server.socket はアクティブなままらしい。試しにこの状態でブラウザからアクセスすると、ちゃんとページは表示されるしプロセスも復活してくる。なので、止めるときは .socket のほうをちゃんと指定する。

$ sudo systemctl stop test-server.socket

エラーなくプロセスが止まっていれば ok 。
サーバ起動時に自動起動してほしければ enable して登録しておくと良い。

$ sudo systemctl enable test-server.socket test-server.service

ログを吐く

現状だとログまわりを何も設定していないので journald に記録されている。試しに Unit でフィルタリングしてみると内容が確認できるはず。

$ journalctl -u test-server
...

そして Ubuntu 22.04 時点だとデフォルトの設定では journald に送られた内容は rsyslog にも送られるようになっている。 /var/log/syslog を確認してみると、他から送られてきたメッセージの中に test-server からのものが確認できると思う。
見つからない場合は journald の設定が変更されている可能性があるので /etc/systemd/journald.conf を確認してみると良いかも。

このままだといろんな情報がごちゃまぜになってて追いかけづらいので、必要なものだけフィルタリングして個別のファイルに書き出してみる。 /etc/rsyslog.d/ 以下に設定ファイルを追加すると起動時に読み込まれるようになっているので、ファイル名に適当な優先順位を加えて追加する。

# /etc/rsyslog.d/40-test-server.conf

:programname, isequal, "test-server" /var/log/test-server.log

& stop

今回は単純に test-server.service から送られてきたもの全てを /var/log/test-server.log に書き出すようにした。プラス & stop をつけることでフィルタリングしたものがココで止まるようにしている。
ファイルが保存できたら rsyslog を再起動する。

$ sudo systemctl restart rsyslog

エラーなく再起動できたら、ブラウザで一度アクセスしてみたあとに /var/log/ を確認してみると test-server.log が作られて吐き出された内容が書かれているはず。

ログをローテートさせる

ここまで出来たらあとは logrotate 使ってログをぐりぐりさせるだけ。
/etc/logrotate.d/test-server という名前で適当な設定を書く。

# for test-server

/var/log/test-server.log
{
    rotate 7
    daily
    nocreate
    missingok
    notifempty
    compress
    postrotate
        systemctl restart test-server
    endscript
}

rotate の数や daily or weekly なのかは実際のアクセス数やリソースに合わせて変えてもらえればと思う。

念の為、テストで走らせてみる。

sudo logrotate /etc/logrotate.conf --debug

test-server まわりでエラーがなければ ok 。あとは数日開けて確認してみると良いと思う。

おわりに

やってることシンプルだけど文字に起こすと長い。。。
次からこーゆーのはスクラップにしたほうが相性が良い気がした。

Discussion

坦々狸坦々狸

.socket 使うとroot 以外のPermission denied 回避出来るのですね
スクラップだと気付けなかったと思うので記事に書いてもらえて良かったです😆

higehige

そう言っていただけると書いた甲斐がありました。ありがとうございます。
これからもなるだけ記事で書いていこうと思います :-)