环境变量解释:.env 文件、秘密和配置正确完成

  1. 首页
  2. 博客
  3. 开发运营

开发运营

环境变量解释:.env 文件、秘密和配置正确完成

环境变量允许相同的代码在笔记本电脑、CI 和生产环境中以不同的设置运行。以下是它们的真实行为,以及我们在测试它们时遇到的惊喜。

阅读约 8 分钟
环境变量解释:.env 文件、秘密和配置正确完成

要点速览

  • 环境变量是进程从其环境接收的键值设置。
  • 它们将配置(例如端口和数据库 URL)保留在代码之外。
  • 值始终是字符串,因此“假”是真值,除非您解析它。
  • 切勿提交具有真实机密的 .env 文件,并在应用程序启动时验证所需的设置。

您的应用程序需要一个数据库 URL。在你的笔记本电脑上,它是一个本地数据库,在 CI 中是一个一次性数据库,在生产中是一个托管集群。对其中任何一个进行硬编码都意味着更改每个环境的代码。 环境变量解决了这个问题。代码读取DATABASE_URL,每个环境提供自己的值。

这个想法是十二因素应用程序方法的核心,该方法表示部署之间不同的配置属于环境。我们在 Windows 上使用 Python 3.13 测试了以下行为。显示的每个输出都来自该运行。

什么是环境变量?

每个正在运行的进程都有一组命名文本值,称为其环境。熟悉的示例有 PATH,它告诉 shell 在哪里查找程序,以及 HOME 或 USERPROFILE。您自己的应用程序可以使用相同的机制进行 APP_PORT、LOG_LEVEL 或 DATABASE_URL 等设置。

设置环境变量

壳牌 对于本次会议 对于一个命令
bash/zsh 导出APP_PORT=8080 APP_PORT=8080 python app.py
电源外壳 $env:APP_PORT = "8080" 设置它,运行,然后删除它
Windows命令 设置 APP_PORT=8080 未内置

这些仅持续当前终端会话。要使它们永久存在,请将它们添加到 shell 配置文件中,例如 ~/.bashrc,或者在 Windows 上使用系统环境设置或 setx。对于应用程序,通常最好在部署时提供它们,而不是在系统范围内设置它们。

在代码中读取它们

import os

port = os.environ.get("APP_PORT", "8000")   # default if missing
print(port)                                  # 8000

db = os.environ["DATABASE_URL_MISSING_DEMO"]
# KeyError: 'DATABASE_URL_MISSING_DEMO'

使用 .get() 作为可选设置的默认值。使用方括号(它会引发 KeyError)来表示应用程序在没有方括号的情况下无法工作的设置。其他语言类似:Node.js 中的 process.env.APP_PORT、Java 中的 System.getenv("APP_PORT") 和 Go 中的 os.Getenv("APP_PORT")。

一切都是字符串

这吸引了各种语言的人们。我们设置一个端口,然后添加“1”:

os.environ["APP_PORT"] = "8080"
port = os.environ["APP_PORT"]
print(type(port).__name__, repr(port), port + "1")
# str '8080' 80801

os.environ["APP_PORT"] = 8080
# TypeError: str expected, not int

该值以文本形式返回,并添加生成的 80801,而不是 8081。显式转换,如 int(os.environ["APP_PORT"]) 中所示。布尔值更糟糕:

os.environ["DEBUG"] = "false"
print(bool(os.environ["DEBUG"]))   # True

在 Python 中任何非空字符串都是真值,因此 "false" 变成了 True。调试模式将在生产中打开。故意解析布尔值:

def env_bool(name, default=False):
    value = os.environ.get(name)
    if value is None:
        return default
    return value.strip().lower() in {"1", "true", "yes", "on"}

env_bool("DEBUG")                   # False
env_bool("NOT_SET", default=True)   # True

进程如何继承它们

子进程接收其父进程环境的副本。我们启动子 Python 进程来查看这一点:

child sees: 8080 inherited     # default: full copy of the parent's environment
child sees: 9000 None          # given a custom env: only what we passed
parent sees child var: None    # a child's changes never flow back

接下来是三个实践课程。您导出的变量对于您启动的每个程序都是可见的,包括您没有编写的脚本,这对机密很重要。通过自定义环境会替换整个事情,因此请包含孩子需要的任何内容。并且脚本无法更改其父 shell 的变量,这就是工具要求您 source 文件而不是运行它的原因。

.env 文件

手动输入许多变量会很乏味,因此大多数项目将本地开发值保存在 .env 文件中:

# local development only
APP_PORT=8080
DATABASE_URL="postgres://dev:devonly@localhost:5432/shop"
FEATURE_SEARCH=true

python-dotenv 和 Node.js 的 dotenv 等库会在启动时加载此文件。 Node.js 20.6 及更高版本也可以使用 --env-file 本地加载。为了了解这个想法,我们编写了一个小型解析器,它将上面的文件转换为:

{'APP_PORT': '8080',
 'DATABASE_URL': 'postgres://dev:devonly@localhost:5432/shop',
 'FEATURE_SEARCH': 'true'}

在实践中使用真实的库。它处理我们的 10 行版本忽略的引用、转义、注释和多行值。默认情况下,大多数 dotenv 加载器不会覆盖已设置的变量,因此真实的环境值胜过文件。这通常就是您想要的。

提示: 在第一次提交之前将 .env 添加到 .gitignore,并使用每个变量名称和安全占位符值提交 .env.example。新的开发人员复制它并填写自己的值。

启动时验证配置

丢失的变量应该立即停止应用程序并发出明确的消息,而不是在一小时后导致令人困惑的错误。我们在加载时检查了所需的设置:

REQUIRED = ["DATABASE_URL", "SECRET_KEY"]

def check_required(env):
    missing = [k for k in REQUIRED if not env.get(k)]
    if missing:
        raise SystemExit(f"Missing required settings: {', '.join(missing)}")

# Missing required settings: SECRET_KEY

类型化配置库更进一步。 Python 中的 Pydantic 设置、Node.js 中的 zod 或 envalid 以及 Java 中的 Spring 配置属性都会立即转换类型并报告每个问题。

环境变量和秘密

环境变量是将 API 密钥等秘密传递给应用程序的常用方法,它们比硬编码要好得多。他们确实有弱点需要了解:

  • 正如我们的测试所示,子进程继承它们。
  • 它们可能出现在崩溃报告、调试页面和转储环境的日志中。
  • 在某些系统上,同一用户运行的其他进程可以读取它们。

对于生产,许多团队将机密保存在专用管理器中,例如 HashiCorp Vault、AWS Secrets Manager、Azure Key Vault 或 Google Secret Manager,并在运行时注入它们。有些人更喜欢将机密安装为文件而不是变量。无论哪种方式,都不要在日志中打印配置,并轮换曾经提交给 Git 的任何秘密,即使在您将其从历史记录中删除之后也是如此。

在 Docker 和 CI 中

# Docker (not run for this article)
docker run -e APP_PORT=8080 --env-file .env.production myapp

# GitHub Actions (not run for this article)
env:
  APP_PORT: 8080
  API_KEY: ${{ secrets.API_KEY }}

CI 平台以加密方式存储机密,并在日志中隐藏它们。避免在 Dockerfile 中使用 ENV 将机密烘焙到 Docker 镜像中,因为它们在镜像层中仍然可读。有关完整设置,请参阅 我们的 Docker Compose 教程 和 GitHub Actions CI/CD 指南。

最佳实践

  • 使用带有前缀的清晰大写名称,例如 SHOP_DB_URL。
  • 读取一个模块中的所有配置,转换类型,并在启动时验证。
  • 仅为非敏感设置提供安全默认值。
  • 将 .env 排除在 Git 之外并维护 .env.example。
  • 在生产中使用机密管理器来获取敏感值。
  • 记录每个变量、其类型和示例值。

调试环境变量问题

当应用程序表现得好像缺少设置时,请按顺序完成这些检查:

  1. 它设置在正确的位置吗? 在一个终端选项卡中导出的变量在另一个终端选项卡中或在系统启动的服务中不存在。
  2. 应用程序是否已重新启动? 正在运行的进程会保留其启动时的环境。之后更改变量在重新启动之前不会产生任何影响。
  3. 名称准确吗? Linux 和 macOS 上的名称区分大小写,因此 App_Port 和 APP_PORT 是不同的变量。 Windows 将它们视为相同的。
  4. .env 文件是否覆盖它,或者被忽略? 检查您的加载程序是否覆盖现有值,以及它实际读取哪个文件。
  5. 是否存在杂散引号或空格? 使用尾随空格或额外引号复制的值可能无法解析或无法匹配,其方式很难发现。

要查看进程真正收到的内容,请打印变量名称,而不是秘密值。在 Linux 和 macOS 上,printenv 列出当前 shell 的环境,在 PowerShell 中 Get-ChildItem Env: 执行相同的操作。在容器内,运行 env 到 docker exec 显示容器的视图,该视图通常与您自己的 shell 的视图不同。

最后一点提示:在启动时记录一行,列出哪些可选设置正在使用其默认值。它不需要花费任何成本,而且它可以回答最多的问题“为什么要这样做?”立即提问。

最后的话

环境变量很简单,但它们隐藏了一些陷阱:一切都是字符串,孩子们继承它们,并且秘密可以通过它们泄漏。将它们视为应用程序的输入。在一个地方读取它们,尽早转换和验证它们,并在提交的文件中保留真正的秘密。

常见问题

什么是环境变量?

它是进程从其环境接收的命名文本值,例如 APP_PORT=8080。应用程序将它们用于环境之间变化的设置。

什么是 .env 文件?

KEY=value 行的文本文件,在启动时由 python-dotenv 等库加载。主要用于本地开发,不应该泄露真正的秘密。

为什么我的环境变量是字符串?

操作系统将环境值存储为文本。使用 int() 转换数字并显式解析布尔值,因为字符串“false”算作 true。

环境变量的秘密是否安全?

它们比硬编码更好,但是子进程继承它们并且日志可以公开它们。生产系统通常使用专用的机密管理器。

脚本可以更改其父 shell 的变量吗?

不会。子进程获取环境的副本,因此它的更改不会回流。要在当前 shell 中设置变量,请改为使用该文件。

继续学习

与Stack Quora合作

需要测试、WordPress 或 SEO 方面的帮助?

我们帮助团队交付更好的软件并发展他们的网站。告诉我们您正在做什么,我们将建议明确的下一步。

继续阅读

更多指南 测试、人工智能和现代开发

喜欢这篇文章吗?选择一个主题并直接跳至我们最新的教程。

人工智能

人工智能幻觉解释:语言模型为何会产生幻觉以及如何减少它

人工智能幻觉解释:它们是什么,为什么语言模型产生它们,常见类型,如何通过基础和提示来减少它们,以及我们进行的简单检查。

阅读文章 →
DevOps

环境变量解释:.env 文件、秘密和配置正确完成

使用我们运行的 Python 示例解释环境变量:读取和设置它们、为什么每个值都是字符串、.env 文件、进程继承、秘密和验证。

阅读文章 →
软件开发

API 速率限制解释:算法、标头和处理 429 错误

API 速率限制通过我们运行的模拟进行了解释:令牌桶、固定窗口、滑动窗口、429 响应、速率限制标头、带回退的重试和测试限制。

阅读文章 →
测试自动化

Python 中的模拟:unittest.mock、补丁以及导致测试谎言的错误

使用unittest.mock 在Python 中进行模拟,经过测试:模拟和return_value、side_effect、修补使用名称的位置、spec 和autospec、模拟日期以及常见错误。

阅读文章 →
软件测试

代码覆盖率解释:行覆盖率与分支覆盖率以及 100% 的真正含义

用真实的coverage.py运行来解释代码覆盖率:行、分支和条件覆盖率,阅读报告,为什么100%覆盖率仍然会错过错误,以及合理的目标。

阅读文章 →
数据库与 SQL

SQL 中的 DELETE、TRUNCATE 和 DROP:差异、回滚以及何时使用它们

DELETE、TRUNCATE 和 DROP 通过经过测试的 SQL 进行了解释:各自删除什么、回滚、触发器、身份重置、速度,以及 PostgreSQL、MySQL、SQL Server 和 SQLite 有何不同。

阅读文章 →
数据库与 SQL

SQL 视图解释:创建、使用和避免陷阱(附示例)

SQL 视图通过我们运行的查询进行解释:创建视图、实时结果、隐藏列、性能、可更新和物化视图、破坏的依赖关系和最佳实践。

阅读文章 →
软件开发

TypeScript 接口与类型:真正的差异,使用编译器进行测试

TypeScript 接口与类型,使用真实编译器进行检查:两者都可以做什么,只有一个可以做什么,声明合并,扩展与交集以及要遵循的简单规则。

阅读文章 →
软件开发

Python *args 和 **kwargs 解释:灵活的函数和清晰的示例

Python *args 和 **kwargs 用我们运行的代码进行了解释:打包和解包、参数顺序、仅关键字和仅位置参数、装饰器和常见错误。

阅读文章 →
软件开发

Python Lambda 函数:语法、实际用途以及何时不使用它们

Python lambda 函数用我们运行的代码进行了解释:语法、使用键排序、映射和过滤器、闭包、后期绑定陷阱,以及何时普通 def 是更好的选择。

阅读文章 →
人工智能

什么是人工智能代理?它们是如何工作的,只需运行一个简单的代理循环

什么是人工智能代理?关于代理循环、工具、内存和护栏的简单指南,以及我们运行的一个小型 Python 循环、真正的风险以及何时简单的聊天机器人是更好的选择。

阅读文章 →
DevOps

监控与可观察性:真正的区别,包括日志、指标和跟踪

监控与可观察性解释:已知和未知的故障、日志、指标和跟踪、检查事件演练、黄金信号、OpenTelemetry 和有用的警报。

阅读文章 →
DevOps

Docker Volumes 与 Bind Mounts:以正确的方式保留容器数据

Docker 卷与绑定挂载解释:为什么容器数据消失、命名卷、绑定挂载、tmpfs、Compose 示例、备份、权限以及何时使用它们。

阅读文章 →
测试自动化

TDD 与 BDD:主要区别、以两种方式构建的一个功能以及何时使用每种方式

TDD 与 BDD 的解释是通过一种双向构建的折扣功能:使用我们运行的代码进行红绿重构、Given-When-Then 场景、工具、权衡以及何时使用每种方案。

阅读文章 →
软件开发

Git merge 与 rebase 的区别:示例及如何选择

用真实的 log 输出、对比表、rebase 黄金法则和安全的团队工作流,讲清 Git merge 与 rebase 的区别。

阅读文章 →
软件开发

Python 列表推导式:语法、示例与何时避免使用

Python 列表推导式详解:语法、过滤、if-else、嵌套循环、实测耗时,以及何时普通 for 循环更合适。

阅读文章 →
数据库与 SQL

SQL JOIN 详解:附示例(INNER、LEFT、RIGHT、FULL)

通过示例表和查询,一步步讲解 SQL JOIN:INNER、LEFT、RIGHT、FULL、CROSS 和 SELF,并介绍常见错误与性能技巧。

阅读文章 →