使用 Azure 应用服务 Web 应用配置 Python

本教程介绍在 Azure 应用服务 Web 应用中创作和配置符合基本 Web 服务器网关接口 (WSGI) 的 Python 应用程序的选项。

其中讲解了 Git 部署的一些功能,如使用 requirements.txt 安装虚拟环境和包。

在 Azure 门户上创建 Web 应用

本教程假设已有 Azure 订阅和 Azure 门户访问权限。

如果没有现成的 Web 应用,则可从 Azure 门户中创建一个。 单击左上角的“新建”按钮,并依次单击“Web + 移动” > “Web 应用”。

Git 发布

按照 从本地 Git 部署到 Azure 应用服务的说明为新创建的 Web 应用配置 Git 发布。 本教程使用 Git 来创建、管理 Python Web 应用以及将其发布到 Azure 应用服务。

在设置 Git 发布之后,会创建 Git 存储库并使其与你的 Web 应用相关联。 随即会显示该存储库的 URL,之后其可用于将数据从本地开发环境推送到云。 要通过 Git 发布应用程序,请确保还安装了 Git 客户端,并按照提供的说明将 Web 应用内容推送到 Azure 应用服务。

应用程序概述

接下来几节会创建以下文件。 这些文件应放在 Git 存储库的根目录中。

app.py
requirements.txt
runtime.txt
web.config
ptvs_virtualenv_proxy.py

WSGI 处理程序

WSGI 是 PEP 3333 所述的 Python 标准,用于定义 Web 服务器和 Python 之间的接口。 它提供了使用 Python 编写各种 Web 应用程序和框架所需的标准化接口。 当今常用的 Python Web 框架都使用 WSGI。 Azure 应用服务 Web 应用支持任何此类框架;此外,高级用户可以创作自己的框架,只要自定义处理程序遵循 WSGI 规范准则即可。

下面是定义自定义处理程序的 app.py 的一个示例:

def wsgi_app(environ, start_response):
    status = '200 OK'
    response_headers = [('Content-type', 'text/plain')]
    start_response(status, response_headers)
    response_body = 'Hello World'
    yield response_body.encode()

if __name__ == '__main__':
    from wsgiref.simple_server import make_server

    httpd = make_server('localhost', 5555, wsgi_app)
    httpd.serve_forever()

可以使用 python app.py 在本地运行此应用程序,并在 Web 浏览器中浏览到 http://localhost:5555

虚拟环境

尽管上述示例应用程序不需要任何外部包,但应用程序很可能需要一些外部包。

为了便于管理外部包依赖项,Azure Git 部署支持创建虚拟环境。

当 Azure 在存储库的根目录中检测到 requirements.txt 文件时,自动创建名为 env的虚拟环境。 仅第一次部署会执行此操作,也可以在所选的 Python 运行时发生更改后进行任何部署的过程中执行此操作。

可能需要创建虚拟环境用于开发,但不将其包括在 Git 存储库中。

包管理

Requirements.txt 中列出的包将使用 pip 自动安装到虚拟环境中。 每次部署时都会发生这种情况,但如果已安装包,则 pip 会跳过安装。

示例 requirements.txt

azure==0.8.4

Python 版本

Azure 将按以下优先级确定要用于其虚拟环境的 Python 的版本:

  1. 在根文件夹中的 runtime.txt 内指定的版本
  2. 由 Python 设置在 Web 应用配置中指定的版本(Azure 门户中 Web 应用的“设置” > “应用程序设置”边栏选项卡)
  3. 如果未指定以上任何项,则 python 2.7 是默认值

内容的有效值

\runtime.txt

是:

  • python-2.7
  • python-3.4

如果指定了 micro 版本(第三个数字)时,将其忽略。

示例 runtime.txt

python-2.7

Web.config

需要创建一个 web.config 文件以指定服务器处理请求的方式。

请注意,如果存储库中有一个 Web.x.y 文件,其中 x.y 与所选的 Python 运行时匹配,则 Azure 会自动将相应的文件复制为 web.config。

以下 web.config 示例依赖于某个虚拟环境代理脚本(下一节介绍)。 它们与上述示例 app.py 中所用的 WSGI 处理程序配合使用。

Python 2.7 的示例 web.config

