Docekr学习笔记四构建并测试web应用程序

  1. 构建Sinatra应用程序创建sinatra工作目录,编写Dockerfile文件构建一个镜像,并利用该镜像生成容器应用。重点我们将讲述如何将多个容器连接形成应用栈。
    $ md sinatra && cd sinatra
    $ vi Dockerfile
    
    FROM ubuntu
    MAINTAINER Kael Wang zb55@foxmail.com
    ENV REFRESHED_AT 2016-12-12
    RUN apt-get 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"]
    
    $ docker build -t kaelwang/sinatra .
    Sending build context to Docker daemon 10.24 kB
    Step 1 : FROM ubuntu
     ---> 44f18eb3c13f
    Step 2 : MAINTAINER Kael Wang zb55@foxmail.com
     ---> Using cache
     ---> 0230268896a0
    Step 3 : ENV REFRESHED_AT 2016-12-12
     ---> Using cache
     ---> 4aad3f178a57
    Step 4 : RUN apt-get update
     ---> Using cache
     ---> 254a26583566
    Step 5 : RUN apt-get -y install ruby ruby-dev build-essential redis-tools
     ---> Using cache
     ---> 3a0a3f602557
    Step 6 : RUN gem install --no-rdoc --no-ri sinatra json redis
     ---> Using cache
     ---> 933b8b6d6d84
    Step 7 : RUN mkdir -p /opt/webapp
     ---> Using cache
     ---> 710d3db42f72
    Step 8 : EXPOSE 4567
     ---> Using cache
     ---> 01c8718f0607
    Step 9 : CMD /opt/webapp/bin/webapp
     ---> Running in 2acb0e4a3bc3
     ---> d9d3d801d9e1
    Removing intermediate container 2acb0e4a3bc3
    Successfully built d9d3d801d9e1
    
    docker run -d -p 4567 --name webapp -v $PWD/webapp:/opt/webapp kaelwang/sinatra
    19a8e0112a32e93505dcf49d2852f9035d5ecbbf4a3fe960269e78a1187af755
    
    $ docker logs webapp
    [2016-07-27 06:40:04] INFO  WEBrick 1.3.1
    [2016-07-27 06:40:04] INFO  ruby 2.3.1 (2016-04-26) [x86_64-linux-gnu]
    == Sinatra (v1.4.7) has taken the stage on 4567 for development with backup from WEBrick
    [2016-07-27 06:40:04] INFO  WEBrick::HTTPServer#start: pid=1 port=4567
    
    $ docker ps -a
    CONTAINER ID        IMAGE                      COMMAND                  CREATED             STATUS                   PORTS                         NAMES
    19a8e0112a32        kaelwang/sinatra           "/opt/webapp/bin/weba"   5 seconds ago       Up 3 seconds             0.0.0.0:32770->4567/tcp       webapp
    

    目前,我们已经建立了sinatra容器应用,此应用还很基础,指示接受输入参数,并将输入转化为JSON输出。现在,我们用curl命令来测试这个应用程序。

    $ curl -i -H 'Accept:application/json' -d 'name=Foo&status=Bar' http://localhost:32770/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: Wed, 27 Jul 2016 06:46:33 GMT
    Connection: Keep-Alive
    
    {"name":"Foo","status":"Bar"}
    

    我们可以看到,给sinatra应用程序传入了一些参数,并转化为了json散列后输出:{“name”:”Foo”,”status”:”Bar”}成功。

    我们添加一个服务,这个服务运行在另一个容器里,把当前的示例应用程序扩展为真正的应用栈。

  2. 构建Redis镜像和容器扩展sinatra应用程序,加入Redis后端数据库,并在Redis数据库中存储输入的参数。为了达到这个目的,要构建全新的镜像和容器运行redis数据库。之后,利用Docker的特性来关联两个容器。
    $ md redisdocker&&cd redisdocker
    $ vi Dockerfile
    
    FROM ubuntu
    MAINTAINER Kael Wang zb55@foxmail.com
    ENV REFRESHED_AT 2016-12-12
    RUN apt-get update
    RUN apt-get -y isntall redis-server redis-tools
    EXPOSE 6379
    ENTRYPOINT ["/usr/bin/redis-server"]
    CMD []
    
    $ docker build -t kaelwang/redis .
    Sending build context to Docker daemon 2.048 kB
    Step 1 : FROM ubuntu
     ---> 44f18eb3c13f
    Step 2 : MAINTAINER Kael Wang zb55@foxmail.com
     ---> Using cache
     ---> 0230268896a0
    Step 3 : ENV REFRESHED_AT 2016-12-12
     ---> Using cache
     ---> 4aad3f178a57
    Step 4 : RUN apt-get update
     ---> Using cache
     ---> 254a26583566
    Step 5 : RUN apt-get -y install redis-server redis-tools
     ---> Using cache
     ---> d0cf8437fb46
    Step 6 : EXPOSE 6379
     ---> Using cache
     ---> 7a8d1aa862b4
    Step 7 : ENTRYPOINT /usr/bin/redis-server
     ---> Running in c0ce2aeb9eda
     ---> dd4f375c2236
    Removing intermediate container c0ce2aeb9eda
    Step 8 : CMD 
     ---> Running in a386c0559e6c
     ---> e730c0e265d3
    Removing intermediate container a386c0559e6c
    Successfully built e730c0e265d3
    

    从这个镜像构建容器

    $ docker run -d -p 6379 --name redis kaelwang/redis
    6fdcfc399370872475d654563c24c54c3c9a1c67806a2d7eee7a2eaea8175269
    
    $ docker port redis
    6379/tcp -> 0.0.0.0:32771
    

    我们用本机redis测试一下容器里的redis服务是否工作

    $ redis-cli  -h 127.0.0.1 -p 32771
    127.0.0.1:32771> 
    
  3. Docker容器网络知识现在来更新sinatra应用程序,让其连接到redis并存储传入的参数。为此,需要能够与redis服务器对话,要做到这一点,可以有几种方法。第一种涉及Docker自己的网络栈。到目前为止,我们看到的Docker容器都是公开端口并绑定到本地网络接口的,这样可以吧容器里的服务在本地 Docker宿主机所在的外部网络上公开,除了这样方法,Docker这个特性还有种内部网络。在安装Docker时,会创建一个新的网络接口,名字是 docker0.每个Docker容器都会在这个接口上分配一个ip地址。
    $ ip a show docker0
    3: docker0: <broadcast,multicast,up,lower_up> mtu 9001 qdisc noqueue state UP group default 
        link/ether 02:42:3e:8e:ae:4d brd ff:ff:ff:ff:ff:ff
        inet 172.17.0.1/16 scope global docker0
           valid_lft forever preferred_lft forever
        inet6 fe80::42:3eff:fe8e:ae4d/64 scope link 
           valid_lft forever preferred_lft forever
    

    docker0是一个虚拟的以太网桥,用于连接容器和本地宿主机。docker0复合RFC1918的私有ip地址,范围是172.16.x.x到172.30.x.x同时Docker网络必须还要依赖防火墙和NAT规则才能允许被建立连接

    $ iptables -t nat -L -n
    Chain PREROUTING (policy ACCEPT)
    target     prot opt source               destination         
    DOCKER     all  --  0.0.0.0/0            0.0.0.0/0            ADDRTYPE match dst-type LOCAL
    
    Chain INPUT (policy ACCEPT)
    target     prot opt source               destination         
    
    Chain OUTPUT (policy ACCEPT)
    target     prot opt source               destination         
    DOCKER     all  --  0.0.0.0/0           !127.0.0.0/8          ADDRTYPE match dst-type LOCAL
    
    Chain POSTROUTING (policy ACCEPT)
    target     prot opt source               destination         
    MASQUERADE  all  --  172.17.0.0/16        0.0.0.0/0           
    MASQUERADE  tcp  --  172.17.0.3           172.17.0.3           tcp dpt:80
    MASQUERADE  tcp  --  172.17.0.4           172.17.0.4           tcp dpt:4567
    MASQUERADE  tcp  --  172.17.0.5           172.17.0.5           tcp dpt:6379
    
    Chain DOCKER (2 references)
    target     prot opt source               destination         
    DNAT       tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:80 to:172.17.0.3:80
    DNAT       tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:32770 to:172.17.0.4:4567
    DNAT       tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:32771 to:172.17.0.5:6379
    

    容器默认是无法访问的,从宿主网络与容器通信时,必须明确指定打开的端口,下面我们以DNAT即目标NAT这个规则为例,这个规则吧容器里的访问路由到Docker宿主机的32771端口。

  4. 通过Docker内网连接Redis
    $ docker inspect redis
    	.
    	.
    	.
        "NetworkSettings": {
        "Bridge": "",
        "SandboxID": "b4bb7703aa7d658f8309abbfcdf7fe575ac5faa3546805e33dc44be86e3932cb",
        "HairpinMode": false,
        "LinkLocalIPv6Address": "",
        "LinkLocalIPv6PrefixLen": 0,
        "Ports": {
            "6379/tcp": [
                {
                    "HostIp": "0.0.0.0",
                    "HostPort": "32771"
                }
            ]
        },
        "SandboxKey": "/var/run/docker/netns/b4bb7703aa7d",
        "SecondaryIPAddresses": null,
        "SecondaryIPv6Addresses": null,
        "EndpointID": "8e96a4e055c46a34331f3ae3b3c455ecdcddb6a16b2650fe53cdb638127e5255",
        "Gateway": "172.17.0.1",
        "GlobalIPv6Address": "",
        "GlobalIPv6PrefixLen": 0,
        "IPAddress": "172.17.0.5",
        "IPPrefixLen": 16,
        "IPv6Gateway": "",
        "MacAddress": "02:42:ac:11:00:05",
        "Networks": {
            "bridge": {
                "EndpointID": "8e96a4e055c46a34331f3ae3b3c455ecdcddb6a16b2650fe53cdb638127e5255",
                "Gateway": "172.17.0.1",
                "IPAddress": "172.17.0.5",
                "IPPrefixLen": 16,
                "IPv6Gateway": "",
                "GlobalIPv6Address": "",
                "GlobalIPv6PrefixLen": 0,
                "MacAddress": "02:42:ac:11:00:05"
            }
        }
    	.
    	.
    	.
    

    docker inspect命令展示Docker容器的细节,这里只显示网络信息。容器的ip地址为172.17.0.5,并使用网关172.17.0.1即 docker0。也可以看到容器6379端口被映射到本地宿主机32771端口。只是,因为运行在本地Docker宿主机上,所以不是一定要用映射的端 口,也可以直接使用Docker0分配的内网地址

    $ redis-cli  -h 172.17.0.5
    172.17.0.5:6379> 
    

    但是,通过这种方法连接两个容器,有两个大问题,第一,要在应用内对redis容器的ip地址做硬编程,第二,如果重启或容器意外中断后恢复,Docker会改变容器的ip地址。

  5. 引用容器名字让两个容器相连首先删掉原来我们构建的Sinatra和redis容器,然后重新构建一个新的Sinatra和redis容器
    $ docker stop redis sinatra
    $ docker rm redis sinatra
    

    实现连接

    $ docker run -d --name redis kaelwang/redis
    a843b4e5adaa3630a08ebdbe8828a6028fbf6331ba537f082f4484dff8a6b27b
    
    $ docker run -p 4567 --name webapp --link redis:db -t -i -v $PWD/webapp:/opt/webapp kaelwang/sinatra /bin/bash
    root@d205ff072682:/# cat /etc/hosts
    172.17.0.4      d205ff072682
    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.5      db a843b4e5adaa redis
    
    root@d205ff072682:/# env
    HOSTNAME=d205ff072682
    DB_NAME=/webapp/db
    DB_PORT_6379_TCP_PORT=6379
    TERM=xterm
    DB_PORT=tcp://172.17.0.5:6379
    DB_PORT_6379_TCP=tcp://172.17.0.5:6379
    LS_COLORS=rs=0:di=01;34:ln=01;...(此处信息较多省略)
    DB_ENV_REFRESHED_AT=2016-12-12
    PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
    REFRESHED_AT=2016-12-12
    PWD=/
    DB_PORT_6379_TCP_ADDR=172.17.0.5
    DB_PORT_6379_TCP_PROTO=tcp
    SHLVL=1
    HOME=/root
    _=/usr/bin/env
    root@d205ff072682:/# 
    

    –link标志创建了两个容器间的父子相连,这个标志需要两个参数,一个是要连接的容器名字,另一个是连接后容器的别名。此处别名是db,别名让 我们可以访问公开的信息,而无需关注底层容器的名字,连接让父容器有能力访问子容器,并且把子容器的一些连接细节分享给父容器。这些细节有助于配置应用程 序并使用这个连接。

    连接也能得到一些安全上的额好处。启动redis容器时,并没有使用-p标志公开redis的端口。通过父容器直接访问任意子容器的公开端口。且只 有使用的–link标志连接到这个容器的容器才能连接到这个端口,容器的端口不需要对本地宿主机公开,现在我们已经拥有了一个非常安全的模型,在这个模 型里,容器话的应用程序限制了可被攻击的界面,减少了公开暴露的网络。也可以吧多个容器连接到一起,比如一个redis示例服务于多个web应用服务器。 出于一些原因,我们可以在启动Docker守护进程时加上–icc=false标志,关闭所有没有连接的容器间的通讯,这样强制Docker只允许有连 接的容器之间相互通信。

    我们通过env查看环境变量发现有一些以别名DB开头的,Docker在连接webapp和redis容器时,自动创建了这些以DB开头的环境变量。 这些自动创建的环境变量包含以下信息。

    • 子容器的名字
    • 容器里运行的服务所使用的协议,ip和端口
    • 容器里运行的不同的服务所指定的协议,ip和端口
    • 容器里由Docker设置的环境变量的值。
  6. 使用容器连接来通信
    1. 使用环境变量里的一些连接信息。
    2. 使用DNS和/etc/hosts信息。

    应用程序会在本地查找名叫db的主机,找到/etc/hosts文件里的项解析到正确的ip地址,这也解决了硬编码IP地址的问题,因为hosts文件会随着你的Docker启动时自动更新相应信息。

    这个web应用程序栈由以下几部分组成

    • 一个运行Sinatra和web服务器容器
    • 一个Redis数据库容器
    • 两个容器间的一个安全连接

    这样就可以在本地环境构建,复制,迭代开发用于生产的应用程序,甚至很多复杂的多层应用程序。

分类: Docker

0 条评论

发表回复

Avatar placeholder

您的邮箱地址不会被公开。 必填项已用 * 标注