概要
本篇文章主要介绍了如何在代码需要运行在不同环境时比较清晰明了地管理项目配置,不至于要为了不同环境下的配置而对代码做出修改(一般有测试环境和生产环境,可能还会有较为重要的项目有预发布环境,当然,如果已经有配置下发服务的话那就不用那么费事了,可以直接用配置下发的服务来获取项目配置)
问题
以一个简单的 web 服务为例,由于要对外服务需要自动重启并且以守护进程的方式进行运行选择使用 supervisor 来进行部署(关于 supervisor 好处还有很多,可以看 链接 ),我在测试环境以及生产环境下各有一套环境,那么这个时候有什么办法做到 “一套代码两套配置” 让项目能够根据部署的机器来自行选择配置呢?
脑洞1:
获取本机 ip 信息,根据所在网段或者是否在生产环境 ip 列表来判断是否在生产环境中,然后选择配置
脑洞2:
生产环境机器数量不大的话可以用比较费力的办法,包括准备两套代码、准备两个同名配置文件每次修改等等,不是很推荐,这边只是为了举个例子
解决方案
目前我所使用的解决方案是使用环境变量,supervisor 不仅支持使用虚拟环境,也同时支持启动项目时配置环境变量,配置如下所示:
[program:your_project] command=your_project/env/bin/python your_project/app.py directory=your_project environment=PATH="/root/bin:%(ENV_PATH)s",PYTHONPATH="your_project:$(ENV_PYTHONPATH)s",PROJECT_ENV="dev" exitcodes=0 redirect_stderr = true autorestart = true stdout_logfile = /data/log/project.log stdout_logfile_maxbytes = 50MB stdout_logfile_backups = 2 stderr_logfile = /data/log/project.log loglevel = info stopasgroup = True |
需要注意的是其中
command=your_project/env/bin/python your_project/app.
这里使用了虚拟环境,这样你就不用担心在生产环境下单服务器多应用时各个应用的环境产生冲突了。
还要注意
environment=PATH="/root/bin:%(ENV_PATH)s",PYTHONPATH="your_project:$(ENV_PYTHONPATH)s",PROJECT_ENV="dev"
这边就是这个设计方案的核心之一了,通过对于环境变量的设置,来标明你的项目是在生产环境下还是在测试环境下。
接下来介绍对应的代码,以在服务被启动时自动地判断环境并读取对应的配置(这边的代码是假定配置写在 .py 文件中的,如果你的代码是以别的形式,比如说 yaml 之类的,可能还需要改动一下),配置的目录结构很简单,就是 settings/ 目录下有 __init__.py, dev.py 以及 prod.py,以下代码是写在 settings/__init__.py 里的,这样写可以在代码第一次被加载的时候就把配置加载进来。
import importlib import os import logging from settings.dev import * # 这里获取了环境变量 env = os.getenv('PROJECT_ENV') # 获取现有的全局变量 global_variable = globals() prod = dict() if env == 'prod': # 通过 importlib 来引入 .py 文件里的配置 # 如果是别的格式的配置文件,需要修改这一步 # 这里的 settings.prod 是配置文件的相对路径 prod = importlib.import_module('settings.prod') for key in dir(prod): if not key.startswith('__'): # 为避免覆盖原有的全局变量,故先用 prod 作为配置载体,再一条条导入 global_variable[key] = getattr(prod, key) |
一般项目来说这样子已经可以用了,对于一些写 sdk 的朋友来说可能觉得这样还不够简略,希望能够让同学们更简单地用上自己写的 sdk,可以写个类来管理配置,代码如下:
class SettingManager(object): def __init__(self, env_var, setting_dir): """ """ self._content = dict() self.env_var = env_var self.env = os.getenv(self.env_var, None) if self.env: self.read('.'.join([setting_dir, self.env])) def __del__(self): self._content.clear() def __getitem__(self, item): return self._content.get(item, None) def __setitem__(self, key, value): self._content[key] = value return value def __getattr__(self, item): return self._content.get(item, None) def __setattr__(self, key, value): if key.startswith('_'): object.__setattr__(self, key, value) self._content[key] = value return value def read(self, relative_path): """read settings from python file""" try: settings = importlib.import_module(relative_path) except ImportError: raise ImportError('Wrong relative path provided.') keys = [key for key in dir(settings) if not key.startswith('__')] for key in keys: # same key in different files may cause cover problem self._content[key] = getattr(settings, key) return self |
SettingManager 会在实例化的时候直接去读取配置文件,类里面还提供了 magic method,既可以像字典一样操作,也可以通过属性的方式来读取或者设置配置
>>> from settings import SettingManager >>> setting = SettingManger('PROJECT_MANAGER', 'settings') >>> setting.PROJECT_CONFIG1 "for zhihu" >>> setting['PROJECT_CONFIG1'] = 1 >>> setting['PROJECT_CONFIG1'] 1 |
这样子做的话对于上层使用你 sdk 的同学来说就完全透明了,只要引入 SettingManager 并且在环境变量中设定需要使用的配置文件的相对路径就可以了(对于SettingManger('PROJECT_MANAGER', 'settings'),只要设置环境变量为 dev 或者 prod 就可以选择配置环境了)