🦭

Goで実装したリバースプロキシでローカル環境で HTTPS 通信する

2023/08/11に公開

この記事では、Go で実装したリバースプロキシを使用して、ローカル環境で HTTPS 通信を実現する方法を紹介します。
今回使用したコードは以下のリポジトリにあります。
https://github.com/kngnkg/go-reverse-proxy

はじめに

docker-composeで構築したローカルの開発環境でも HTTPS で通信させたくなることがあります。実現する方法としては、リバースプロキシを使用する方法があります。

リバースプロキシを実装するには Nginx などを使用する方法が一般的ですが、今回は Go で実装してみます。Go の標準ライブラリである net/http/httputil パッケージの、 ReverseProxy を使用してリバースプロキシを実装できます。

構成

以下のような構成にします。

構成図

  • ブラウザ-リバースプロキシ間: HTTPS
  • Next.js コンテナ-リバースプロキシ間: HTTPS
  • リバースプロキシ-Next.js コンテナ/API コンテナ間: HTTP

手順

1. docker-compose.yml の作成

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を経由してwebappapiにアクセスしたいので、reverse-proxyのポートのみホストマシンにマッピングします。

2. Dockerfile の作成

# ./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 内で名前解決できるように、localhosthost.docker.internal の2つを指定します。

mkcert -cert-file ./cert/localhost.pem -key-file ./cert/localhost-key.pem localhost "host.docker.internal"

./certlocalhost.pemlocalhost-key.pem が作成されます。

4. main.go の作成

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 を使用することで、簡単にリバースプロキシを実装できます。

参考

https://pkg.go.dev/net/http/httputil#ReverseProxy

https://kido0617.github.io/go/2016-08-10-reverse-proxy/

GitHubで編集を提案

Discussion