如何優雅地管理項目配置

概要

本篇文章主要介紹了如何在代碼需要運行在不同環境時比較清晰明了地管理項目配置,不至於要為了不同環境下的配置而對代碼做出修改(一般有測試環境和生產環境,可能還會有較為重要的項目有預發布環境,當然,如果已經有配置下發服務的話那就不用那麼費事了,可以直接用配置下發的服務來獲取項目配置)

問題

以一個簡單的 web 服務為例,由於要對外服務需要自動重啟並且以守護進程的方式進行運行選擇使用 supervisor 來進行部署(關於 supervisor 好處還有很多,可以看 鏈接),我在測試環境以及生產環境下各有一套環境,那麼這個時候有什麼辦法做到 「一套代碼兩套配置」 讓項目能夠根據部署的機器來自行選擇配置呢?

腦洞1:

獲取本機 ip 信息,根據所在網段或者是否在生產環境 ip 列表來判斷是否在生產環境中,然後選擇配置

腦洞2:

生產環境機器數量不大的話可以用比較費力的辦法,包括準備兩套代碼、準備兩個同名配置文件每次修改等等,不是很推薦,這邊只是為了舉個例子

解決方案

目前我所使用的解決方案是使用環境變數,supervisor 不僅支持使用虛擬環境,也同時支持啟動項目時配置環境變數,配置如下所示:

[program:your_project]ncommand=your_project/env/bin/python your_project/app.pyndirectory=your_projectnenvironment=PATH="/root/bin:%(ENV_PATH)s",PYTHONPATH="your_project:$(ENV_PYTHONPATH)s",PROJECT_ENV="dev"nexitcodes=0nredirect_stderr = truenautorestart = truenstdout_logfile = /data/log/project.lognstdout_logfile_maxbytes = 50MBnstdout_logfile_backups = 2nstderr_logfile = /data/log/project.lognloglevel = infonstopasgroup = Truen

需要注意的是其中

command=your_project/env/bin/python your_project/app.n

這裡使用了虛擬環境,這樣你就不用擔心在生產環境下單伺服器多應用時各個應用的環境產生衝突了。

還要注意

environment=PATH="/root/bin:%(ENV_PATH)s",PYTHONPATH="your_project:$(ENV_PYTHONPATH)s",PROJECT_ENV="dev"n

這邊就是這個設計方案的核心之一了,通過對於環境變數的設置,來標明你的項目是在生產環境下還是在測試環境下。

接下來介紹對應的代碼,以在服務被啟動時自動地判斷環境並讀取對應的配置(這邊的代碼是假定配置寫在 .py 文件中的,如果你的代碼是以別的形式,比如說 yaml 之類的,可能還需要改動一下),配置的目錄結構很簡單,就是 settings/ 目錄下有 __init__.py, dev.py 以及 prod.py,以下代碼是寫在 settings/__init__.py 里的,這樣寫可以在代碼第一次被載入的時候就把配置載入進來。

import importlibnimport osnimport loggingnfrom settings.dev import *nn# 這裡獲取了環境變數nenv = os.getenv(PROJECT_ENV)n# 獲取現有的全局變數nglobal_variable = globals()nprod = dict()nnif env == prod:n # 通過 importlib 來引入 .py 文件里的配置n # 如果是別的格式的配置文件,需要修改這一步n # 這裡的 settings.prod 是配置文件的相對路徑n prod = importlib.import_module(settings.prod)n for key in dir(prod):n if not key.startswith(__):n # 為避免覆蓋原有的全局變數,故先用 prod 作為配置載體,再一條條導入n global_variable[key] = getattr(prod, key)n

一般項目來說這樣子已經可以用了,對於一些寫 sdk 的朋友來說可能覺得這樣還不夠簡略,希望能夠讓同學們更簡單地用上自己寫的 sdk,可以寫個類來管理配置,代碼如下:

class SettingManager(object):nn def __init__(self, env_var, setting_dir):n """n n """ n self._content = dict()n self.env_var = env_varn self.env = os.getenv(self.env_var, None)n if self.env:n self.read(..join([setting_dir, self.env]))nn def __del__(self):n self._content.clear()nn def __getitem__(self, item):n return self._content.get(item, None)nn def __setitem__(self, key, value):n self._content[key] = valuen return valuenn def __getattr__(self, item):n return self._content.get(item, None)nn def __setattr__(self, key, value):n if key.startswith(_):n object.__setattr__(self, key, value)n self._content[key] = valuen return valuenn def read(self, relative_path):n """read settings from python file"""n try:n settings = importlib.import_module(relative_path)n except ImportError:n raise ImportError(Wrong relative path provided.)n keys = [key for key in dir(settings) if not key.startswith(__)]n for key in keys:n # same key in different files may cause cover problemn self._content[key] = getattr(settings, key)n return selfn

SettingManager 會在實例化的時候直接去讀取配置文件,類裡面還提供了 magic method,既可以像字典一樣操作,也可以通過屬性的方式來讀取或者設置配置

>>> from settings import SettingManagern>>> setting = SettingManger(PROJECT_MANAGER, settings)n>>> setting.PROJECT_CONFIG1n"for zhihu"n>>> setting[PROJECT_CONFIG1] = 1n>>> setting[PROJECT_CONFIG1]n1n

這樣子做的話對於上層使用你 sdk 的同學來說就完全透明了,只要引入 SettingManager 並且在環境變數中設定需要使用的配置文件的相對路徑就可以了(對於SettingManger(PROJECT_MANAGER, settings),只要設置環境變數為 dev 或者 prod 就可以選擇配置環境了)

github 廣告時間

項目地址:

NoneGG/aredis

其實水了那麼多我是想給自己寫的非同步 redis 客戶端打廣告來著的,aredis 經過迭代現在已經支持迭代器以及緩存功能了,原有的介紹可以看這邊,前排求 pr 和建議,下一步打算做 Python 3.4 的兼容,但是之後需要做什麼還有點迷,redis 4.0 暫時還只是出了 beta 版本的樣子 Orz


推薦閱讀:

Python如何識別二維碼
爬取豆瓣有關張國榮日記(二)—— 策略源碼知識點
20170403Python控制流if、while、for語句學習
TensorFlow初步(3)
Python資料庫起航篇|零基礎起步

TAG:Python | Python开发 | Python入门 |