1つのホスト上に、NginxコンテナとPHPコンテナを合わせて1つとするアプリケーションが2つ以上存在する場合はどうすればいいでしょうか。ホストのグローバルIPアドレスに複数のドメインからAレコードが紐付けられていれば、80や443ポートで競合が発生してしまいます。
そのため動的ポートマッピングができるリバースプロキシサーバーが必要です。今回はそのサーバーをGoで実装してみました。また後述しますが、この動的ポートマッピングはDocker-composeのnetwork機能とNginxなどを組み合わせても実現することができます。
Goで動的ポートマッピングを実装する
コード自体は非常にシンプルです。
- サーバーの起動
- コンテナリストの取得
- ポートの解決
また各アプリケーションのdocker-composeは以下のようになっており、ホスト上に紐付けるポートは動的にします。
portsを"80:80"
のように"ホスト:コンテナ"
とせず、"80"
と設定しておけばホスト側には動的なポートが設定されます。
services:
rp:
image: nginx:latest
ports:
- "80"
depends_on:
- web
web:
image: node:latest
....
1. サーバーの起動
基本となるReverseProxyサーバーは、"net/http/httputil"
のReverseProxy
を使うことで簡単に実装することができます。
package main
func main() {
rp := &httputil.ReverseProxy{Director: dypm.Director}
server := http.Server{
Addr: ":80",
Handler: rp,
}
if err := server.ListenAndServe(); err != nil {
log.Fatal(err.Error())
}
log.Println("Start dynamic port mapping!")
}
2. コンテナリストの取得
次はDockerのUnixドメインソケットを叩き、起動されているコンテナリストを取得します。
コンテナの取得自体はDockerのgo-sdkがあるため、そちらを利用すれば簡単にできます。
package dypm
type Containers []*Container
type Container struct {
Name string
PublicPort uint16
}
// Dockerのgo-sdkを使い、コンテナリストを取得する
func getContainers() (*Containers, error) {
var containers Containers
ctx := context.Background()
cli, err := client.NewEnvClient()
if err != nil {
return nil, errors.Wrap(err, "Failed to create docker client")
}
listContainers, err := cli.ContainerList(ctx, types.ContainerListOptions{})
if err != nil {
return nil, errors.Wrap(err, "Failed to list containers")
}
for _, container := range listContainers {
if len(container.Names) > 0 && len(container.Ports) > 0 {
containers = append(containers, &Container{
Name: container.Names[0],
PublicPort: container.Ports[0].PublicPort,
})
}
}
return &containers, nil
}
// コンテナ名からポートを解決する
// Nginxのポートが80や443のように複数ある場合などは、現在対応できていません。(が、難しくないはずです。)
func resolveContainerPort(containerName string) string {
containers, err := getContainers()
if err != nil {
log.Printf("%+v\n", errors.Wrapf(err, "Failed to get containers. request container name: %s", containerName))
return ""
}
for _, container := range *containers {
// docker-composeのnginxコンテナの名前をrpとしている場合、_rp_1という文字列が後ろに付加される
if fmt.Sprintf("/%s_rp_1", containerName) == container.Name {
return fmt.Sprint(container.PublicPort)
}
}
return ""
}
3. ポートの解決
次にアクセスしてきたホスト名を元にコンテナ名からポートを取得します。
そしてそれをDirector内で書き換えています。
package dypm
func Director(request *http.Request) {
request.URL.Scheme = "http"
containerName := resolveContainerName(strings.Split(request.Host, ":")[0])
if len(containerName) > 0 {
port := resolveContainerPort(containerName)
if len(port) > 0 {
request.URL.Host = ":" + port
log.Print(request.URL.Host)
}
}
}
func resolveContainerName(requestHost string) string {
// my-applicationディレクトリにあるdocker-composeを立ち上げるとmyapplicationというネットワークが作成される
hostMap := map[string]string{
"my-application": "myapplication",
}
for host, containerName := range hostMap {
if strings.HasPrefix(requestHost, host) {
return containerName
}
}
return ""
}
これで動的ポートマッピングサーバーの完成です
以上で、動的ポートマッピングができるRPサーバーを開発できました。
またこれらを実現する別の手法として、docker-composeのnetworkオプションを使うという手法があります。
Dockerは各アプリケーションのネットワークを簡単に接続することができるため、複数のDockerアプリケーションの親となるネットワークを作り、そこにNginxなどを用いてドメインを元に各アプリケーションホストへ流すことができます。
今回はGoで実装しましたが、docker-composeで作るかどちらでもいいような気がします。何かドメイン固有の処理をしたい場合は、h2oのmrubyなどでも限界があるため、上記で実装したようにGoで作る方が望ましいかも知れません。
参考:docker-compose で別の docker-compose.yml で作ったコンテナとリンクする (ネットワークを繋げる) - Qiita