Goで実装したリバースプロキシでローカル環境で HTTPS 通信する
この記事では、Go で実装したリバースプロキシを使用して、ローカル環境で HTTPS 通信を実現する方法を紹介します。
今回使用したコードは以下のリポジトリにあります。
はじめに
docker-composeで構築したローカルの開発環境でも HTTPS で通信させたくなることがあります。実現する方法としては、リバースプロキシを使用する方法があります。
リバースプロキシを実装するには Nginx などを使用する方法が一般的ですが、今回は Go で実装してみます。Go の標準ライブラリである net/http/httputil
パッケージの、 ReverseProxy を使用してリバースプロキシを実装できます。
構成
以下のような構成にします。
- ブラウザ-リバースプロキシ間: HTTPS
- Next.js コンテナ-リバースプロキシ間: HTTPS
- リバースプロキシ-Next.js コンテナ/API コンテナ間: HTTP
手順
docker-compose.yml
の作成
1. version: "3.9"
services:
reverse-proxy:
hostname: reverse-proxy
build:
context: .
dockerfile: ./reverse-proxy/Dockerfile
volumes:
- .:/workspace:cached
ports:
- "1443:443"
- "3000:444"
webapp:
hostname: webapp
build:
context: .
dockerfile: ./webapp/Dockerfile
volumes:
- .:/workspace:cached
api:
hostname: api
build:
context: .
dockerfile: ./api/Dockerfile
volumes:
- .:/workspace:cached
hostname
hostname
を設定することで、Docker Network 内で名前解決ができるようになります。例えば、webapp
コンテナにアクセスする際は、webapp:3000
と指定します。
ポートマッピング
ports:
- "1443:443"
- "3000:444"
ホストマシンからreverse-proxy
を経由してwebapp
やapi
にアクセスしたいので、reverse-proxy
のポートのみホストマシンにマッピングします。
Dockerfile
の作成
2. # ./reverse-proxy/Dockerfile
FROM golang:1.20.3
ENV GO111MODULE on
WORKDIR /workspace/reverse-proxy
CMD ["go", "run", "main.go"]
その他のコンテナの Dockerfile
については割愛します。
3. 証明書と秘密鍵の作成
mkcertを使用して、ホストマシン上で証明書と秘密鍵を作成します。Docker Network 内で名前解決できるように、localhost
と host.docker.internal
の2つを指定します。
mkcert -cert-file ./cert/localhost.pem -key-file ./cert/localhost-key.pem localhost "host.docker.internal"
./cert
に localhost.pem
と localhost-key.pem
が作成されます。
main.go
の作成
4. net/http/httputil
パッケージの ReverseProxy を使用してリバースプロキシを実装します。
package main
import (
"log"
"net/http"
"net/http/httputil"
"strconv"
"sync"
)
func runProxyServer(port int, forwardHost string) {
director := func(request *http.Request) {
request.URL.Scheme = "http"
request.URL.Host = forwardHost
}
rp := &httputil.ReverseProxy{Director: director}
server := http.Server{
Addr: ":" + strconv.Itoa(port),
Handler: rp,
}
if err := server.ListenAndServeTLS("../cert/localhost.pem", "../cert/localhost-key.pem"); err != nil {
log.Fatal(err.Error())
}
}
func main() {
var wg sync.WaitGroup
wg.Add(2)
go func() {
runProxyServer(443, "api:8080")
wg.Done()
}()
go func() {
runProxyServer(444, "webapp:3000")
wg.Done()
}()
wg.Wait()
}
runProxyServer()
プロキシサーバーを起動する関数です。
- 第2引数の
forwardHost
には、リバースプロキシを経由してアクセスしたいコンテナのホスト名を指定します。 -
director
でリクエストを転送先のホスト (forwardHost
) に書き換えます。転送先との通信は HTTP なので、URL のスキームをhttp
に書き換えます。 -
server.ListenAndServeTLS()
で先ほど作成した証明書と秘密鍵を指定し、 HTTPS でサーバーを起動します。
main()
webapp
(Next.js コンテナ) とapi
(API コンテナ) の両方を HTTPS で通信させるために、別ゴルーチンで2つのサーバーを起動しています。
アプリケーションの設定
Next.js と API の設定です。本題ではないので、詳しくはGitHubを参照してください。
1. API コンテナ
Hello World !
を返すだけの API です。
// ./api/main.go
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "https://localhost:3000")
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
fmt.Fprint(w, "Hello World !")
})
http.ListenAndServe(":8080", nil)
}
2. Next.js コンテナの設定
証明書のインストール
サーバー側でリクエストする際に証明書が必要なので、Dockerfile
内 でインストールします。
# ./webapp/Dockerfile
COPY cert/localhost.pem /usr/local/share/ca-certificates/localhost.crt
RUN update-ca-certificates
自己証明書の検証を無効化
自己証明書を使用しているので、リクエスト時に署名検証に失敗しエラーになります。.env.local
に以下の環境変数を設定して証明書の検証を無効化します。本番環境などでは設定しないでください。
# ./webapp/.env.local
NODE_TLS_REJECT_UNAUTHORIZED=0
コンポーネントの作成
今回はapp routerを使用します。
クライアント側でリクエストを送信する ./components/ClientComponent.tsx
です。
// ./components/ClientComponent.tsx
"use client";
import * as React from "react";
const fetchFromClient = async () => {
// クライアント側で実行されるので、`localhost:1443`を指定します。
const res = await fetch("https://localhost:1443");
if (!res.ok) {
throw new Error("Failed to fetch data");
}
const data = await res.text();
return data;
};
export const ClientComponent: React.FC = () => {
const [data, setData] = React.useState<string | null>(null);
const onClick = async () => {
const data = await fetchFromClient();
setData(data);
};
return (
<>
<p>From Client: {data}</p>
<button onClick={onClick} className="bg-blue-500 text-white py-2 px-4 rounded">fetch</button>
</>
);
};
サーバー側でリクエストを送信する ./app/page.tsx
です。
// ./app/page.tsx
import { ClientComponent } from "../components/ClientComponent";
const fetchFromServer = async () => {
// サーバー側で実行されるので、`host.docker.internal:1443`を指定します。
const res = await fetch("https://host.docker.internal:1443");
if (!res.ok) {
throw new Error("Failed to fetch data");
}
const data = await res.text();
return data;
};
export default async function Home() {
const data = await fetchFromServer();
return (
<main className="flex items-center justify-center">
<div>
<p>From Server: {data}</p>
<ClientComponent />
</div>
</main>
);
}
動作確認
1. コンテナの起動
docker-compose up -d
2. ブラウザでアクセス
https://localhost:3000にアクセスします。
サーバー側でフェッチできていることが確認できます。
3. ボタンをクリック
クライアント側でフェッチできることが確認できます。
まとめ
リバースプロキシを使用してローカル環境での HTTPS 通信を実現する方法を紹介しました。Go の標準ライブラリである net/http/httputil
パッケージの ReverseProxy を使用することで、簡単にリバースプロキシを実装できます。
参考
Discussion