Web应用随时可能被攻击者利用来夺取整个主机的权限,这是很常见也是很恐怖的一件事。为了更高的安全性,就需要将不同应用之间进行隔离(尤其是在这些应用属于不同的用户的情况下),然而这种隔离的实现一直是个挑战。到目前为止,隔离性的实现方法已经有了很多,然而它们要么太过昂贵(时间的层面以及资源的层面),要么太过复杂(无论对开发者还是对管理员)。
本文将讨论如何让“容器化”的Python Web应用跑在安全的沙箱里,严格的坚守在其各自的环境中(当然,除非你指定它们与其他应用进行“连接”)。我将一步一步的介绍如何创建一个Docker容器,如何用这个容器来跑我们的Python Web应用,以及如何用一个Dockerfile来描述整个构建过程以实现完整的自动化。
Docker项目提供了一些可以搭配使用的上层工具,这些工具基于Linux内核的一些功能创建。整个项目的目标是帮助开发者和系统管理员门无痛的迁移应用(以及它们所涉及的所有依赖项),让应用在各种系统和机器上都能欢快的跑起来。
实现这个目标的关键在于一个叫做docker容器的运行环境,这个环境实际上是一个具备安全属性的LXC(Linux Containers)。容器的创建使用了Docker镜像,Docker镜像可以手动敲命令创建,也可以通过Dockerfiles实现自动化创建。
注:关于Docker的基础知识(守护进程、CLI、镜像等等),可参阅本系列的篇文章Docker Explained: Getting Started。
新版本的Docker(译注:本文撰写时间为2013年12月17日,当时的Docker新版本为0.7.1)可以在Ubuntu/Debian和CentOS/RHEL等多个Linux发行版上部署(你也可以使用DigitalOcean上现成的Docker镜像,该镜像基于Ubuntu 13.04创建)。
下面快速介绍一下Ubuntu上的安装流程。
更新系统:
sudo aptitude update
sudo aptitude -y upgrade
检查系统是否支持aufs:
sudo aptitude install linux-image-extra-`uname -r`
往apt-key添加Docker仓库的密钥(用于软件包的验证):
sudo sh -c "wget -qO- https://get.docker.io/gpg | apt-key add -"
往aptitude软件源添加Docker仓库:
sudo sh -c "echo deb http://get.docker.io/ubuntu docker main\
> /etc/apt/sources.list.d/docker.list"
添加之后再更新一次系统:
sudo aptitude update
后,下载并安装Docker:
sudo aptitude install lxc-docker
Ubuntu默认的防火墙(UFW)的默认设置是拒绝一切转发(forwarding),但是Docker需要转发,所以也需要设置一下UFW。
用nano编辑器打开UFW配置文件:
sudo nano /etc/default/ufw
找到DEFAULT_FORWARD_POLICY
这一行,将
DEFAULT_FORWARD_POLICY="DROP"
替换为:
DEFAULT_FORWARD_POLICY="ACCEPT"
按 CTRL+X 再按 Y 键,保存退出。
后,把UFW重启一下:
sudo ufw reload
在正式开始之前,我们还是先复习一下上次在基础篇中介绍过的一些基本命令。
一般来说,Docker守护进程在你安装完成了之后就已经在后台运行,等待接收来自Docker命令行的指令。不过有的时候我们也需要手动启动Docker守护进程:
sudo docker -d &
Docker命令行的基本语法如下:
sudo docker [option] [command] [arguments]
注:Docker的运行需要sudo权限。
下面列出了目前可用的Docker命令(译注:可以参考InfoQ中文站文章深入浅出Docker(二):Docker命令行探秘):
attach 附着到一个运行的容器上
build 从一个Dockerfile建立镜像
commit 将一个变更后的容器创建为一个新镜像
cp 在容器和本地文件系统之间复制文件/目录
create 创建一个新的容器
diff 检测容器文件系统的变更
events 从服务器获取实时事件
exec 在一个运行中的容器内执行命令
export 将一个容器的文件系统输出为tar压缩包
history 显示一个镜像的历史
images 列出镜像列表
import 从tarball压缩包导入内容以创建一个文件系统镜像
info 显示系统信息
inspect 返回容器或镜像的底层信息
kill 杀死一个运行中的容器
load 从一个tar压缩包或STDIN加载一个镜像
login 登入Docker注册表(Docker registry)
logout 从Docker注册表登出
logs 抓取一个容器的日志
network 管理Docker网络
pause 暂停一个容器内的所有进程
port 列出该容器的所有端口映射或指定端口映射
ps 列出容器列表
pull 从注册表拉取一个镜像或仓库
push 往注册表推送一个镜像或仓库
rename 重命名容器
restart 重启容器
rm 删除一个或多个容器
rmi 删除一个或多个镜像
run 在一个新的容器中运行一条命令
save 将镜像保存至tar压缩包
search 在Docker Hub搜索镜像
start 启动一个或多个容器
stats 显示容器资源使用情况的实时信息流
stop 停止一个运行中的容器
tag 向注册表标记一个镜像
top 显示一个容器下运行的进程
unpause 恢复运行一个容器里所有被暂停的进程
update 更新容器的资源
version 显示Docker版本信息
volume 管理Docker卷
wait 阻塞对指定容器的其他调用方法,直到容器停止后退出阻塞。
我们已经完成了Docker的安装,也熟悉了基本的命令,现在可以为我们的Python WSGI应用创建Docker容器了。
注:本章介绍的方法主要是练习用,并不适合于生产环境。生产环境下适用的自动化流程将在后续章节中介绍。
Docker的run指令会基于Ubunt镜像创建一个新的容器。接下来,我们要用 -t 标识给这个容器附着(attach)一个终端,并运行一个 bash 进程。
我们将暴露这个容器的80端口用于从外部访问。以后在更加复杂的环境中,你可能需要给多个实例做负载均衡,把不同的容器“连接”起来,再用一个反向代理容器去访问它们。
sudo docker run -i -t -p 80:80 ubuntu /bin/bash
注:运行这个命令时,Docker可能需要先下载一个Ubuntu镜像,下载完毕后才创建新容器。
注意:你的终端会“附着”(attach)在新创建的容器上。要与容器分离并回到之前的终端访问点,可以按 CTRL+P 接着 CTRL+Q 执行脱离操作。“附着”在一个Docker容器上,基本上相当于从一个VPS内部访问另一个VPS。
从脱离的状态想要回到附着的状态,需要执行如下步骤:
sudo docker ps
列出所有运行中的容器
sudo docker attach [id]
完成当前终端到该容器的附着
注意:我们在容器内部做的一切操作都将仅限于在容器内部执行,对宿主机是完全没有影响的。
要在容器内部署Python WSGI应用(以及我们所需要的工具),首先我们需要对应的软件仓库。然而在Docker默认的Ubuntu镜像里并没有提供仓库(Docker的设计者认为这样有利于保持事物的简化),因此我们需要给我们这个打底镜像添加Ubuntu的universe软件仓库:
echo "deb http://archive.ubuntu.com/ubuntu/ $(lsb_release -sc) main universe" >> /etc/apt/sources.list
更新一下软件列表:
apt-get update
再给我们的容器安装一些必要的工具:
apt-get install -y tar \
git \
curl \
nano \
wget \
dialog \
net-tools
build-essential
本文将用一个简单的Flask应用作为示范。如果你用的是其他框架也没关系,安装部署的方法都是一样的。
再提醒一次:以下所有的命令都是在容器内部执行的,不会影响到宿主机。你可以想象成自己在一个全新的VPS上进行操作。
安装Python和pip:
# 安装pip依赖:setuptools
apt-get install -y python python-dev python-distribute python-pip
安装我们的应用之前,还是让我们再确认一下所有的依赖都已经就绪。首先是我们的框架——Flask。
因为我们已经装好了pip,所以就直接用pip来安装Flask:
pip install flask
装好了Flask,创建一个“my_application”文件夹:
mkdir my_application
cd my_application
注:如果你想直接部署自己的应用(而不是这里的示范应用),可以参看下面的“小贴士”部分。
我们的示范应用是一个单页面的“Hello World” Flask应用。下面用nano来创建app.py:
nano app.py
把下面这些内容复制到新创建的文件里:
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "Hello World!"
if __name__ == "__main__":
app.run()
按 CTRL+X 再按 Y 键,保存退出。
或者,你也可以使用“requirements.txt”来定义应用的依赖项(比如Flask)。我们还是用nano来创建文件:
nano requirements.txt
在文件里输入你所有的依赖项(下面只列出两个,如果你需要别的请自行添加):
flask
cherrypy
按 CTRL+X 再按 Y 键,保存退出。
注:你可以用pip来生成自定义的依赖项列表。具体的操作方法可以参考这篇Common Python Tools: Using virtualenv, Installing with Pip, and Managing Packages。
后,我们这个应用的文件组织结构是这样的:
/my_application
|
|- requirements.txt # 描述依赖项的文件
|- /app # 应用模块(你的应用应该在这个目录下)
|- app.py # WSGI文件,里面应该包含“app”的实例名称(callable)
|- server.py # 可选,用于运行应用服务器(CherryPy)
注:关于“server.py”,请参阅下面的章节“配置我们的Python WSGI应用”。
注意:上面这些应用的文件、目录都是在容器内部创建的。如果你要在宿主机上自动化构建镜像(这个过程将在下面有关Dockerfile的章节中介绍),则你的宿主机上放置Dockerfile的目录下也需要同样的文件结构。
上述步骤描述了在容器内创建应用目录的过程。然而在真实场景下,我们往往需要从软件仓库拉取源代码。
要把你的软件仓库复制到容器内部,有几个方法可以实现。下面介绍其中的两个:
# 方法1
# 用git下载源代码
# 用法:git clone [源代码所在的URL]
# 示范:
git clone https://github.com/mitsuhiko/flask/tree/master/examples/flaskr
# 方法2
# 下载源代码压缩文件
# 用法:wget [源代码压缩文件所在的URL]
# 示范:(用真实的URL替换掉下面这个假的)
wget http://www.github.com/example_usr/application/tarball/v.v.x
# 解压缩文件
# 用法:tar vxzf [文件名 .tar (.gz)]
# 示范:(用真实的文件名替换掉下面这个假的)
tar vxzf application.tar.gz
# 用pip下载安装应用依赖
# 下载 requirements.txt (可以用 pip freeze output 生成),再用pip全部安装:
# 用法:curl [requirements.txt 文件的URL] | pip install -r -
# 示范:(用真实的URL替换掉下面这个假的)
curl http://www.github.com/example_usr/application/requirements.txt | pip install -r -
要运行这个应用,我们需要一个Web服务器。运行这个WSGI应用的Web服务器需要安装在代码所在的同一台容器中,作为该Docker容器运行的进程。
注:我们在示范中将使用CherryPy自带的HTTP Web服务器,这是一个比较简单而且可以用在生产环境的选择。你也可以用Gunicorn甚至uSWGI(可以让它们跑在Nginx的后面),我们其他的教程中介绍过这种用法。
用pip下载安装CherryPy:
pip install cherrypy
创建“server.py”,用于服务“app.py”里面的Web应用:
nano server.py
把下面的内容复制粘贴到server.py里:
# 导入应用的语法:
# from app import application
# 示范:
from app import app
# 导入 CherryPy
import cherrypy
if __name__ == '__main__':
# 挂载应用
cherrypy.tree.graft(app, "/")
# 从默认服务器上分离
cherrypy.server.unsubscribe()
# 实例化一个新的服务器对象
server = cherrypy._cpserver.Server()
# 配置该服务器对象
server.socket_host = "0.0.0.0"
server.socket_port = 80
server.thread_pool = 30
# SSL相关配置
# server.ssl_module = 'pyopenssl'
# server.ssl_certificate = 'ssl/certificate.crt'
# server.ssl_private_key = 'ssl/private.key'
# server.ssl_certificate_chain = 'ssl/bundle.crt'
# 订阅这个服务器对象
server.subscribe()
# 启动服务器引擎
cherrypy.engine.start()
cherrypy.engine.block()
完成!现在我们就有了一个“Docker化”的Python Web应用,安全的跑在自己专属的沙箱里。只要输入下面的一行命令,它就可以给成千上万个客户端请求提供服务:
python server.py
这是让服务器在前台运行的指令。按下 CTRL+C 终止运行。如果想在后台运行服务器,可输入下面的指令:
python server.py &
后台运行的应用需要用进程管理器(比如htop)来终止运行(kill或stop)。
注:有关CherryPy上跑Python应用的配置,可参阅这篇教程:How to deploy Python WSGI apps Using CherryPy Web Server。
简单的测试一下应用的运行状态(以及端口的分配状态):在浏览器中访问 http://[容器所在的VPS的IP地址] ,应该能够看到“Hello World!”。
上面简单的说过,手动创建容器的这个方法并不适合用于生产环境的部署。生产环境里应该用Dockerfile进行构建流程自动化。
我们已经知道了如何在容器内部进行外部资源的下载和安装,那么Dockerfile其实也是一样的原理。一个Dockerfile定义了Docker要如何生成一个镜像,这个镜像可以直接用来跑我们的Python应用。
先了解一下Dockerfile的基本功能。
Dockerfile是一种脚本文件,其中包含了一系列顺序执行的命令,Docker通过执行这些命令就可以创建一个新的Docker镜像。这极大的方便了部署。
Dockerfile一般会先用 FROM 命令定义一个打底的镜像,然后执行一系列的动作,动作全部执行完毕之后就形成了终的镜像,并将完成的镜像提交给宿主机。
使用:
# 在当前位置用Dockerfile创建一个镜像
# 将生成的镜像标记为 [name] (比如nginx)
# 示范:sudo docker build -t [name] .
sudo docker build -t nginx_img .
注:我们还有一篇专门介绍dockerfile的文章可供查阅:Docker Explained: Using Dockerfiles to Automate Building of Images
### Add
从宿主机复制文件到容器
### CMD
设置要执行或者要发给ENTRYPOINT的默认命令
### ENTRYPOINT
设置容器内默认要启动的应用
### ENV
设置环境变量(key = value)
### EXPOSE
暴露一个端口
### FROM
设置打底镜像(base)
### MAINTAINER
设置Dockerfile的作者/所有者信息
### RUN
执行一条命令并提交执行后的(容器)镜像
### USER
设置从镜像运行容器的用户名
### VOLUME
从宿主机加载一个目录给容器
### WORKDIR
设置CMD运行时所在的目录
在当前路径下用nano编辑器创建Dockerfile:
sudo nano Dockerfile
注:下面的内容需要按顺序添加到Dockerfile中。
Dockerfile的基本项包括 FROM 原始镜像(比如Ubuntu)以及维护者姓名 MAINTAINER:
############################################################
# 创建Python WSGI应用容器的Dockerfile
# 基于Ubuntu
############################################################
# 设置Ubuntu为打底镜像
FROM ubuntu
# 文件作者/维护者
MAINTAINER Maintaner Name
# 添加软件资源库的URL
RUN echo "deb http://archive.ubuntu.com/ubuntu/ $(lsb_release -sc) main universe" >> /etc/apt/sources.list
# 更新资源列表
RUN apt-get update
RUN apt-get install -y tar git curl nano wget dialog net-tools build-essential
注:上述的有些工具可能你用不到,不过为了以防万一还是都装进来先。
一些Python需要的工具(pip)好还是都先装起来。你的框架(WAF)和Web服务器(WAS)都需要有它们才能安装。
RUN apt-get install -y python python-dev python-distribute python-pip
部署应用可以使用Docker的 ADD 命令直接复制源代码,也可以用 REQUIREMENTS 文件来一步到位。
注:如果你打算用一个文件描述所有的代码位置,可以参考下面的文件结构。
文件结构示范
/my_application
|
|- requirements.txt # 描述依赖项的文件
|- /app # 应用模块(你的应用应该在这个目录下)
|- app.py # WSGI文件,里面应该包含“app”的实例名称(callable)
|- server.py # 可选,用于运行应用服务器(CherryPy)
这个文件结构的创建过程在前面的章节中已经介绍过,这里不再赘述。总之,以上述文件结构为例,则再给Dockerfile末尾添加如下内容,将源代码复制到容器内:
ADD /my_application /my_application
如果源代码是公网的Git仓库,则可以使用如下内容:
RUN git clone [你的源码仓库URL]
接下来,再从 requirements.txt 导入所有的依赖项:
# 用pip来下载安装 requirements.txt 里面的东西
RUN pip install -r /my_application/requirements.txt
# 暴露端口
EXPOSE 80
# 设置CMD运行的默认路径
WORKDIR /my_application
# 要运行的默认命令
# 该命令在新容器创建时开始执行
# 比如启动CherryPy来运行服务
CMD python server.py
现在,整个Dockerfile看起来应该是这样的:
############################################################
# Dockerfile to build Python WSGI Application Containers
# Based on Ubuntu
############################################################
# Set the base image to Ubuntu
FROM ubuntu
# File Author / Maintainer
MAINTAINER Maintaner Name
# Add the application resources URL
RUN echo "deb http://archive.ubuntu.com/ubuntu/ $(lsb_release -sc) main universe" >> /etc/apt/sources.list
# Update the sources list
RUN apt-get update
# Install basic applications
RUN apt-get install -y tar git curl nano wget dialog net-tools build-essential
# Install Python and Basic Python Tools
RUN apt-get install -y python python-dev python-distribute python-pip
# Copy the application folder inside the container
ADD /my_application /my_application
# Get pip to download and install requirements:
RUN pip install -r /my_application/requirements.txt
# Expose ports
EXPOSE 80
# Set the default directory where CMD will execute
WORKDIR /my_application
# Set the default command to execute
# when creating a new container
# i.e. using CherryPy to serve the application
CMD python server.py
按 CTRL+X 再按 Y 键,保存退出。
在之前的基础教程部分我们提到过,Dockerfile的工作方式利用到了 docker build 命令。
我们通过Dockerfile指示docker从包含源代码的路径复制内容到容器内部,因此在构建之前务必要确认Dockerfile与代码路径的相对位置。
这样一个Docker镜像可以快速创建起来一个可以运行我们的Python应用的容器,我们需要做的只是输入这样一行指令:
sudo docker build -t my_application_img .
我们把这个镜像命名为 my_application_img 。为要从这个镜像启动一个新的容器,只需要输入下面的命令:
sudo docker run -name my_application_instance -p 80:80 -i -t my_application_img
然后就可以在浏览器里输入你VPS的IP地址,访问应用了。
本站文章版权归原作者及原出处所有 。内容为作者个人观点, 并不代表本站赞同其观点和对其真实性负责,本站只提供参考并不构成任何投资及应用建议。本站是一个个人学习交流的平台,网站上部分文章为转载,并不用于任何商业目的,我们已经尽可能的对作者和来源进行了通告,但是能力有限或疏忽,造成漏登,请及时联系我们,我们将根据著作权人的要求,立即更正或者删除有关内容。本站拥有对此声明的最终解释权。