Docker でコンテナ名をサブドメインにしてアクセス可能にする方法

この記事は [リクルートライフスタイル Advent Calendar 2015 - Qiita](http://qiita.com/advent-calendar/2015/recruitlifestyle) の11日目です。
ホットペッパービューティーで開発を担当しているmoremagicです。
アプリ基盤チームで開発者のみなさんを助けるためのお仕事をしております。

# はじめに
みなさん Docker 使ってますか？
僕も最近使い始めたのですが、めちゃくちゃ便利ですよね！

でもコンテナの数が増えてくると、ポート番号を覚えておく必要があり大変です。。
また、ポート番号でしか どのサービスにアクセスしているかが分からないのも ﾜｶﾘﾆｸい。
ポート番号でアクセスするコンテナを切り替えるのではなく、
コンテナ名が そのままドメイン名に入ったらわかりやすいのではないでしょうか。

ということで今回は Dockerでコンテナ名をサブドメインにしてアクセスするようにしたお話をしてみます。

# コンテナ名をサブドメインにするとはどういうこと？
たとえば、、、
Dockerを起動しているサーバの名前が `example.com`
コンテナ名 `dev-tomcat`
ポート 8080 を 49000 ポート にフォワーディングしているとした場合

通常、上記の dev-tomcatコンテナ の 8080ポートにアクセスしたい場合
8080ポートは 49000ポートにフォワードしているため
以下のようにアクセスします。

http://example.com:49000/

起動したコンテナが1つだけなら別に大変でもなんでもないのですが
コンテナの数や、サービスが増えてきたら覚えておくのが大変。
間違えたら違うサービスに接続しちゃいますしね。

なので、人間にもわかりやすく

http://dev-tomcat-8080.example.com

上記のようにアクセスできるようにしてしまおうというお話です。
これならコンテナ名と実際コンテナ内で動作しているポートさえ覚えておけばアクセス可能ですね！

# 作戦
サーバ内で コンテナ名をサブドメインにしてアクセス可能にする 名前解決コンテナ を作成し起動します。
httpプロキシで実現しているため、httpプロトコルのみサブドメイン解決が可能になります。

![docker-proxy.png](https://qiita-image-store.s3.amazonaws.com/0/102603/49be938f-e44e-6d1a-bf39-8b6382c9a868.png)


名前解決コンテナを起動するだけで定期的にDockerのコンテナ情報をコンテナ内に保管します。
コンテナを起動するたびに設定ファイルを書き換えたりすることなく コンテナ名でアクセスができます。

# 前提
Docker環境があり、Dockerが動作しているホストが名前解決できること。
Docker の RemoteAPI を有効にしていること
※Docker Version:1.8.1 で検証済み

# 手順
1. Docker で Imageを作成する
2. Image からコンテナを起動する
3. 名前解決を楽しむ

## Docker で Imageを作成する
以下のファイルを準備してDocker Imageを作成します
Dockerfile以外は Dockerfileの記述に合わせて適当なフォルダに配置してください。

### Dockerファイル
Dockerfile はこんな感じです。
ubuntu:14.04 をベースにして redis, python3, nginx をインストール
起動シェルを作って起動時に叩くようしています

```lang:Dockerfile
FROM ubuntu:14.04

～省略～

# python3 install
RUN apt-get install -y python3 python3-pip && apt-get clean
RUN pip3 install redis
ADD redis/regist.py /usr/sbin/regist.py
RUN chmod +x  /usr/sbin/regist.py

# redis
RUN apt-get update && apt-get install -fy redis-server
ADD redis/redis.conf /etc/redis/redis.conf

# nginx install
RUN apt-get -y install nginx lua-nginx-redis && apt-get clean
ADD nginx/default /etc/nginx/sites-available/
ADD nginx/rewrite.lua /etc/nginx/sites-available/
ADD nginx/cert/ /etc/nginx/cert/

# 起動シェル作成
RUN printf '#!/bin/bash \n\
/usr/bin/redis-server & \n\
/usr/sbin/regist.py > /dev/null & \n\
/etc/init.d/nginx start \n\
/etc/init.d/nginx reload \n\
/usr/sbin/sshd -D \n\
tail -f /var/null \n\
' >> /etc/service.sh \
    && chmod +x /etc/service.sh

EXPOSE 22 6379 80 443
CMD /etc/service.sh

```

### nginx
動作の要、nginx の設定ファイルはこんな感じです。
80,443でサーバが待ち構えて、リライト→プロクシするように設定してます。

```lang:default
server {
    listen 80 default_server;
    listen [::]:80 default_server ipv6only=on;

    root /usr/share/nginx/html;
    index index.html index.htm;

    server_name localhost;
    location / {
        set $upstream "";
        rewrite_by_lua_file /etc/nginx/sites-available/rewrite.lua;

        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-Host $http_host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_pass http://$upstream;

        client_max_body_size 200M;
    }
}

server {
    listen 443;
    server_name localhost;

    ssl on;
    ssl_certificate /etc/nginx/cert/ssl.crt;
    ssl_certificate_key /etc/nginx/cert/ssl.key;
    ssl_session_timeout 5m;

    proxy_set_header Host $http_host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    location / {
        set $upstream "";
        rewrite_by_lua_file /etc/nginx/sites-available/rewrite.lua;

        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-Host $http_host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_pass https://$upstream;

        client_max_body_size 200M;
    }
}
```

### rewrite.lua
ホスト名から コンテナ名、ポート番号を取得。
redisに問い合わせて 実際のコンテナを返却します

```lang:rewrite.lua
local routes = _G.routes

if routes == nil then
    routes = {}
    ngx.log(ngx.ALERT, "[[[Route cache is empty.]]")
end

local container_name = string.sub(ngx.var.http_host, 1, string.find(ngx.var.http_host, "%.")-1)
local route = routes[container_name]
if route == nil then
    local Redis  = require "nginx.redis"
    local client = Redis:new()

    client:set_timeout(1000)
    local ok, err = client:connect("127.0.0.1", 6379)
    if not ok then
        ngx.log(ngx.ERR, "************ Redis connection failure: " .. err)
        return
     end

    route = client:get(container_name)
end

ngx.log(ngx.ALERT, route)

-- fallback to redis for lookups
if route ~= nil then
    ngx.var.upstream = route
    routes[container_name] = route
else
    ngx.log(ngx.ALERT, "=ng=[[[route null]]]")
    ngx.exit(ngx.HTTP_NOT_FOUND)
end
```

### python
Redisにコンテナ情報を蓄えていく陰の立役者です。
DockerのRemoteAPI経由でコンテナ情報を3秒ごとに更新し続けます。
ずーっとループで動かしてますがもうちょっと方法があるかも。。。

```lang:regist.py
#!/usr/bin/python3

import os
import sys
import time
import json
import redis
import urllib.request

DOCKER_HOST = os.getenv('DOCKER_HOST')
REDIS_ADDR = '127.0.0.1'
REDIS_PORT = 6379


def redisDump():
  conn = redis.Redis(host=REDIS_ADDR, port=REDIS_PORT)
  for key in conn.keys():
    print(key)
    print(conn.get(key))
  return conn.keys()

def addData(datas):
  conn = redis.Redis(host=REDIS_ADDR, port=REDIS_PORT)
  for key in set(list(datas.keys()) + list(conn.keys())):
    if isinstance(key, bytes):
      key = key.decode('utf-8')
    if key in datas:
      conn.set(key, datas[key])
    else:
      conn.delete(key)

def getContainers():
  response = urllib.request.urlopen('http://' + DOCKER_HOST + '/containers/json?all=1')
  jsonData = json.loads(response.read().decode('utf-8'))

  datas = {}
  for con in jsonData:
    name = con['Names'][-1][1:]
    con_ip = getIpAddress(con['Id'])

    for port in con['Ports']:
      key = name + '-' + str(port['PrivatePort'])
      value=con_ip + ':' + str(port['PrivatePort'])
      datas[key] = value

  return datas


def getIpAddress(con_id):
  response = urllib.request.urlopen('http://' + DOCKER_HOST + '/containers/' + con_id + '/json')
  jsonData = json.loads(response.read().decode('utf-8'))
  #print(json.dumps(jsonData))
  ret = jsonData['NetworkSettings']['IPAddress']
  return ret

while True:
  addData(getContainers())
  print( redisDump() )
  sys.stdout.flush()
  time.sleep(3)
```

### イメージのビルド
Dockerfile が格納してあるフォルダに移動して以下コマンドをたたきます！

`# docker build -t docker-discovery .`

## Image からコンテナを起動する
先ほど作成したImageを起動します。
コンテナ内からホストで動作しているDocker の DockerRemoteAPIを使用してコンテナ情報を取得しますので起動時におまじないをかけてあげます。

`# docker run -d -p 80:80 -p 443:443 -e DOCKER_HOST=<Docker Host のIPアドレス>:<RemoteAPI port> --name docker-discovery docker-discovery`

1．80、443ポートをフォワードしておきます。
2．環境変数 DOCKER_HOST を指定してます

これだけで名前解決ができるようになります！

# 名前解決を楽しむ
docker が動作しているホストが example.com だとすると
以下のURLで該当のコンテナに接続することができます。

`http://{コンテナ名}-{公開しているコンテナ内のポート番号}.example.com`


ためしにTeamCityコンテナを起動してみましょう。
`docker run -dP --name teamcity moremagic/teamcity`

このコンテナは 8111 ポートを外部公開しますので 以下URLでアクセスできます。
http://teamcity-8111.example.com

![image](https://qiita-image-store.s3.amazonaws.com/0/102603/570abcb2-da67-6b16-f568-3cf2184f721b.png)

Docker を楽しんでください！