読者です 読者をやめる 読者になる 読者になる

at kaneshin

Free space for me.

Goで書いたWebをUnix domain socketで公開する方法

Golang

Unix domain socketをGoのWebアプリケーションで使用する方法です。tcpによるリッスンの情報はたくさんありますが、ソケットを介した方法があまりなかったので紹介します。

TL;DR

成果物は→kaneshin/playground/go/unixsocket

Unix domain socketの置き場

特に用意していない場合に、ソケットファイルの置き場を作成します。 /tmpを経由するのはセキュリティの都合上よろしくないので/var/runを経由するようにします。 /var/runへの追加はsystemdの機能でテンポラリなディレクトリを作成するようにします。まずは/etc/tmpfiles.dに設定ファイルを追加しておきます。

$ cat /etc/tmpfiles.d/gopher.conf
d /var/run/gopher 0755 [UID] [GID] -

/var/run/gopherというディレクトリを作成するようにしています。 ※[UID], [GID]は各自で設定してください。

設定ファイルを登録して、再起動をかけます。

$ systemd-tmpfiles --create /etc/tmpfiles.d/gopher.conf
$ systemctl daemon-reload

これでUnix domain socketの置く場所作成は完了です。

nginx configuration

ほぼ定型文です。今回は/var/run/gopher/go.sockをリッスンします。

upstream backend {
    server unix:/var/run/gopher/go.sock;
}

server {
    listen      80;
    server_name go.example.com;
    root        /var/www/html;

    location / {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_redirect off;
        proxy_pass http://backend/;
    }
}         

アプリケーションコード

GoでUnix domain socketをリッスンするのは非常に簡単です。

func main() {
    mux := http.NewServeMux()

    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "<h1>It works!</h1>\n")
    })

    mux.HandleFunc("/header", func(w http.ResponseWriter, r *http.Request) {
        b, err := json.MarshalIndent(r.Header, "", "  ")
        if err != nil {
            fmt.Fprintf(w, err.Error())
        } else {
            w.Header().Add("Content-Type", "application/json")
            fmt.Fprintf(w, "%v\n", string(b))
        }
    })

net/httpが持つ ServerMux にハンドラを登録したあと、ソケットを介してリッスンします。

   listener, err := net.Listen("unix", sock)
    if err != nil {
        fmt.Fprintln(os.Stderr, err.Error())
        os.Exit(1)
    }
    defer func() {
        if err := listener.Close(); err != nil {
            log.Println("Error:", err.Error())
        }
    }()
    shutdown(listener)
    if err := http.Serve(listener, mux); err != nil {
        fmt.Fprintln(os.Stderr, err.Error())
        os.Exit(1)
    }
}

割り込みで終了のシグナルが来た場合にリスナーをクローズ (listener.Close()) するようにしておきます。

func shutdown(listener net.Listener) {
    c := make(chan os.Signal, 2)
    signal.Notify(c, os.Interrupt, syscall.SIGTERM)
    go func() {
        s := <-c
        log.Println("Got signal:", s)
        if err := listener.Close(); err != nil {
            log.Println("Error:", err.Error())
        }
        os.Exit(1)
    }()
}

クローズしないとソケットファイルが生き残り続けます。

起動

go buildなどでバイナリにして起動していただければそのまま動くと思います。 502 Bad Gatewayの文字が現れたときはエラーログを見てもらえればいいのですが、大抵はパーミッションによるエラーだと思います。

パーミッションの解決方法(例)

www-dataユーザで実行することを仮定すると

$ cat /etc/tmpfiles.d/gopher.conf
d /var/run/gopher 0755 www-data www-data -

としてsystemdへ登録しなおし、その後にgo buildで作成したバイナリをwww-dataで起動してあげればそのまま動くはずです。

$ go build -o /tmp/bin
$ sudo -u www-data /tmp/bin

おわりに

今回のコードは kaneshin/playground/go/unixsocket にあります。

毎回、「リッスン」を使用する度に思うのですが、「リッスン」は既に動詞なのに「リッスンする」という言葉を使用しないと日本語では伝わりにくいですよね。