2016年11月17日星期四

Docker_008:测试动态网站

环境:MAC OS  X 10.12.1 + Docker 1.12.1

Sinatra 是个开发框架,基于 Sinatra 开发的应用接收输入参数,然后将其转化为 JSON 输出。

1. 基于 Sinatra 框架的应用
$ cd /Users/maping/mygit/dockerbook-code/code/5/sinatra

$ cat Dockerfile

FROM ubuntu:16.04
MAINTAINER James Turnbull "james@example.com"
ENV REFRESHED_AT 2014-06-01

RUN apt-get -yqq update
RUN apt-get -y install ruby ruby-dev build-essential redis-tools
RUN gem install --no-rdoc --no-ri sinatra json redis

RUN mkdir -p /opt/webapp

EXPOSE 4567

CMD [ "/opt/webapp/bin/webapp" ]

$ cat webapp/bin/webapp
#!/usr/bin/ruby

$:.unshift(File.expand_path(File.join(File.dirname(__FILE__), "..", "lib")))

require 'app'

App.run!

$ docker build -t jamtur01/sinatra .

$ chmod +x webapp/bin/webapp

$ docker run -d -p 4567 --name webapp -v $PWD/webapp:/opt/webapp jamtur01/sinatra
注意,这里没有在 docker run 命令行上提供要运行的命令,而是定义在 Dockerfile 中。

$ docker logs -f webapp // 持续输出容器的 STDERR 和 STDOUT 里的内容

$ docker top webapp // 查看容器里正在运行的进程

$ docker port webapp 4567
访问 http://localhost:<port>

$ curl -i -H 'Accept: application/json' -d 'name=Foo&status=Bar' http://localhost:<port>/json
输出如下:
HTTP/1.1 200 OK
Content-Type: text/html;charset=utf-8
Content-Length: 29
X-Xss-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Server: WEBrick/1.3.1 (Ruby/2.3.1/2016-04-26)
Date: Sat, 19 Nov 2016 14:02:52 GMT
Connection: Keep-Alive

{"name":"Foo","status":"Bar"}

2. 扩展 Sinatra 应用,加入 Redis 后端数据库。
$ cd /Users/maping/mygit/dockerbook-code/code/5/sinatra/redis

$ cat Dockerfile
FROM ubuntu:16.04
MAINTAINER James Turnbull "james@example.com"
ENV REFRESHED_AT 2014-06-01

RUN apt-get -yqq update && apt-get -yqq install redis-server redis-tools

EXPOSE 6379

ENTRYPOINT ["/usr/bin/redis-server"]
CMD []

$ docker build -t jamtur01/redis .

$ docker run -d -p 6379 --name redis jamtur01/redis

$ docker port redis 6379

2.1 安装 Redis 客户端,我的 MAC 已安装,验证可以连到 Redis Server。
$ redis-cli -h 127.0.0.1 -p < port >
127.0.0.1:32769>

2.2 如何让 Sinatra 应用容器与 Redis 容器通信?
方式1:通过容器的公开端口绑定到本地网络接口。
方式2:使用 Docker 内部网络
安装 Docker 时,会创建一个新的网络接口:docker0。每个容器都会在这个接口上分配一个 IP 地址。docker0 是一个虚拟的以太网桥,用于连接容器和本地宿主网络。Docker 每创建一个容器就会创建一组互联的网络接口,一端作为容器里的 eth0 接口,另一端类似 vethec6a 这种名字,作为宿主机的一个端口。每个 veth*接口绑定到 docker0 网桥,Docker 创建了一个虚拟子网,这个子网由宿主机和所有的Docker 容器共享。

$ ip a show docker0
可以看到所有 Docker 容器的网关地址。

$ iptables -t nat -L -n

启动并进入一个容器,查看 IP 地址
$ docker run -it ubuntu bash
root@63725436b96f:/# ip a shwow eth0
root@63725436b96f:/# traceroute www.baidu.com

以上两种方式看上去都可行,但是要在应用程序里对 Redis 容器的 IP 地址硬编码;另外,如果重启容器,Docker 会改变容器的 IP 地址。
那么应该怎么办呢?
谢天谢地,Docker 有个叫做 link 的功能非常有用,它可以把多个 Docker 容器连接起来。

先删除旧的容器
$ docker stop redis
$ docker rm redis
$ docker run -d --name redis jamtur01/redis
$ docker stop webapp
$ docker rm webapp


2.3 让 Sinatra 应用容器和 Redis 容器互联
$ docker run -p 4567 --name webapp --link redis:db -it -v $PWD/webapp:/opt/webapp jamtur01/sinatra /bin/bash

