PaaSに「VPN機能付きGoバイナリ」をポン置きする技術 (Tailscale / tsnet)
RailwayやFly.ioなどのPaaSは、OSの設定を意識せずに手軽にデプロイできるため、内部向けのWebアプリ運用において非常に便利です。
しかし、社内ツールなどを運用する場合、認証やIPアドレス制限といった「セキュリティ」が課題になります。私のチームではTailscaleを利用してVPNによるアクセス制限を行っていますが、PaaS環境とTailscaleの組み合わせには特有の難しさがあります。
本記事では、Go言語のライブラリである tsnet を使い、PaaS上に「VPN機能を内包した単一バイナリ」をデプロイするという、シンプルかつ強力な解決策を紹介します。
PaaS × Tailscale の課題
通常、サーバーをTailscaleネットワークに参加させる場合、ホストOSにTailscaleクライアント(デーモン)をインストールします。しかし、PaaS環境では以下の理由からこれが困難です。
- OSレベルの操作制限: そもそもOSにログインしてインストール作業ができない。
-
特権権限の欠如:
root権限や、ネットワークインターフェース(TUNデバイス)の作成権限がないことが多い。
これらを解決するために「Subnet Router」を用意したり、コンテナに Tailscale を同居させる方法もありますが、構成が複雑になりがちです。
もっとシンプルに、「アプリのバイナリを1つ置くだけ」 で解決する方法があります。それが tsnet です。
tsnet とは何か?
tsnet は、Tailscaleが公式に提供しているGo言語用ライブラリです。
- 特徴: Goのプロセス自体が「Tailscaleノード」として振る舞います。
- メリット: OSのroot権限やTUNデバイスを必要とせず、ユーザー空間(Userland)だけで動作します。
つまり、OS側にTailscaleをインストールする必要はなく、Goのコード内で完結します。これにより、PaaSのような制約の多い環境でも、環境変数さえ渡せればTailscaleネットワークに参加できるようになります。
活用例:PocketBase × tsnet
私がよく利用する PocketBase(Go製のBaaS)は、この構成と相性が抜群です。
PocketBaseを使えばフロントエンドもバックエンドも1つのGoバイナリにまとめることができますが、そこに tsnet を組み込むことで、「外部からは一切アクセスできないが、社内VPNからは繋がるフルスタックアプリ」 を、バイナリ一つをPaaSに置くだけで実現できます。
実装:GoコードにTailscaleを組み込む
使い方は非常にシンプルです。標準の net/http を使う感覚で実装できます。
-
tsnet.Serverを作成する -
Server.Listen()でリスナーを取得する -
http.Serve()にそのリスナーを渡す
以下は、"Hello from tsnet" を返すだけの最小構成のサンプルコードです。
package main
import (
"fmt"
"log"
"net/http"
"os"
"tailscale.com/tsnet"
)
func main() {
srv := &tsnet.Server{
Hostname: "my-tsnet-app", // Tailscale用のホスト名
}
defer srv.Close()
// Tailscaleネットワーク上でのみアクセス可能なリクエストを待ち受ける
ln, err := srv.Listen("tcp", ":80")
if err != nil {
log.Fatal(err)
}
defer ln.Close()
// ハンドラの設定
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello from tsnet!")
})
log.Printf("listening on tailnet at http://%s", srv.Hostname)
// http.ListenAndServe ではなく、tsnetのリスナーを使って Serve する
log.Fatal(http.Serve(ln, mux))
}
ポイントは、http.ListenAndServe の代わりに、tsnetが提供する ln (net.Listener) を使っている点だけです。これだけで、通信経路はTailscaleによって暗号化・保護され、コードの中身は通常のWebサーバー実装のまま維持できます。
動作確認
まずはローカルで動かしてみましょう。環境変数が設定されていない場合、初回起動時に認証用URLが表示されます。
go run .
...
2025/11/24 18:58:56 To start this tsnet server, restart with TS_AUTHKEY set, or go to: https://login.tailscale.com/a/10e01b23345bc
ターミナルに表示されたURLへブラウザでアクセスし、認証を行います。成功すると、TailscaleのAdmin Consoleに my-tsnet-app というデバイスが追加されます。
別のTailscale参加端末から、MagicDNS(例: http://my-tsnet-app)でアクセスしてレスポンスが返ってくれば成功です。
curl http://my-tsnet-app
Hello from tsnet!
PaaSへのデプロイ
デプロイは標準的なGoのDockerfileでOKです。環境変数 TS_AUTHKEY にキーを渡すだけで動作しますが、データの永続化設定によって挙動が異なります。用途に合わせて使い分けてください。
1. Ephemeral(使い捨て)運用
最も手軽な方法です。デプロイのたびに新しいデバイスとして認識され、IPも変わります。
- 設定: Auth Keyを Ephemeral (使い捨て) に設定。
- コード:
Dir指定なし。
2. Stateful(永続化)運用
IPアドレスを固定したい場合に利用します。
- 設定: PaaS側でVolumeを作成し(例:
/data)、コンテナにマウント。 - コード:
Dirにマウントしたパスを指定。
srv := &tsnet.Server{
Hostname: "my-tsnet-app",
AuthKey: os.Getenv("TS_AUTHKEY"),
Dir: "/data/tsnet",
}
Dir を指定すると、初回認証後のトークンがそこにキャッシュされるため、2回目以降は TS_AUTHKEY が無効になっても再接続が可能になります
まとめ
tsnet を利用することで、PaaSの便利なデプロイ体験を損なうことなく、セキュアな内部向けアプリケーションを構築できます。
- OS設定不要、サイドカー不要
- 環境変数を一つ足すだけでVPNに参加
- Goバイナリ一つで完結するポータビリティ
社内ツールの置き場所に困っているGoエンジニアの方は、ぜひ試してみてください。
Discussion