Loading

基于FastAPI、Pydantic与Apollo的现代化配置管理实践

在现代Web应用开发中,尤其是在微服务架构下,配置管理是一个至关重要的环节。一个优秀的配置方案需要满足以下几点:支持不同环境(开发、测试、生产)的隔离,能够安全地处理敏感信息(如数据库密码、API密钥),并且易于维护和更新。

本文将介绍一种基于Python FastAPI框架的现代化配置管理方案。该方案利用pydantic-settings进行类型安全的环境变量加载,并集成Apollo配置中心,实现一个分层、灵活且强大的配置初始化流程。

核心理念:分层加载与集中管理

我们的配置加载遵循一个清晰的优先级顺序,这使得本地开发和生产部署都变得非常简单:

  1. 本地.env文件:为本地开发提供便利,可以存放非敏感的默认配置或开发环境特定的配置。
  2. 环境变量:这是容器化部署(如Docker、Kubernetes)的标准实践。环境变量可以覆盖.env文件中的同名配置。
  3. Apollo配置中心:作为最终的、最高优先级的配置源。应用启动时,从Apollo获取配置并注入到环境变量中,从而覆盖任何本地或预设的环境变量。

这种分层设计的好处是:

  • 开发友好:开发者只需维护一个.env文件即可快速启动项目。
  • 部署灵活:运维人员可以通过标准的环境变量来配置容器。
  • 生产稳健:所有生产环境的配置都由Apollo集中管理,支持动态更新、权限控制和版本历史,大大提高了安全性和可维护性。

技术选型

  • FastAPI: 高性能的现代Web框架。
  • Pydantic-settings: pydantic的扩展,可以轻松地从环境变量中加载配置并进行类型验证,生成一个强类型的配置对象。
  • Apollo: 一个功能强大的分布式配置中心,广泛应用于生产环境。

实现步骤详解

我们将通过一个具体的代码示例来展示如何实现这一整套配置流程。

1. 项目结构

首先,我们约定一个项目结构。配置文件config.py位于config目录中,而.env文件位于项目的根目录。

/my-fastapi-app
|-- main.py
|-- .env
|-- requirements.txt
|-- /config
|   |-- config.py

2. 定义配置模型

我们使用pydantic-settings的BaseSettings来定义配置模型。这将配置分为两部分:一部分是连接Apollo所需的配置,另一部分是应用自身的业务配置。

config/config.py:

import os

import cachetools
from dotenv import load_dotenv
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import MySQLDsn, RedisDsn
from typing import Optional

# 计算项目根目录和.env文件路径
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
ENV_PATH = os.path.join(BASE_DIR, '.env')

class ApolloConnectionSettings(BaseSettings):
    """
    Apollo 连接配置。
    从项目根目录的 .env 文件或环境变量加载。
    这些字段设为Optional,是为了在不使用Apollo的场景下(如纯本地开发)也能正常工作。
    """
    APOLLO_URL: Optional[str] = None
    APOLLO_APP_ID: Optional[str] = None
    APOLLO_SECRET: Optional[str] = None
    APOLLO_NAMESPACE: Optional[str] = None
    APOLLO_CLUSTER: Optional[str] = None
    APOLLO_OVERRIDE: Optional[bool] = None
    APOLLO_ENABLE: Optional[bool] = None

class AppSettings(BaseSettings):
    """
    应用程序主配置。
    该模型定义了应用所需的所有配置项。
    pydantic-settings会自动从环境变量中读取并填充这些字段。
    """
    # ---- 固定元数据 ----
    PROJECT_NAME: str = "my-fastapi-project"
    PROJECT_DESC: str = "一个很棒的FastAPI项目"
    PROJECT_VERSION: str = "0.1.0"
    API_V1_STR: str = "/api/v1"

    # ---- 环境配置 ----
    ENVIRONMENT: str = "dev"
    DEBUG: bool = False

    # ---- 数据库配置 (从Apollo获取) ----
    DATABASE_URL: Optional[MySQLDsn] = None
    DB_MIN_SIZE: int = 5
    DB_MAX_SIZE: int = 20
    DB_ECHO: bool = False

    # ---- Redis配置 (从Apollo获取) ----
    REDIS_URL: Optional[RedisDsn] = None

    # ---- 日志配置 ----
    LOG_LEVEL: str = "INFO"
    LOG_FORMAT: Optional[str] = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
    LOG_FILE: Optional[str] = None

    # pydantic-settings的模型配置
    # 不再需要指定 env_file,因为配置由 Apollo 设置到环境变量
    model_config = SettingsConfigDict(
        case_sensitive=False,  # 环境变量不区分大小写
        env_prefix="",  	   # 环境变量没有前缀
        extra="allow"          # 允许额外的字段
    )

代码解析:

  • ApolloConnectionSettings: 专门用于存储连接Apollo所需的信息。APOLLO_ENABLE作为一个开关,可以方便地决定是否启用Apollo。
  • AppSettings: 定义了应用的所有配置,包括数据库URL、Redis URL等。注意DATABASE_URL和REDIS_URL的类型是MySQLDsn和RedisDsn,这利用了Pydantic强大的类型验证功能,确保URL格式正确。
  • model_config: case_sensitive=False使得我们可以用debug=TrueDEBUG=True这样的环境变量。

3. 实现配置加载逻辑

接下来是整个流程的核心:一个函数负责按需初始化Apollo,另一个函数作为单例工厂来提供最终的配置对象。

config/config.py (续):

def init_apollo_if_needed() -> None:
    """
    如果配置启用Apollo,则初始化连接并拉取配置。
    Apollo客户端会把拉取到的配置设置为当前进程的环境变量。
    """
    # 1. 首先加载本地.env文件,为Apollo连接参数提供可能的值
    load_dotenv(ENV_PATH)
    
    # 2. 加载Apollo连接配置
    apollo_conn = ApolloConnectionSettings()

    # 3. 检查是否启用Apollo并且关键参数已提供
    if apollo_conn.APOLLO_ENABLE and apollo_conn.APOLLO_URL and apollo_conn.APOLLO_APP_ID:
        print("[CONFIG] Apollo is enabled. Attempting to connect...")
        try:
            # pip install pyapollo
            from pyapollo import ApolloClient

            client = ApolloClient(
                app_id=apollo_conn.APOLLO_APP_ID,
                cluster=apollo_conn.APOLLO_CLUSTER,
                config_server_url=apollo_conn.APOLLO_URL,
                secret=apollo_conn.APOLLO_SECRET
            )
            # 启动客户端,假设在后台拉取配置并会更新到os.environ
            client.start(catch_signals=False) 
            print("[CONFIG] Apollo client started and configurations are being loaded into environment variables.")
        except ImportError:
            print("[CONFIG] WARNING: `pyapollo` library not found. Skipping Apollo initialization.")
        except Exception as e:
            print(f"[CONFIG] ERROR: Failed to connect to Apollo: {e}")
            # 根据策略,连接失败可以选择退出或继续使用现有环境配置
            # raise e 

@cachetools.cached(cache=cachetools.TTLCache(maxsize=1, ttl=600)) 
def get_settings() -> AppSettings:
    """
    获取应用配置的单例函数。
    整个应用都应该通过这个函数来获取配置。
    """
    # 1. 尝试从Apollo加载配置并设置到环境变量
    init_apollo_if_needed()

    # 2. 实例化AppSettings
    # pydantic-settings会自动从环境变量中读取所有配置
    # 如果Apollo成功运行,此时的环境变量已经被Apollo更新
    settings = AppSettings()

    # 3. 在开发环境下打印配置以供调试
    if settings.ENVIRONMENT.lower() == "dev":
        print("\n[CONFIG] Application settings loaded:")
        for field, value in settings.model_dump().items():
            # 对敏感字段进行脱敏
            if any(sensitive in field.lower() for sensitive in ["secret", "password", "token", "key"]):
                value = "******"
            print(f"[CONFIG] - {field}: {value}")
        print("-" * 30)

    return settings