<?xml version="1.0"?>
<configuration>
  <appSettings>
    <add key="WSGI_ALT_VIRTUALENV_HANDLER" value="app.wsgi_app" />
    <add key="WSGI_ALT_VIRTUALENV_ACTIVATE_THIS"
         value="D:\home\site\wwwroot\env\Scripts\activate_this.py" />
    <add key="WSGI_HANDLER"
         value="ptvs_virtualenv_proxy.get_virtualenv_handler()" />
    <add key="PYTHONPATH" value="D:\home\site\wwwroot" />
  </appSettings>
  <system.web>
    <compilation debug="true" targetFramework="4.0" />
  </system.web>
  <system.webServer>
    <modules runAllManagedModulesForAllRequests="true" />
    <handlers>
      <remove name="Python27_via_FastCGI" />
      <remove name="Python34_via_FastCGI" />
      <add name="Python FastCGI"
           path="handler.fcgi"
           verb="*"
           modules="FastCgiModule"
           scriptProcessor="D:\Python27\python.exe|D:\Python27\Scripts\wfastcgi.py"
           resourceType="Unspecified"
           requireAccess="Script" />
    </handlers>
    <rewrite>
      <rules>
        <rule name="Static Files" stopProcessing="true">
          <conditions>
            <add input="true" pattern="false" />
          </conditions>
        </rule>
        <rule name="Configure Python" stopProcessing="true">
          <match url="(.*)" ignoreCase="false" />
          <conditions>
            <add input="{REQUEST_URI}" pattern="^/static/.*" ignoreCase="true" negate="true" />
          </conditions>
          <action type="Rewrite"
                  url="handler.fcgi/{R:1}"
                  appendQueryString="true" />
        </rule>
      </rules>
    </rewrite>
  </system.webServer>
</configuration>

Python 3.4 的示例 web.config

<?xml version="1.0"?>
<configuration>
  <appSettings>
    <add key="WSGI_ALT_VIRTUALENV_HANDLER" value="app.wsgi_app" />
    <add key="WSGI_ALT_VIRTUALENV_ACTIVATE_THIS"
         value="D:\home\site\wwwroot\env\Scripts\python.exe" />
    <add key="WSGI_HANDLER"
         value="ptvs_virtualenv_proxy.get_venv_handler()" />
    <add key="PYTHONPATH" value="D:\home\site\wwwroot" />
  </appSettings>
  <system.web>
    <compilation debug="true" targetFramework="4.0" />
  </system.web>
  <system.webServer>
    <modules runAllManagedModulesForAllRequests="true" />
    <handlers>
      <remove name="Python27_via_FastCGI" />
      <remove name="Python34_via_FastCGI" />
      <add name="Python FastCGI"
           path="handler.fcgi"
           verb="*"
           modules="FastCgiModule"
           scriptProcessor="D:\Python34\python.exe|D:\Python34\Scripts\wfastcgi.py"
           resourceType="Unspecified"
           requireAccess="Script" />
    </handlers>
    <rewrite>
      <rules>
        <rule name="Static Files" stopProcessing="true">
          <conditions>
            <add input="true" pattern="false" />
          </conditions>
        </rule>
        <rule name="Configure Python" stopProcessing="true">
          <match url="(.*)" ignoreCase="false" />
          <conditions>
            <add input="{REQUEST_URI}" pattern="^/static/.*" ignoreCase="true" negate="true" />
          </conditions>
          <action type="Rewrite" url="handler.fcgi/{R:1}" appendQueryString="true" />
        </rule>
      </rules>
    </rewrite>
  </system.webServer>
</configuration>

静态文件由 Web 服务器直接处理,无需通过 Python 代码,从而提高性能。

在上述示例中,磁盘上静态文件的位置应与 URL 中的位置匹配。 也就是说,http://pythonapp.chinacloudsites.cn/static/site.css 的请求将为磁盘上 \static\site.css 处的文件服务。

WSGI_ALT_VIRTUALENV_HANDLER 是指定 WSGI 处理程序的位置。 在上述示例中,该位置为 app.wsgi_app,因为处理程序是根文件夹中 app.py 中的一个名为 wsgi_app 的函数。

PYTHONPATH,但是,如果通过在 requirements.txt 中指定将所有依赖项全部安装到虚拟环境中,则不需要更改。

虚拟环境代理

使用以下脚本可检索 WSGI 处理程序、激活虚拟环境以及记录错误。 该脚本是通用的,无需修改即可使用。

ptvs_virtualenv_proxy.py的内容:

 # ############################################################################
 #
 # Copyright (c) Microsoft Corporation. 
 #
 # This source code is subject to terms and conditions of the Apache License, Version 2.0. A 
 # copy of the license can be found in the License.html file at the root of this distribution. If 
 # you cannot locate the Apache License, Version 2.0, please send an email to 
 # vspython@microsoft.com. By using this source code in any fashion, you are agreeing to be bound 
 # by the terms of the Apache License, Version 2.0.
 #
 # You must not remove this notice, or any other, from this software.
 #
 # ###########################################################################

import datetime
import os
import sys
import traceback

if sys.version_info[0] == 3:
    def to_str(value):
        return value.decode(sys.getfilesystemencoding())

    def execfile(path, global_dict):
        """Execute a file"""
        with open(path, 'r') as f:
            code = f.read()
        code = code.replace('\r\n', '\n') + '\n'
        exec(code, global_dict)