说明:
(1)--name 给容器命名
(2)--link 创建两个容器之间的父子连接,一个是要连接的容器名字,另一个是连接后容器的别名。别名可以让我们访问公开的信息,而无需关注底层容器的名字。这里是 redis:db。
(3)启动 Redis 容器时,没有使用 -p 公开端口,因为不需要这样做。父容器直接访问任意子容器的公开端口。并且,只有使用 --link 连接到子容器的父容器才能连接到这个端口。子容器的端口不需要对宿主机公开,这是一个非常安全的模型。

可以让这个 Redis 实例服务于多个 Web 应用程序,如下:
$ docker run -p 4567 --name webapp2 --link redis:db -it -v $PWD/webapp:/opt/webapp jamtur01/sinatra /bin/bash
$ docker run -p 4567 --name webapp3 --link redis:db -it -v $PWD/webapp:/opt/webapp jamtur01/sinatra /bin/bash

注意,被连接的容器必须运行在同一个 Docker 宿主机上,不同宿主机上运行的容器无法连接。

Docker 在父容器的以下两个地方写入了连接信息:
(1)/etc/hosts 文件中
root@7844b851130b:/# cat /etc/hosts
输出如下:
127.0.0.1    localhost
::1    localhost ip6-localhost ip6-loopback
fe00::0    ip6-localnet
ff00::0    ip6-mcastprefix
ff02::1    ip6-allnodes
ff02::2    ip6-allrouters
172.17.0.2    db c6aaeb59594c redis // redis 容器的 IP 地址和主机名
172.17.0.3    7844b851130b // 容器自己的 IP 地址和主机名
(2)包含连接信息的环境变量中
root@7844b851130b:/# env
输出如下:
HOSTNAME=7844b851130b
DB_NAME=/webapp/db
DB_PORT_6379_TCP_PORT=6379
TERM=xterm
DB_PORT=tcp://172.17.0.2:6379
DB_PORT_6379_TCP=tcp://172.17.0.2:6379
LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.Z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.jpg=01;35:*.jpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=00;36:*.au=00;36:*.flac=00;36:*.m4a=00;36:*.mid=00;36:*.midi=00;36:*.mka=00;36:*.mp3=00;36:*.mpc=00;36:*.ogg=00;36:*.ra=00;36:*.wav=00;36:*.oga=00;36:*.opus=00;36:*.spx=00;36:*.xspf=00;36:
DB_ENV_REFRESHED_AT=2014-06-01
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
REFRESHED_AT=2014-06-01
PWD=/
DB_PORT_6379_TCP_ADDR=172.17.0.2
DB_PORT_6379_TCP_PROTO=tcp
SHLVL=1
HOME=/root
no_proxy=localhost、*.local、169.254/16
DB_ENV_no_proxy=localhost、*.local、169.254/16
_=/usr/bin/env

Docker 在连接 webapp 和 redis 容器时,自动创建了这些以 DB 开头的环境变量。以 DB 开头是因为 DB 是创建连接时的别名。

当容器重启时,这两个地方的变量值会自动修改,这样就避免了程序硬编码的问题。

在容器里启动应用程序
root@
7844b851130b:/# nohup /opt/webapp/bin/webapp &

在宿主机上测试应用程序
$ docker port webapp
$ curl -i -H 'Accept: application/json' -d 'name=Foo&status=Bar' http://localhost:<port>/json

输出如下:
HTTP/1.1 200 OK
Content-Type: text/html;charset=utf-8
Content-Length: 33
X-Xss-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Server: WEBrick/1.3.1 (Ruby/2.3.1/2016-04-26)
Date: Sat, 19 Nov 2016 14:11:56 GMT
Connection: Keep-Alive



{"name":"Foo","status":"Bar"}


访问 http://localhost:<port>

2.4 程序是如何连接 Redis 的? 
$ cat webapp_redis/lib/app.rb 
require "rubygems"
require "sinatra"
require "json"
require "redis"

class App < Sinatra::Application

      redis = Redis.new(:host => 'db', :port => '6379')

      set :bind, '0.0.0.0'

      get '/' do
        "<h1>DockerBook Test Redis-enabled Sinatra app</h1>"
      end

      get '/json' do
        params = redis.get "params"
        params.to_json
      end

      post '/json/?' do
        redis.set "params", [params].to_json
        params.to_json
      end
end

应用程序会在本地查找名叫 db 的主机,找到 /etc/hosts 文件里的项并正确解析。
这种方式称为使用本地 DNS 方法,注意这里硬编码了 redis 端口。
还有一种方法是读取环境变量。
require "uri"
......
uri = URI.parse(ENV['DB_PORT'])
redis = Redis.new(:host => uri.host, :port => uri.port)
......

没有评论: