Apache SetEnv在mod_wsgi中未按预期工作

29

我写了一个Flask应用程序,使用了一个可以通过环境变量配置的外部库。注意:这个外部库是我自己写的,所以如果需要的话,我可以进行更改。当从命令行运行并使用以下命令运行Flask服务器时:

# env = python virtual environment
ENV_VAR=foo ./env/bin/python myapp/webui.py

一切都按预期运行。但是在部署到 Apache 并使用 SetEnv 后,它就不再起作用了。事实上,将 os.environ 打印到 stderr(以便它出现在 Apache 日志中)可以发现,wsgi 进程似乎处在一个非常不同的环境中(例如,os.environ['PWD'] 看起来相差很远。实际上,它指向我的开发文件夹。

为了帮助确定问题,以下是应用程序的相关部分,作为一个独立的 hello-world 应用程序。错误输出和观察结果在帖子的最后。

应用程序文件夹布局:

Python 应用程序:

.
├── myapp.ini
├── setup.py
└── testenv
    ├── __init__.py
    ├── model
    │   └── __init__.py
    └── webui.py

Apache文件夹 (/var/www/michel/testenv):

.
├── env
│   ├── [...]
├── logs
│   ├── access.log
│   └── error.log
└── wsgi
└── app.wsgi

myapp.ini

[app]
somevar=somevalue

setup.py

from setuptools import setup, find_packages

setup(
    name="testenv",
    version='1.0dev1',
    description="A test app",
    long_description="Hello World!",
    author="Some Author",
    author_email="author@example.com",
    license="BSD",
    include_package_data=True,
    install_requires = [
      'flask',
      ],
    packages=find_packages(exclude=["tests.*", "tests"]),
    zip_safe=False,
)

测试环境/初始化.py

# empty

测试环境/模型/init.py

from os.path import expanduser, join, exists
from os import getcwd, getenv, pathsep
import logging
import sys

__version__ = '1.0dev1'

LOG = logging.getLogger(__name__)

def find_config():
    """
    Searches for an appropriate config file. If found, return the filename, and
    the parsed search path
    """

    path = [getcwd(), expanduser('~/.mycompany/myapp'), '/etc/mycompany/myapp']
    env_path = getenv("MYAPP_PATH")
    config_filename = getenv("MYAPP_CONFIG", "myapp.ini")
    if env_path:
        path = env_path.split(pathsep)

    detected_conf = None
    for dir in path:
        conf_name = join(dir, config_filename)
        if exists(conf_name):
            detected_conf = conf_name
            break
    return detected_conf, path

def load_config():
    """
    Load the config file.
    Raises an OSError if no file was found.
    """
    from ConfigParser import SafeConfigParser

    conf, path = find_config()
    if not conf:
        raise OSError("No config file found! Search path was %r" % path)

    parser = SafeConfigParser()
    parser.read(conf)
    LOG.info("Loaded settings from %r" % conf)
    return parser

try:
    CONF = load_config()
except OSError, ex:
    # Give a helpful message instead of a scary stack-trace
    print >>sys.stderr, str(ex)
    sys.exit(1)

测试环境/web界面.py

from testenv.model import CONF

from flask import Flask

app = Flask(__name__)

@app.route('/')
def index():
    return "Hello World %s!" % CONF.get('app', 'somevar')

if __name__ == '__main__':
    app.debue = True
    app.run()

Apache配置

<VirtualHost *:80>
    ServerName testenv-test.my.fq.dn
    ServerAlias testenv-test

    WSGIDaemonProcess testenv user=michel threads=5
    WSGIScriptAlias / /var/www/michel/testenv/wsgi/app.wsgi
    SetEnv MYAPP_PATH /var/www/michel/testenv/config

    <Directory /var/www/michel/testenv/wsgi>
        WSGIProcessGroup testenv
        WSGIApplicationGroup %{GLOBAL}
        Order deny,allow
        Allow from all
    </Directory>

    ErrorLog /var/www/michel/testenv/logs/error.log
    LogLevel warn

    CustomLog /var/www/michel/testenv/logs/access.log combined

</VirtualHost>

app.wsgi

->

应用程序.wsgi

activate_this = '/var/www/michel/testenv/env/bin/activate_this.py'
execfile(activate_this, dict(__file__=activate_this))

from os import getcwd
import logging, sys

from testenv.webui import app as application

# You may want to change this if you are using another logging setup
logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)

