GoでDockerの動的ポートマッピングサーバーを実装する

1つのホスト上に、NginxコンテナとPHPコンテナを合わせて1つとするアプリケーションが2つ以上存在する場合はどうすればいいでしょうか。ホストのグローバルIPアドレスに**複数のドメインからAレコードが紐付けられていれば、80や443ポートで競合が発生**してしまいます。

そのため動的ポートマッピングができるリバースプロキシサーバーが必要です。今回はそのサーバーをGoで実装してみました。また後述しますが、**この動的ポートマッピングはDocker-composeのnetwork機能とNginxなどを組み合わせても実現する**ことができます。

## Goで動的ポートマッピングを実装する
コード自体は非常にシンプルです。

- サーバーの起動
- コンテナリストの取得
- ポートの解決

また各アプリケーションのdocker-composeは以下のようになっており、ホスト上に紐付けるポートは動的にします。
portsを`"80:80"`のように`"ホスト:コンテナ"`とせず、**`"80"`と設定しておけばホスト側には動的なポートが設定**されます。

```docker-compose.yml
services:
  rp:
    image: nginx:latest
    ports:
      - "80"
    depends_on:
      - web
  web:
    image: node:latest
    ....
```

### 1. サーバーの起動
基本となるReverseProxyサーバーは、`"net/http/httputil"`の`ReverseProxy`を使うことで簡単に実装することができます。

```dypm/main.go
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があるため、そちらを利用すれば簡単にできます。

```dypm/lib/containers.go
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内で書き換えています。

```dypm/lib/director.go
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](https://qiita.com/reneice/items/20e981062b093264cd0a)