# 在模块加载时直接调用,生成全局可用的配置实例
settings = get_settings()

代码解析:

  • init_apollo_if_needed(): 这个函数首先加载本地的.env文件,这样APOLLO_URL等参数就可以在.env中配置。然后,它检查是否启用了Apollo,如果启用,就初始化Apollo客户端。关键点在于:Apollo客户端库(如pyapollo)在获取到配置后,会将其写入os.environ,即当前进程的环境变量中。
  • @lru_cache(maxsize=1): 这是一个非常巧妙的装饰器,它将get_settings函数变成了一个单例工厂。第一次调用时,函数体会被执行,配置被加载和初始化;后续所有调用都会立即返回第一次缓存的结果,避免了重复加载,并确保了全局配置的一致性。
  • settings = get_settings(): 在模块的末尾执行一次,这样其他任何模块只需from config.config import settings就可以直接使用已经初始化好的配置实例。

另外这里使用load_dotenv(ENV_PATH)手动加载配置文件,如果改成在ApolloConnectionSettings类里新增如下代码,这样虽然实例化的类里有加载到配置,但是os.env里是没有的,可能导致后续apollo里加载不到配置:

model_config = SettingsConfigDict(
        # 指定 env_file 为项目根目录下的 .env 文件
        env_file=ENV_PATH,
        env_file_encoding="utf-8",
        case_sensitive=False,  # 环境变量不区分大小写
        extra="allow"  # 允许额外字段
    )

4. 在FastAPI应用中使用

现在,在你的FastAPI应用中使用配置就变得非常简单了。

main.py:

from fastapi import FastAPI
from config.config import settings  # 导入配置实例

# 使用配置来初始化应用
app = FastAPI(
    title=settings.PROJECT_NAME,
    description=settings.PROJECT_DESC,
    version=settings.PROJECT_VERSION,
    debug=settings.DEBUG
)

@app.get("/")
def read_root():
    return {
        "message": f"Welcome to {settings.PROJECT_NAME}",
        "environment": settings.ENVIRONMENT,
        "database_url_is_set": settings.DATABASE_URL is not None
    }

# 假设你有一个数据库初始化函数
def init_database():
    db_url = str(settings.DATABASE_URL)
    print(f"Initializing database with URL: {db_url[:db_url.find('@')]}@... (credentials masked)")
    # ... 数据库连接池创建逻辑 ...

init_database()

完整的配置流程总结

  1. 应用启动: main.py被执行,导入config.config模块。
  2. 配置模块加载: config.py被加载,代码执行到settings = get_settings()
  3. 首次获取配置: get_settings()被调用。
  4. Apollo初始化: init_apollo_if_needed()被调用。
    • load_dotenv()加载.env文件。
    • ApolloConnectionSettings读取到Apollo的连接信息。
    • 如果APOLLO_ENABLE=True,Apollo客户端启动,从远端拉取DATABASE_URL, REDIS_URL等配置,并设置到环境变量中
  5. 最终配置生成: AppSettings()被实例化。pydantic-settings从环境变量中读取配置。此时,如果Apollo已成功加载,它读取到的是Apollo设置的值;否则,它读取到的是.env或启动时传入的环境变量的值。
  6. 配置单例缓存: get_settings的结果被@lru_cache缓存。
  7. 全局使用: settings实例在config.config模块中可用,任何地方导入即可使用。

结论

通过结合pydantic-settings的类型安全和Apollo的集中管理能力,我们为FastAPI应用构建了一个既灵活又强大的配置系统。该方案优雅地处理了不同环境下的配置差异,简化了开发和部署流程,并极大地提升了生产环境配置的安全性和可维护性,是值得在实际项目中采纳的优秀实践。

参考

posted @ 2020-03-02 21:33  飞鸿影  阅读(1023)  评论(0)    收藏  举报