else:
    def to_str(value):
        return value.encode(sys.getfilesystemencoding())

def log(txt):
    """Logs fatal errors to a log file if WSGI_LOG env var is defined"""
    log_file = os.environ.get('WSGI_LOG')
    if log_file:
        f = open(log_file, 'a+')
        try:
            f.write('%s: %s' % (datetime.datetime.now(), txt))
        finally:
            f.close()

ptvsd_secret = os.getenv('WSGI_PTVSD_SECRET')
if ptvsd_secret:
    log('Enabling ptvsd ...\n')
    try:
        import ptvsd
        try:
            ptvsd.enable_attach(ptvsd_secret)
            log('ptvsd enabled.\n')
        except: 
            log('ptvsd.enable_attach failed\n')
    except ImportError:
        log('error importing ptvsd.\n')

def get_wsgi_handler(handler_name):
    if not handler_name:
        raise Exception('WSGI_ALT_VIRTUALENV_HANDLER env var must be set')

    if not isinstance(handler_name, str):
        handler_name = to_str(handler_name)

    module_name, _, callable_name = handler_name.rpartition('.')
    should_call = callable_name.endswith('()')
    callable_name = callable_name[:-2] if should_call else callable_name
    name_list = [(callable_name, should_call)]
    handler = None
    last_tb = ''

    while module_name:
        try:
            handler = __import__(module_name, fromlist=[name_list[0][0]])
            last_tb = ''
            for name, should_call in name_list:
                handler = getattr(handler, name)
                if should_call:
                    handler = handler()
            break
        except ImportError:
            module_name, _, callable_name = module_name.rpartition('.')
            should_call = callable_name.endswith('()')
            callable_name = callable_name[:-2] if should_call else callable_name
            name_list.insert(0, (callable_name, should_call))
            handler = None
            last_tb = ': ' + traceback.format_exc()

    if handler is None:
        raise ValueError('"%s" could not be imported%s' % (handler_name, last_tb))

    return handler

activate_this = os.getenv('WSGI_ALT_VIRTUALENV_ACTIVATE_THIS')
if not activate_this:
    raise Exception('WSGI_ALT_VIRTUALENV_ACTIVATE_THIS is not set')

def get_virtualenv_handler():
    log('Activating virtualenv with %s\n' % activate_this)
    execfile(activate_this, dict(__file__=activate_this))

    log('Getting handler %s\n' % os.getenv('WSGI_ALT_VIRTUALENV_HANDLER'))
    handler = get_wsgi_handler(os.getenv('WSGI_ALT_VIRTUALENV_HANDLER'))
    log('Got handler: %r\n' % handler)
    return handler

def get_venv_handler():
    log('Activating venv with executable at %s\n' % activate_this)
    import site
    sys.executable = activate_this
    old_sys_path, sys.path = sys.path, []

    site.main()

    sys.path.insert(0, '')
    for item in old_sys_path:
        if item not in sys.path:
            sys.path.append(item)

    log('Getting handler %s\n' % os.getenv('WSGI_ALT_VIRTUALENV_HANDLER'))
    handler = get_wsgi_handler(os.getenv('WSGI_ALT_VIRTUALENV_HANDLER'))
    log('Got handler: %r\n' % handler)
    return handler

自定义 Git 部署

如果这些条件都成立,Azure 将确定你的应用程序使用 Python:

  • 根文件夹中的 requirements.txt 文件
  • 根文件夹中的任何 .py 文件或指定 python 的 runtime.txt

如果是这种情况,它将使用特定于 Python 的部署脚本,此脚本执行文件的标准同步以及其他 Python 操作,例如:

  • 自动管理虚拟环境
  • 使用 pip 来安装 requirements.txt 中列出的软件包
  • 根据所选的 Python 版本创建相应的 web.config。
  • 收集 Django 应用程序的静态文件

您可以控制默认部署步骤的某些方面,而无需自定义脚本。

如果您想要跳过所有特定于 Python 的部署步骤,可以创建此空文件:

\.skipPythonDeployment

为了更大程度控制部署,可以通过创建以下文件来覆盖默认部署脚本:

\.deployment
\deploy.cmd

可以使用 Azure 命令行接口创建这些文件。 从项目文件夹使用以下命令:

azure site deploymentscript --python

这些文件不存在时,Azure 创建一个临时部署脚本然后运行此脚本。 它等同于使用以上命令创建的脚本。

故障排除 - 软件包安装

在 Azure 上运行时,有些软件包可能无法使用 pip 进行安装。 可能只是该软件包在 Python 软件包索引中不可用。 可能需要一个编译器(在运行 Azure 应用服务中的 Web 应用的计算机上未提供编译器)。