LOG = logging.getLogger(__name__)
LOG.debug('Current path: {0}'.format(getcwd()))

# Application config
application.debug = False

# vim: set ft=python :

错误和观察

这是 Apache 错误日志的输出。

[Thu Jan 26 10:48:15 2012] [error] No config file found! Search path was ['/home/users/michel', '/home/users/michel/.mycompany/myapp', '/etc/mycompany/myapp']
[Thu Jan 26 10:48:15 2012] [error] [client 10.115.192.101] mod_wsgi (pid=17946): Target WSGI script '/var/www/michel/testenv/wsgi/app.wsgi' cannot be loaded as Python module.
[Thu Jan 26 10:48:15 2012] [error] [client 10.115.192.101] mod_wsgi (pid=17946): SystemExit exception raised by WSGI script '/var/www/michel/testenv/wsgi/app.wsgi' ignored.
[Thu Jan 26 10:48:15 2012] [error] [client 10.115.192.101] Traceback (most recent call last):
[Thu Jan 26 10:48:15 2012] [error] [client 10.115.192.101]   File "/var/www/michel/testenv/wsgi/app.wsgi", line 10, in <module>
[Thu Jan 26 10:48:15 2012] [error] [client 10.115.192.101]     from testenv.webui import app as application
[Thu Jan 26 10:48:15 2012] [error] [client 10.115.192.101]   File "/var/www/michel/testenv/env/lib/python2.6/site-packages/testenv-1.0dev1-py2.6.egg/testenv/webui.py", line 1, in <module>
[Thu Jan 26 10:48:15 2012] [error] [client 10.115.192.101]     from testenv.model import CONF
[Thu Jan 26 10:48:15 2012] [error] [client 10.115.192.101]   File "/var/www/michel/testenv/env/lib/python2.6/site-packages/testenv-1.0dev1-py2.6.egg/testenv/model/__init__.py", line 51, in <module>
[Thu Jan 26 10:48:15 2012] [error] [client 10.115.192.101]     sys.exit(1)
[Thu Jan 26 10:48:15 2012] [error] [client 10.115.192.101] SystemExit: 1
我的第一个观察是环境变量MYAPP_PATHos.environ中没有出现(尽管这在输出中不可见,但我测试过了,它确实不存在!)。因此,“解析器”将回退到默认路径。
我的第二个观察是配置文件的搜索路径将/home/users/michel列为os.getcwd()的返回值。实际上,我期望得到的是/var/www/michel/testenv中的某些内容。
我的直觉告诉我,我所做的配置解析方式不正确。主要是因为代码在导入时执行。这让我想到,也许配置解析代码是在WSGI环境正确设置之前执行的。我正在追求正确的方向吗?
短暂的讨论/旁观问题
在这种情况下,你会怎样进行配置解析?考虑到“model”子文件夹实际上是一个外部模块,应该在非WSGI应用程序中也起作用,并且应该提供一种配置数据库连接的方法。
就个人而言,我喜欢搜索配置文件的方式,同时仍然能够对其进行覆盖。只是代码在导入时执行的事实让我的蜘蛛感觉异常强烈。其背后的逻辑是:通过使用此模块的其他开发人员完全隐藏了配置处理(抽象屏障),它“只是工作”。他们只需要导入模块(当然还要有现有的配置文件),就可以直接开始而不知道任何数据库细节。这也为他们提供了一种轻松的方法来使用不同的数据库(dev/test/deployment)并轻松地在它们之间切换。
现在,在mod_wsgi中不再如此 :(
更新:
刚才,为了测试我上面的想法,我将webui.py更改如下:
import os

from flask import Flask, jsonify

app = Flask(__name__)

@app.route('/')
def index():
    return jsonify(os.environ)

if __name__ == '__main__':
    app.debue = True
    app.run()
网页上的输出如下:
{
    LANG: "C",
    APACHE_RUN_USER: "www-data",
    APACHE_PID_FILE: "/var/run/apache2.pid",
    PWD: "/home/users/michel/tmp/testenv",
    APACHE_RUN_GROUP: "www-data",
    PATH: "/usr/local/bin:/usr/bin:/bin",
    HOME: "/home/users/michel/"
}
这显示了与其他调试方法所看到的相同环境。因此,我的初步想法是错误的。但现在我意识到了更奇怪的事情。 os.environment ['PWD'] 设置为我拥有开发文件的文件夹。这根本不是应用程序运行的位置。更奇怪的是,os.getcwd() 返回 /home/users/michel?这与我在 os.environ 中看到的不一致。它不应该与 os.environ ['PWD'] 相同吗?
最重要的问题仍然是:为什么没有在 os.environ 中找到由apache的 SetEnv(在这种情况下为MYAPP_PATH)设置的值?

1
你写这个问题的工作似乎比你实际应用程序的工作还要多。写得很好 :) - Cerin
5
@Cerin:我认为这对其他人有帮助。因此,我会尽可能详细地写出我的问题。也许有人可以从问题和答案中学到一些东西 ^_^ - exhuma
2个回答

