反对将环境设置与代码混淆

社区文章 发布于 2024年6月17日

在开发和部署软件时,我们处理代码和其他资产,但我们也处理配置,其中包括技术运行的环境。经验丰富的工程师和架构师都知道将机器与其环境分开管理的重要性。然而,这样做需要遵守纪律,而纪律很容易松懈。我注意到,在许多 AI 和数据科学领域的示例项目中,自动从本地驱动器加载 .env 等代码变得很常见。我想我第一次听说诸如 python-dotenvpython-decouple 等工具,都是与 AI 代码相关联的,通常是作为建立 API 密钥和其他秘密的一种方式。

当您的 AI 应用程序架构中的模块倍增时(我们知道这在 AI 项目中发生得有多快),这种不幸的纪律松懈可能会导致许多边界问题。让程序惰性地读取通过环境传递的变量要好得多。十二要素应用程序方法论带来的最健康的一点是,它理解了经典 UNIX 环境的实用性,我希望通过本文,能为您提供关于如何正确做到这一点的见解,即在部署/配置管理中单独设置环境,而代码则读取它。不再跨越这个边界。

Girl in front of burning house meme: Worked fine in dev. Ops problem now.

公平地说,环境变量在很多方面都很笨重,而且跨平台、甚至跨程序启动器和 shell 变体之间的差异可能会使事情变得棘手,但这正是重点——您根据主机环境定制环境设置,重要的是正确的值能够传递给程序。至少使用环境变量,您几乎可以在任何平台上使用它们。

注意:本指南非常偏向 Linux/BSD,所有代码片段均在 Mac OS X 的 zsh shell 下测试。

设置环境的常用工具/方法

从一个读取环境的 Python 小代码示例 envtest.py 开始

import os
print(os.getenv('HELLO', 'NOT FOUND'))

如果你运行

HELLO="Wide world"
python envtest.py

你会得到 NOT FOUND,因为在第一个命令中设置的环境变量不会自动传递给第二个命令的 python 进程的子 shell。

修改 shell 环境以使其对未来的命令生效

你可以 export 一个变量,然后它将被注入到后续命令的子 shell 中(你以后可以使用 unset 来删除任何变量)

export HELLO="Wide world"
python envtest.py

输出:Wide world

注意:您可以使用 printenv 随时查看环境变量的值。

printenv HELLO

你可能会想使用 echo

echo $HELLO

但后者会在某些情况下给你带来惊喜,所以我更喜欢用 printenv 进行这种检查。

您会想熟悉一些 shell 替换和转义的含义。例如,如果您尝试在值中使用感叹号

export HELLO="Wide world!"
python envtest.py

你会遇到问题,因为 ! 标记着 shell 历史扩展。你可以使用反斜杠转义

export HELLO="Wide world\!"
python envtest.py

$ 相同,它用于标记 shell 变量。

export HELLO="Wide world\$"
python envtest.py

您也可以使用单引号来避免插值,但请注意,这将同时阻止所有变量的扩展。

export HELLO='Wide world!'
python envtest.py

还有其他一些您需要注意的字符。了解更多(“在 Bash 中转义字符”)

老式点号或 source

任何熟悉经典 UNIX shell 的人可能都见过这些。POSIX 支持 . 命令从文件执行 shell 命令。bash shell 及其派生,例如 zsh,提供了一个近义词:source

创建一个名为 spam1.sh 的文件,内容如下:

HELLO="Wide world\!"

设置成导出模式后,使用 . 加载此文件。

unset HELLO
set -a     # Set to export all shell variables to the environment
source spam1.sh
set +a
python envtest.py

您也可以使用 . 形式,但那样就需要 shell 文件的具体路径(本例中为 ./)。

unset HELLO
set -a     # Set to export all shell variables to the environment
. ./spam1.sh
set +a
python envtest.py

直接为子 shell 设置

就像您希望在编程中最小化全局变量的使用一样,您可能也希望最小化全局环境变量的使用。您可以为给定的命令设置单个变量

HELLO="Wide world\!" python envtest.py

输出:Wide world!

HELLO='Wide world!' python envtest.py

您也可以内联设置多个变量,如下所示

VAR1="val1" VAR2="val2" VAR3="val3" /path/to/command_to_run

这当然会变得乏味、容易出错且难以阅读,因此您需要找到一次注入多个环境变量的方法。

使用 python-dotenv [你说什么?!]

是的,我开始时将 python-dotenv 与不良习惯联系在一起,但实际上在命令行模式下使用它来在程序外部设置环境是没问题的。

示例环境变量文件代码 spam1.env

HELLO=Wide world!

请注意,这种 env 文件格式,也称为 DotEnv 格式,非正式地也称为属性文件格式,与 shell 文件格式不同。它没有单引号或双引号,等号周围没有空格(这在 shell 格式中会被忽略,但在 .env 格式中会无效),并且不需要转义 !$ 等字符。

以下是实际操作:

dotenv -f spam1.env run -- python envtest.py

输出:Wide world!

检查您的启动器或其他此类应用程序

例如,我注意到,似乎很少有人知道用于 Python/ASGI 的 uvicorn 网络服务器实现支持 --env-file 选项,该选项可以读取文件并将其中定义的环境变量传递给 ASGI 应用程序。

Docker 用户注意事项

您可以通过在 Dockerfile 中使用 ENV 指令、在 docker run 命令中使用 -e,或者通过各种方式为 docker compose 设置环境变量,以多种细微不同的方式设置环境。

另请参阅:“将环境变量传递给 Docker 容器”

关于秘密的特别注意事项

您确实应该为您的 API 密钥、秘密等使用秘密管理器或保险库。我使用 1passwd,我将以此为例。您可以创建一个包含基于 URI 的秘密引用的环境文件(例如 op://app-prod/db/password;您可以从 UI 复制秘密引用 URL)。然后 op run 命令会扫描环境变量中是否存在此类引用,将其在环境中(而非文件中)替换为应用程序中的相应值,从而在加载秘密的子进程中运行指定的命令。

op run --env-file=secrets-obscured.env -- python some_db_code.py

注意:我遇到过 op run 在 shell 中已经导出相同变量时,会忽略文件中非 1password 引用变量的情况,我怀疑这是一个 bug,但为了确保万无一失,我习惯于采用双保险的方法:

dotenv -f secrets-obscured.env run -- op run --env-file=secrets-obscured.env -- python some_db_code.py

另请参阅:“Docker Compose 文件中的秘密作为环境变量”

在 VS Code 中将属性文件与 .env 文件扩展名关联

我注意到 VS Code 默认情况下会识别名为 .env 的文件是属性文件,但对于 dev.envprod.env 等文件名则不够智能。您可以通过设置 files.associations 首选项来解决此问题,例如:

    "files.associations": {
        "*.env": "properties"
    }

或通过图形用户界面

image/png

大家好世界!

将您的 API 密钥和其他秘密放入秘密管理器或保险库中,并研究如何在您的部署环境中最好地设置不同的受控环境。然后您可以在开发工作流中使用本地环境文件或 shell 脚本,并让代码本身完全不知道其环境是如何建立的。您很快就会发现您的软件管理和演进运行得更顺畅,尤其是在协作项目中。

社区

注册登录发表评论