at kaneshin

Free space for me.

Golang で書いた Web アプリケーションを UNIX ドメインソケットで公開

net/http パッケージで使用される ListenAndServe 関数は tcp による Listen のため、 UNIX ドメインソケットで Listen するには自前で準備する必要があります。

func (srv *Server) ListenAndServe() error {
    addr := srv.Addr
    if addr == "" {
        addr = ":http"
    }
    ln, err := net.Listen("tcp", addr)
    if err != nil {
        return err
    }
    return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
}

src/net/http/server.go - The Go Programming Language

UNIX ドメインソケット

ソケットファイルの置き場を作成します。/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 に登録して再起動します。

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

これで UNIX ドメインソケットの作成場所は設定完了です。

NGINX

ほぼ定型文です。今回は /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/;
    }
}         

Web アプリケーション

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

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

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

    listener, err := net.Listen("unix", "/var/run/gopher/go.sock")
    if err != nil {
        log.Fatalf("error: %v", err)
    }
    defer func() {
        if err := listener.Close(); err != nil {
            log.Printf("error: %v", err)
        }
    }()

    shutdown(listener)
    if err := http.Serve(listener, mux); err != nil {
        log.Fatalf("error: %v", err)
    }
}

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

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

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

起動

バイナリにして起動すればそのまま動くと思います。"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 にあります。