26
请注意,WSGI环境是通过应用程序对象的environ参数在每个请求中传递的。这个环境与保存在os.environ中的进程环境完全无关。 SetEnv指令对os.environ没有影响,并且没有通过Apache配置指令影响进程环境中内容的方法。
因此,您必须采取其他措施来获取来自apache的MY_PATH,而不是使用getenvironos.environ['PWD']
Flask将wsgi环境添加到请求中,而不是app.environ,这是由基础的werkzeug完成的。因此,在每个应用程序请求上,apache将添加MYAPP_CONF键,并且您可以在任何可以访问请求的地方访问它,例如:request.environ.get('MYAPP_CONFIG')

12

@rapadura的回答是正确的,即您无法直接访问Apache配置中的SetEnv值,但您可以绕过此问题。

如果您在app.wsgi文件中的application周围添加一个包装器,您可以在每个请求上设置os.environ。请参见以下修改后的app.wsgi示例:

activate_this = '/var/www/michel/testenv/env/bin/activate_this.py'
execfile(activate_this, dict(__file__=activate_this))

from os import environ, getcwd
import logging, sys

from testenv.webui import app as _application

# You may want to change this if you are using another logging setup
logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)

LOG = logging.getLogger(__name__)
LOG.debug('Current path: {0}'.format(getcwd()))

# Application config
_application.debug = False

def application(req_environ, start_response):
    environ['MYAPP_CONF'] = req_environ['MYAPP_CONF']
    return _application(req_environ, start_response)
如果你在Apache配置中设置了更多的环境变量,那么你需要在application包装函数中显式设置每个变量。

2
我使用变量命名约定,例如MYAPP_FOO,MYAPP_BAR等等,然后在包装器中可以使用循环。对于具有多个配置选项的应用程序非常有效。_env = {k.replace('MYAPP_',''): v for k,v in req_environ.items() if k.startswith('MYAPP_')} os.environ.update(_env) - Ben Whaley
或者你可以将变量放在.wsgi文件中,而不是放在Apache配置文件中。 - Ben Whaley
3
通常情况下,.wsgi文件与应用程序本身一起存储在版本控制系统中,这意味着它不适合存储“机密”或仅适用于运行环境的配置值(例如db/test/prod具有不同的数据库用户名/密码)。使用环境变量可以在每个环境(dev/test/prod)中检出相同的代码,并假定环境变量设置正确,则应该只需运行。这就是为什么将它们存储在Apache配置而不是.wsgi文件中的原因。 - jerrykan
1
我认为这取决于您如何管理配置。如果您有一个配置管理解决方案,wsgi文件可以是一个模板,CM工具只需填充变量即可。关键是有许多处理此问题的方法。 - Ben Whaley
@BenWhaley 大多数PAAS提供商都假定您正在使用环境变量,这就是为什么它已经成为常规。如果您真的想使用某种模板(我建议不要这样做),那么配置值最好与其他配置值一起存储(即在处理Flask app.config的地方),而不是在.wsig文件中。 - jerrykan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接