在此部分中,我们将考察解决此问题的方法。

请求轮

如果软件包安装需要编译器,您应尝试联系软件包所有者以请求为软件包提供轮。

现在使用最新提供的 Microsoft Visual C++ Compiler for Python 2.7,就可以更轻松地构建具有针对 Python 2.7 的本机代码的软件包。

构建轮(需要 Windows)

注意:使用此选项时,确保使用匹配 Azure 应用服务中 Web 应用上所用的平台/体系结构/版本(Windows/32 位/2.7 或 3.4)的 Python 环境编译此软件包。

如果软件包由于需要编译器而未安装,您可以在本地计算机上安装编译器,然后为软件包构建轮,随后将轮包含在您的存储库中。

Mac/Linux 用户:如果没有 Windows 计算机的访问权限,请参阅创建运行 Windows 的虚拟机,了解如何在 Azure 上创建 VM。 可以使用它来构建轮、将其添加到存储库以及在必要时放弃虚拟机。

对于 Python 2.7,可以安装 Microsoft Visual C++ Compiler for Python 2.7

对于 Python 3.4,可以安装 Microsoft Visual C++ 2010 Express

要构建轮,你需要轮软件包:

env\scripts\pip install wheel

将使用 pip wheel 编译依赖项:

env\scripts\pip wheel azure==0.8.4

这将在 \wheelhouse 文件夹中创建 .whl 文件。 将 \wheelhouse 文件夹和轮文件添加到您的存储库。

编辑 requirements.txt,在顶部添加 --find-links 选项。 这会让 pip 在本地文件夹中查找完全匹配项,然后转至 python 软件包索引。

--find-links wheelhouse
azure==0.8.4

如果想要将所有依赖项包含在 \wheelhouse 文件夹中而根本不使用 python 软件包索引,则可以通过将 --no-index 添加到 requirements.txt 之上来强制 pip 忽略软件包索引。

--no-index

自定义安装

可以自定义部署脚本以在虚拟环境中使用备用安装程序(例如 easy_install)来安装软件包。 请参阅 deploy.cmd 以了解注释掉的示例。 确保此类软件包不列入 requirements.txt 中,从而防止 pip 将其安装。

将其添加到部署脚本:

env\scripts\easy_install somepackage

还可以使用 easy_install 从 exe 安装程序进行安装(有些兼容 zip,所以 easy_install 支持它们)。 将安装程序添加到存储库,然后通过传递可执行文件的路径来调用 easy_install。

将其添加到部署脚本:

env\scripts\easy_install "%DEPLOYMENT_SOURCE%\installers\somepackage.exe"

将虚拟环境包含在存储库中(需要 Windows)

注意:使用此选项时,确保使用匹配 Azure 应用服务中 Web 应用上所用的平台/体系结构/版本(Windows/32 位/2.7 或 3.4)的虚拟环境。

如果存储库中包含虚拟环境,您可以通过创建一个空文件来防止部署脚本在 Azure 上执行虚拟环境管理:

.skipPythonDeployment

建议删除应用上的现有虚拟环境,防止在自动管理虚拟环境时出现剩余文件。

故障排除 - 虚拟环境

如果部署脚本检测到兼容的虚拟环境已存在,它将跳过在 Azure 上创建虚拟环境的过程。 这可以显著加快部署。 通过 pip,将跳过已安装的软件包。

在某些情况下,您可能想要强制删除该虚拟环境。 如果您决定将虚拟环境包含为您的存储库的一部分,需要执行此操作。 如果您需要删除某些软件包或测试对 requirements.txt 的更改,可能也会执行此操作。

有几个选项用于管理 Azure 上的现有虚拟环境:

选项 1:使用 FTP

通过 FTP 客户端,连接到服务器,然后您就可以删除 env 文件夹。 请注意,某些 FTP 客户端(例如 Web 浏览器)可能为只读而不允许您删除文件夹,因此您要确保使用具备此功能的 FTP 客户端。 在 Azure 门户上,Web 应用的边栏选项卡中显示 FTP 主机名和用户。

选项 2:切换运行时

这是利用以下事实的一个替代方法:部署脚本不匹配期望的 Python 版本时,它将删除 env 文件夹。 这将有效地删除现有环境,然后创建新环境。

  1. 切换到其他版本的 Python(通过 runtime.txt 或 Azure 门户中的“应用程序设置”边栏选项卡)
  2. git 推送一些更改 (如果有任何 pip 安装错误,请忽略)
  3. 切换回初始版本的 Python
  4. git 再次推送某些更改

选项 3:自定义部署脚本

如果您已自定义部署脚本,则可以更改 deploy.cmd 中的代码以强制其删除 env 文件夹。

后续步骤

有关详细信息,请参阅 Python 开发人员中心

更改内容