LoginSignup
1
3

More than 5 years have passed since last update.

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

Posted at

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

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3