Edited at

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

More than 1 year has passed since last update.

この記事は リクルートライフスタイル Advent Calendar 2015 - Qiita の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

名前解決コンテナを起動するだけで定期的に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 をインストール

起動シェルを作って起動時に叩くようしています


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でサーバが待ち構えて、リライト→プロクシするように設定してます。


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に問い合わせて 実際のコンテナを返却します


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秒ごとに更新し続けます。

ずーっとループで動かしてますがもうちょっと方法があるかも。。。


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

Docker を楽しんでください!